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 {
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,
});
}
......
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]',
},
},
},
};
......@@ -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,18 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
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) =>
tokenTypes.find((key) => Object.values(filters[key].urlParam).includes(urlParamKey));
tokenTypes.find((tokenType) => getUrlParams(tokenType).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())
......@@ -164,11 +172,15 @@ export const getFilterTokens = (locationSearch) => {
return filterTokens.concat(searchTokens);
};
const getFilterType = (data) =>
SPECIAL_FILTER_VALUES.includes(data) ? SPECIAL_FILTER : NORMAL_FILTER;
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 = getFilterType(token.value.data);
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 +190,8 @@ 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 = getFilterType(token.value.data);
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],
});
......
......@@ -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',
......
......@@ -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,
};
},
......
<script>
import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
import { DEFAULT_LABEL_ANY, DEFAULT_LABEL_NONE } from '../constants';
import { DEFAULT_NONE_ANY } from '../constants';
export default {
baseWeights: ['0', '1', '2', '3', '4', '5'],
......@@ -22,7 +22,7 @@ export default {
data() {
return {
weights: this.$options.baseWeights,
defaultWeights: this.config.defaultWeights || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY],
defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY,
};
},
methods: {
......
......@@ -20,6 +20,13 @@ export const locationSearch = [
'not[weight]=3',
].join('&');
export const locationSearchWithSpecialValues = [
'assignee_id=None',
'my_reaction_emoji=None',
'iteration_id=Current',
'weight=None',
].join('&');
export const filteredTokens = [
{ type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
......@@ -41,6 +48,13 @@ export const filteredTokens = [
{ 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 = {
author_username: 'homer',
'not[author_username]': 'marge',
......@@ -58,6 +72,13 @@ export const apiParams = {
'not[weight]': '3',
};
export const apiParamsWithSpecialValues = {
assignee_id: 'None',
my_reaction_emoji: 'None',
iteration_id: 'Current',
weight: 'None',
};
export const urlParams = {
author_username: ['homer'],
'not[author_username]': ['marge'],
......@@ -74,3 +95,10 @@ export const urlParams = {
weight: ['1'],
'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 {
convertToApiParams,
......@@ -53,18 +62,32 @@ describe('getFilterTokens', () => {
it('returns filtered tokens given "window.location.search"', () => {
expect(getFilterTokens(locationSearch)).toEqual(filteredTokens);
});
it('returns filtered tokens given "window.location.search" with special values', () => {
expect(getFilterTokens(locationSearchWithSpecialValues)).toEqual(
filteredTokensWithSpecialValues,
);
});
});
describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
});
describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
it('returns url params given filtered tokens with special values', () => {
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});
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