Commit e1bf0f6a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '229266-mlunoe-analytics-filter-module-list-values-follow-up' into 'master'

Follow-up on "Feat(Analytics filter module): lists + values"

See merge request gitlab-org/gitlab!40975
parents bc886f7f 3c6a8d53
import { isEmpty } from 'lodash';
import { queryToObject } from '~/lib/utils/url_utility';
/**
* Strips enclosing quotations from a string if it has one.
*
......@@ -29,3 +32,133 @@ export const uniqueTokens = tokens => {
return uniques;
}, []);
};
/**
* Creates a token from a type and a filter. Example returned object
* { type: 'myType', value: { data: 'myData', operator: '= '} }
* @param {String} type the name of the filter
* @param {Object}
* @param {Object.value} filter value to be returned as token data
* @param {Object.operator} filter operator to be retuned as token operator
* @return {Object}
* @return {Object.type} token type
* @return {Object.value} token value
*/
function createToken(type, filter) {
return { type, value: { data: filter.value, operator: filter.operator } };
}
/**
* This function takes a filter object and translates it into a token array
* @param {Object} filters
* @param {Object.myFilterName} a single filter value or an array of filters
* @return {Array} tokens an array of tokens created from filter values
*/
export function prepareTokens(filters = {}) {
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
return memo;
}
if (Array.isArray(value)) {
return [...memo, ...value.map(filterValue => createToken(key, filterValue))];
}
return [...memo, createToken(key, value)];
}, []);
}
export function processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
const tokenValue = value.data;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
}
/**
* This function takes a filter object and maps it into a query object. Example filter:
* { myFilterName: { value: 'foo', operator: '=' } }
* gets translated into:
* { myFilterName: 'foo', 'not[myFilterName]': null }
* @param {Object} filters
* @param {Object.myFilterName} a single filter value or an array of filters
* @return {Object} query object with both filter name and not-name with values
*/
export function filterToQueryObject(filters = {}) {
return Object.keys(filters).reduce((memo, key) => {
const filter = filters[key];
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);
} else {
selected = filter?.operator === '=' ? filter.value : null;
unselected = filter?.operator === '!=' ? filter.value : null;
}
if (isEmpty(selected)) {
selected = null;
}
if (isEmpty(unselected)) {
unselected = null;
}
return { ...memo, [key]: selected, [`not[${key}]`]: unselected };
}, {});
}
/**
* Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter`
* 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.operator} `=` or `!=`
*/
function extractNameAndOperator(filterName) {
// eslint-disable-next-line @gitlab/require-i18n-strings
if (filterName.startsWith('not[') && filterName.endsWith(']')) {
return { filterName: filterName.slice(4, -1), operator: '!=' };
}
return { filterName, operator: '=' };
}
/**
* 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`
* @return {Object} filter object with filter names and their values
*/
export function urlQueryToFilter(query = '') {
const filters = queryToObject(query, { gatherArrays: true });
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
return memo;
}
const { filterName, operator } = extractNameAndOperator(key);
let previousValues = [];
if (Array.isArray(memo[filterName])) {
previousValues = memo[filterName];
}
if (Array.isArray(value)) {
const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator }));
return { ...memo, [filterName]: [...previousValues, ...newAdditions] };
}
return { ...memo, [filterName]: { value, operator } };
}, {});
}
---
title: Fixed an issue where not all URL query parameters would apply to the filter
bar on initial load in the Value Stream Analytics page
merge_request: 40975
author:
type: fixed
......@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import MilestoneToken from '../../shared/components/tokens/milestone_token.vue';
import LabelToken from '../../shared/components/tokens/label_token.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { processFilters } from '../../shared/utils';
import { processFilters } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
export default {
components: {
......
......@@ -10,7 +10,11 @@ import {
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { prepareTokens, processFilters } from '../../shared/utils';
import {
prepareTokens,
processFilters,
filterToQueryObject,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
export default {
name: 'FilterBar',
......@@ -75,6 +79,7 @@ export default {
title: __('Assignees'),
type: 'assignees',
token: AuthorToken,
defaultAuthors: [],
initialAuthors: this.assigneesData,
unique: false,
operators: [{ value: '=', description: 'is', default: 'true' }],
......@@ -83,17 +88,12 @@ export default {
];
},
query() {
const selectedLabelList = this.selectedLabelList?.length ? this.selectedLabelList : null;
const selectedAssigneeList = this.selectedAssigneeList?.length
? this.selectedAssigneeList
: null;
return {
return filterToQueryObject({
milestone_title: this.selectedMilestone,
author_username: this.selectedAuthor,
label_name: selectedLabelList,
assignee_username: selectedAssigneeList,
};
label_name: this.selectedLabelList,
assignee_username: this.selectedAssigneeList,
});
},
},
methods: {
......@@ -105,22 +105,21 @@ export default {
'fetchAssignees',
]),
initialFilterValue() {
const {
selectedMilestone: milestone = null,
selectedAuthor: author = null,
selectedAssigneeList: assignees = [],
selectedLabelList: labels = [],
} = this;
return prepareTokens({ milestone, author, assignees, labels });
return prepareTokens({
milestone: this.selectedMilestone,
author: this.selectedAuthor,
assignees: this.selectedAssigneeList,
labels: this.selectedLabelList,
});
},
handleFilter(filters) {
const { labels, milestone, author, assignees } = processFilters(filters);
this.setFilters({
selectedAuthor: author ? author[0].value : null,
selectedMilestone: milestone ? milestone[0].value : null,
selectedAssigneeList: assignees ? assignees.map(a => a.value) : [],
selectedLabelList: labels ? labels.map(l => l.value) : [],
selectedAuthor: author ? author[0] : null,
selectedMilestone: milestone ? milestone[0] : null,
selectedAssigneeList: assignees || [],
selectedLabelList: labels || [],
});
},
},
......
......@@ -3,6 +3,7 @@ import { GlToast } from '@gitlab/ui';
import CycleAnalytics from './components/base.vue';
import createStore from './store';
import { buildCycleAnalyticsInitialData } from '../shared/utils';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast);
......@@ -19,8 +20,19 @@ export default () => {
analyticsSimilaritySearch: hasAnalyticsSimilaritySearch = false,
} = gon?.features;
const {
author_username = null,
milestone_title = null,
assignee_username = [],
label_name = [],
} = urlQueryToFilter(window.location.search);
store.dispatch('initializeCycleAnalytics', {
...initialData,
selectedAuthor: author_username,
selectedMilestone: milestone_title,
selectedAssigneeList: assignee_username,
selectedLabelList: label_name,
featureFlags: {
hasDurationChart,
hasPathNavigation,
......
......@@ -283,8 +283,8 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
labelsPath,
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
selectedAssigneeList,
selectedLabelList,
} = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags);
......@@ -294,8 +294,8 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
dispatch('filters/initialize', {
selectedAuthor,
selectedMilestone,
selectedAssignees,
selectedLabels,
selectedAssigneeList,
selectedLabelList,
}),
dispatch('durationChart/setLoading', true),
dispatch('typeOfWork/setLoading', true),
......
import dateFormat from 'dateformat';
import { dateFormats } from './constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { dateFormats } from './constants';
export const toYmd = date => dateFormat(date, dateFormats.isoDate);
......@@ -79,10 +79,6 @@ export const buildCycleAnalyticsInitialData = ({
groupFullPath = null,
groupParentId = null,
groupAvatarUrl = null,
author = null,
milestone = null,
labels = null,
assignees = null,
labelsPath = '',
milestonesPath = '',
} = {}) => ({
......@@ -102,10 +98,6 @@ export const buildCycleAnalyticsInitialData = ({
selectedProjects: projects
? buildProjectsFromJSON(projects).map(convertObjectPropsToCamelCase)
: [],
selectedAuthor: author,
selectedMilestone: milestone,
selectedLabels: labels ? JSON.parse(labels) : [],
selectedAssignees: assignees ? JSON.parse(assignees) : [],
labelsPath,
milestonesPath,
});
......@@ -114,28 +106,3 @@ export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'na
if (!searchTerm?.length) return data;
return data.filter(item => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
};
export const prepareTokens = (tokens = {}) => {
const { milestone = null, author = null, assignees = [], labels = [] } = tokens;
const authorToken = author ? [{ type: 'author', value: { data: author } }] : [];
const milestoneToken = milestone ? [{ type: 'milestone', value: { data: milestone } }] : [];
const assigneeTokens = assignees?.map(data => ({ type: 'assignees', value: { data } })) || [];
const labelTokens = labels?.map(data => ({ type: 'labels', value: { data } })) || [];
return [...authorToken, ...milestoneToken, ...assigneeTokens, ...labelTokens];
};
export function processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
const tokenValue = value.data;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
}
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import * as utils from 'ee/analytics/shared/utils';
import FilterBar from 'ee/analytics/code_review_analytics/components/filter_bar.vue';
import createFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { mockMilestones, mockLabels } from '../mock_data';
const localVue = createLocalVue();
......
......@@ -2,12 +2,12 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as utils from 'ee/analytics/shared/utils';
import storeConfig from 'ee/analytics/cycle_analytics/store';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import initialFiltersState from 'ee/analytics/shared/store/modules/filters/state';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { filterMilestones, filterLabels } from '../../shared/store/modules/filters/mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
......@@ -29,9 +29,13 @@ const initialFilterBarState = {
const defaultParams = {
milestone_title: null,
'not[milestone_title]': null,
author_username: null,
'not[author_username]': null,
assignee_username: null,
'not[assignee_username]': null,
label_name: null,
'not[label_name]': null,
};
async function shouldMergeUrlParams(wrapper, result) {
......@@ -167,8 +171,8 @@ describe('Filter bar', () => {
expect(setFiltersMock).toHaveBeenCalledWith(
expect.anything(),
{
selectedLabelList: [selectedLabelList[0].title],
selectedMilestone: selectedMilestone[0].title,
selectedLabelList: [{ value: selectedLabelList[0].title, operator: '=' }],
selectedMilestone: { value: selectedMilestone[0].title, operator: '=' },
selectedAssigneeList: [],
selectedAuthor: null,
},
......@@ -177,13 +181,22 @@ describe('Filter bar', () => {
});
});
describe.each`
stateKey | payload | paramKey
${'selectedMilestone'} | ${'12.0'} | ${'milestone_title'}
${'selectedAuthor'} | ${'rootUser'} | ${'author_username'}
${'selectedLabelList'} | ${['Afternix', 'Brouceforge']} | ${'label_name'}
${'selectedAssigneeList'} | ${['rootUser', 'secondaryUser']} | ${'assignee_username'}
`('with a $stateKey updates the $paramKey url parameter', ({ stateKey, payload, paramKey }) => {
describe.each([
['selectedMilestone', 'milestone_title', { value: '12.0', operator: '=' }, '12.0'],
['selectedAuthor', 'author_username', { value: 'rootUser', operator: '=' }, 'rootUser'],
[
'selectedLabelList',
'label_name',
[{ value: 'Afternix', operator: '=' }, { value: 'Brouceforge', operator: '=' }],
['Afternix', 'Brouceforge'],
],
[
'selectedAssigneeList',
'assignee_username',
[{ value: 'rootUser', operator: '=' }, { value: 'secondaryUser', operator: '=' }],
['rootUser', 'secondaryUser'],
],
])('with a %s updates the %s url parameter', (stateKey, paramKey, payload, result) => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.mergeUrlParams = jest.fn();
......@@ -199,7 +212,7 @@ describe('Filter bar', () => {
it(`sets the ${paramKey} url parameter`, () => {
return shouldMergeUrlParams(wrapper, {
...defaultParams,
[paramKey]: payload,
[paramKey]: result,
});
});
});
......
......@@ -3,8 +3,6 @@ import {
buildProjectFromDataset,
buildCycleAnalyticsInitialData,
filterBySearchTerm,
prepareTokens,
processFilters,
} from 'ee/analytics/shared/utils';
const groupDataset = {
......@@ -83,10 +81,6 @@ describe('buildCycleAnalyticsInitialData', () => {
${'createdBefore'} | ${null}
${'createdAfter'} | ${null}
${'selectedProjects'} | ${[]}
${'selectedAuthor'} | ${null}
${'selectedMilestone'} | ${null}
${'selectedLabels'} | ${[]}
${'selectedAssignees'} | ${[]}
${'labelsPath'} | ${''}
${'milestonesPath'} | ${''}
`('will set a default value for "$field" if is not present', ({ field, value }) => {
......@@ -140,62 +134,6 @@ describe('buildCycleAnalyticsInitialData', () => {
});
});
describe('selectedAssignees', () => {
it('will be set given an array of assignees', () => {
const selectedAssignees = ['krillin', 'chiao-tzu'];
expect(
buildCycleAnalyticsInitialData({ assignees: JSON.stringify(selectedAssignees) }),
).toMatchObject({
selectedAssignees,
});
});
it.each`
field | value
${'selectedAssignees'} | ${null}
${'selectedAssignees'} | ${[]}
${'selectedAssignees'} | ${''}
`('will be an empty array if given a value of `$value`', ({ value, field }) => {
expect(buildCycleAnalyticsInitialData({ projects: value })).toMatchObject({
[field]: [],
});
});
});
describe('selectedLabels', () => {
it('will be set given an array of labels', () => {
const selectedLabels = ['krillin', 'chiao-tzu'];
expect(
buildCycleAnalyticsInitialData({ labels: JSON.stringify(selectedLabels) }),
).toMatchObject({ selectedLabels });
});
it.each`
field | value
${'selectedLabels'} | ${null}
${'selectedLabels'} | ${[]}
${'selectedLabels'} | ${''}
`('will be an empty array if given a value of `$value`', ({ value, field }) => {
expect(buildCycleAnalyticsInitialData({ projects: value })).toMatchObject({
[field]: [],
});
});
});
describe.each`
field | key | value
${'milestone'} | ${'selectedMilestone'} | ${'cell-saga'}
${'author'} | ${'selectedAuthor'} | ${'cell'}
`('$field', ({ field, value, key }) => {
it(`will set ${key} field with the given value`, () => {
expect(buildCycleAnalyticsInitialData({ [field]: value })).toMatchObject({ [key]: value });
});
it(`will set ${key} to null if omitted`, () => {
expect(buildCycleAnalyticsInitialData()).toMatchObject({ [key]: null });
});
});
describe.each`
field | value
${'createdBefore'} | ${'2019-12-31'}
......@@ -235,50 +173,3 @@ describe('buildCycleAnalyticsInitialData', () => {
});
});
});
describe('prepareTokens', () => {
describe('with empty data', () => {
it('returns an empty array', () => {
expect(prepareTokens()).toEqual([]);
expect(prepareTokens({})).toEqual([]);
expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
[],
);
});
});
it.each`
token | value | result
${'milestone'} | ${'v1.0'} | ${[{ type: 'milestone', value: { data: 'v1.0' } }]}
${'author'} | ${'mr.popo'} | ${[{ type: 'author', value: { data: 'mr.popo' } }]}
${'labels'} | ${['z-fighters']} | ${[{ type: 'labels', value: { data: 'z-fighters' } }]}
${'assignees'} | ${['krillin', 'piccolo']} | ${[{ type: 'assignees', value: { data: 'krillin' } }, { type: 'assignees', value: { data: 'piccolo' } }]}
`('with $token=$value sets the $token key', ({ token, value, result }) => {
const res = prepareTokens({ [token]: value });
expect(res).toEqual(result);
});
});
describe('processFilters', () => {
it('processes multiple filter values', () => {
const result = processFilters([
{ type: 'milestone', value: { data: 'my-milestone', operator: '=' } },
{ type: 'labels', value: { data: 'my-label', operator: '=' } },
]);
expect(result).toStrictEqual({
labels: [{ value: 'my-label', operator: '=' }],
milestone: [{ value: 'my-milestone', operator: '=' }],
});
});
it('does not remove wrapping double quotes from the data', () => {
const result = processFilters([
{ type: 'milestone', value: { data: '"milestone with spaces"', operator: '=' } },
]);
expect(result).toStrictEqual({
milestone: [{ value: '"milestone with spaces"', operator: '=' }],
});
});
});
import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
stripQuotes,
uniqueTokens,
prepareTokens,
processFilters,
filterToQueryObject,
urlQueryToFilter,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
tokenValueAuthor,
......@@ -20,7 +27,7 @@ describe('Filtered Search Utils', () => {
`(
'returns string $outputValue when called with string $inputValue',
({ inputValue, outputValue }) => {
expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue);
expect(stripQuotes(inputValue)).toBe(outputValue);
},
);
});
......@@ -28,7 +35,7 @@ describe('Filtered Search Utils', () => {
describe('uniqueTokens', () => {
it('returns tokens array with duplicates removed', () => {
expect(
filteredSearchUtils.uniqueTokens([
uniqueTokens([
tokenValueAuthor,
tokenValueLabel,
tokenValueMilestone,
......@@ -40,13 +47,172 @@ describe('Filtered Search Utils', () => {
it('returns tokens array as it is if it does not have duplicates', () => {
expect(
filteredSearchUtils.uniqueTokens([
tokenValueAuthor,
tokenValueLabel,
tokenValueMilestone,
tokenValuePlain,
]),
uniqueTokens([tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValuePlain]),
).toHaveLength(4);
});
});
});
describe('prepareTokens', () => {
describe('with empty data', () => {
it('returns an empty array', () => {
expect(prepareTokens()).toEqual([]);
expect(prepareTokens({})).toEqual([]);
expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
[],
);
});
});
it.each([
[
'milestone',
{ value: 'v1.0', operator: '=' },
[{ type: 'milestone', value: { data: 'v1.0', operator: '=' } }],
],
[
'author',
{ value: 'mr.popo', operator: '!=' },
[{ type: 'author', value: { data: 'mr.popo', operator: '!=' } }],
],
[
'labels',
[{ value: 'z-fighters', operator: '=' }],
[{ type: 'labels', value: { data: 'z-fighters', operator: '=' } }],
],
[
'assignees',
[{ value: 'krillin', operator: '=' }, { value: 'piccolo', operator: '!=' }],
[
{ type: 'assignees', value: { data: 'krillin', operator: '=' } },
{ type: 'assignees', value: { data: 'piccolo', operator: '!=' } },
],
],
[
'foo',
[{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
[
{ type: 'foo', value: { data: 'bar', operator: '!=' } },
{ type: 'foo', value: { data: 'baz', operator: '!=' } },
],
],
])('gathers %s=%j into result=%j', (token, value, result) => {
const res = prepareTokens({ [token]: value });
expect(res).toEqual(result);
});
});
describe('processFilters', () => {
it('processes multiple filter values', () => {
const result = processFilters([
{ type: 'foo', value: { data: 'foo', operator: '=' } },
{ type: 'bar', value: { data: 'bar1', operator: '=' } },
{ type: 'bar', value: { data: 'bar2', operator: '!=' } },
]);
expect(result).toStrictEqual({
foo: [{ value: 'foo', operator: '=' }],
bar: [{ value: 'bar1', operator: '=' }, { value: 'bar2', operator: '!=' }],
});
});
it('does not remove wrapping double quotes from the data', () => {
const result = processFilters([
{ type: 'foo', value: { data: '"value with spaces"', operator: '=' } },
]);
expect(result).toStrictEqual({
foo: [{ value: '"value with spaces"', operator: '=' }],
});
});
});
describe('filterToQueryObject', () => {
describe('with empty data', () => {
it('returns an empty object', () => {
expect(filterToQueryObject()).toEqual({});
expect(filterToQueryObject({})).toEqual({});
expect(filterToQueryObject({ author_username: null, label_name: [] })).toEqual({
author_username: null,
label_name: null,
'not[author_username]': null,
'not[label_name]': null,
});
});
});
it.each([
[
'author_username',
{ value: 'v1.0', operator: '=' },
{ author_username: 'v1.0', 'not[author_username]': null },
],
[
'author_username',
{ value: 'v1.0', operator: '!=' },
{ author_username: null, 'not[author_username]': 'v1.0' },
],
[
'label_name',
[{ value: 'z-fighters', operator: '=' }],
{ label_name: ['z-fighters'], 'not[label_name]': null },
],
[
'label_name',
[{ value: 'z-fighters', operator: '!=' }],
{ label_name: null, 'not[label_name]': ['z-fighters'] },
],
[
'foo',
[{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }],
{ foo: ['bar', 'baz'], 'not[foo]': null },
],
[
'foo',
[{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
{ foo: null, 'not[foo]': ['bar', 'baz'] },
],
[
'foo',
[{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '=' }],
{ foo: ['baz'], 'not[foo]': ['bar'] },
],
])('gathers filter values %s=%j into query object=%j', (token, value, result) => {
const res = filterToQueryObject({ [token]: value });
expect(res).toEqual(result);
});
});
describe('urlQueryToFilter', () => {
describe('with empty data', () => {
it('returns an empty object', () => {
expect(urlQueryToFilter()).toEqual({});
expect(urlQueryToFilter('')).toEqual({});
expect(urlQueryToFilter('author_username=&milestone_title=&')).toEqual({});
});
});
it.each([
['author_username=v1.0', { author_username: { value: 'v1.0', operator: '=' } }],
['not[author_username]=v1.0', { author_username: { value: 'v1.0', operator: '!=' } }],
['foo=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
['foo=bar&foo[]=baz', { foo: [{ value: 'baz', operator: '=' }] }],
['not[foo]=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
[
'foo[]=bar&foo[]=baz&not[foo]=',
{ foo: [{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }] },
],
[
'foo[]=&not[foo][]=bar&not[foo][]=baz',
{ foo: [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }] },
],
[
'foo[]=baz&not[foo][]=bar',
{ foo: [{ value: 'baz', operator: '=' }, { value: 'bar', operator: '!=' }] },
],
['not[foo][]=bar', { foo: [{ value: 'bar', operator: '!=' }] }],
])('gathers filter values %s into query object=%j', (query, result) => {
const res = urlQueryToFilter(query);
expect(res).toEqual(result);
});
});
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