diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens')
5 files changed, 380 insertions, 53 deletions
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 new file mode 100644 index 00000000000..6ebc5431012 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -0,0 +1,167 @@ +<script> +import { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlDropdownSectionHeader, + GlLoadingIcon, +} from '@gitlab/ui'; + +import { DEBOUNCE_DELAY } from '../constants'; +import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; + +export default { + components: { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlDropdownSectionHeader, + GlLoadingIcon, + }, + props: { + tokenConfig: { + type: Object, + required: true, + }, + tokenValue: { + type: Object, + required: true, + }, + tokenActive: { + type: Boolean, + required: true, + }, + tokensListLoading: { + type: Boolean, + required: true, + }, + tokenValues: { + type: Array, + required: true, + }, + fnActiveTokenValue: { + type: Function, + required: true, + }, + defaultTokenValues: { + type: Array, + required: false, + default: () => [], + }, + recentTokenValuesStorageKey: { + type: String, + required: false, + default: '', + }, + valueIdentifier: { + type: String, + required: false, + default: 'id', + }, + fnCurrentTokenValue: { + type: Function, + required: false, + default: null, + }, + }, + data() { + return { + searchKey: '', + recentTokenValues: this.recentTokenValuesStorageKey + ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey) + : [], + loading: false, + }; + }, + computed: { + isRecentTokenValuesEnabled() { + return Boolean(this.recentTokenValuesStorageKey); + }, + recentTokenIds() { + return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name); + }, + currentTokenValue() { + if (this.fnCurrentTokenValue) { + return this.fnCurrentTokenValue(this.tokenValue.data); + } + return this.tokenValue.data.toLowerCase(); + }, + activeTokenValue() { + return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue); + }, + /** + * Return all the tokenValues when searchKey is present + * otherwise return only the tokenValues which aren't + * present in "Recently used" + */ + availableTokenValues() { + return this.searchKey + ? this.tokenValues + : this.tokenValues.filter( + (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]), + ); + }, + }, + watch: { + tokenActive: { + immediate: true, + handler(newValue) { + if (!newValue && !this.tokenValues.length) { + this.$emit('fetch-token-values', this.tokenValue.data); + } + }, + }, + }, + methods: { + handleInput({ data }) { + this.searchKey = data; + setTimeout(() => { + if (!this.tokensListLoading) this.$emit('fetch-token-values', data); + }, DEBOUNCE_DELAY); + }, + handleTokenValueSelected(activeTokenValue) { + if (this.isRecentTokenValuesEnabled) { + setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); + } + }, + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="tokenConfig" + v-bind="{ ...this.$parent.$props, ...this.$parent.$attrs }" + v-on="this.$parent.$listeners" + @input="handleInput" + @select="handleTokenValueSelected(activeTokenValue)" + > + <template #view-token="viewTokenProps"> + <slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> + </template> + <template #view="viewTokenProps"> + <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> + </template> + <template #suggestions> + <template v-if="defaultTokenValues.length"> + <gl-filtered-search-suggestion + v-for="token in defaultTokenValues" + :key="token.value" + :value="token.value" + > + {{ token.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider /> + </template> + <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey"> + <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> + <slot name="token-values-list" :token-values="recentTokenValues"></slot> + <gl-dropdown-divider /> + </template> + <gl-loading-icon v-if="tokensListLoading" /> + <template v-else> + <slot name="token-values-list" :token-values="availableTokenValues"></slot> + </template> + </template> + </gl-filtered-search-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 98190d716c9..f2f4787d80b 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 @@ -10,7 +10,7 @@ import { debounce } from 'lodash'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __ } from '~/locale'; -import { DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants'; +import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; export default { @@ -33,7 +33,7 @@ export default { data() { return { emojis: this.config.initialEmojis || [], - defaultEmojis: this.config.defaultEmojis || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY], + defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY, loading: true, }; }, @@ -47,6 +47,16 @@ export default { ); }, }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.emojis.length) { + this.fetchEmojiBySearchTerm(this.value.data); + } + }, + }, + }, methods: { fetchEmojiBySearchTerm(searchTerm) { this.loading = true; 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 101c7150c55..1450807b11d 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 @@ -1,15 +1,18 @@ <script> -import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; +import { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + GlLoadingIcon, +} from '@gitlab/ui'; import { debounce } from 'lodash'; - import createFlash from '~/flash'; -import { isNumeric } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; -import { DEBOUNCE_DELAY } from '../constants'; -import { stripQuotes } from '../filtered_search_utils'; +import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; export default { components: { + GlDropdownDivider, GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon, @@ -32,29 +35,16 @@ export default { }, computed: { currentValue() { - /* - * When the URL contains the epic_iid, we'd get: '123' - */ - if (isNumeric(this.value.data)) { - return parseInt(this.value.data, 10); - } - - /* - * When the token is added in current session it'd be: 'Foo::&123' - */ - const id = this.value.data.split('::&')[1]; - - if (id) { - return parseInt(id, 10); - } - - return this.value.data; + return Number(this.value.data); + }, + defaultEpics() { + return this.config.defaultEpics || DEFAULT_NONE_ANY; + }, + idProperty() { + return this.config.idProperty || 'id'; }, activeEpic() { - const currentValueIsString = typeof this.currentValue === 'string'; - return this.epics.find( - (epic) => epic[currentValueIsString ? 'title' : 'iid'] === this.currentValue, - ); + return this.epics.find((epic) => epic[this.idProperty] === this.currentValue); }, }, watch: { @@ -72,20 +62,8 @@ export default { this.loading = true; this.config .fetchEpics(searchTerm) - .then(({ data }) => { - this.epics = data; - }) - .catch(() => createFlash({ message: __('There was a problem fetching epics.') })) - .finally(() => { - this.loading = false; - }); - }, - fetchSingleEpic(iid) { - this.loading = true; - this.config - .fetchSingleEpic(iid) - .then(({ data }) => { - this.epics = [data]; + .then((response) => { + this.epics = Array.isArray(response) ? response : response.data; }) .catch(() => createFlash({ message: __('There was a problem fetching epics.') })) .finally(() => { @@ -93,17 +71,13 @@ export default { }); }, searchEpics: debounce(function debouncedSearch({ data }) { - if (isNumeric(data)) { - return this.fetchSingleEpic(data); - } - return this.fetchEpicsBySearchTerm(data); + this.fetchEpicsBySearchTerm(data); }, DEBOUNCE_DELAY), - getEpicValue(epic) { - return `${epic.title}::&${epic.iid}`; + getEpicDisplayText(epic) { + return `${epic.title}::&${epic[this.idProperty]}`; }, }, - stripQuotes, }; </script> @@ -115,17 +89,25 @@ export default { @input="searchEpics" > <template #view="{ inputValue }"> - <span>{{ activeEpic ? getEpicValue(activeEpic) : $options.stripQuotes(inputValue) }}</span> + {{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }} </template> <template #suggestions> + <gl-filtered-search-suggestion + v-for="epic in defaultEpics" + :key="epic.value" + :value="epic.value" + > + {{ epic.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultEpics.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion v-for="epic in epics" - :key="epic.id" - :value="getEpicValue(epic)" + :key="epic[idProperty]" + :value="String(epic[idProperty])" > - <div>{{ epic.title }}</div> + {{ epic.title }} </gl-filtered-search-suggestion> </template> </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 new file mode 100644 index 00000000000..7b6a590279a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue @@ -0,0 +1,110 @@ +<script> +import { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; + +export default { + components: { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + iterations: this.config.initialIterations || [], + defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS, + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data; + }, + activeIteration() { + return this.iterations.find((iteration) => iteration.title === this.currentValue); + }, + }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.iterations.length) { + this.fetchIterationBySearchTerm(this.currentValue); + } + }, + }, + }, + methods: { + fetchIterationBySearchTerm(searchTerm) { + const fetchPromise = this.config.fetchPath + ? this.config.fetchIterations(this.config.fetchPath, searchTerm) + : this.config.fetchIterations(searchTerm); + + this.loading = true; + + fetchPromise + .then((response) => { + this.iterations = Array.isArray(response) ? response : response.data; + }) + .catch(() => createFlash({ message: __('There was a problem fetching iterations.') })) + .finally(() => { + this.loading = false; + }); + }, + searchIterations: debounce(function debouncedSearch({ data }) { + this.fetchIterationBySearchTerm(data); + }, DEBOUNCE_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchIterations" + > + <template #view="{ inputValue }"> + {{ activeIteration ? activeIteration.title : inputValue }} + </template> + <template #suggestions> + <gl-filtered-search-suggestion + v-for="iteration in defaultIterations" + :key="iteration.value" + :value="iteration.value" + > + {{ iteration.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultIterations.length" /> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="iteration in iterations" + :key="iteration.title" + :value="iteration.title" + > + {{ iteration.title }} + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-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 new file mode 100644 index 00000000000..72116f0e991 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue @@ -0,0 +1,58 @@ +<script> +import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui'; +import { DEFAULT_NONE_ANY } from '../constants'; + +export default { + baseWeights: ['0', '1', '2', '3', '4', '5'], + components: { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + weights: this.$options.baseWeights, + defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY, + }; + }, + methods: { + updateWeights({ data }) { + const weight = parseInt(data, 10); + this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)]; + }, + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + 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"> + {{ weight }} + </gl-filtered-search-suggestion> + </template> + </gl-filtered-search-token> +</template> |