1
0

feat(filter): resolve label and project ids back to titles when loading a filter

This commit is contained in:
kolaente
2024-03-08 18:59:26 +01:00
parent 55b806d311
commit 1d2f3ca546
4 changed files with 198 additions and 68 deletions

View File

@ -1,10 +1,11 @@
import {describe, expect, it} from 'vitest'
import {transformFilterStringForApi} from '@/helpers/filters'
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
const nullResolver = (title: string) => null
const nullTitleToIdResolver = (title: string) => null
const nullIdToTitleResolver = (id: number) => null
describe('Filter Transformation', () => {
const cases = {
const fieldCases = {
'done': 'done',
'priority': 'priority',
'percentDone': 'percent_done',
@ -16,70 +17,140 @@ 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', () => {
const transformed = transformFilterStringForApi(c + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver)
for (const c in cases) {
it('should transform all filter params for ' + c + ' to snake_case', () => {
const transformed = transformFilterStringForApi(c + ' = ipsum', nullResolver, nullResolver)
expect(transformed).toBe(fieldCases[c] + ' = ipsum')
})
}
expect(transformed).toBe(cases[c] + ' = ipsum')
it('should correctly resolve labels', () => {
const transformed = transformFilterStringForApi(
'labels = lorem',
(title: string) => 1,
nullTitleToIdResolver,
)
expect(transformed).toBe('labels = 1')
})
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
}
},
nullTitleToIdResolver,
)
expect(transformed).toBe('labels = 1&& due_date = now && labels = 2')
})
it('should correctly resolve projects', () => {
const transformed = transformFilterStringForApi(
'project = lorem',
nullTitleToIdResolver,
(title: string) => 1,
)
expect(transformed).toBe('project = 1')
})
it('should correctly resolve multiple projects', () => {
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
}
},
)
expect(transformed).toBe('project = 1&& due_date = now || project = 2')
})
})
describe('To API', () => {
for (const c in fieldCases) {
it('should transform all filter params for ' + c + ' to snake_case', () => {
const transformed = transformFilterStringFromApi(fieldCases[c] + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver)
expect(transformed).toBe(c + ' = ipsum')
})
}
it('should correctly resolve labels', () => {
const transformed = transformFilterStringFromApi(
'labels = 1',
(id: number) => 'lorem',
nullIdToTitleResolver,
)
expect(transformed).toBe('labels = lorem')
})
}
it('should correctly resolve labels', () => {
const transformed = transformFilterStringForApi(
'labels = lorem',
(title: string) => 1,
nullResolver,
)
expect(transformed).toBe('labels = 1')
})
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
}
},
nullIdToTitleResolver,
)
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
}
},
nullResolver,
)
expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum')
})
expect(transformed).toBe('labels = 1&& due_date = now && labels = 2')
})
it('should correctly resolve projects', () => {
const transformed = transformFilterStringFromApi(
'project = 1',
nullIdToTitleResolver,
(id: number) => 'lorem',
)
it('should correctly resolve projects', () => {
const transformed = transformFilterStringForApi(
'project = lorem',
nullResolver,
(title: string) => 1,
)
expect(transformed).toBe('project = lorem')
})
expect(transformed).toBe('project = 1')
})
it('should correctly resolve multiple projects', () => {
const transformed = transformFilterStringForApi(
'project = lorem && dueDate = now || project = ipsum',
nullResolver,
(title: string) => {
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
},
)
it('should correctly resolve multiple projects', () => {
const transformed = transformFilterStringFromApi(
'project = lorem && due_date = now || project = ipsum',
nullIdToTitleResolver,
(id: number) => {
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
},
)
expect(transformed).toBe('project = 1&& due_date = now || project = 2')
expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum')
})
})
})

View File

@ -1,4 +1,5 @@
import {snakeCase} from 'snake-case'
import {camelCase} from 'camel-case'
export const DATE_FIELDS = [
'dueDate',
@ -56,6 +57,10 @@ export const FILTER_JOIN_OPERATOR = [
export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)'
function getFieldPattern(field: string): RegExp {
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
}
export function transformFilterStringForApi(
filter: string,
labelResolver: (title: string) => number | null,
@ -63,7 +68,7 @@ export function transformFilterStringForApi(
): string {
// Transform labels to ids
LABEL_FIELDS.forEach(field => {
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
const pattern = getFieldPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
@ -78,7 +83,7 @@ export function transformFilterStringForApi(
})
// Transform projects to ids
PROJECT_FIELDS.forEach(field => {
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
const pattern = getFieldPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
@ -99,3 +104,48 @@ export function transformFilterStringForApi(
return filter
}
export function transformFilterStringFromApi(
filter: string,
labelResolver: (id: number) => string | null,
projectResolver: (id: number) => string | null,
): string {
// 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)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const labelTitle = labelResolver(Number(keyword.trim()))
if (labelTitle !== null) {
filter = filter.replace(keyword, labelTitle)
}
}
}
})
// Transform projects to ids
PROJECT_FIELDS.forEach(field => {
const pattern = getFieldPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const project = projectResolver(Number(keyword.trim()))
if (project !== null) {
filter = filter.replace(keyword, project)
}
}
}
})
return filter
}