diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens')
5 files changed, 183 insertions, 165 deletions
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 aeb698a3adb..2e7b3e149b2 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 @@ -1,25 +1,18 @@ <script> -import { - GlFilteredSearchToken, - GlAvatar, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; -import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABEL_ANY } from '../constants'; + +import BaseToken from './base_token.vue'; export default { components: { - GlFilteredSearchToken, + BaseToken, GlAvatar, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { config: { @@ -30,37 +23,28 @@ export default { type: Object, required: true, }, + active: { + type: Boolean, + required: true, + }, }, data() { return { authors: this.config.initialAuthors || [], defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], - loading: true, + preloadedAuthors: this.config.preloadedAuthors || [], + loading: false, }; }, - computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeAuthor() { - return this.authors.find((author) => author.username.toLowerCase() === this.currentValue); - }, - activeAuthorAvatar() { - return this.avatarUrl(this.activeAuthor); + methods: { + getActiveAuthor(authors, currentValue) { + return authors.find((author) => author.username.toLowerCase() === currentValue); }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.authors.length) { - this.fetchAuthorBySearchTerm(this.value.data); - } - }, + getAvatarUrl(author) { + return author.avatarUrl || author.avatar_url; }, - }, - methods: { fetchAuthorBySearchTerm(searchTerm) { + this.loading = true; const fetchPromise = this.config.fetchPath ? this.config.fetchAuthors(this.config.fetchPath, searchTerm) : this.config.fetchAuthors(searchTerm); @@ -72,63 +56,56 @@ export default { // return response differently. this.authors = Array.isArray(res) ? res : res.data; }) - .catch(() => createFlash(__('There was a problem fetching users.'))) + .catch(() => + createFlash({ + message: __('There was a problem fetching users.'), + }), + ) .finally(() => { this.loading = false; }); }, - avatarUrl(author) { - return author.avatarUrl || author.avatar_url; - }, - searchAuthors: debounce(function debouncedSearch({ data }) { - this.fetchAuthorBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token - :config="config" - v-bind="{ ...$props, ...$attrs }" - v-on="$listeners" - @input="searchAuthors" + <base-token + :token-config="config" + :token-value="value" + :token-active="active" + :tokens-list-loading="loading" + :token-values="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" > - <template #view="{ inputValue }"> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> <gl-avatar - v-if="activeAuthor" + v-if="activeTokenValue" :size="16" - :src="activeAuthorAvatar" + :src="getAvatarUrl(activeTokenValue)" shape="circle" class="gl-mr-2" /> - <span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span> + <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span> </template> - <template #suggestions> + <template #token-values-list="{ tokenValues }"> <gl-filtered-search-suggestion - v-for="author in defaultAuthors" - :key="author.value" - :value="author.value" + v-for="author in tokenValues" + :key="author.username" + :value="author.username" > - {{ author.text }} - </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultAuthors.length" /> - <gl-loading-icon v-if="loading" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="author in authors" - :key="author.username" - :value="author.username" - > - <div class="d-flex"> - <gl-avatar :size="32" :src="avatarUrl(author)" /> - <div> - <div>{{ author.name }}</div> - <div>@{{ author.username }}</div> - </div> + <div class="gl-display-flex"> + <gl-avatar :size="32" :src="getAvatarUrl(author)" /> + <div> + <div>{{ author.name }}</div> + <div>@{{ author.username }}</div> </div> - </gl-filtered-search-suggestion> - </template> + </div> + </gl-filtered-search-suggestion> </template> - </gl-filtered-search-token> + </base-token> </template> 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 6ebc5431012..fb6b9e4bc0d 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 @@ -48,6 +48,11 @@ export default { required: false, default: () => [], }, + preloadedTokenValues: { + type: Array, + required: false, + default: () => [], + }, recentTokenValuesStorageKey: { type: String, required: false, @@ -78,7 +83,10 @@ export default { return Boolean(this.recentTokenValuesStorageKey); }, recentTokenIds() { - return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name); + return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + }, + preloadedTokenIds() { + return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); }, currentTokenValue() { if (this.fnCurrentTokenValue) { @@ -98,7 +106,9 @@ export default { return this.searchKey ? this.tokenValues : this.tokenValues.filter( - (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]), + (tokenValue) => + !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) && + !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), ); }, }, @@ -120,7 +130,15 @@ export default { }, DEBOUNCE_DELAY); }, handleTokenValueSelected(activeTokenValue) { - if (this.isRecentTokenValuesEnabled) { + // 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 && + activeTokenValue && + !this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier]) + ) { setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); } }, @@ -158,6 +176,11 @@ export default { <slot name="token-values-list" :token-values="recentTokenValues"></slot> <gl-dropdown-divider /> </template> + <slot + v-if="preloadedTokenValues.length" + name="token-values-list" + :token-values="preloadedTokenValues" + ></slot> <gl-loading-icon v-if="tokensListLoading" /> <template v-else> <slot name="token-values-list" :token-values="availableTokenValues"></slot> 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 f2f4787d80b..9ba7f3d1a1d 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 @@ -7,7 +7,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; @@ -65,7 +65,11 @@ export default { .then((res) => { this.emojis = Array.isArray(res) ? res : res.data; }) - .catch(() => createFlash(__('There was a problem fetching emojis.'))) + .catch(() => + createFlash({ + message: __('There was a problem fetching emojis.'), + }), + ) .finally(() => { this.loading = false; }); 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 1450807b11d..d21fa9a344a 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 @@ -11,6 +11,7 @@ import { __ } from '~/locale'; import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; export default { + separator: '::&', components: { GlDropdownDivider, GlFilteredSearchToken, @@ -34,17 +35,35 @@ export default { }; }, computed: { + idProperty() { + return this.config.idProperty || 'iid'; + }, currentValue() { - return Number(this.value.data); + const epicIid = Number(this.value.data); + if (epicIid) { + return epicIid; + } + return this.value.data; }, defaultEpics() { return this.config.defaultEpics || DEFAULT_NONE_ANY; }, - idProperty() { - return this.config.idProperty || 'id'; - }, activeEpic() { - return this.epics.find((epic) => epic[this.idProperty] === this.currentValue); + if (this.currentValue && this.epics.length) { + // Check if current value is an epic ID. + if (typeof this.currentValue === 'number') { + return this.epics.find((epic) => epic[this.idProperty] === this.currentValue); + } + + // Current value is a string. + const [groupPath, idProperty] = this.currentValue?.split('::&'); + return this.epics.find( + (epic) => + epic.group_full_path === groupPath && + epic[this.idProperty] === parseInt(idProperty, 10), + ); + } + return null; }, }, watch: { @@ -58,10 +77,10 @@ export default { }, }, methods: { - fetchEpicsBySearchTerm(searchTerm = '') { + fetchEpicsBySearchTerm({ epicPath = '', search = '' }) { this.loading = true; this.config - .fetchEpics(searchTerm) + .fetchEpics({ epicPath, search }) .then((response) => { this.epics = Array.isArray(response) ? response : response.data; }) @@ -71,11 +90,21 @@ export default { }); }, searchEpics: debounce(function debouncedSearch({ data }) { - this.fetchEpicsBySearchTerm(data); + let epicPath = this.activeEpic?.web_url; + + // When user visits the page with token value already included in filters + // 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)) { + const [groupPath, epicIid] = data.split(this.$options.separator); + epicPath = `/groups/${groupPath}/-/epics/${epicIid}`; + } + this.fetchEpicsBySearchTerm({ epicPath, search: data }); }, DEBOUNCE_DELAY), getEpicDisplayText(epic) { - return `${epic.title}::&${epic[this.idProperty]}`; + return `${epic.title}${this.$options.separator}${epic.iid}`; }, }, }; @@ -104,8 +133,8 @@ export default { <template v-else> <gl-filtered-search-suggestion v-for="epic in epics" - :key="epic[idProperty]" - :value="String(epic[idProperty])" + :key="epic.id" + :value="`${epic.group_full_path}::&${epic[idProperty]}`" > {{ epic.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 76b005772ec..20b8cbfe933 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 @@ -1,27 +1,20 @@ <script> -import { - GlToken, - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; -import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABELS } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; +import BaseToken from './base_token.vue'; + export default { components: { + BaseToken, GlToken, - GlFilteredSearchToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { config: { @@ -32,43 +25,24 @@ export default { type: Object, required: true, }, + active: { + type: Boolean, + required: true, + }, }, data() { return { labels: this.config.initialLabels || [], defaultLabels: this.config.defaultLabels || DEFAULT_LABELS, - loading: true, + loading: false, }; }, - computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeLabel() { - return this.labels.find( - (label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue), + methods: { + getActiveLabel(labels, currentValue) { + return labels.find( + (label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue), ); }, - containerStyle() { - if (this.activeLabel) { - const { color, textColor } = convertObjectPropsToCamelCase(this.activeLabel); - - return { backgroundColor: color, color: textColor }; - } - return {}; - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.labels.length) { - this.fetchLabelBySearchTerm(this.value.data); - } - }, - }, - }, - methods: { /** * There's an inconsistency between private and public API * for labels where label name is included in a different @@ -84,6 +58,16 @@ export default { getLabelName(label) { return label.name || label.title; }, + getContainerStyle(activeLabel) { + if (activeLabel) { + const { color: backgroundColor, textColor: color } = convertObjectPropsToCamelCase( + activeLabel, + ); + + return { backgroundColor, color }; + } + return {}; + }, fetchLabelBySearchTerm(searchTerm) { this.loading = true; this.config @@ -94,55 +78,56 @@ export default { // return response differently. this.labels = Array.isArray(res) ? res : res.data; }) - .catch(() => createFlash(__('There was a problem fetching labels.'))) + .catch(() => + createFlash({ + message: __('There was a problem fetching labels.'), + }), + ) .finally(() => { this.loading = false; }); }, - searchLabels: debounce(function debouncedSearch({ data }) { - if (!this.loading) this.fetchLabelBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token - :config="config" - v-bind="{ ...$props, ...$attrs }" - v-on="$listeners" - @input="searchLabels" + <base-token + :token-config="config" + :token-value="value" + :token-active="active" + :tokens-list-loading="loading" + :token-values="labels" + :fn-active-token-value="getActiveLabel" + :default-token-values="defaultLabels" + :recent-token-values-storage-key="config.recentTokenValuesStorageKey" + @fetch-token-values="fetchLabelBySearchTerm" > - <template #view-token="{ inputValue, cssClasses, listeners }"> - <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners" - >~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token + <template + #view-token="{ viewTokenProps: { inputValue, cssClasses, listeners, activeTokenValue } }" + > + <gl-token + variant="search-value" + :class="cssClasses" + :style="getContainerStyle(activeTokenValue)" + v-on="listeners" + >~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token > </template> - <template #suggestions> + <template #token-values-list="{ tokenValues }"> <gl-filtered-search-suggestion - v-for="label in defaultLabels" - :key="label.value" - :value="label.value" + v-for="label in tokenValues" + :key="label.id" + :value="getLabelName(label)" > - {{ label.text }} + <div class="gl-display-flex gl-align-items-center"> + <span + :style="{ backgroundColor: label.color }" + class="gl-display-inline-block mr-2 p-2" + ></span> + <div>{{ getLabelName(label) }}</div> + </div> </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultLabels.length" /> - <gl-loading-icon v-if="loading" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="label in labels" - :key="label.id" - :value="getLabelName(label)" - > - <div class="gl-display-flex gl-align-items-center"> - <span - :style="{ backgroundColor: label.color }" - class="gl-display-inline-block mr-2 p-2" - ></span> - <div>{{ getLabelName(label) }}</div> - </div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> |