diff --git a/frontend/src/helpers/filters.test.ts b/frontend/src/helpers/filters.test.ts index a1910ff26..6f04f5757 100644 --- a/frontend/src/helpers/filters.test.ts +++ b/frontend/src/helpers/filters.test.ts @@ -17,7 +17,7 @@ describe('Filter Transformation', () => { 'assignees': 'assignees', 'labels': 'labels', } - + describe('For api', () => { for (const c in fieldCases) { it('should transform all filter params for ' + c + ' to snake_case', () => { @@ -37,23 +37,35 @@ describe('Filter Transformation', () => { expect(transformed).toBe('labels = 1') }) + const multipleDummyResolver = (title: string) => { + switch (title) { + case 'lorem': + return 1 + case 'ipsum': + return 2 + default: + return null + } + } + it('should correctly resolve multiple labels', () => { const transformed = transformFilterStringForApi( 'labels = lorem && dueDate = now && labels = ipsum', - (title: string) => { - switch (title) { - case 'lorem': - return 1 - case 'ipsum': - return 2 - default: - return null - } - }, + multipleDummyResolver, nullTitleToIdResolver, ) - expect(transformed).toBe('labels = 1&& due_date = now && labels = 2') + expect(transformed).toBe('labels = 1 && due_date = now && labels = 2') + }) + + it('should correctly resolve multiple labels with an in clause', () => { + const transformed = transformFilterStringForApi( + 'labels in lorem, ipsum && dueDate = now', + multipleDummyResolver, + nullTitleToIdResolver, + ) + + expect(transformed).toBe('labels in 1, 2 && due_date = now') }) it('should correctly resolve projects', () => { @@ -70,19 +82,20 @@ describe('Filter Transformation', () => { const transformed = transformFilterStringForApi( 'project = lorem && dueDate = now || project = ipsum', nullTitleToIdResolver, - (title: string) => { - switch (title) { - case 'lorem': - return 1 - case 'ipsum': - return 2 - default: - return null - } - }, + multipleDummyResolver, ) - expect(transformed).toBe('project = 1&& due_date = now || project = 2') + expect(transformed).toBe('project = 1 && due_date = now || project = 2') + }) + + it('should correctly resolve multiple projects with in', () => { + const transformed = transformFilterStringForApi( + 'project in lorem, ipsum', + nullTitleToIdResolver, + multipleDummyResolver, + ) + + expect(transformed).toBe('project in 1, 2') }) }) @@ -104,24 +117,36 @@ describe('Filter Transformation', () => { expect(transformed).toBe('labels = lorem') }) - + + const multipleIdToTitleResolver = (id: number) => { + switch (id) { + case 1: + return 'lorem' + case 2: + return 'ipsum' + default: + return null + } + } + it('should correctly resolve multiple labels', () => { const transformed = transformFilterStringFromApi( 'labels = 1 && due_date = now && labels = 2', - (id: number) => { - switch (id) { - case 1: - return 'lorem' - case 2: - return 'ipsum' - default: - return null - } - }, + multipleIdToTitleResolver, nullIdToTitleResolver, ) - expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum') + expect(transformed).toBe('labels = lorem && dueDate = now && labels = ipsum') + }) + + it('should correctly resolve multiple labels in', () => { + const transformed = transformFilterStringFromApi( + 'labels in 1, 2', + multipleIdToTitleResolver, + nullIdToTitleResolver, + ) + + expect(transformed).toBe('labels in lorem, ipsum') }) it('should correctly resolve projects', () => { @@ -136,21 +161,22 @@ describe('Filter Transformation', () => { it('should correctly resolve multiple projects', () => { const transformed = transformFilterStringFromApi( - 'project = lorem && due_date = now || project = ipsum', + 'project = 1 && due_date = now || project = 2', nullIdToTitleResolver, - (id: number) => { - switch (id) { - case 1: - return 'lorem' - case 2: - return 'ipsum' - default: - return null - } - }, + multipleIdToTitleResolver, ) expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum') }) + + it('should correctly resolve multiple projects in', () => { + const transformed = transformFilterStringFromApi( + 'project in 1, 2', + nullIdToTitleResolver, + multipleIdToTitleResolver, + ) + + expect(transformed).toBe('project in lorem, ipsum') + }) }) }) diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 87d2b7263..9f720e5d9 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -55,7 +55,7 @@ export const FILTER_JOIN_OPERATOR = [ ')', ] -export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)' +export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|in)' function getFieldPattern(field: string): RegExp { return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') @@ -66,11 +66,11 @@ export function transformFilterStringForApi( labelResolver: (title: string) => number | null, projectResolver: (title: string) => number | null, ): string { - + if (filter.trim() === '') { return '' } - + // Transform labels to ids LABEL_FIELDS.forEach(field => { const pattern = getFieldPattern(field) @@ -80,10 +80,17 @@ export function transformFilterStringForApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const labelId = labelResolver(keyword.trim()) - if (labelId !== null) { - filter = filter.replace(keyword, String(labelId)) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const labelId = labelResolver(k) + if (labelId !== null) { + filter = filter.replace(k, String(labelId)) + } + }) } } }) @@ -96,10 +103,17 @@ export function transformFilterStringForApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const projectId = projectResolver(keyword.trim()) - if (projectId !== null) { - filter = filter.replace(keyword, String(projectId)) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const projectId = projectResolver(k) + if (projectId !== null) { + filter = filter.replace(k, String(projectId)) + } + }) } } }) @@ -117,16 +131,16 @@ export function transformFilterStringFromApi( labelResolver: (id: number) => string | null, projectResolver: (id: number) => string | null, ): string { - + if (filter.trim() === '') { return '' } - + // Transform all attributes from snake case AVAILABLE_FILTER_FIELDS.forEach(f => { filter = filter.replace(snakeCase(f), f) }) - + // Transform labels to their titles LABEL_FIELDS.forEach(field => { const pattern = getFieldPattern(field) @@ -136,10 +150,17 @@ export function transformFilterStringFromApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const labelTitle = labelResolver(Number(keyword.trim())) - if (labelTitle !== null) { - filter = filter.replace(keyword, labelTitle) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const labelTitle = labelResolver(parseInt(k)) + if (labelTitle !== null) { + filter = filter.replace(k, labelTitle) + } + }) } } }) @@ -153,10 +174,17 @@ export function transformFilterStringFromApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const project = projectResolver(Number(keyword.trim())) - if (project !== null) { - filter = filter.replace(keyword, project) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const project = projectResolver(parseInt(k)) + if (project !== null) { + filter = filter.replace(k, project) + } + }) } } })