From 403acc26ae3c0e57d8badc28e61a17534bb38c03 Mon Sep 17 00:00:00 2001 From: Coung Ngo Date: Thu, 6 May 2021 17:11:20 +0100 Subject: [PATCH 1/2] Add None/Any search tokens to issues page refactor Add None/Any/Current search tokens to: - Assignee token - My-Reaction token - Iteration token Weight already shows None/Any tokens Added behind `vue_issues_list` feature flag defaulted to off, as part of an ongoing refactor from Haml. https://gitlab.com/gitlab-org/gitlab/-/issues/322755 --- .../components/issues_list_app.vue | 5 +- .../javascripts/issues_list/constants.js | 129 ++++++++++++++---- app/assets/javascripts/issues_list/utils.js | 27 +++- .../filtered_search_bar/constants.js | 23 ++-- .../tokens/emoji_token.vue | 4 +- .../tokens/weight_token.vue | 4 +- spec/frontend/issues_list/mock_data.js | 28 ++++ spec/frontend/issues_list/utils_spec.js | 25 +++- 8 files changed, 194 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 372c15be931be1..142438bec216f1 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -34,6 +34,7 @@ import { import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; @@ -186,7 +187,7 @@ export default { token: AuthorToken, dataType: 'user', unique: true, - defaultAuthors: [], + defaultAuthors: DEFAULT_NONE_ANY, fetchAuthors: this.fetchUsers, }, { @@ -213,7 +214,6 @@ export default { token: EmojiToken, unique: true, operators: [{ value: '=', description: __('is') }], - defaultEmojis: [], fetchEmojis: this.fetchEmojis, }, { @@ -237,7 +237,6 @@ export default { icon: 'iteration', token: IterationToken, unique: true, - defaultIterations: [], fetchIterations: this.fetchIterations, }); } diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index ee515079668067..3b01d0df523be6 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -1,4 +1,9 @@ import { __, s__ } from '~/locale'; +import { + FILTER_ANY, + FILTER_CURRENT, + FILTER_NONE, +} from '~/vue_shared/components/filtered_search_bar/constants'; // Maps sort order as it appears in the URL query to API `order_by` and `sort` params. const PRIORITY = 'priority'; @@ -194,81 +199,149 @@ export const FILTERED_SEARCH_TERM = 'filtered-search-term'; export const OPERATOR_IS = '='; export const OPERATOR_IS_NOT = '!='; +export const NORMAL_FILTER = 'normalFilter'; +export const SPECIAL_FILTER = 'specialFilter'; +export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT]; + export const filters = { author_username: { apiParam: { - [OPERATOR_IS]: 'author_username', - [OPERATOR_IS_NOT]: 'not[author_username]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'author_username', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[author_username]', + }, }, urlParam: { - [OPERATOR_IS]: 'author_username', - [OPERATOR_IS_NOT]: 'not[author_username]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'author_username', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[author_username]', + }, }, }, assignee_username: { apiParam: { - [OPERATOR_IS]: 'assignee_username', - [OPERATOR_IS_NOT]: 'not[assignee_username]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'assignee_username', + [SPECIAL_FILTER]: 'assignee_id', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[assignee_username]', + }, }, urlParam: { - [OPERATOR_IS]: 'assignee_username[]', - [OPERATOR_IS_NOT]: 'not[assignee_username][]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'assignee_username[]', + [SPECIAL_FILTER]: 'assignee_id', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[assignee_username][]', + }, }, }, milestone: { apiParam: { - [OPERATOR_IS]: 'milestone', - [OPERATOR_IS_NOT]: 'not[milestone]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'milestone', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[milestone]', + }, }, urlParam: { - [OPERATOR_IS]: 'milestone_title', - [OPERATOR_IS_NOT]: 'not[milestone_title]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'milestone_title', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[milestone_title]', + }, }, }, labels: { apiParam: { - [OPERATOR_IS]: 'labels', - [OPERATOR_IS_NOT]: 'not[labels]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'labels', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[labels]', + }, }, urlParam: { - [OPERATOR_IS]: 'label_name[]', - [OPERATOR_IS_NOT]: 'not[label_name][]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'label_name[]', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[label_name][]', + }, }, }, my_reaction_emoji: { apiParam: { - [OPERATOR_IS]: 'my_reaction_emoji', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'my_reaction_emoji', + [SPECIAL_FILTER]: 'my_reaction_emoji', + }, }, urlParam: { - [OPERATOR_IS]: 'my_reaction_emoji', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'my_reaction_emoji', + [SPECIAL_FILTER]: 'my_reaction_emoji', + }, }, }, confidential: { apiParam: { - [OPERATOR_IS]: 'confidential', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'confidential', + }, }, urlParam: { - [OPERATOR_IS]: 'confidential', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'confidential', + }, }, }, iteration: { apiParam: { - [OPERATOR_IS]: 'iteration_title', - [OPERATOR_IS_NOT]: 'not[iteration_title]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'iteration_title', + [SPECIAL_FILTER]: 'iteration_id', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[iteration_title]', + }, }, urlParam: { - [OPERATOR_IS]: 'iteration_title', - [OPERATOR_IS_NOT]: 'not[iteration_title]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'iteration_title', + [SPECIAL_FILTER]: 'iteration_id', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[iteration_title]', + }, }, }, weight: { apiParam: { - [OPERATOR_IS]: 'weight', - [OPERATOR_IS_NOT]: 'not[weight]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'weight', + [SPECIAL_FILTER]: 'weight', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[weight]', + }, }, urlParam: { - [OPERATOR_IS]: 'weight', - [OPERATOR_IS_NOT]: 'not[weight]', + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'weight', + [SPECIAL_FILTER]: 'weight', + }, + [OPERATOR_IS_NOT]: { + [NORMAL_FILTER]: 'not[weight]', + }, }, }, }; diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js index 30d122e7bbefaf..ea3dbf749b5564 100644 --- a/app/assets/javascripts/issues_list/utils.js +++ b/app/assets/javascripts/issues_list/utils.js @@ -11,12 +11,15 @@ import { LABEL_PRIORITY_DESC, MILESTONE_DUE_ASC, MILESTONE_DUE_DESC, + NORMAL_FILTER, POPULARITY_ASC, POPULARITY_DESC, PRIORITY_ASC, PRIORITY_DESC, RELATIVE_POSITION_ASC, sortParams, + SPECIAL_FILTER, + SPECIAL_FILTER_VALUES, UPDATED_ASC, UPDATED_DESC, WEIGHT_ASC, @@ -124,13 +127,21 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) const tokenTypes = Object.keys(filters); -const urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam)); +const urlParamKeys = tokenTypes.flatMap((key) => + Object.values(filters[key].urlParam).flatMap((filterObj) => Object.values(filterObj)), +); const getTokenTypeFromUrlParamKey = (urlParamKey) => - tokenTypes.find((key) => Object.values(filters[key].urlParam).includes(urlParamKey)); + tokenTypes.find((key) => + Object.values(filters[key].urlParam) + .flatMap((filterObj) => Object.values(filterObj)) + .includes(urlParamKey), + ); const getOperatorFromUrlParamKey = (tokenType, urlParamKey) => - Object.entries(filters[tokenType].urlParam).find(([, urlParam]) => urlParam === urlParamKey)[0]; + Object.entries(filters[tokenType].urlParam).find(([, filterObj]) => + Object.values(filterObj).includes(urlParamKey), + )[0]; const convertToFilteredTokens = (locationSearch) => Array.from(new URLSearchParams(locationSearch).entries()) @@ -168,7 +179,10 @@ export const convertToApiParams = (filterTokens) => filterTokens .filter((token) => token.type !== FILTERED_SEARCH_TERM) .reduce((acc, token) => { - const apiParam = filters[token.type].apiParam[token.value.operator]; + const filterType = SPECIAL_FILTER_VALUES.includes(token.value.data) + ? SPECIAL_FILTER + : NORMAL_FILTER; + const apiParam = filters[token.type].apiParam[token.value.operator][filterType]; return Object.assign(acc, { [apiParam]: acc[apiParam] ? `${acc[apiParam]},${token.value.data}` : token.value.data, }); @@ -178,7 +192,10 @@ export const convertToUrlParams = (filterTokens) => filterTokens .filter((token) => token.type !== FILTERED_SEARCH_TERM) .reduce((acc, token) => { - const urlParam = filters[token.type].urlParam[token.value.operator]; + const filterType = SPECIAL_FILTER_VALUES.includes(token.value.data) + ? SPECIAL_FILTER + : NORMAL_FILTER; + const urlParam = filters[token.type].urlParam[token.value.operator]?.[filterType]; return Object.assign(acc, { [urlParam]: acc[urlParam] ? acc[urlParam].concat(token.value.data) : [token.value.data], }); 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 e2868879425195..519b461c0157d5 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 @@ -3,21 +3,24 @@ import { __ } from '~/locale'; export const DEBOUNCE_DELAY = 200; -const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') }; -export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') }; -export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') }; -export const DEFAULT_LABEL_CURRENT = { value: 'Current', text: __('Current') }; +export const FILTER_NONE = 'None'; +export const FILTER_ANY = 'Any'; +export const FILTER_CURRENT = 'Current'; -export const DEFAULT_ITERATIONS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEFAULT_LABEL_CURRENT]; +export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) }; +export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) }; +export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; -export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL]; +export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([ + { value: FILTER_CURRENT, text: __(FILTER_CURRENT) }, +]); -export const DEFAULT_MILESTONES = [ - DEFAULT_LABEL_NONE, - DEFAULT_LABEL_ANY, +export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; + +export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([ { value: 'Upcoming', text: __('Upcoming') }, { value: 'Started', text: __('Started') }, -]; +]); export const SortDirection = { descending: 'descending', 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 269e29a6dff07b..f2f4787d80bfab 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, }; }, 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 cfad79b9afaacf..72116f0e991f0f 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,6 +1,6 @@