diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js index a7dad506e6d832b09abb5450ab73193c545a2e4c..3f14a8a8a2652d25df25201865055e64b9767ee5 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js @@ -94,6 +94,7 @@ export class GitLabDropdown { dataType: this.options.dataType, beforeSend: this.toggleLoading.bind(this), success: (data) => { + this.dropdown.trigger('done.remote.loading.gl.dropdown'); this.fullData = data; this.parseData(this.fullData); this.focusTextInput(); @@ -220,7 +221,12 @@ export class GitLabDropdown { } toggleLoading() { - return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + const menu = this.dropdown[0].querySelector('.dropdown-menu'); + const isLoading = menu.classList.contains(LOADING_CLASS); + + this.dropdown.trigger(`toggle.${isLoading ? 'off' : 'on'}.loading.gl.dropdown`); + + menu.classList.toggle(LOADING_CLASS); } togglePage() { diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js index 2cb9e9a56a3e88f9191a1c5f3203fad03d0e282c..271347feebd9bf20f94a5e5aa37f64cbea89a67f 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js @@ -17,9 +17,11 @@ export class GitLabDropdownFilter { const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); const filterRemoteDebounced = debounce(() => { + options.instance.dropdown.trigger('filtering.gl.dropdown'); $inputContainer.parent().addClass('is-loading'); return this.options.query(this.input.val(), (data) => { + options.instance.dropdown.trigger('done.filtering.gl.dropdown'); $inputContainer.parent().removeClass('is-loading'); return this.options.callback(data); }); diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js index 6a3d20261929eed2a3e4acc105da5346ee62b93e..38236707e06e3d734964a765502458143a82ddf8 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/index.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js @@ -5,7 +5,10 @@ export default function initDeprecatedJQueryDropdown($el, opts) { // eslint-disable-next-line func-names return $el.each(function () { if (!$.data(this, 'deprecatedJQueryDropdown')) { - $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts)); + const instance = new GitLabDropdown(this, opts); + + $.data(this, 'deprecatedJQueryDropdown', instance); + this.GitLabDropdownInstance = instance; } }); } diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js index 8c2e2a5df67a9b94f42e722fdf82dcf8b3dab96f..ef49bd29a40b547c03a1ef3e1cf88f36209f7620 100644 --- a/app/assets/javascripts/issuable/issuable_context.js +++ b/app/assets/javascripts/issuable/issuable_context.js @@ -8,6 +8,29 @@ export default class IssuableContext { this.userSelect = new UsersSelect(currentUser); this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search'); + this.reviewersSelect.dropdowns.forEach((glDropdownInstance) => { + const jQueryWrapper = glDropdownInstance.dropdown; + const domElement = jQueryWrapper[0]; + const content = domElement.querySelector('.dropdown-content'); + const loader = domElement.querySelector('.dropdown-loading'); + const spinner = loader.querySelector('.gl-spinner-container'); + const realParent = loader.parentNode; + + domElement.classList.add('non-blocking-loader'); + spinner.classList.remove('gl-mt-7'); + spinner.classList.add('gl-mt-2'); + + jQueryWrapper.on('shown.bs.dropdown', () => { + glDropdownInstance.filterInput.focus(); + }); + jQueryWrapper.on('toggle.on.loading.gl.dropdown filtering.gl.dropdown', () => { + content.appendChild(loader); + }); + jQueryWrapper.on('done.remote.loading.gl.dropdown done.filtering.gl.dropdown', () => { + realParent.appendChild(loader); + }); + }); + $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() { return $(this).submit(); }); diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index ab707e7e69c6fec65fbd27f3e867ee147fbe03d0..1b36f02226dacec88cfd1a738a401996623d5a81 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -32,45 +32,46 @@ function UsersSelect(currentUser, els, options = {}) { const { handleClick, states } = options; - $els.each((i, dropdown) => { - const userSelect = this; - const $dropdown = $(dropdown); - const options = { - states, - projectId: $dropdown.data('projectId'), - groupId: $dropdown.data('groupId'), - showCurrentUser: $dropdown.data('currentUser'), - todoFilter: $dropdown.data('todoFilter'), - todoStateFilter: $dropdown.data('todoStateFilter'), - iid: $dropdown.data('iid'), - issuableType: $dropdown.data('issuableType'), - targetBranch: $dropdown.data('targetBranch'), - authorId: $dropdown.data('authorId'), - showSuggested: $dropdown.data('showSuggested'), - }; - const showNullUser = $dropdown.data('nullUser'); - const defaultNullUser = $dropdown.data('nullUserDefault'); - const showMenuAbove = $dropdown.data('showMenuAbove'); - const showAnyUser = $dropdown.data('anyUser'); - const firstUser = $dropdown.data('firstUser'); - const defaultLabel = $dropdown.data('defaultLabel'); - const issueURL = $dropdown.data('issueUpdate'); - const $selectbox = $dropdown.closest('.selectbox'); - const $assignToMeLink = $selectbox.next('.assign-to-me-link'); - let $block = $selectbox.closest('.block'); - const abilityName = $dropdown.data('abilityName'); - let $value = $block.find('.value'); - const $collapsedSidebar = $block.find('.sidebar-collapsed-user'); - const $loading = $block.find('.block-loading').addClass('gl-display-none'); - const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; - let selectedId = $dropdown.data('selected'); - let assignTo; - let assigneeTemplate; - let collapsedAssigneeTemplate; - - const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath'); - const suggestedReviewersHeaderTemplate = template( - `
+ this.dropdowns = $els + .map((i, dropdown) => { + const userSelect = this; + const $dropdown = $(dropdown); + const options = { + states, + projectId: $dropdown.data('projectId'), + groupId: $dropdown.data('groupId'), + showCurrentUser: $dropdown.data('currentUser'), + todoFilter: $dropdown.data('todoFilter'), + todoStateFilter: $dropdown.data('todoStateFilter'), + iid: $dropdown.data('iid'), + issuableType: $dropdown.data('issuableType'), + targetBranch: $dropdown.data('targetBranch'), + authorId: $dropdown.data('authorId'), + showSuggested: $dropdown.data('showSuggested'), + }; + const showNullUser = $dropdown.data('nullUser'); + const defaultNullUser = $dropdown.data('nullUserDefault'); + const showMenuAbove = $dropdown.data('showMenuAbove'); + const showAnyUser = $dropdown.data('anyUser'); + const firstUser = $dropdown.data('firstUser'); + const defaultLabel = $dropdown.data('defaultLabel'); + const issueURL = $dropdown.data('issueUpdate'); + const $selectbox = $dropdown.closest('.selectbox'); + const $assignToMeLink = $selectbox.next('.assign-to-me-link'); + let $block = $selectbox.closest('.block'); + const abilityName = $dropdown.data('abilityName'); + let $value = $block.find('.value'); + const $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + const $loading = $block.find('.block-loading').addClass('gl-display-none'); + const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; + let selectedId = $dropdown.data('selected'); + let assignTo; + let assigneeTemplate; + let collapsedAssigneeTemplate; + + const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath'); + const suggestedReviewersHeaderTemplate = template( + `
<%- header %>
`, - ); + ); - if (selectedId === undefined) { - selectedId = selectedIdDefault; - } + if (selectedId === undefined) { + selectedId = selectedIdDefault; + } - const assignYourself = function () { - const unassignedSelected = $dropdown - .closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); + const assignYourself = function () { + const unassignedSelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); - if (unassignedSelected) { - unassignedSelected.remove(); - } + if (unassignedSelected) { + unassignedSelected.remove(); + } - // Save current selected user to the DOM - const currentUserInfo = $dropdown.data('currentUserInfo') || {}; - const currentUser = userSelect.currentUser || {}; - const fieldName = $dropdown.data('fieldName'); - const userName = currentUserInfo.name; - const userId = currentUserInfo.id || currentUser.id; + // Save current selected user to the DOM + const currentUserInfo = $dropdown.data('currentUserInfo') || {}; + const currentUser = userSelect.currentUser || {}; + const fieldName = $dropdown.data('fieldName'); + const userName = currentUserInfo.name; + const userId = currentUserInfo.id || currentUser.id; - const inputHtmlString = template(` + const inputHtmlString = template(` `)({ fieldName, userName, userId }); - if ($selectbox) { - $dropdown.parent().before(inputHtmlString); - } else { - $dropdown.after(inputHtmlString); + if ($selectbox) { + $dropdown.parent().before(inputHtmlString); + } else { + $dropdown.after(inputHtmlString); + } + }; + + if ($block[0]) { + $block[0].addEventListener('assignYourself', assignYourself); } - }; - if ($block[0]) { - $block[0].addEventListener('assignYourself', assignYourself); - } + const getSelectedUserInputs = function () { + return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`); + }; - const getSelectedUserInputs = function () { - return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`); - }; - - const getSelected = function () { - return getSelectedUserInputs() - .map((index, input) => parseInt(input.value, 10)) - .get(); - }; - - const checkMaxSelect = function () { - const maxSelect = $dropdown.data('maxSelect'); - if (maxSelect) { - const selected = getSelected(); - - if (selected.length > maxSelect) { - const firstSelectedId = selected[0]; - const firstSelected = $dropdown - .closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); - - firstSelected.remove(); - - if ($dropdown.hasClass(elsClassName)) { - emitSidebarEvent('sidebar.removeReviewer', { - id: firstSelectedId, - }); - } else { - emitSidebarEvent('sidebar.removeAssignee', { - id: firstSelectedId, - }); + const getSelected = function () { + return getSelectedUserInputs() + .map((index, input) => parseInt(input.value, 10)) + .get(); + }; + + const checkMaxSelect = function () { + const maxSelect = $dropdown.data('maxSelect'); + if (maxSelect) { + const selected = getSelected(); + + if (selected.length > maxSelect) { + const firstSelectedId = selected[0]; + const firstSelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); + + firstSelected.remove(); + + if ($dropdown.hasClass(elsClassName)) { + emitSidebarEvent('sidebar.removeReviewer', { + id: firstSelectedId, + }); + } else { + emitSidebarEvent('sidebar.removeAssignee', { + id: firstSelectedId, + }); + } } } - } - }; - - const getMultiSelectDropdownTitle = function (selectedUser, isSelected) { - const selectedUsers = getSelected().filter((u) => u !== 0); - - const firstUser = getSelectedUserInputs() - .map((index, input) => ({ - name: input.dataset.meta, - value: parseInt(input.value, 10), - })) - .filter((u) => u.id !== 0) - .get(0); - - if (selectedUsers.length === 0) { - return s__('UsersSelect|Unassigned'); - } else if (selectedUsers.length === 1) { - return firstUser.name; - } else if (isSelected) { - const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id); + }; + + const getMultiSelectDropdownTitle = function (selectedUser, isSelected) { + const selectedUsers = getSelected().filter((u) => u !== 0); + + const firstUser = getSelectedUserInputs() + .map((index, input) => ({ + name: input.dataset.meta, + value: parseInt(input.value, 10), + })) + .filter((u) => u.id !== 0) + .get(0); + + if (selectedUsers.length === 0) { + return s__('UsersSelect|Unassigned'); + } else if (selectedUsers.length === 1) { + return firstUser.name; + } else if (isSelected) { + const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id); + return sprintf(s__('UsersSelect|%{name} + %{length} more'), { + name: selectedUser.name, + length: otherSelected.length, + }); + } return sprintf(s__('UsersSelect|%{name} + %{length} more'), { - name: selectedUser.name, - length: otherSelected.length, + name: firstUser.name, + length: selectedUsers.length - 1, }); - } - return sprintf(s__('UsersSelect|%{name} + %{length} more'), { - name: firstUser.name, - length: selectedUsers.length - 1, - }); - }; - - $assignToMeLink.on('click', (e) => { - e.preventDefault(); - $(e.currentTarget).hide(); - - if ($dropdown.data('multiSelect')) { - assignYourself(); - checkMaxSelect(); - - const currentUserInfo = $dropdown.data('currentUserInfo'); - $dropdown - .find('.dropdown-toggle-text') - .text(getMultiSelectDropdownTitle(currentUserInfo)) - .removeClass('is-default'); - } else { - const $input = $(`input[name="${$dropdown.data('fieldName')}"]`); - $input.val(gon.current_user_id); - selectedId = $input.val(); - $dropdown - .find('.dropdown-toggle-text') - .text(gon.current_user_fullname) - .removeClass('is-default'); - } - }); - - $block.on('click', '.js-assign-yourself', (e) => { - e.preventDefault(); - return assignTo(userSelect.currentUser.id); - }); - - assignTo = function (selected) { - const data = {}; - data[abilityName] = {}; - data[abilityName].assignee_id = selected != null ? selected : null; - $loading.removeClass('gl-display-none'); - $dropdown.trigger('loading.gl.dropdown'); - - return axios.put(issueURL, data).then(({ data }) => { - let user = {}; - let tooltipTitle; - $dropdown.trigger('loaded.gl.dropdown'); - $loading.addClass('gl-display-none'); - if (data.assignee) { - user = { - name: data.assignee.name, - username: data.assignee.username, - avatar: data.assignee.avatar_url, - }; - tooltipTitle = escape(user.name); + }; + + $assignToMeLink.on('click', (e) => { + e.preventDefault(); + $(e.currentTarget).hide(); + + if ($dropdown.data('multiSelect')) { + assignYourself(); + checkMaxSelect(); + + const currentUserInfo = $dropdown.data('currentUserInfo'); + $dropdown + .find('.dropdown-toggle-text') + .text(getMultiSelectDropdownTitle(currentUserInfo)) + .removeClass('is-default'); } else { - user = { - name: s__('UsersSelect|Unassigned'), - username: '', - avatar: '', - }; - tooltipTitle = s__('UsersSelect|Assignee'); + const $input = $(`input[name="${$dropdown.data('fieldName')}"]`); + $input.val(gon.current_user_id); + selectedId = $input.val(); + $dropdown + .find('.dropdown-toggle-text') + .text(gon.current_user_fullname) + .removeClass('is-default'); } - $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', tooltipTitle); - fixTitle($collapsedSidebar); + }); - return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + $block.on('click', '.js-assign-yourself', (e) => { + e.preventDefault(); + return assignTo(userSelect.currentUser.id); }); - }; - collapsedAssigneeTemplate = template( - `<% if( avatar ) { %> <% } else { %> ${spriteIcon( - 'user', - )} <% } %>`, - ); - assigneeTemplate = template( - `<% if (username) { %> <% if( avatar ) { %> <% } %> <%- name %> @<%- username %> <% } else { %> + + assignTo = function (selected) { + const data = {}; + data[abilityName] = {}; + data[abilityName].assignee_id = selected != null ? selected : null; + $loading.removeClass('gl-display-none'); + $dropdown.trigger('loading.gl.dropdown'); + + return axios.put(issueURL, data).then(({ data }) => { + let user = {}; + let tooltipTitle; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.addClass('gl-display-none'); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url, + }; + tooltipTitle = escape(user.name); + } else { + user = { + name: s__('UsersSelect|Unassigned'), + username: '', + avatar: '', + }; + tooltipTitle = s__('UsersSelect|Assignee'); + } + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', tooltipTitle); + fixTitle($collapsedSidebar); + + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); + }; + collapsedAssigneeTemplate = template( + `<% if( avatar ) { %> <% } else { %> ${spriteIcon( + 'user', + )} <% } %>`, + ); + assigneeTemplate = template( + `<% if (username) { %> <% if( avatar ) { %> <% } %> <%- name %> @<%- username %> <% } else { %> ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), { openingTag: '', closingTag: '', })} <% } %>`, - ); - return initDeprecatedJQueryDropdown($dropdown, { - showMenuAbove, - data(term, callback) { - return userSelect.users(term, options, (users) => { - // GitLabDropdownFilter returns this.instance - // GitLabDropdownRemote returns this.options.instance - const deprecatedJQueryDropdown = this.instance || this.options.instance; - deprecatedJQueryDropdown.options.processData(term, users, callback); - }); - }, - processData(term, dataArg, callback) { - // Sometimes the `dataArg` can contain special dropdown items like - // dividers which we don't want to consider here. - const data = dataArg.filter((x) => !x.type); - - let users = data; - - // Only show assigned user list when there is no search term - if ($dropdown.hasClass('js-multiselect') && term.length === 0) { - const selectedInputs = getSelectedUserInputs(); - - // Potential duplicate entries when dealing with issue board - // because issue board is also managed by vue - const selectedUsers = uniqBy(selectedInputs, (a) => a.value) - .filter((input) => { - const userId = parseInt(input.value, 10); - const inUsersArray = users.find((u) => u.id === userId); - - return !inUsersArray && userId !== 0; - }) - .map((input) => { - const userId = parseInt(input.value, 10); - const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset; - return { - avatar_url: avatarUrl || avatar_url || gon.default_avatar_url, - id: userId, - name, - username, - can_merge: parseBoolean(canMerge), - }; - }); + ); + + return initDeprecatedJQueryDropdown($dropdown, { + showMenuAbove, + data(term, callback) { + return userSelect.users(term, options, (users) => { + // GitLabDropdownFilter returns this.instance + // GitLabDropdownRemote returns this.options.instance + const deprecatedJQueryDropdown = this.instance || this.options.instance; + deprecatedJQueryDropdown.options.processData(term, users, callback); + }); + }, + processData(term, dataArg, callback) { + // Sometimes the `dataArg` can contain special dropdown items like + // dividers which we don't want to consider here. + const data = dataArg.filter((x) => !x.type); + + let users = data; + + // Only show assigned user list when there is no search term + if ($dropdown.hasClass('js-multiselect') && term.length === 0) { + const selectedInputs = getSelectedUserInputs(); + + // Potential duplicate entries when dealing with issue board + // because issue board is also managed by vue + const selectedUsers = uniqBy(selectedInputs, (a) => a.value) + .filter((input) => { + const userId = parseInt(input.value, 10); + const inUsersArray = users.find((u) => u.id === userId); + + return !inUsersArray && userId !== 0; + }) + .map((input) => { + const userId = parseInt(input.value, 10); + const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset; + return { + avatar_url: avatarUrl || avatar_url || gon.default_avatar_url, + id: userId, + name, + username, + can_merge: parseBoolean(canMerge), + }; + }); - users = data.concat(selectedUsers); - } + users = data.concat(selectedUsers); + } - let anyUser; - let index; - let len; - let name; - let obj; - let showDivider; - if (term.length === 0) { - showDivider = 0; - if (firstUser) { - // Move current user to the front of the list - for (index = 0, len = users.length; index < len; index += 1) { - obj = users[index]; - if (obj.username === firstUser) { - users.splice(index, 1); - users.unshift(obj); - break; + let anyUser; + let index; + let len; + let name; + let obj; + let showDivider; + if (term.length === 0) { + showDivider = 0; + if (firstUser) { + // Move current user to the front of the list + for (index = 0, len = users.length; index < len; index += 1) { + obj = users[index]; + if (obj.username === firstUser) { + users.splice(index, 1); + users.unshift(obj); + break; + } } } - } - if (showNullUser) { - showDivider += 1; - users.unshift({ - beforeDivider: true, - name: s__('UsersSelect|Unassigned'), - id: 0, - }); - } - if (showAnyUser) { - showDivider += 1; - name = showAnyUser; - if (name === true) { - name = s__('UsersSelect|Any User'); + if (showNullUser) { + showDivider += 1; + users.unshift({ + beforeDivider: true, + name: s__('UsersSelect|Unassigned'), + id: 0, + }); } - anyUser = { - beforeDivider: true, - name, - id: null, - }; - users.unshift(anyUser); - } - - if (showDivider) { - users.splice(showDivider, 0, { type: 'divider' }); - } - - if ($dropdown.hasClass('js-multiselect')) { - const selected = getSelected().filter((i) => i !== 0); - - if ($dropdown.data('showSuggested')) { - const suggested = this.suggestedUsers(users); - if (suggested.length) { - users = users.filter( - (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1), - ); - users.splice(showDivider + 1, 0, ...suggested); + if (showAnyUser) { + showDivider += 1; + name = showAnyUser; + if (name === true) { + name = s__('UsersSelect|Any User'); } + anyUser = { + beforeDivider: true, + name, + id: null, + }; + users.unshift(anyUser); } - if (selected.length > 0) { - if ($dropdown.data('dropdownHeader')) { - showDivider += 1; - users.splice(showDivider, 0, { - type: 'header', - content: $dropdown.data('dropdownHeader'), - }); + if (showDivider) { + users.splice(showDivider, 0, { type: 'divider' }); + } + + if ($dropdown.hasClass('js-multiselect')) { + const selected = getSelected().filter((i) => i !== 0); + + if ($dropdown.data('showSuggested')) { + const suggested = this.suggestedUsers(users); + if (suggested.length) { + users = users.filter( + (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1), + ); + users.splice(showDivider + 1, 0, ...suggested); + } } - const selectedUsers = users - .filter((u) => selected.indexOf(u.id) !== -1) - .sort((a, b) => a.name > b.name); + if (selected.length > 0) { + if ($dropdown.data('dropdownHeader')) { + showDivider += 1; + users.splice(showDivider, 0, { + type: 'header', + content: $dropdown.data('dropdownHeader'), + }); + } - users = users.filter((u) => selected.indexOf(u.id) === -1); + const selectedUsers = users + .filter((u) => selected.indexOf(u.id) !== -1) + .sort((a, b) => a.name > b.name); - selectedUsers.forEach((selectedUser) => { - showDivider += 1; - users.splice(showDivider, 0, selectedUser); - }); + users = users.filter((u) => selected.indexOf(u.id) === -1); - users.splice(showDivider + 1, 0, { type: 'divider' }); + selectedUsers.forEach((selectedUser) => { + showDivider += 1; + users.splice(showDivider, 0, selectedUser); + }); + + users.splice(showDivider + 1, 0, { type: 'divider' }); + } } } - } - callback(users); - if (showMenuAbove) { - $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); - } - }, - suggestedUsers(users) { - const selected = getSelected().filter((i) => i !== 0); - const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1); - - if (!suggestedUsers.length) return []; - - const items = [ - { - type: 'header', - content: suggestedReviewersHeaderTemplate({ - header: $dropdown.data('suggestedReviewersHeader'), - }), - }, - ...suggestedUsers, - { type: 'header', content: $dropdown.data('allMembersHeader') }, - ]; - return items; - }, - filterable: true, - filterRemote: true, - search: { - fields: ['name', 'username'], - }, - selectable: true, - fieldName: $dropdown.data('fieldName'), - toggleLabel(selected, el, deprecatedJQueryDropdown) { - const inputValue = deprecatedJQueryDropdown.filterInput.val(); - - if (this.multiSelect && inputValue === '') { - // Remove non-users from the fullData array - const users = deprecatedJQueryDropdown.filteredFullData(); - const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown); - - // Update the data model - this.processData(inputValue, users, callback); - } + callback(users); + if (showMenuAbove) { + $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); + } + }, + suggestedUsers(users) { + const selected = getSelected().filter((i) => i !== 0); + const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1); + + if (!suggestedUsers.length) return []; + + const items = [ + { + type: 'header', + content: suggestedReviewersHeaderTemplate({ + header: $dropdown.data('suggestedReviewersHeader'), + }), + }, + ...suggestedUsers, + { type: 'header', content: $dropdown.data('allMembersHeader') }, + ]; + return items; + }, + filterable: true, + filterRemote: true, + search: { + fields: ['name', 'username'], + }, + selectable: true, + fieldName: $dropdown.data('fieldName'), + toggleLabel(selected, el, deprecatedJQueryDropdown) { + const inputValue = deprecatedJQueryDropdown.filterInput.val(); + + if (this.multiSelect && inputValue === '') { + // Remove non-users from the fullData array + const users = deprecatedJQueryDropdown.filteredFullData(); + const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown); + + // Update the data model + this.processData(inputValue, users, callback); + } - if (this.multiSelect) { - return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); - } + if (this.multiSelect) { + return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); + } - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); - if (selected.text) { - return selected.text; + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); + if (selected.text) { + return selected.text; + } + return selected.name; } - return selected.name; - } - $dropdown.find('.dropdown-toggle-text').addClass('is-default'); - return defaultLabel; - }, - defaultLabel, - hidden() { - if ($dropdown.hasClass('js-multiselect')) { - if ($dropdown.hasClass(elsClassName)) { - if (!$dropdown.closest('.merge-request-form').length) { - $dropdown.data('deprecatedJQueryDropdown').clearMenu(); - $dropdown.closest('.selectbox').children('input[type="hidden"]').remove(); + $dropdown.find('.dropdown-toggle-text').addClass('is-default'); + return defaultLabel; + }, + defaultLabel, + hidden() { + if ($dropdown.hasClass('js-multiselect')) { + if ($dropdown.hasClass(elsClassName)) { + if (!$dropdown.closest('.merge-request-form').length) { + $dropdown.data('deprecatedJQueryDropdown').clearMenu(); + $dropdown.closest('.selectbox').children('input[type="hidden"]').remove(); + } + emitSidebarEvent('sidebar.saveReviewers'); + } else { + emitSidebarEvent('sidebar.saveAssignees'); } - emitSidebarEvent('sidebar.saveReviewers'); - } else { - emitSidebarEvent('sidebar.saveAssignees'); } - } - if (!$dropdown.data('alwaysShowSelectbox')) { - $selectbox.hide(); + if (!$dropdown.data('alwaysShowSelectbox')) { + $selectbox.hide(); - // Recalculate where .value is because vue might have changed it - $block = $selectbox.closest('.block'); - $value = $block.find('.value'); - // display:block overrides the hide-collapse rule - $value.css('display', ''); - } + // Recalculate where .value is because vue might have changed it + $block = $selectbox.closest('.block'); + $value = $block.find('.value'); + // display:block overrides the hide-collapse rule + $value.css('display', ''); + } - $('.dropdown-input-field', $block).val(''); - }, - multiSelect: $dropdown.hasClass('js-multiselect'), - inputMeta: $dropdown.data('inputMeta'), - clicked(options) { - const { $el, e, isMarking } = options; - const user = options.selectedObj; + $('.dropdown-input-field', $block).val(''); + }, + multiSelect: $dropdown.hasClass('js-multiselect'), + inputMeta: $dropdown.data('inputMeta'), + clicked(options) { + const { $el, e, isMarking } = options; + const user = options.selectedObj; - dispose($el); + dispose($el); - if ($dropdown.hasClass('js-multiselect')) { - const isActive = $el.hasClass('is-active'); - const previouslySelected = $dropdown - .closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`); + if ($dropdown.hasClass('js-multiselect')) { + const isActive = $el.hasClass('is-active'); + const previouslySelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`); - // Enables support for limiting the number of users selected - // Automatically removes the first on the list if more users are selected - checkMaxSelect(); + // Enables support for limiting the number of users selected + // Automatically removes the first on the list if more users are selected + checkMaxSelect(); - if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { - // Unassigned selected - previouslySelected.each((index, element) => { - element.remove(); - }); - if ($dropdown.hasClass(elsClassName)) { - emitSidebarEvent('sidebar.removeAllReviewers'); + if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { + // Unassigned selected + previouslySelected.each((index, element) => { + element.remove(); + }); + if ($dropdown.hasClass(elsClassName)) { + emitSidebarEvent('sidebar.removeAllReviewers'); + } else { + emitSidebarEvent('sidebar.removeAllAssignees'); + } + } else if (isActive) { + // user selected + if ($dropdown.hasClass(elsClassName)) { + emitSidebarEvent('sidebar.addReviewer', user); + } else { + emitSidebarEvent('sidebar.addAssignee', user); + } + + // Remove unassigned selection (if it was previously selected) + const unassignedSelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); + + if (unassignedSelected) { + unassignedSelected.remove(); + } } else { - emitSidebarEvent('sidebar.removeAllAssignees'); + if (previouslySelected.length === 0) { + // Select unassigned because there is no more selected users + this.addInput($dropdown.data('fieldName'), 0, {}); + } + + // User unselected + if ($dropdown.hasClass(elsClassName)) { + emitSidebarEvent('sidebar.removeReviewer', user); + } else { + emitSidebarEvent('sidebar.removeAssignee', user); + } } - } else if (isActive) { - // user selected - if ($dropdown.hasClass(elsClassName)) { - emitSidebarEvent('sidebar.addReviewer', user); + + if (getSelected().find((u) => u === gon.current_user_id)) { + $assignToMeLink.hide(); } else { - emitSidebarEvent('sidebar.addAssignee', user); + $assignToMeLink.show(); } + } - // Remove unassigned selection (if it was previously selected) - const unassignedSelected = $dropdown - .closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); + const page = $('body').attr('data-page'); + const isIssueIndex = page === 'projects:issues:index'; + const isMRIndex = page === page && page === 'projects:merge_requests:index'; + if ( + $dropdown.hasClass('js-filter-bulk-update') || + $dropdown.hasClass('js-issuable-form-dropdown') + ) { + e.preventDefault(); - if (unassignedSelected) { - unassignedSelected.remove(); - } - } else { - if (previouslySelected.length === 0) { - // Select unassigned because there is no more selected users - this.addInput($dropdown.data('fieldName'), 0, {}); - } + const isSelecting = user.id !== selectedId; + selectedId = isSelecting ? user.id : selectedIdDefault; - // User unselected - if ($dropdown.hasClass(elsClassName)) { - emitSidebarEvent('sidebar.removeReviewer', user); + if (selectedId === gon.current_user_id) { + $('.assign-to-me-link').hide(); } else { - emitSidebarEvent('sidebar.removeAssignee', user); + $('.assign-to-me-link').show(); } + return; } - - if (getSelected().find((u) => u === gon.current_user_id)) { - $assignToMeLink.hide(); - } else { - $assignToMeLink.show(); + if (handleClick) { + e.preventDefault(); + handleClick(user, isMarking); + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else if (!$dropdown.hasClass('js-multiselect')) { + const selected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}']`) + .val(); + return assignTo(selected); } - } - const page = $('body').attr('data-page'); - const isIssueIndex = page === 'projects:issues:index'; - const isMRIndex = page === page && page === 'projects:merge_requests:index'; - if ( - $dropdown.hasClass('js-filter-bulk-update') || - $dropdown.hasClass('js-issuable-form-dropdown') - ) { - e.preventDefault(); - - const isSelecting = user.id !== selectedId; - selectedId = isSelecting ? user.id : selectedIdDefault; + // Automatically close dropdown after assignee is selected + // since CE has no multiple assignees + // EE does not have a max-select + if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) { + // Close the dropdown + $dropdown.dropdown('toggle'); + } + }, + id(user) { + return user.id; + }, + opened(e) { + const $el = $(e.currentTarget); + const selected = getSelected(); + $el.find('.is-active').removeClass('is-active'); + + function highlightSelected(id) { + $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); + } - if (selectedId === gon.current_user_id) { - $('.assign-to-me-link').hide(); + if (selected.length > 0) { + getSelected().forEach((selectedId) => highlightSelected(selectedId)); } else { - $('.assign-to-me-link').show(); + highlightSelected(selectedId); } - return; - } - if (handleClick) { - e.preventDefault(); - handleClick(user, isMarking); - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } else if (!$dropdown.hasClass('js-multiselect')) { - const selected = $dropdown - .closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}']`) - .val(); - return assignTo(selected); - } + }, + updateLabel: $dropdown.data('dropdownTitle'), + renderRow(user) { + const username = user.username ? `@${user.username}` : ''; + const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; - // Automatically close dropdown after assignee is selected - // since CE has no multiple assignees - // EE does not have a max-select - if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) { - // Close the dropdown - $dropdown.dropdown('toggle'); - } - }, - id(user) { - return user.id; - }, - opened(e) { - const $el = $(e.currentTarget); - const selected = getSelected(); - $el.find('.is-active').removeClass('is-active'); - - function highlightSelected(id) { - $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); - } - - if (selected.length > 0) { - getSelected().forEach((selectedId) => highlightSelected(selectedId)); - } else { - highlightSelected(selectedId); - } - }, - updateLabel: $dropdown.data('dropdownTitle'), - renderRow(user) { - const username = user.username ? `@${user.username}` : ''; - const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; + let selected = false; - let selected = false; + if (this.multiSelect) { + selected = getSelected().find((u) => user.id === u); - if (this.multiSelect) { - selected = getSelected().find((u) => user.id === u); - - const { fieldName } = this; - const field = $dropdown - .closest('.selectbox') - .find(`input[name='${fieldName}'][value='${user.id}']`); + const { fieldName } = this; + const field = $dropdown + .closest('.selectbox') + .find(`input[name='${fieldName}'][value='${user.id}']`); - if (field.length) { - selected = true; + if (field.length) { + selected = true; + } + } else { + selected = user.id === selectedId; } - } else { - selected = user.id === selectedId; - } - let img = ''; - if (user.beforeDivider != null) { - `
  • ${escape( - user.name, - )}
  • `; - } else { - // 0 margin, because it's now handled by a wrapper - img = ``; - } + let img = ''; + if (user.beforeDivider != null) { + `
  • ${escape( + user.name, + )}
  • `; + } else { + // 0 margin, because it's now handled by a wrapper + img = ``; + } - return userSelect.renderRow( - options.issuableType, - user, - selected, - username, - img, - elsClassName, - ); - }, - }); - }); + return userSelect.renderRow( + options.issuableType, + user, + selected, + username, + img, + elsClassName, + ); + }, + }) + .get() + .map((dropdown) => dropdown.GitLabDropdownInstance); + }) + .get(); } // Return users list. Filtered by query diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 884cb70cb9fcc19e03725a51bebbcaca8878b2ce..a467d9e8c8afca4a1a768f50d0fc41f3df8f7f96 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -287,6 +287,23 @@ } } + .non-blocking-loader & { + &.is-loading{ + .dropdown-content { + display: block; + height: 2rem; + + ul{ + display: none; + } + } + } + + .dropdown-loading{ + position: relative; + } + } + ul { margin: 0; padding: 0;