fix(filters): parse labels and projects correctly when using in filter operator

This commit is contained in:
kolaente 2024-03-11 15:16:39 +01:00
parent 3896c680d3
commit 0529f30e77
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
2 changed files with 118 additions and 64 deletions

View File

@ -17,7 +17,7 @@ describe('Filter Transformation', () => {
'assignees': 'assignees', 'assignees': 'assignees',
'labels': 'labels', 'labels': 'labels',
} }
describe('For api', () => { describe('For api', () => {
for (const c in fieldCases) { for (const c in fieldCases) {
it('should transform all filter params for ' + c + ' to snake_case', () => { it('should transform all filter params for ' + c + ' to snake_case', () => {
@ -37,23 +37,35 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('labels = 1') 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', () => { it('should correctly resolve multiple labels', () => {
const transformed = transformFilterStringForApi( const transformed = transformFilterStringForApi(
'labels = lorem && dueDate = now && labels = ipsum', 'labels = lorem && dueDate = now && labels = ipsum',
(title: string) => { multipleDummyResolver,
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
},
nullTitleToIdResolver, 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', () => { it('should correctly resolve projects', () => {
@ -70,19 +82,20 @@ describe('Filter Transformation', () => {
const transformed = transformFilterStringForApi( const transformed = transformFilterStringForApi(
'project = lorem && dueDate = now || project = ipsum', 'project = lorem && dueDate = now || project = ipsum',
nullTitleToIdResolver, nullTitleToIdResolver,
(title: string) => { multipleDummyResolver,
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
},
) )
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') 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', () => { it('should correctly resolve multiple labels', () => {
const transformed = transformFilterStringFromApi( const transformed = transformFilterStringFromApi(
'labels = 1 && due_date = now && labels = 2', 'labels = 1 && due_date = now && labels = 2',
(id: number) => { multipleIdToTitleResolver,
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
},
nullIdToTitleResolver, 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', () => { it('should correctly resolve projects', () => {
@ -136,21 +161,22 @@ describe('Filter Transformation', () => {
it('should correctly resolve multiple projects', () => { it('should correctly resolve multiple projects', () => {
const transformed = transformFilterStringFromApi( const transformed = transformFilterStringFromApi(
'project = lorem && due_date = now || project = ipsum', 'project = 1 && due_date = now || project = 2',
nullIdToTitleResolver, nullIdToTitleResolver,
(id: number) => { multipleIdToTitleResolver,
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
},
) )
expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum') 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')
})
}) })
}) })

View File

@ -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 { function getFieldPattern(field: string): RegExp {
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig')
@ -66,11 +66,11 @@ export function transformFilterStringForApi(
labelResolver: (title: string) => number | null, labelResolver: (title: string) => number | null,
projectResolver: (title: string) => number | null, projectResolver: (title: string) => number | null,
): string { ): string {
if (filter.trim() === '') { if (filter.trim() === '') {
return '' return ''
} }
// Transform labels to ids // Transform labels to ids
LABEL_FIELDS.forEach(field => { LABEL_FIELDS.forEach(field => {
const pattern = getFieldPattern(field) const pattern = getFieldPattern(field)
@ -80,10 +80,17 @@ export function transformFilterStringForApi(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match const [matched, prefix, operator, space, keyword] = match
if (keyword) { if (keyword) {
const labelId = labelResolver(keyword.trim()) let keywords = [keyword.trim()]
if (labelId !== null) { if (operator === 'in' || operator === '?=') {
filter = filter.replace(keyword, String(labelId)) 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match const [matched, prefix, operator, space, keyword] = match
if (keyword) { if (keyword) {
const projectId = projectResolver(keyword.trim()) let keywords = [keyword.trim()]
if (projectId !== null) { if (operator === 'in' || operator === '?=') {
filter = filter.replace(keyword, String(projectId)) 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, labelResolver: (id: number) => string | null,
projectResolver: (id: number) => string | null, projectResolver: (id: number) => string | null,
): string { ): string {
if (filter.trim() === '') { if (filter.trim() === '') {
return '' return ''
} }
// Transform all attributes from snake case // Transform all attributes from snake case
AVAILABLE_FILTER_FIELDS.forEach(f => { AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replace(snakeCase(f), f) filter = filter.replace(snakeCase(f), f)
}) })
// Transform labels to their titles // Transform labels to their titles
LABEL_FIELDS.forEach(field => { LABEL_FIELDS.forEach(field => {
const pattern = getFieldPattern(field) const pattern = getFieldPattern(field)
@ -136,10 +150,17 @@ export function transformFilterStringFromApi(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match const [matched, prefix, operator, space, keyword] = match
if (keyword) { if (keyword) {
const labelTitle = labelResolver(Number(keyword.trim())) let keywords = [keyword.trim()]
if (labelTitle !== null) { if (operator === 'in' || operator === '?=') {
filter = filter.replace(keyword, labelTitle) 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match const [matched, prefix, operator, space, keyword] = match
if (keyword) { if (keyword) {
const project = projectResolver(Number(keyword.trim())) let keywords = [keyword.trim()]
if (project !== null) { if (operator === 'in' || operator === '?=') {
filter = filter.replace(keyword, project) keywords = keyword.trim().split(',').map(k => k.trim())
} }
keywords.forEach(k => {
const project = projectResolver(parseInt(k))
if (project !== null) {
filter = filter.replace(k, project)
}
})
} }
} }
}) })