diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/filtered_search_bar')
10 files changed, 107 insertions, 88 deletions
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 9775a9119c6..994ce6a762a 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -10,8 +10,11 @@ export const FILTER_CURRENT = 'Current'; export const OPERATOR_IS = '='; export const OPERATOR_IS_TEXT = __('is'); export const OPERATOR_IS_NOT = '!='; +export const OPERATOR_IS_NOT_TEXT = __('is not'); export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }]; +export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }]; +export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY]; export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) }; export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) }; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js index 37436de907f..571d24b50cf 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js @@ -215,35 +215,35 @@ export function urlQueryToFilter(query = '', options = {}) { /** * Returns array of token values from localStorage - * based on provided recentTokenValuesStorageKey + * based on provided recentSuggestionsStorageKey * - * @param {String} recentTokenValuesStorageKey + * @param {String} recentSuggestionsStorageKey * @returns */ -export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) { - let recentlyUsedTokenValues = []; +export function getRecentlyUsedSuggestions(recentSuggestionsStorageKey) { + let recentlyUsedSuggestions = []; if (AccessorUtilities.isLocalStorageAccessSafe()) { - recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || []; + recentlyUsedSuggestions = JSON.parse(localStorage.getItem(recentSuggestionsStorageKey)) || []; } - return recentlyUsedTokenValues; + return recentlyUsedSuggestions; } /** * Sets provided token value to recently used array - * within localStorage for provided recentTokenValuesStorageKey + * within localStorage for provided recentSuggestionsStorageKey * - * @param {String} recentTokenValuesStorageKey + * @param {String} recentSuggestionsStorageKey * @param {Object} tokenValue */ -export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) { - const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey); +export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenValue) { + const recentlyUsedSuggestions = getRecentlyUsedSuggestions(recentSuggestionsStorageKey); - recentlyUsedTokenValues.splice(0, 0, { ...tokenValue }); + recentlyUsedSuggestions.splice(0, 0, { ...tokenValue }); if (AccessorUtilities.isLocalStorageAccessSafe()) { localStorage.setItem( - recentTokenValuesStorageKey, - JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)), + recentSuggestionsStorageKey, + JSON.stringify(uniqWith(recentlyUsedSuggestions, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)), ); } } diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index 3b261f5ac25..a25a19a006c 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -74,13 +74,13 @@ export default { :config="config" :value="value" :active="active" - :tokens-list-loading="loading" - :token-values="authors" + :suggestions-loading="loading" + :suggestions="authors" :fn-active-token-value="getActiveAuthor" - :default-token-values="defaultAuthors" - :preloaded-token-values="preloadedAuthors" - :recent-token-values-storage-key="config.recentTokenValuesStorageKey" - @fetch-token-values="fetchAuthorBySearchTerm" + :default-suggestions="defaultAuthors" + :preloaded-suggestions="preloadedAuthors" + :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" + @fetch-suggestions="fetchAuthorBySearchTerm" v-on="$listeners" > <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> @@ -93,9 +93,9 @@ export default { /> <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span> </template> - <template #token-values-list="{ tokenValues }"> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="author in tokenValues" + v-for="author in suggestions" :key="author.username" :value="author.username" > diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index bda6b340871..a4804525a53 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -6,9 +6,10 @@ import { GlDropdownSectionHeader, GlLoadingIcon, } from '@gitlab/ui'; +import { debounce } from 'lodash'; import { DEBOUNCE_DELAY } from '../constants'; -import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; +import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; export default { components: { @@ -31,12 +32,12 @@ export default { type: Boolean, required: true, }, - tokensListLoading: { + suggestionsLoading: { type: Boolean, required: false, default: false, }, - tokenValues: { + suggestions: { type: Array, required: false, default: () => [], @@ -44,21 +45,21 @@ export default { fnActiveTokenValue: { type: Function, required: false, - default: (tokenValues, currentTokenValue) => { - return tokenValues.find(({ value }) => value === currentTokenValue); + default: (suggestions, currentTokenValue) => { + return suggestions.find(({ value }) => value === currentTokenValue); }, }, - defaultTokenValues: { + defaultSuggestions: { type: Array, required: false, default: () => [], }, - preloadedTokenValues: { + preloadedSuggestions: { type: Array, required: false, default: () => [], }, - recentTokenValuesStorageKey: { + recentSuggestionsStorageKey: { type: String, required: false, default: '', @@ -77,21 +78,21 @@ export default { data() { return { searchKey: '', - recentTokenValues: this.recentTokenValuesStorageKey - ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey) + recentSuggestions: this.recentSuggestionsStorageKey + ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey) : [], loading: false, }; }, computed: { - isRecentTokenValuesEnabled() { - return Boolean(this.recentTokenValuesStorageKey); + isRecentSuggestionsEnabled() { + return Boolean(this.recentSuggestionsStorageKey); }, recentTokenIds() { - return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]); }, preloadedTokenIds() { - return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]); }, currentTokenValue() { if (this.fnCurrentTokenValue) { @@ -100,17 +101,17 @@ export default { return this.value.data.toLowerCase(); }, activeTokenValue() { - return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue); + return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue); }, /** - * Return all the tokenValues when searchKey is present - * otherwise return only the tokenValues which aren't + * Return all the suggestions when searchKey is present + * otherwise return only the suggestions which aren't * present in "Recently used" */ - availableTokenValues() { + availableSuggestions() { return this.searchKey - ? this.tokenValues - : this.tokenValues.filter( + ? this.suggestions + : this.suggestions.filter( (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) && !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), @@ -121,30 +122,30 @@ export default { active: { immediate: true, handler(newValue) { - if (!newValue && !this.tokenValues.length) { - this.$emit('fetch-token-values', this.value.data); + if (!newValue && !this.suggestions.length) { + this.$emit('fetch-suggestions', this.value.data); } }, }, }, methods: { - handleInput({ data }) { + handleInput: debounce(function debouncedSearch({ data }) { this.searchKey = data; - setTimeout(() => { - if (!this.tokensListLoading) this.$emit('fetch-token-values', data); - }, DEBOUNCE_DELAY); - }, + if (!this.suggestionsLoading) { + this.$emit('fetch-suggestions', data); + } + }, DEBOUNCE_DELAY), handleTokenValueSelected(activeTokenValue) { // Make sure that; // 1. Recently used values feature is enabled // 2. User has actually selected a value // 3. Selected value is not part of preloaded list. if ( - this.isRecentTokenValuesEnabled && + this.isRecentSuggestionsEnabled && activeTokenValue && !this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier]) ) { - setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); + setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue); } }, }, @@ -168,9 +169,9 @@ export default { <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> </template> <template #suggestions> - <template v-if="defaultTokenValues.length"> + <template v-if="defaultSuggestions.length"> <gl-filtered-search-suggestion - v-for="token in defaultTokenValues" + v-for="token in defaultSuggestions" :key="token.value" :value="token.value" > @@ -178,19 +179,19 @@ export default { </gl-filtered-search-suggestion> <gl-dropdown-divider /> </template> - <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey"> + <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey"> <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> - <slot name="token-values-list" :token-values="recentTokenValues"></slot> + <slot name="suggestions-list" :suggestions="recentSuggestions"></slot> <gl-dropdown-divider /> </template> <slot - v-if="preloadedTokenValues.length && !searchKey" - name="token-values-list" - :token-values="preloadedTokenValues" + v-if="preloadedSuggestions.length && !searchKey" + name="suggestions-list" + :suggestions="preloadedSuggestions" ></slot> - <gl-loading-icon v-if="tokensListLoading" /> + <gl-loading-icon v-if="suggestionsLoading" size="sm" /> <template v-else> - <slot name="token-values-list" :token-values="availableTokenValues"></slot> + <slot name="suggestions-list" :suggestions="availableSuggestions"></slot> </template> </template> </gl-filtered-search-token> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue index 694dcd95b5e..5859fd10688 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -97,7 +97,7 @@ export default { {{ branch.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultBranches.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="branch in branches" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue index 9ba7f3d1a1d..d186f46866c 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue @@ -101,7 +101,7 @@ export default { {{ emoji.value }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultEmojis.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="emoji in emojis" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue index d21fa9a344a..aa234cf86d9 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue @@ -56,7 +56,7 @@ export default { } // Current value is a string. - const [groupPath, idProperty] = this.currentValue?.split('::&'); + const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator); return this.epics.find( (epic) => epic.group_full_path === groupPath && @@ -65,6 +65,9 @@ export default { } return null; }, + displayText() { + return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`; + }, }, watch: { active: { @@ -103,8 +106,10 @@ export default { this.fetchEpicsBySearchTerm({ epicPath, search: data }); }, DEBOUNCE_DELAY), - getEpicDisplayText(epic) { - return `${epic.title}${this.$options.separator}${epic.iid}`; + getValue(epic) { + return this.config.useIdValue + ? String(epic[this.idProperty]) + : `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`; }, }, }; @@ -118,7 +123,7 @@ export default { @input="searchEpics" > <template #view="{ inputValue }"> - {{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }} + {{ activeEpic ? displayText : inputValue }} </template> <template #suggestions> <gl-filtered-search-suggestion @@ -129,13 +134,9 @@ export default { {{ epic.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultEpics.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> - <gl-filtered-search-suggestion - v-for="epic in epics" - :key="epic.id" - :value="`${epic.group_full_path}::&${epic[idProperty]}`" - > + <gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)"> {{ epic.title }} </gl-filtered-search-suggestion> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue index 7b6a590279a..ba8b2421726 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue @@ -7,6 +7,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; @@ -30,8 +31,7 @@ export default { data() { return { iterations: this.config.initialIterations || [], - defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS, - loading: true, + loading: false, }; }, computed: { @@ -39,7 +39,12 @@ export default { return this.value.data; }, activeIteration() { - return this.iterations.find((iteration) => iteration.title === this.currentValue); + return this.iterations.find( + (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue), + ); + }, + defaultIterations() { + return this.config.defaultIterations || DEFAULT_ITERATIONS; }, }, watch: { @@ -53,6 +58,9 @@ export default { }, }, methods: { + getValue(iteration) { + return String(getIdFromGraphQLId(iteration.id)); + }, fetchIterationBySearchTerm(searchTerm) { const fetchPromise = this.config.fetchPath ? this.config.fetchIterations(this.config.fetchPath, searchTerm) @@ -95,12 +103,12 @@ export default { {{ iteration.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultIterations.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="iteration in iterations" - :key="iteration.title" - :value="iteration.title" + :key="iteration.id" + :value="getValue(iteration)" > {{ iteration.title }} </gl-filtered-search-suggestion> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index e496d099a42..4d08f81fee9 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -96,12 +96,12 @@ export default { :config="config" :value="value" :active="active" - :tokens-list-loading="loading" - :token-values="labels" + :suggestions-loading="loading" + :suggestions="labels" :fn-active-token-value="getActiveLabel" - :default-token-values="defaultLabels" - :recent-token-values-storage-key="config.recentTokenValuesStorageKey" - @fetch-token-values="fetchLabelBySearchTerm" + :default-suggestions="defaultLabels" + :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" + @fetch-suggestions="fetchLabelBySearchTerm" v-on="$listeners" > <template @@ -115,9 +115,9 @@ export default { >~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token > </template> - <template #token-values-list="{ tokenValues }"> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="label in tokenValues" + v-for="label in suggestions" :key="label.id" :value="getLabelName(label)" > diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue index cda6e4d6726..66ad5ef5b4e 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue @@ -9,6 +9,7 @@ import { debounce } from 'lodash'; import createFlash from '~/flash'; import { __ } from '~/locale'; +import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; @@ -34,7 +35,7 @@ export default { return { milestones: this.config.initialMilestones || [], defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, - loading: true, + loading: false, }; }, computed: { @@ -59,11 +60,16 @@ export default { }, methods: { fetchMilestoneBySearchTerm(searchTerm = '') { + if (this.loading) { + return; + } + this.loading = true; this.config .fetchMilestones(searchTerm) - .then(({ data }) => { - this.milestones = data; + .then((response) => { + const data = Array.isArray(response) ? response : response.data; + this.milestones = data.slice().sort(sortMilestonesByDueDate); }) .catch(() => createFlash({ message: __('There was a problem fetching milestones.') })) .finally(() => { @@ -96,7 +102,7 @@ export default { {{ milestone.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultMilestones.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="milestone in milestones" |