summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue167
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue58
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>