Commit 19de35b0 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '322755-add-none-any-to-issues-page-refactor' into 'master'

Add None/Any search tokens to issues page refactor

See merge request gitlab-org/gitlab!61161
parents 3bc38b54 a1f65ac9
...@@ -34,6 +34,7 @@ import { ...@@ -34,6 +34,7 @@ import {
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; 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 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 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'; import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
...@@ -186,7 +187,7 @@ export default { ...@@ -186,7 +187,7 @@ export default {
token: AuthorToken, token: AuthorToken,
dataType: 'user', dataType: 'user',
unique: true, unique: true,
defaultAuthors: [], defaultAuthors: DEFAULT_NONE_ANY,
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
}, },
{ {
...@@ -213,7 +214,6 @@ export default { ...@@ -213,7 +214,6 @@ export default {
token: EmojiToken, token: EmojiToken,
unique: true, unique: true,
operators: [{ value: '=', description: __('is') }], operators: [{ value: '=', description: __('is') }],
defaultEmojis: [],
fetchEmojis: this.fetchEmojis, fetchEmojis: this.fetchEmojis,
}, },
{ {
...@@ -237,7 +237,6 @@ export default { ...@@ -237,7 +237,6 @@ export default {
icon: 'iteration', icon: 'iteration',
token: IterationToken, token: IterationToken,
unique: true, unique: true,
defaultIterations: [],
fetchIterations: this.fetchIterations, fetchIterations: this.fetchIterations,
}); });
} }
......
import { __, s__ } from '~/locale'; 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. // Maps sort order as it appears in the URL query to API `order_by` and `sort` params.
const PRIORITY = 'priority'; const PRIORITY = 'priority';
...@@ -194,81 +199,149 @@ export const FILTERED_SEARCH_TERM = 'filtered-search-term'; ...@@ -194,81 +199,149 @@ export const FILTERED_SEARCH_TERM = 'filtered-search-term';
export const OPERATOR_IS = '='; export const OPERATOR_IS = '=';
export const OPERATOR_IS_NOT = '!='; 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 = { export const filters = {
author_username: { author_username: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'author_username', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[author_username]', [NORMAL_FILTER]: 'author_username',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'author_username', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[author_username]', [NORMAL_FILTER]: 'author_username',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
}, },
}, },
assignee_username: { assignee_username: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'assignee_username', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[assignee_username]', [NORMAL_FILTER]: 'assignee_username',
[SPECIAL_FILTER]: 'assignee_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'assignee_username[]', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[assignee_username][]', [NORMAL_FILTER]: 'assignee_username[]',
[SPECIAL_FILTER]: 'assignee_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
}, },
}, },
milestone: { milestone: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'milestone', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[milestone]', [NORMAL_FILTER]: 'milestone',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'milestone_title', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[milestone_title]', [NORMAL_FILTER]: 'milestone_title',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
},
}, },
}, },
labels: { labels: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'labels', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[labels]', [NORMAL_FILTER]: 'labels',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[labels]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'label_name[]', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[label_name][]', [NORMAL_FILTER]: 'label_name[]',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
}, },
}, },
my_reaction_emoji: { my_reaction_emoji: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'my_reaction_emoji', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'my_reaction_emoji', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
}, },
}, },
confidential: { confidential: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'confidential', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'confidential', [OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
}, },
}, },
iteration: { iteration: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'iteration_title', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[iteration_title]', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'iteration_title', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[iteration_title]', [NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
},
}, },
}, },
weight: { weight: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'weight', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[weight]', [NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
}, },
urlParam: { urlParam: {
[OPERATOR_IS]: 'weight', [OPERATOR_IS]: {
[OPERATOR_IS_NOT]: 'not[weight]', [NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
}, },
}, },
}; };
...@@ -11,12 +11,15 @@ import { ...@@ -11,12 +11,15 @@ import {
LABEL_PRIORITY_DESC, LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC, MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC, MILESTONE_DUE_DESC,
NORMAL_FILTER,
POPULARITY_ASC, POPULARITY_ASC,
POPULARITY_DESC, POPULARITY_DESC,
PRIORITY_ASC, PRIORITY_ASC,
PRIORITY_DESC, PRIORITY_DESC,
RELATIVE_POSITION_ASC, RELATIVE_POSITION_ASC,
sortParams, sortParams,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
UPDATED_ASC, UPDATED_ASC,
UPDATED_DESC, UPDATED_DESC,
WEIGHT_ASC, WEIGHT_ASC,
...@@ -124,13 +127,18 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) ...@@ -124,13 +127,18 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
const tokenTypes = Object.keys(filters); const tokenTypes = Object.keys(filters);
const urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam)); const getUrlParams = (tokenType) =>
Object.values(filters[tokenType].urlParam).flatMap((filterObj) => Object.values(filterObj));
const urlParamKeys = tokenTypes.flatMap(getUrlParams);
const getTokenTypeFromUrlParamKey = (urlParamKey) => const getTokenTypeFromUrlParamKey = (urlParamKey) =>
tokenTypes.find((key) => Object.values(filters[key].urlParam).includes(urlParamKey)); tokenTypes.find((tokenType) => getUrlParams(tokenType).includes(urlParamKey));
const getOperatorFromUrlParamKey = (tokenType, 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) => const convertToFilteredTokens = (locationSearch) =>
Array.from(new URLSearchParams(locationSearch).entries()) Array.from(new URLSearchParams(locationSearch).entries())
...@@ -164,11 +172,15 @@ export const getFilterTokens = (locationSearch) => { ...@@ -164,11 +172,15 @@ export const getFilterTokens = (locationSearch) => {
return filterTokens.concat(searchTokens); return filterTokens.concat(searchTokens);
}; };
const getFilterType = (data) =>
SPECIAL_FILTER_VALUES.includes(data) ? SPECIAL_FILTER : NORMAL_FILTER;
export const convertToApiParams = (filterTokens) => export const convertToApiParams = (filterTokens) =>
filterTokens filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM) .filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => { .reduce((acc, token) => {
const apiParam = filters[token.type].apiParam[token.value.operator]; const filterType = getFilterType(token.value.data);
const apiParam = filters[token.type].apiParam[token.value.operator][filterType];
return Object.assign(acc, { return Object.assign(acc, {
[apiParam]: acc[apiParam] ? `${acc[apiParam]},${token.value.data}` : token.value.data, [apiParam]: acc[apiParam] ? `${acc[apiParam]},${token.value.data}` : token.value.data,
}); });
...@@ -178,7 +190,8 @@ export const convertToUrlParams = (filterTokens) => ...@@ -178,7 +190,8 @@ export const convertToUrlParams = (filterTokens) =>
filterTokens filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM) .filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => { .reduce((acc, token) => {
const urlParam = filters[token.type].urlParam[token.value.operator]; const filterType = getFilterType(token.value.data);
const urlParam = filters[token.type].urlParam[token.value.operator]?.[filterType];
return Object.assign(acc, { return Object.assign(acc, {
[urlParam]: acc[urlParam] ? acc[urlParam].concat(token.value.data) : [token.value.data], [urlParam]: acc[urlParam] ? acc[urlParam].concat(token.value.data) : [token.value.data],
}); });
......
...@@ -3,21 +3,24 @@ import { __ } from '~/locale'; ...@@ -3,21 +3,24 @@ import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200; export const DEBOUNCE_DELAY = 200;
const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') }; export const FILTER_NONE = 'None';
export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') }; export const FILTER_ANY = 'Any';
export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') }; export const FILTER_CURRENT = 'Current';
export const DEFAULT_LABEL_CURRENT = { value: 'Current', text: __('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 = [ export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }];
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY, export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: 'Upcoming', text: __('Upcoming') }, { value: 'Upcoming', text: __('Upcoming') },
{ value: 'Started', text: __('Started') }, { value: 'Started', text: __('Started') },
]; ]);
export const SortDirection = { export const SortDirection = {
descending: 'descending', descending: 'descending',
......
...@@ -10,7 +10,7 @@ import { debounce } from 'lodash'; ...@@ -10,7 +10,7 @@ import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale'; 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'; import { stripQuotes } from '../filtered_search_utils';
export default { export default {
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
data() { data() {
return { return {
emojis: this.config.initialEmojis || [], emojis: this.config.initialEmojis || [],
defaultEmojis: this.config.defaultEmojis || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY], defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY,
loading: true, loading: true,
}; };
}, },
......
<script> <script>
import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui'; import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
import { DEFAULT_LABEL_ANY, DEFAULT_LABEL_NONE } from '../constants'; import { DEFAULT_NONE_ANY } from '../constants';
export default { export default {
baseWeights: ['0', '1', '2', '3', '4', '5'], baseWeights: ['0', '1', '2', '3', '4', '5'],
...@@ -22,7 +22,7 @@ export default { ...@@ -22,7 +22,7 @@ export default {
data() { data() {
return { return {
weights: this.$options.baseWeights, weights: this.$options.baseWeights,
defaultWeights: this.config.defaultWeights || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY], defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY,
}; };
}, },
methods: { methods: {
......
...@@ -20,6 +20,13 @@ export const locationSearch = [ ...@@ -20,6 +20,13 @@ export const locationSearch = [
'not[weight]=3', 'not[weight]=3',
].join('&'); ].join('&');
export const locationSearchWithSpecialValues = [
'assignee_id=None',
'my_reaction_emoji=None',
'iteration_id=Current',
'weight=None',
].join('&');
export const filteredTokens = [ export const filteredTokens = [
{ type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } }, { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } }, { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
...@@ -41,6 +48,13 @@ export const filteredTokens = [ ...@@ -41,6 +48,13 @@ export const filteredTokens = [
{ type: 'filtered-search-term', value: { data: 'issues' } }, { type: 'filtered-search-term', value: { data: 'issues' } },
]; ];
export const filteredTokensWithSpecialValues = [
{ type: 'assignee_username', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = { export const apiParams = {
author_username: 'homer', author_username: 'homer',
'not[author_username]': 'marge', 'not[author_username]': 'marge',
...@@ -58,6 +72,13 @@ export const apiParams = { ...@@ -58,6 +72,13 @@ export const apiParams = {
'not[weight]': '3', 'not[weight]': '3',
}; };
export const apiParamsWithSpecialValues = {
assignee_id: 'None',
my_reaction_emoji: 'None',
iteration_id: 'Current',
weight: 'None',
};
export const urlParams = { export const urlParams = {
author_username: ['homer'], author_username: ['homer'],
'not[author_username]': ['marge'], 'not[author_username]': ['marge'],
...@@ -74,3 +95,10 @@ export const urlParams = { ...@@ -74,3 +95,10 @@ export const urlParams = {
weight: ['1'], weight: ['1'],
'not[weight]': ['3'], 'not[weight]': ['3'],
}; };
export const urlParamsWithSpecialValues = {
assignee_id: ['None'],
my_reaction_emoji: ['None'],
iteration_id: ['Current'],
weight: ['None'],
};
import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data'; import {
apiParams,
apiParamsWithSpecialValues,
filteredTokens,
filteredTokensWithSpecialValues,
locationSearch,
locationSearchWithSpecialValues,
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data';
import { sortParams } from '~/issues_list/constants'; import { sortParams } from '~/issues_list/constants';
import { import {
convertToApiParams, convertToApiParams,
...@@ -53,18 +62,32 @@ describe('getFilterTokens', () => { ...@@ -53,18 +62,32 @@ describe('getFilterTokens', () => {
it('returns filtered tokens given "window.location.search"', () => { it('returns filtered tokens given "window.location.search"', () => {
expect(getFilterTokens(locationSearch)).toEqual(filteredTokens); expect(getFilterTokens(locationSearch)).toEqual(filteredTokens);
}); });
it('returns filtered tokens given "window.location.search" with special values', () => {
expect(getFilterTokens(locationSearchWithSpecialValues)).toEqual(
filteredTokensWithSpecialValues,
);
});
}); });
describe('convertToApiParams', () => { describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => { it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual(apiParams); expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
}); });
it('returns api params given filtered tokens with special values', () => {
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
}); });
describe('convertToUrlParams', () => { describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => { it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams); expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
}); });
it('returns url params given filtered tokens with special values', () => {
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
}); });
describe('convertToSearchQuery', () => { describe('convertToSearchQuery', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment