summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components/filtered_search_bar
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/filtered_search_bar')
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js67
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue137
9 files changed, 258 insertions, 183 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 2cb1b6a195f..9775a9119c6 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
@@ -21,7 +21,7 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
-export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; // eslint-disable-line @gitlab/require-i18n-strings
+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
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 3e7feb91b27..5ab287150f2 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
@@ -12,7 +12,7 @@ import {
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import { SortDirection } from './constants';
@@ -211,7 +211,9 @@ export default {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
- createFlash(__('An error occurred while parsing recent searches'));
+ createFlash({
+ message: __('An error occurred while parsing recent searches'),
+ });
// Gracefully fail to empty array
return [];
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 e5c8d29e09b..37436de907f 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
@@ -2,7 +2,7 @@ import { isEmpty, uniqWith, isEqual } from 'lodash';
import AccessorUtilities from '~/lib/utils/accessor';
import { queryToObject } from '~/lib/utils/url_utility';
-import { MAX_RECENT_TOKENS_SIZE } from './constants';
+import { MAX_RECENT_TOKENS_SIZE, FILTERED_SEARCH_TERM } from './constants';
/**
* Strips enclosing quotations from a string if it has one.
@@ -23,7 +23,7 @@ export const stripQuotes = (value) => value.replace(/^('|")(.*)('|")$/, '$2');
export const uniqueTokens = (tokens) => {
const knownTokens = [];
return tokens.reduce((uniques, token) => {
- if (typeof token === 'object' && token.type !== 'filtered-search-term') {
+ if (typeof token === 'object' && token.type !== FILTERED_SEARCH_TERM) {
const tokenString = `${token.type}${token.value.operator}${token.value.data}`;
if (!knownTokens.includes(tokenString)) {
uniques.push(token);
@@ -86,21 +86,37 @@ export function processFilters(filters) {
}, {});
}
+function filteredSearchQueryParam(filter) {
+ return filter
+ .map(({ value }) => value)
+ .join(' ')
+ .trim();
+}
+
/**
* This function takes a filter object and maps it into a query object. Example filter:
- * { myFilterName: { value: 'foo', operator: '=' } }
+ * { myFilterName: { value: 'foo', operator: '=' }, search: [{ value: 'my' }, { value: 'search' }] }
* gets translated into:
- * { myFilterName: 'foo', 'not[myFilterName]': null }
+ * { myFilterName: 'foo', 'not[myFilterName]': null, search: 'my search' }
* @param {Object} filters
- * @param {Object.myFilterName} a single filter value or an array of filters
+ * @param {Object} filters.myFilterName a single filter value or an array of filters
+ * @param {Object} options
+ * @param {Object} [options.filteredSearchTermKey] if set, 'filtered-search-term' filters are assigned to this key, 'search' is suggested
* @return {Object} query object with both filter name and not-name with values
*/
-export function filterToQueryObject(filters = {}) {
+export function filterToQueryObject(filters = {}, options = {}) {
+ const { filteredSearchTermKey } = options;
+
return Object.keys(filters).reduce((memo, key) => {
const filter = filters[key];
+ if (typeof filteredSearchTermKey === 'string' && key === FILTERED_SEARCH_TERM) {
+ return { ...memo, [filteredSearchTermKey]: filteredSearchQueryParam(filter) };
+ }
+
let selected;
let unselected;
+
if (Array.isArray(filter)) {
selected = filter.filter((item) => item.operator === '=').map((item) => item.value);
unselected = filter.filter((item) => item.operator === '!=').map((item) => item.value);
@@ -125,7 +141,7 @@ export function filterToQueryObject(filters = {}) {
* and returns the operator with it depending on the filter name
* @param {String} filterName from url
* @return {Object}
- * @return {Object.filterName} extracted filtern ame
+ * @return {Object.filterName} extracted filter name
* @return {Object.operator} `=` or `!=`
*/
function extractNameAndOperator(filterName) {
@@ -138,21 +154,52 @@ function extractNameAndOperator(filterName) {
}
/**
+ * Gathers search term as values
+ * @param {String|Array} value
+ * @returns {Array} List of search terms split by word
+ */
+function filteredSearchTermValue(value) {
+ const values = Array.isArray(value) ? value : [value];
+ return values
+ .filter((term) => term)
+ .join(' ')
+ .split(' ')
+ .map((term) => ({ value: term }));
+}
+
+/**
* This function takes a URL query string and maps it into a filter object. Example query string:
* '?myFilterName=foo'
* gets translated into:
* { myFilterName: { value: 'foo', operator: '=' } }
- * @param {String} query URL quert string, e.g. from `window.location.search`
+ * @param {String} query URL query string, e.g. from `window.location.search`
+ * @param {Object} options
+ * @param {Object} options
+ * @param {String} [options.filteredSearchTermKey] if set, a FILTERED_SEARCH_TERM filter is created to this parameter. `'search'` is suggested
+ * @param {String[]} [options.filterNamesAllowList] if set, only this list of filters names is mapped
+ * @param {Boolean} [options.legacySpacesDecode] if set, plus symbols (+) are not encoded as spaces. `false` is suggested
* @return {Object} filter object with filter names and their values
*/
-export function urlQueryToFilter(query = '') {
- const filters = queryToObject(query, { gatherArrays: true });
+export function urlQueryToFilter(query = '', options = {}) {
+ const { filteredSearchTermKey, filterNamesAllowList, legacySpacesDecode = true } = options;
+
+ const filters = queryToObject(query, { gatherArrays: true, legacySpacesDecode });
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
return memo;
}
+ if (key === filteredSearchTermKey) {
+ return {
+ ...memo,
+ [FILTERED_SEARCH_TERM]: filteredSearchTermValue(value),
+ };
+ }
+
const { filterName, operator } = extractNameAndOperator(key);
+ if (filterNamesAllowList && !filterNamesAllowList.includes(filterName)) {
+ return memo;
+ }
let previousValues = [];
if (Array.isArray(memo[filterName])) {
previousValues = memo[filterName];
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index 4dfc61f1fff..f4317ba90a2 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -24,7 +24,9 @@ export function fetchBranches({ commit, state }, search = '') {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_BRANCHES_ERROR, status);
- createFlash(__('Failed to load branches. Please try again.'));
+ createFlash({
+ message: __('Failed to load branches. Please try again.'),
+ });
});
}
@@ -41,7 +43,9 @@ export const fetchMilestones = ({ commit, state }, search_title = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
- createFlash(__('Failed to load milestones. Please try again.'));
+ createFlash({
+ message: __('Failed to load milestones. Please try again.'),
+ });
});
};
@@ -57,7 +61,9 @@ export const fetchLabels = ({ commit, state }, search = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
- createFlash(__('Failed to load labels. Please try again.'));
+ createFlash({
+ message: __('Failed to load labels. Please try again.'),
+ });
});
};
@@ -80,7 +86,9 @@ function fetchUser(options = {}) {
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
- createFlash(errorMessage);
+ createFlash({
+ message: errorMessage,
+ });
});
}
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>