diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 482d83621e2b48496a1793c09d5692f7b6338eb4..fb1fc9cd32e9e2541b50a79600dea33642fa88a8 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown { valueAttribute: 'data-text', }, ], + hideOnClick: false, }; } diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index 673e9bb4c0ff31e2c8e560f5e81508aa88c701e5..868d47e91b3e4b40ead36b3d09458b1526d0c7a8 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown'; const SELECTED_CLASS = 'droplab-item-selected'; const ACTIVE_CLASS = 'droplab-item-active'; const IGNORE_CLASS = 'droplab-item-ignore'; -const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding'; // Matches `{{anything}}` and `{{ everything }}`. const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; @@ -14,5 +13,4 @@ export { ACTIVE_CLASS, TEMPLATE_REGEX, IGNORE_CLASS, - IGNORE_HIDING_CLASS, }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 5eb0a339a1cef19978592daf1be690308deb0285..3cc316c3f3e3667cc0a7df8769579261955ef0a7 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -1,13 +1,14 @@ import utils from './utils'; -import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; class DropDown { - constructor(list, config = {}) { + constructor(list, config = { }) { this.currentIndex = 0; this.hidden = true; this.list = typeof list === 'string' ? document.querySelector(list) : list; this.items = []; this.eventWrapper = {}; + this.hideOnClick = config.hideOnClick !== false; if (config.addActiveClassToDropdownButton) { this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle'); @@ -37,15 +38,17 @@ class DropDown { clickEvent(e) { if (e.target.tagName === 'UL') return; - if (e.target.classList.contains(IGNORE_CLASS)) return; + if (e.target.closest(`.${IGNORE_CLASS}`)) return; - const selected = utils.closest(e.target, 'LI'); + const selected = e.target.closest('li'); if (!selected) return; this.addSelectedClass(selected); e.preventDefault(); - if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide(); + if (this.hideOnClick) { + this.hide(); + } const listEvent = new CustomEvent('click.dl', { detail: { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 73524d5cf605c8a202ba480636857fea1f4623f0..ae517c41cb27e991c40bf14fcaf08e5aeea36aee 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -449,9 +449,11 @@ img.emoji { .prepend-top-10 { margin-top: 10px; } .prepend-top-15 { margin-top: 15px; } .prepend-top-default { margin-top: $gl-padding !important; } +.prepend-top-16 { margin-top: 16px; } .prepend-top-20 { margin-top: 20px; } .prepend-left-4 { margin-left: 4px; } .prepend-left-5 { margin-left: 5px; } +.prepend-left-8 { margin-left: 8px; } .prepend-left-10 { margin-left: 10px; } .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 691df098c7021e7e7057e538c67641e5722bade7..1d7b0b602cc07bb9ee994683aa14b98c9fe68022 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -736,10 +736,6 @@ } } -.droplab-item-ignore { - pointer-events: none; -} - .pika-single.animate-picker.is-bound, .pika-single.animate-picker.is-bound.is-hidden { /* diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index be96c8ee964e371abc55ebc2706ab82488c0da84..a2ea155a10ec9c95729675d8a571ef1a80616586 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -182,6 +182,7 @@ label { .help-block { margin-bottom: 0; + margin-top: #{$grid-size / 2}; } .gl-field-error { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index c48e58af6916655c3031ec4b49118412650370f7..6763af4e98b77773ea04e65dfc4f986cd83321ca 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -181,11 +181,6 @@ ul.related-merge-requests > li { } .create-mr-dropdown-wrap { - .branch-message, - .ref-message { - display: none; - } - .ref::selection { color: $placeholder-text-color; } @@ -216,6 +211,17 @@ ul.related-merge-requests > li { transform: translateY(0); display: none; margin-top: 4px; + + // override dropdown item styles + .btn.btn-success { + @include btn-default; + @include btn-green; + + border-style: solid; + border-width: 1px; + line-height: $line-height-base; + width: auto; + } } .create-merge-request-dropdown-toggle { @@ -225,66 +231,6 @@ ul.related-merge-requests > li { margin-left: 0; } } - - .droplab-item-ignore { - pointer-events: auto; - } - - .create-item { - cursor: pointer; - margin: 0 1px; - - &:hover, - &:focus { - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; - } - } - - li.divider { - margin: 8px 10px; - } - - li:not(.divider) { - padding: 8px 9px; - - &:last-child { - padding-bottom: 8px; - } - - &.droplab-item-selected { - .icon-container { - i { - visibility: visible; - } - } - - .description { - display: block; - } - } - - &.droplab-item-ignore { - padding-top: 8px; - } - - .icon-container { - float: left; - - i { - visibility: hidden; - } - } - - .description { - padding-left: 22px; - } - - input, - span { - margin: 4px 0 0; - } - } } .discussion-reply-holder .note-edit-form { diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 37b00a14fc635d539c5ee7f513aff5f023bb70f2..36e240372141e9b9b0e3752520f3bb155238ebfb 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -21,30 +21,33 @@ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } } = icon('caret-down') - %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } } - - if can_create_merge_request - %li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } } - .menu-item.droplab-item-ignore-hiding - .icon-container.droplab-item-ignore-hiding= icon('check') - .description.droplab-item-ignore-hiding Create merge request and branch - - %li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } } - .menu-item.droplab-item-ignore-hiding - .icon-container.droplab-item-ignore-hiding= icon('check') - .description.droplab-item-ignore-hiding Create branch - %li.divider - - %li.droplab-item-ignore - Branch name - %input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" } - %span.js-branch-message.branch-message.droplab-item-ignore - - %li.droplab-item-ignore - Source (branch or tag) - %input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } } - %span.js-ref-message.ref-message.droplab-item-ignore - - %li.droplab-item-ignore - %button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } } - Create merge request - + .droplab-dropdown + %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } } + - if can_create_merge_request + %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } } + .menu-item + = icon('check', class: 'icon') + = _('Create merge request and branch') + + %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } } + .menu-item + = icon('check', class: 'icon') + = _('Create branch') + %li.divider.droplab-item-ignore + + %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16 + .form-group + %label{ for: 'new-branch-name' } + = _('Branch name') + %input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" } + %span.js-branch-message.help-block + + .form-group + %label{ for: 'source-name' } + = _('Source (branch or tag)') + %input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } } + %span.js-ref-message.help-block + + .form-group + %button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } } + = _('Create merge request') diff --git a/changelogs/unreleased/winh-new-branch-dropdown-style.yml b/changelogs/unreleased/winh-new-branch-dropdown-style.yml new file mode 100644 index 0000000000000000000000000000000000000000..007e9e9f4536ce1ff3352ea79e5af94abe98ec07 --- /dev/null +++ b/changelogs/unreleased/winh-new-branch-dropdown-style.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup new branch/merge request form in issues +merge_request: 16854 +author: +type: fixed diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index 1225fe2cb66e010935cafaa84ccb8c1fbbbf195e..896a04a1a07573046cb6be000f1b0245f996c326 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -1,8 +1,8 @@ import DropDown from '~/droplab/drop_down'; import utils from '~/droplab/utils'; -import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants'; +import { SELECTED_CLASS } from '~/droplab/constants'; -describe('DropDown', function () { +describe('DropLab DropDown', function () { describe('class constructor', function () { beforeEach(function () { spyOn(DropDown.prototype, 'getItems'); @@ -128,93 +128,131 @@ describe('DropDown', function () { beforeEach(function () { this.classList = jasmine.createSpyObj('classList', ['contains']); this.list = { dispatchEvent: () => {} }; - this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} }; - this.event = { preventDefault: () => {}, target: { classList: this.classList } }; + this.dropdown = { + hideOnClick: true, + hide: () => {}, + list: this.list, + addSelectedClass: () => {}, + }; + this.event = { + preventDefault: () => {}, + target: { + classList: this.classList, + closest: () => null, + }, + }; this.customEvent = {}; - this.closestElement = {}; + this.dummyListItem = document.createElement('li'); + spyOn(this.event.target, 'closest').and.callFake((selector) => { + if (selector === 'li') { + return this.dummyListItem; + } + + return null; + }); spyOn(this.dropdown, 'hide'); spyOn(this.dropdown, 'addSelectedClass'); spyOn(this.list, 'dispatchEvent'); spyOn(this.event, 'preventDefault'); spyOn(window, 'CustomEvent').and.returnValue(this.customEvent); - spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined); this.classList.contains.and.returnValue(false); + }); + it('should call event.target.closest', function () { DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - it('should call utils.closest', function () { - expect(utils.closest).toHaveBeenCalledWith(this.event.target, 'LI'); + expect(this.event.target.closest).toHaveBeenCalledWith('.droplab-item-ignore'); + expect(this.event.target.closest).toHaveBeenCalledWith('li'); }); it('should call addSelectedClass', function () { - expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.dummyListItem); }); it('should call .preventDefault', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.event.preventDefault).toHaveBeenCalled(); }); it('should call .hide', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.dropdown.hide).toHaveBeenCalled(); }); it('should construct CustomEvent', function () { - expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); - }); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); - it('should call .classList.contains checking for IGNORE_CLASS', function () { - expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS); + expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); }); it('should call .dispatchEvent with the customEvent', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent); }); describe('if the target is a UL element', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } }; - - spyOn(this.event, 'preventDefault'); - utils.closest.calls.reset(); + this.event.target = document.createElement('ul'); - DropDown.prototype.clickEvent.call(this.dropdown, this.event); + spyOn(this.event.target, 'closest'); }); it('should return immediately', function () { - expect(utils.closest).not.toHaveBeenCalled(); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.event.target.closest).not.toHaveBeenCalled(); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); }); }); - describe('if the target has the IGNORE_CLASS class', function () { + describe('if the target has the droplab-item-ignore class', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } }; + this.ignoredButton = document.createElement('button'); + this.ignoredButton.classList.add('droplab-item-ignore'); + this.event.target = this.ignoredButton; - spyOn(this.event, 'preventDefault'); - this.classList.contains.and.returnValue(true); - utils.closest.calls.reset(); + spyOn(this.ignoredButton, 'closest').and.callThrough(); + }); + it('does not select element', function () { DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - it('should return immediately', function () { - expect(utils.closest).not.toHaveBeenCalled(); + expect(this.ignoredButton.closest.calls.count()).toBe(1); + expect(this.ignoredButton.closest).toHaveBeenCalledWith('.droplab-item-ignore'); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); }); }); describe('if no selected element exists', function () { beforeEach(function () { this.event.preventDefault.calls.reset(); - this.clickEvent = DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - - it('should return undefined', function () { - expect(this.clickEvent).toBe(undefined); + this.dummyListItem = null; }); it('should return before .preventDefault is called', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.event.preventDefault).not.toHaveBeenCalled(); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); + }); + }); + + describe('if hideOnClick is false', () => { + beforeEach(function () { + this.dropdown.hideOnClick = false; + this.dropdown.hide.calls.reset(); + }); + + it('should not call .hide', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.dropdown.hide).not.toHaveBeenCalled(); }); }); }); @@ -278,20 +316,23 @@ describe('DropDown', function () { describe('addEvents', function () { beforeEach(function () { - this.list = { addEventListener: () => {} }; + this.list = { + addEventListener: () => {}, + querySelectorAll: () => [], + }; this.dropdown = { list: this.list, clickEvent: () => {}, closeDropdown: () => {}, eventWrapper: {}, }; + }); + it('should call .addEventListener', function () { spyOn(this.list, 'addEventListener'); DropDown.prototype.addEvents.call(this.dropdown); - }); - it('should call .addEventListener', function () { expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function)); });