diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/vue_shared/components/filtered_search_bar | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/filtered_search_bar')
12 files changed, 279 insertions, 331 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 994ce6a762a..2e9634819a0 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 @@ -2,10 +2,14 @@ import { __ } from '~/locale'; export const DEBOUNCE_DELAY = 200; export const MAX_RECENT_TOKENS_SIZE = 3; +export const WEIGHT_TOKEN_SUGGESTIONS_SIZE = 21; export const FILTER_NONE = 'None'; export const FILTER_ANY = 'Any'; export const FILTER_CURRENT = 'Current'; +export const FILTER_UPCOMING = 'Upcoming'; +export const FILTER_STARTED = 'Started'; +export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY]; export const OPERATOR_IS = '='; export const OPERATOR_IS_TEXT = __('is'); @@ -24,11 +28,9 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([ { value: FILTER_CURRENT, text: __(FILTER_CURRENT) }, ]); -export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; - export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([ - { value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings - { value: 'Started', text: __('Started') }, // eslint-disable-line @gitlab/require-i18n-strings + { value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) }, + { value: FILTER_STARTED, text: __(FILTER_STARTED) }, ]); export const SortDirection = { @@ -36,12 +38,14 @@ export const SortDirection = { ascending: 'ascending', }; +export const FILTERED_SEARCH_LABELS = 'labels'; export const FILTERED_SEARCH_TERM = 'filtered-search-term'; export const TOKEN_TITLE_AUTHOR = __('Author'); export const TOKEN_TITLE_ASSIGNEE = __('Assignee'); export const TOKEN_TITLE_MILESTONE = __('Milestone'); export const TOKEN_TITLE_LABEL = __('Label'); +export const TOKEN_TITLE_TYPE = __('Type'); export const TOKEN_TITLE_MY_REACTION = __('My-Reaction'); export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential'); export const TOKEN_TITLE_ITERATION = __('Iteration'); diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 5ab287150f2..9dc5c5db276 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -16,7 +16,7 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import { SortDirection } from './constants'; -import { stripQuotes, uniqueTokens } from './filtered_search_utils'; +import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils'; export default { components: { @@ -223,9 +223,14 @@ export default { // Put any searches that may have come in before // we fetched the saved searches ahead of the already saved ones - const resultantSearches = this.recentSearchesStore.setRecentSearches( + let resultantSearches = this.recentSearchesStore.setRecentSearches( this.recentSearchesStore.state.recentSearches.concat(searches), ); + // If visited URL has search params, add them to recent search store + if (filterEmptySearchTerm(this.filterValue).length) { + resultantSearches = this.recentSearchesStore.addRecentSearch(this.filterValue); + } + this.recentSearchesService.save(resultantSearches); this.recentSearches = resultantSearches; }); 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 571d24b50cf..6573f366b52 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 @@ -247,3 +247,12 @@ export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenVa ); } } + +/** + * Removes `FILTERED_SEARCH_TERM` tokens with empty data + * + * @param filterTokens array of filtered search tokens + * @return {Array} array of filtered search tokens + */ +export const filterEmptySearchTerm = (filterTokens = []) => + filterTokens.filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data); 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 a25a19a006c..ae5d3965de1 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 @@ -31,19 +31,25 @@ export default { data() { return { authors: this.config.initialAuthors || [], - defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], - preloadedAuthors: this.config.preloadedAuthors || [], loading: false, }; }, + computed: { + defaultAuthors() { + return this.config.defaultAuthors || [DEFAULT_LABEL_ANY]; + }, + preloadedAuthors() { + return this.config.preloadedAuthors || []; + }, + }, methods: { - getActiveAuthor(authors, currentValue) { - return authors.find((author) => author.username.toLowerCase() === currentValue); + getActiveAuthor(authors, data) { + return authors.find((author) => author.username.toLowerCase() === data.toLowerCase()); }, getAvatarUrl(author) { return author.avatarUrl || author.avatar_url; }, - fetchAuthorBySearchTerm(searchTerm) { + fetchAuthors(searchTerm) { this.loading = true; const fetchPromise = this.config.fetchPath ? this.config.fetchAuthors(this.config.fetchPath, searchTerm) @@ -76,11 +82,11 @@ export default { :active="active" :suggestions-loading="loading" :suggestions="authors" - :fn-active-token-value="getActiveAuthor" + :get-active-token-value="getActiveAuthor" :default-suggestions="defaultAuthors" :preloaded-suggestions="preloadedAuthors" :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" - @fetch-suggestions="fetchAuthorBySearchTerm" + @fetch-suggestions="fetchAuthors" v-on="$listeners" > <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> @@ -91,7 +97,7 @@ export default { shape="circle" class="gl-mr-2" /> - <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span> + {{ activeTokenValue ? activeTokenValue.name : inputValue }} </template> <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion 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 a4804525a53..d1326e96794 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 @@ -8,7 +8,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; -import { DEBOUNCE_DELAY } from '../constants'; +import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants'; import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; export default { @@ -42,12 +42,10 @@ export default { required: false, default: () => [], }, - fnActiveTokenValue: { + getActiveTokenValue: { type: Function, required: false, - default: (suggestions, currentTokenValue) => { - return suggestions.find(({ value }) => value === currentTokenValue); - }, + default: (suggestions, data) => suggestions.find(({ value }) => value === data), }, defaultSuggestions: { type: Array, @@ -69,11 +67,6 @@ export default { required: false, default: 'id', }, - fnCurrentTokenValue: { - type: Function, - required: false, - default: null, - }, }, data() { return { @@ -81,7 +74,6 @@ export default { recentSuggestions: this.recentSuggestionsStorageKey ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey) : [], - loading: false, }; }, computed: { @@ -94,14 +86,16 @@ export default { preloadedTokenIds() { return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]); }, - currentTokenValue() { - if (this.fnCurrentTokenValue) { - return this.fnCurrentTokenValue(this.value.data); - } - return this.value.data.toLowerCase(); - }, activeTokenValue() { - return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue); + return this.getActiveTokenValue(this.suggestions, this.value.data); + }, + availableDefaultSuggestions() { + if (this.value.operator === OPERATOR_IS_NOT) { + return this.defaultSuggestions.filter( + (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value), + ); + } + return this.defaultSuggestions; }, /** * Return all the suggestions when searchKey is present @@ -117,6 +111,29 @@ export default { !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), ); }, + showDefaultSuggestions() { + return this.availableDefaultSuggestions.length; + }, + showRecentSuggestions() { + return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey; + }, + showPreloadedSuggestions() { + return this.preloadedSuggestions.length && !this.searchKey; + }, + showAvailableSuggestions() { + return this.availableSuggestions.length; + }, + showSuggestions() { + // These conditions must match the template under `#suggestions` slot + // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65817#note_632619411 + return ( + this.showDefaultSuggestions || + this.showRecentSuggestions || + this.showPreloadedSuggestions || + this.suggestionsLoading || + this.showAvailableSuggestions + ); + }, }, watch: { active: { @@ -168,10 +185,10 @@ export default { <template #view="viewTokenProps"> <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> </template> - <template #suggestions> - <template v-if="defaultSuggestions.length"> + <template v-if="showSuggestions" #suggestions> + <template v-if="showDefaultSuggestions"> <gl-filtered-search-suggestion - v-for="token in defaultSuggestions" + v-for="token in availableDefaultSuggestions" :key="token.value" :value="token.value" > @@ -179,13 +196,13 @@ export default { </gl-filtered-search-suggestion> <gl-dropdown-divider /> </template> - <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey"> + <template v-if="showRecentSuggestions"> <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> <slot name="suggestions-list" :suggestions="recentSuggestions"></slot> <gl-dropdown-divider /> </template> <slot - v-if="preloadedSuggestions.length && !searchKey" + v-if="showPreloadedSuggestions" name="suggestions-list" :suggestions="preloadedSuggestions" ></slot> 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 5859fd10688..4ecfc1cf40c 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 @@ -1,27 +1,19 @@ <script> -import { - GlToken, - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; - -import { DEBOUNCE_DELAY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; export default { components: { - GlToken, - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -34,82 +26,62 @@ export default { data() { return { branches: this.config.initialBranches || [], - defaultBranches: this.config.defaultBranches || [], - loading: true, + loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeBranch() { - return this.branches.find((branch) => branch.name.toLowerCase() === this.currentValue); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.branches.length) { - this.fetchBranchBySearchTerm(this.value.data); - } - }, + defaultBranches() { + return this.config.defaultBranches || []; }, }, methods: { - fetchBranchBySearchTerm(searchTerm) { + getActiveBranch(branches, data) { + return branches.find((branch) => branch.name.toLowerCase() === data.toLowerCase()); + }, + fetchBranches(searchTerm) { this.loading = true; this.config .fetchBranches(searchTerm) .then(({ data }) => { this.branches = data; }) - .catch(() => createFlash({ message: __('There was a problem fetching branches.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching branches.') }); + }) .finally(() => { this.loading = false; }); }, - searchBranches: debounce(function debouncedSearch({ data }) { - this.fetchBranchBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultBranches" + :suggestions="branches" + :suggestions-loading="loading" + :get-active-token-value="getActiveBranch" + @fetch-suggestions="fetchBranches" v-on="$listeners" - @input="searchBranches" > - <template #view-token="{ inputValue }"> - <gl-token variant="search-value">{{ - activeBranch ? activeBranch.name : inputValue - }}</gl-token> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + {{ activeTokenValue ? activeTokenValue.name : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="branch in defaultBranches" - :key="branch.value" - :value="branch.value" + v-for="branch in suggestions" + :key="branch.id" + :value="branch.name" > - {{ branch.text }} + <div class="gl-display-flex"> + <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> + {{ branch.name }} + </div> </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultBranches.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="branch in branches" - :key="branch.id" - :value="branch.name" - > - <div class="gl-display-flex"> - <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> - <div>{{ branch.name }}</div> - </div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> 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 d186f46866c..5a69751a2cc 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 @@ -1,26 +1,21 @@ <script> -import { - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; - -import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_NONE_ANY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; export default { components: { - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -33,87 +28,63 @@ export default { data() { return { emojis: this.config.initialEmojis || [], - defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY, - loading: true, + loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeEmoji() { - return this.emojis.find( - (emoji) => emoji.name.toLowerCase() === stripQuotes(this.currentValue), - ); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.emojis.length) { - this.fetchEmojiBySearchTerm(this.value.data); - } - }, + defaultEmojis() { + return this.config.defaultEmojis || DEFAULT_NONE_ANY; }, }, methods: { - fetchEmojiBySearchTerm(searchTerm) { + getActiveEmoji(emojis, data) { + return emojis.find((emoji) => emoji.name.toLowerCase() === stripQuotes(data).toLowerCase()); + }, + fetchEmojis(searchTerm) { this.loading = true; this.config .fetchEmojis(searchTerm) - .then((res) => { - this.emojis = Array.isArray(res) ? res : res.data; + .then((response) => { + this.emojis = Array.isArray(response) ? response : response.data; + }) + .catch(() => { + createFlash({ message: __('There was a problem fetching emojis.') }); }) - .catch(() => - createFlash({ - message: __('There was a problem fetching emojis.'), - }), - ) .finally(() => { this.loading = false; }); }, - searchEmojis: debounce(function debouncedSearch({ data }) { - this.fetchEmojiBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultEmojis" + :suggestions="emojis" + :suggestions-loading="loading" + :get-active-token-value="getActiveEmoji" + @fetch-suggestions="fetchEmojis" v-on="$listeners" - @input="searchEmojis" > - <template #view="{ inputValue }"> - <gl-emoji v-if="activeEmoji" :data-name="activeEmoji.name" /> - <span v-else>{{ inputValue }}</span> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + <gl-emoji v-if="activeTokenValue" :data-name="activeTokenValue.name" /> + <template v-else>{{ inputValue }}</template> </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="emoji in defaultEmojis" - :key="emoji.value" - :value="emoji.value" + v-for="emoji in suggestions" + :key="emoji.name" + :value="emoji.name" > - {{ emoji.value }} + <div class="gl-display-flex"> + <gl-emoji class="gl-mr-3" :data-name="emoji.name" /> + {{ emoji.name }} + </div> </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultEmojis.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="emoji in emojis" - :key="emoji.name" - :value="emoji.name" - > - <div class="gl-display-flex"> - <gl-emoji :data-name="emoji.name" /> - <span class="gl-ml-3">{{ emoji.name }}</span> - </div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> 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 aa234cf86d9..9f68308808e 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 @@ -8,7 +8,7 @@ import { import { debounce } from 'lodash'; import createFlash from '~/flash'; import { __ } from '~/locale'; -import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; +import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants'; export default { separator: '::&', @@ -48,6 +48,14 @@ export default { defaultEpics() { return this.config.defaultEpics || DEFAULT_NONE_ANY; }, + availableDefaultEpics() { + if (this.value.operator === OPERATOR_IS_NOT) { + return this.defaultEpics.filter( + (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value), + ); + } + return this.defaultEpics; + }, activeEpic() { if (this.currentValue && this.epics.length) { // Check if current value is an epic ID. @@ -99,7 +107,7 @@ export default { // We don't have any information about selected token except for its // group path and iid joined by separator, so we need to manually // compose epic path from it. - if (data.includes(this.$options.separator)) { + if (data.includes?.(this.$options.separator)) { const [groupPath, epicIid] = data.split(this.$options.separator); epicPath = `/groups/${groupPath}/-/epics/${epicIid}`; } @@ -127,13 +135,13 @@ export default { </template> <template #suggestions> <gl-filtered-search-suggestion - v-for="epic in defaultEpics" + v-for="epic in availableDefaultEpics" :key="epic.value" :value="epic.value" > {{ epic.text }} </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultEpics.length" /> + <gl-dropdown-divider v-if="availableDefaultEpics.length" /> <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)"> 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 ba8b2421726..c1d1bc7da91 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 @@ -1,24 +1,21 @@ <script> -import { - GlDropdownDivider, - GlFilteredSearchSuggestion, - GlFilteredSearchToken, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; -import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_ITERATIONS } from '../constants'; export default { components: { - GlDropdownDivider, + BaseToken, GlFilteredSearchSuggestion, - GlFilteredSearchToken, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -35,84 +32,58 @@ export default { }; }, computed: { - currentValue() { - return this.value.data; - }, - activeIteration() { - return this.iterations.find( - (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue), - ); - }, defaultIterations() { return this.config.defaultIterations || DEFAULT_ITERATIONS; }, }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.iterations.length) { - this.fetchIterationBySearchTerm(this.currentValue); - } - }, - }, - }, methods: { - getValue(iteration) { - return String(getIdFromGraphQLId(iteration.id)); + getActiveIteration(iterations, data) { + return iterations.find((iteration) => this.getValue(iteration) === data); }, - fetchIterationBySearchTerm(searchTerm) { - const fetchPromise = this.config.fetchPath - ? this.config.fetchIterations(this.config.fetchPath, searchTerm) - : this.config.fetchIterations(searchTerm); - + fetchIterations(searchTerm) { this.loading = true; - - fetchPromise + this.config + .fetchIterations(searchTerm) .then((response) => { this.iterations = Array.isArray(response) ? response : response.data; }) - .catch(() => createFlash({ message: __('There was a problem fetching iterations.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching iterations.') }); + }) .finally(() => { this.loading = false; }); }, - searchIterations: debounce(function debouncedSearch({ data }) { - this.fetchIterationBySearchTerm(data); - }, DEBOUNCE_DELAY), + getValue(iteration) { + return String(getIdFromGraphQLId(iteration.id)); + }, }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultIterations" + :suggestions="iterations" + :suggestions-loading="loading" + :get-active-token-value="getActiveIteration" + @fetch-suggestions="fetchIterations" v-on="$listeners" - @input="searchIterations" > - <template #view="{ inputValue }"> - {{ activeIteration ? activeIteration.title : inputValue }} + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + {{ activeTokenValue ? activeTokenValue.title : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="iteration in defaultIterations" - :key="iteration.value" - :value="iteration.value" + v-for="iteration in suggestions" + :key="iteration.id" + :value="getValue(iteration)" > - {{ iteration.text }} + {{ iteration.title }} </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultIterations.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="iteration in iterations" - :key="iteration.id" - :value="getValue(iteration)" - > - {{ iteration.title }} - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> 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 4d08f81fee9..c31f3a25fb1 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 @@ -5,7 +5,7 @@ import createFlash from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; -import { DEFAULT_LABELS } from '../constants'; +import { DEFAULT_NONE_ANY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; import BaseToken from './base_token.vue'; @@ -33,14 +33,18 @@ export default { data() { return { labels: this.config.initialLabels || [], - defaultLabels: this.config.defaultLabels || DEFAULT_LABELS, loading: false, }; }, + computed: { + defaultLabels() { + return this.config.defaultLabels || DEFAULT_NONE_ANY; + }, + }, methods: { - getActiveLabel(labels, currentValue) { + getActiveLabel(labels, data) { return labels.find( - (label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue), + (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(), ); }, /** @@ -68,7 +72,7 @@ export default { } return {}; }, - fetchLabelBySearchTerm(searchTerm) { + fetchLabels(searchTerm) { this.loading = true; this.config .fetchLabels(searchTerm) @@ -98,10 +102,10 @@ export default { :active="active" :suggestions-loading="loading" :suggestions="labels" - :fn-active-token-value="getActiveLabel" + :get-active-token-value="getActiveLabel" :default-suggestions="defaultLabels" :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" - @fetch-suggestions="fetchLabelBySearchTerm" + @fetch-suggestions="fetchLabels" v-on="$listeners" > <template 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 66ad5ef5b4e..4b9ad6d8f91 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 @@ -1,27 +1,22 @@ <script> -import { - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; - -import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_MILESTONES } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; export default { components: { - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -34,36 +29,21 @@ export default { data() { return { milestones: this.config.initialMilestones || [], - defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeMilestone() { - return this.milestones.find( - (milestone) => milestone.title.toLowerCase() === stripQuotes(this.currentValue), - ); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.milestones.length) { - this.fetchMilestoneBySearchTerm(this.value.data); - } - }, + defaultMilestones() { + return this.config.defaultMilestones || DEFAULT_MILESTONES; }, }, methods: { - fetchMilestoneBySearchTerm(searchTerm = '') { - if (this.loading) { - return; - } - + getActiveMilestone(milestones, data) { + return milestones.find( + (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(), + ); + }, + fetchMilestones(searchTerm) { this.loading = true; this.config .fetchMilestones(searchTerm) @@ -71,47 +51,40 @@ export default { const data = Array.isArray(response) ? response : response.data; this.milestones = data.slice().sort(sortMilestonesByDueDate); }) - .catch(() => createFlash({ message: __('There was a problem fetching milestones.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching milestones.') }); + }) .finally(() => { this.loading = false; }); }, - searchMilestones: debounce(function debouncedSearch({ data }) { - this.fetchMilestoneBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultMilestones" + :suggestions="milestones" + :suggestions-loading="loading" + :get-active-token-value="getActiveMilestone" + @fetch-suggestions="fetchMilestones" v-on="$listeners" - @input="searchMilestones" > - <template #view="{ inputValue }"> - <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + %{{ activeTokenValue ? activeTokenValue.title : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="milestone in defaultMilestones" - :key="milestone.value" - :value="milestone.value" + v-for="milestone in suggestions" + :key="milestone.id" + :value="milestone.title" > - {{ milestone.text }} + {{ milestone.title }} </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultMilestones.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="milestone in milestones" - :key="milestone.id" - :value="milestone.title" - > - <div>{{ milestone.title }}</div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue index 72116f0e991..280fb234576 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue @@ -1,15 +1,20 @@ <script> -import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui'; -import { DEFAULT_NONE_ANY } from '../constants'; +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants'; + +const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString()); export default { - baseWeights: ['0', '1', '2', '3', '4', '5'], components: { - GlDropdownDivider, + BaseToken, GlFilteredSearchSuggestion, - GlFilteredSearchToken, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -21,38 +26,41 @@ export default { }, data() { return { - weights: this.$options.baseWeights, - defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY, + weights, }; }, + computed: { + defaultWeights() { + return this.config.defaultWeights || DEFAULT_NONE_ANY; + }, + }, methods: { - updateWeights({ data }) { - const weight = parseInt(data, 10); - this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)]; + getActiveWeight(weightSuggestions, data) { + return weightSuggestions.find((weight) => weight === data); + }, + updateWeights(searchTerm) { + const weight = parseInt(searchTerm, 10); + this.weights = Number.isNaN(weight) ? weights : [String(weight)]; }, }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultWeights" + :suggestions="weights" + :get-active-token-value="getActiveWeight" + @fetch-suggestions="updateWeights" v-on="$listeners" - @input="updateWeights" > - <template #suggestions> - <gl-filtered-search-suggestion - v-for="weight in defaultWeights" - :key="weight.value" - :value="weight.value" - > - {{ weight.text }} - </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultWeights.length" /> - <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight"> + <template #suggestions-list="{ suggestions }"> + <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight"> {{ weight }} </gl-filtered-search-suggestion> </template> - </gl-filtered-search-token> + </base-token> </template> |