diff options
Diffstat (limited to 'app')
11 files changed, 310 insertions, 266 deletions
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js new file mode 100644 index 00000000000..54ea936252e --- /dev/null +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -0,0 +1,16 @@ +export default IssuableTokenKeys => { + const wipToken = { + key: 'wip', + type: 'string', + param: '', + symbol: '', + icon: 'admin', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: true, + capitalizeTokenValue: true, + }; + + IssuableTokenKeys.tokenKeys.push(wipToken); + IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken); +}; diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js new file mode 100644 index 00000000000..e2f9c03ee65 --- /dev/null +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -0,0 +1,133 @@ +import DropdownHint from './dropdown_hint'; +import DropdownUser from './dropdown_user'; +import DropdownNonUser from './dropdown_non_user'; +import DropdownEmoji from './dropdown_emoji'; +import NullDropdown from './null_dropdown'; +import DropdownAjaxFilter from './dropdown_ajax_filter'; +import DropdownUtils from './dropdown_utils'; + +export default class AvailableDropdownMappings { + constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) { + this.container = container; + this.baseEndpoint = baseEndpoint; + this.groupsOnly = groupsOnly; + this.includeAncestorGroups = includeAncestorGroups; + this.includeDescendantGroups = includeDescendantGroups; + } + + getAllowedMappings(supportedTokens) { + return this.buildMappings(supportedTokens, this.getMappings()); + } + + buildMappings(supportedTokens, availableMappings) { + const allowedMappings = { + hint: { + reference: null, + gl: DropdownHint, + element: this.container.querySelector('#js-dropdown-hint'), + }, + }; + + supportedTokens.forEach(type => { + if (availableMappings[type]) { + allowedMappings[type] = availableMappings[type]; + } + }); + + return allowedMappings; + } + + getMappings() { + return { + author: { + reference: null, + gl: DropdownUser, + element: this.container.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: DropdownUser, + element: this.container.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: DropdownNonUser, + extraArguments: { + endpoint: this.getMilestoneEndpoint(), + symbol: '%', + }, + element: this.container.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: DropdownNonUser, + extraArguments: { + endpoint: this.getLabelsEndpoint(), + symbol: '~', + preprocessing: DropdownUtils.duplicateLabelPreprocessing, + }, + element: this.container.querySelector('#js-dropdown-label'), + }, + 'my-reaction': { + reference: null, + gl: DropdownEmoji, + element: this.container.querySelector('#js-dropdown-my-reaction'), + }, + wip: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-wip'), + }, + confidential: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-confidential'), + }, + status: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-status'), + }, + type: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-type'), + }, + tag: { + reference: null, + gl: DropdownAjaxFilter, + extraArguments: { + endpoint: this.getRunnerTagsEndpoint(), + symbol: '~', + }, + element: this.container.querySelector('#js-dropdown-runner-tag'), + }, + }; + } + + getMilestoneEndpoint() { + return `${this.baseEndpoint}/milestones.json`; + } + + getLabelsEndpoint() { + let endpoint = `${this.baseEndpoint}/labels.json?`; + + if (this.groupsOnly) { + endpoint = `${endpoint}only_group_labels=true&`; + } + + if (this.includeAncestorGroups) { + endpoint = `${endpoint}include_ancestor_groups=true&`; + } + + if (this.includeDescendantGroups) { + endpoint = `${endpoint}include_descendant_groups=true`; + } + + return endpoint; + } + + getRunnerTagsEndpoint() { + return `${this.baseEndpoint}/admin/runners/tag_list.json`; + } +} diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 57847d4ad9f..cb0a84b490b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,14 +1,9 @@ +import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings'; import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; import FilteredSearchTokenKeys from './filtered_search_token_keys'; import DropdownUtils from './dropdown_utils'; -import DropdownHint from './dropdown_hint'; -import DropdownEmoji from './dropdown_emoji'; -import DropdownNonUser from './dropdown_non_user'; -import DropdownUser from './dropdown_user'; -import DropdownAjaxFilter from './dropdown_ajax_filter'; -import NullDropdown from './null_dropdown'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { @@ -50,114 +45,15 @@ export default class FilteredSearchDropdownManager { setupMapping() { const supportedTokens = this.filteredSearchTokenKeys.getKeys(); - const allowedMappings = { - hint: { - reference: null, - gl: DropdownHint, - element: this.container.querySelector('#js-dropdown-hint'), - }, - }; - const availableMappings = { - author: { - reference: null, - gl: DropdownUser, - element: this.container.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: DropdownUser, - element: this.container.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: DropdownNonUser, - extraArguments: { - endpoint: this.getMilestoneEndpoint(), - symbol: '%', - }, - element: this.container.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: DropdownNonUser, - extraArguments: { - endpoint: this.getLabelsEndpoint(), - symbol: '~', - preprocessing: DropdownUtils.duplicateLabelPreprocessing, - }, - element: this.container.querySelector('#js-dropdown-label'), - }, - 'my-reaction': { - reference: null, - gl: DropdownEmoji, - element: this.container.querySelector('#js-dropdown-my-reaction'), - }, - wip: { - reference: null, - gl: DropdownNonUser, - element: this.container.querySelector('#js-dropdown-wip'), - }, - confidential: { - reference: null, - gl: DropdownNonUser, - element: this.container.querySelector('#js-dropdown-confidential'), - }, - status: { - reference: null, - gl: NullDropdown, - element: this.container.querySelector('#js-dropdown-admin-runner-status'), - }, - type: { - reference: null, - gl: NullDropdown, - element: this.container.querySelector('#js-dropdown-admin-runner-type'), - }, - tag: { - reference: null, - gl: DropdownAjaxFilter, - extraArguments: { - endpoint: this.getRunnerTagsEndpoint(), - symbol: '~', - }, - element: this.container.querySelector('#js-dropdown-runner-tag'), - }, - }; - - supportedTokens.forEach(type => { - if (availableMappings[type]) { - allowedMappings[type] = availableMappings[type]; - } - }); - - this.mapping = allowedMappings; - } - - getMilestoneEndpoint() { - const endpoint = `${this.baseEndpoint}/milestones.json`; - - return endpoint; - } - - getLabelsEndpoint() { - let endpoint = `${this.baseEndpoint}/labels.json?`; - - if (this.groupsOnly) { - endpoint = `${endpoint}only_group_labels=true&`; - } - - if (this.includeAncestorGroups) { - endpoint = `${endpoint}include_ancestor_groups=true&`; - } - - if (this.includeDescendantGroups) { - endpoint = `${endpoint}include_descendant_groups=true`; - } - - return endpoint; - } + const availableMappings = new AvailableDropdownMappings( + this.container, + this.baseEndpoint, + this.groupsOnly, + this.includeAncestorGroups, + this.includeDescendantGroups, + ); - getRunnerTagsEndpoint() { - return `${this.baseEndpoint}/admin/runners/tag_list.json`; + this.mapping = availableMappings.getAllowedMappings(supportedTokens); } static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 48534bdf815..11ed85504ec 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -88,21 +88,4 @@ export default class FilteredSearchTokenKeys { this.tokenKeys.push(confidentialToken); this.tokenKeysWithAlternative.push(confidentialToken); } - - addExtraTokensForMergeRequests() { - const wipToken = { - key: 'wip', - type: 'string', - param: '', - symbol: '', - icon: 'admin', - tag: 'Yes or No', - lowercaseValueOnSubmit: true, - uppercaseTokenName: true, - capitalizeTokenValue: true, - }; - - this.tokenKeys.push(wipToken); - this.tokenKeysWithAlternative.push(wipToken); - } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index addf1ad94df..7746908714e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,10 +1,6 @@ -import _ from 'underscore'; -import AjaxCache from '~/lib/utils/ajax_cache'; +import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value'; import { objectToQueryString } from '~/lib/utils/common_utils'; -import Flash from '../flash'; import FilteredSearchContainer from './container'; -import UsersCache from '../lib/utils/users_cache'; -import DropdownUtils from './dropdown_utils'; export default class FilteredSearchVisualTokens { static getLastVisualTokenBeforeInput() { @@ -20,21 +16,6 @@ export default class FilteredSearchVisualTokens { }; } - /** - * Returns a computed API endpoint - * and query string composed of values from endpointQueryParams - * @param {String} endpoint - * @param {String} endpointQueryParams - */ - static getEndpointWithQueryParams(endpoint, endpointQueryParams) { - if (!endpointQueryParams) { - return endpoint; - } - - const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); - return `${endpoint}?${queryString}`; - } - static unselectTokens() { const otherTokens = FilteredSearchContainer.container.querySelectorAll( '.js-visual-token .selectable.selected', @@ -76,124 +57,15 @@ export default class FilteredSearchVisualTokens { `; } - static setTokenStyle(tokenContainer, backgroundColor, textColor) { - const token = tokenContainer; - - token.style.backgroundColor = backgroundColor; - token.style.color = textColor; - - if (textColor === '#FFFFFF') { - const removeToken = token.querySelector('.remove-token'); - removeToken.classList.add('inverted'); - } - - return token; - } - - static updateLabelTokenColor(tokenValueContainer, tokenValue) { - const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); - const { baseEndpoint } = filteredSearchInput.dataset; - const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( - `${baseEndpoint}/labels.json`, - filteredSearchInput.dataset.endpointQueryParams, - ); - - return AjaxCache.retrieve(labelsEndpoint) - .then(labels => { - const matchingLabel = (labels || []).find( - label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, - ); - - if (!matchingLabel) { - return; - } - - FilteredSearchVisualTokens.setTokenStyle( - tokenValueContainer, - matchingLabel.color, - matchingLabel.text_color, - ); - }) - .catch(() => new Flash('An error occurred while fetching label colors.')); - } - - static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { - const username = tokenValue.replace(/^@/, ''); - return ( - UsersCache.retrieve(username) - .then(user => { - if (!user) { - return; - } - - /* eslint-disable no-param-reassign */ - tokenValueContainer.dataset.originalValue = tokenValue; - tokenValueElement.innerHTML = ` - <img class="avatar s20" src="${user.avatar_url}" alt=""> - ${_.escape(user.name)} - `; - /* eslint-enable no-param-reassign */ - }) - // ignore error and leave username in the search bar - .catch(() => {}) - ); - } - - static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { - const container = tokenValueContainer; - const element = tokenValueElement; - const value = tokenValue; - - return ( - import(/* webpackChunkName: 'emoji' */ '../emoji') - .then(Emoji => { - Emoji.initEmojiMap() - .then(() => { - if (!Emoji.isEmojiNameValid(value)) { - return; - } - - container.dataset.originalValue = value; - element.innerHTML = Emoji.glEmojiTag(value); - }) - // ignore error and leave emoji name in the search bar - .catch(err => { - throw err; - }); - }) - // ignore error and leave emoji name in the search bar - .catch(importError => { - throw importError; - }) - ); - } - static renderVisualTokenValue(parentElement, tokenName, tokenValue) { + const tokenType = tokenName.toLowerCase(); const tokenValueContainer = parentElement.querySelector('.value-container'); const tokenValueElement = tokenValueContainer.querySelector('.value'); tokenValueElement.innerText = tokenValue; - if (['none', 'any'].includes(tokenValue.toLowerCase())) { - return; - } + const visualTokenValue = new VisualTokenValue(tokenValue, tokenType); - const tokenType = tokenName.toLowerCase(); - - if (tokenType === 'label') { - FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); - } else if (tokenType === 'author' || tokenType === 'assignee') { - FilteredSearchVisualTokens.updateUserTokenAppearance( - tokenValueContainer, - tokenValueElement, - tokenValue, - ); - } else if (tokenType === 'my-reaction') { - FilteredSearchVisualTokens.updateEmojiTokenAppearance( - tokenValueContainer, - tokenValueElement, - tokenValue, - ); - } + visualTokenValue.render(tokenValueContainer, tokenValueElement); } static addVisualTokenElement(name, value, options = {}) { @@ -328,6 +200,21 @@ export default class FilteredSearchVisualTokens { } } + /** + * Returns a computed API endpoint + * and query string composed of values from endpointQueryParams + * @param {String} endpoint + * @param {String} endpointQueryParams + */ + static getEndpointWithQueryParams(endpoint, endpointQueryParams) { + if (!endpointQueryParams) { + return endpoint; + } + + const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); + return `${endpoint}?${queryString}`; + } + static editToken(token) { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js new file mode 100644 index 00000000000..7f6f41c18f7 --- /dev/null +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -0,0 +1,125 @@ +import _ from 'underscore'; +import FilteredSearchContainer from '~/filtered_search/container'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import Flash from '~/flash'; +import UsersCache from '~/lib/utils/users_cache'; + +export default class VisualTokenValue { + constructor(tokenValue, tokenType) { + this.tokenValue = tokenValue; + this.tokenType = tokenType; + } + + render(tokenValueContainer, tokenValueElement) { + const { tokenType } = this; + + if (['none', 'any'].includes(tokenType)) { + return; + } + + if (tokenType === 'label') { + this.updateLabelTokenColor(tokenValueContainer); + } else if (tokenType === 'author' || tokenType === 'assignee') { + this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement); + } else if (tokenType === 'my-reaction') { + this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement); + } + } + + updateUserTokenAppearance(tokenValueContainer, tokenValueElement) { + const { tokenValue } = this; + const username = this.tokenValue.replace(/^@/, ''); + + return ( + UsersCache.retrieve(username) + .then(user => { + if (!user) { + return; + } + + /* eslint-disable no-param-reassign */ + tokenValueContainer.dataset.originalValue = tokenValue; + tokenValueElement.innerHTML = ` + <img class="avatar s20" src="${user.avatar_url}" alt=""> + ${_.escape(user.name)} + `; + /* eslint-enable no-param-reassign */ + }) + // ignore error and leave username in the search bar + .catch(() => {}) + ); + } + + updateLabelTokenColor(tokenValueContainer) { + const { tokenValue } = this; + const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); + const { baseEndpoint } = filteredSearchInput.dataset; + const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( + `${baseEndpoint}/labels.json`, + filteredSearchInput.dataset.endpointQueryParams, + ); + + return AjaxCache.retrieve(labelsEndpoint) + .then(labels => { + const matchingLabel = (labels || []).find( + label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, + ); + + if (!matchingLabel) { + return; + } + + VisualTokenValue.setTokenStyle( + tokenValueContainer, + matchingLabel.color, + matchingLabel.text_color, + ); + }) + .catch(() => new Flash('An error occurred while fetching label colors.')); + } + + static setTokenStyle(tokenValueContainer, backgroundColor, textColor) { + const token = tokenValueContainer; + + token.style.backgroundColor = backgroundColor; + token.style.color = textColor; + + if (textColor === '#FFFFFF') { + const removeToken = token.querySelector('.remove-token'); + removeToken.classList.add('inverted'); + } + + return token; + } + + updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement) { + const container = tokenValueContainer; + const element = tokenValueElement; + const value = this.tokenValue; + + return ( + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(Emoji => { + Emoji.initEmojiMap() + .then(() => { + if (!Emoji.isEmojiNameValid(value)) { + return; + } + + container.dataset.originalValue = value; + element.innerHTML = Emoji.glEmojiTag(value); + }) + // ignore error and leave emoji name in the search bar + .catch(err => { + throw err; + }); + }) + // ignore error and leave emoji name in the search bar + .catch(importError => { + throw importError; + }) + ); + } +} diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index 260484726f3..ff758fcb4fe 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -1,10 +1,11 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 339ce67438a..12a26fd88fa 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,10 +1,11 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index ec39db12e74..0bcca22e40f 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -2,12 +2,13 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index bd6f1c05949..57fbd360d46 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -1,5 +1,5 @@ %ul.content-list.mr-list.issuable-list - - if @merge_requests.exists? + - if @merge_requests.present? = render @merge_requests - else = render 'shared/empty_states/merge_requests' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index bdba47ed14d..f43be304e6b 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -71,6 +71,7 @@ = render 'shared/issuable/user_dropdown_item', user: User.new(username: '{{username}}', name: '{{name}}'), avatar: { lazy: true, url: '{{avatar_url}}' } + = render_if_exists 'shared/issuable/approver_dropdown' #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'None' } } |