feat(filter): resolve label and project ids back to titles when loading a filter
This commit is contained in:
parent
b22e7dcbaf
commit
4a21f35742
|
@ -43,7 +43,7 @@ import {useRoute} from 'vue-router'
|
||||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||||
import {useLabelStore} from '@/stores/labels'
|
import {useLabelStore} from '@/stores/labels'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {transformFilterStringForApi} from '@/helpers/filters'
|
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
hasTitle: {
|
hasTitle: {
|
||||||
|
@ -52,7 +52,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const modelValue = defineModel()
|
const modelValue = defineModel<TaskFilterParams>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const projectId = computed(() => {
|
const projectId = computed(() => {
|
||||||
|
@ -72,12 +72,16 @@ const params = ref<TaskFilterParams>({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Using watchDebounced to prevent the filter re-triggering itself.
|
// Using watchDebounced to prevent the filter re-triggering itself.
|
||||||
// FIXME: Only here until this whole component changes a lot with the new filter syntax.
|
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
modelValue,
|
() => modelValue.value,
|
||||||
(value) => {
|
(value: TaskFilterParams) => {
|
||||||
// FIXME: filters should only be converted to snake case in the last moment
|
const val = value
|
||||||
params.value = objectToSnakeCase(value)
|
val.filter = transformFilterStringFromApi(
|
||||||
|
val?.filter || '',
|
||||||
|
labelId => labelStore.getLabelById(labelId),
|
||||||
|
projectId => projectStore.projects.value[projectId] || null,
|
||||||
|
)
|
||||||
|
params.value = val
|
||||||
},
|
},
|
||||||
{immediate: true, debounce: 500, maxWait: 1000},
|
{immediate: true, debounce: 500, maxWait: 1000},
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {describe, expect, it} from 'vitest'
|
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', () => {
|
describe('Filter Transformation', () => {
|
||||||
|
|
||||||
const cases = {
|
const fieldCases = {
|
||||||
'done': 'done',
|
'done': 'done',
|
||||||
'priority': 'priority',
|
'priority': 'priority',
|
||||||
'percentDone': 'percent_done',
|
'percentDone': 'percent_done',
|
||||||
|
@ -16,70 +17,140 @@ describe('Filter Transformation', () => {
|
||||||
'assignees': 'assignees',
|
'assignees': 'assignees',
|
||||||
'labels': 'labels',
|
'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) {
|
expect(transformed).toBe(fieldCases[c] + ' = ipsum')
|
||||||
it('should transform all filter params for ' + c + ' to snake_case', () => {
|
})
|
||||||
const transformed = transformFilterStringForApi(c + ' = ipsum', nullResolver, nullResolver)
|
}
|
||||||
|
|
||||||
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', () => {
|
expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum')
|
||||||
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 = 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', () => {
|
expect(transformed).toBe('project = lorem')
|
||||||
const transformed = transformFilterStringForApi(
|
})
|
||||||
'project = lorem',
|
|
||||||
nullResolver,
|
|
||||||
(title: string) => 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(transformed).toBe('project = 1')
|
it('should correctly resolve multiple projects', () => {
|
||||||
})
|
const transformed = transformFilterStringFromApi(
|
||||||
|
'project = lorem && due_date = now || project = ipsum',
|
||||||
it('should correctly resolve multiple projects', () => {
|
nullIdToTitleResolver,
|
||||||
const transformed = transformFilterStringForApi(
|
(id: number) => {
|
||||||
'project = lorem && dueDate = now || project = ipsum',
|
switch (id) {
|
||||||
nullResolver,
|
case 1:
|
||||||
(title: string) => {
|
return 'lorem'
|
||||||
switch (title) {
|
case 2:
|
||||||
case 'lorem':
|
return 'ipsum'
|
||||||
return 1
|
default:
|
||||||
case 'ipsum':
|
return null
|
||||||
return 2
|
}
|
||||||
default:
|
},
|
||||||
return null
|
)
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(transformed).toBe('project = 1&& due_date = now || project = 2')
|
expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {snakeCase} from 'snake-case'
|
import {snakeCase} from 'snake-case'
|
||||||
|
import {camelCase} from 'camel-case'
|
||||||
|
|
||||||
export const DATE_FIELDS = [
|
export const DATE_FIELDS = [
|
||||||
'dueDate',
|
'dueDate',
|
||||||
|
@ -56,6 +57,10 @@ export const FILTER_JOIN_OPERATOR = [
|
||||||
|
|
||||||
export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)'
|
export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)'
|
||||||
|
|
||||||
|
function getFieldPattern(field: string): RegExp {
|
||||||
|
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
|
||||||
|
}
|
||||||
|
|
||||||
export function transformFilterStringForApi(
|
export function transformFilterStringForApi(
|
||||||
filter: string,
|
filter: string,
|
||||||
labelResolver: (title: string) => number | null,
|
labelResolver: (title: string) => number | null,
|
||||||
|
@ -63,7 +68,7 @@ export function transformFilterStringForApi(
|
||||||
): string {
|
): string {
|
||||||
// Transform labels to ids
|
// Transform labels to ids
|
||||||
LABEL_FIELDS.forEach(field => {
|
LABEL_FIELDS.forEach(field => {
|
||||||
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
|
const pattern = getFieldPattern(field)
|
||||||
|
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
while ((match = pattern.exec(filter)) !== null) {
|
while ((match = pattern.exec(filter)) !== null) {
|
||||||
|
@ -78,7 +83,7 @@ export function transformFilterStringForApi(
|
||||||
})
|
})
|
||||||
// Transform projects to ids
|
// Transform projects to ids
|
||||||
PROJECT_FIELDS.forEach(field => {
|
PROJECT_FIELDS.forEach(field => {
|
||||||
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&\|\(\)]+\\1?)?', 'ig')
|
const pattern = getFieldPattern(field)
|
||||||
|
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
while ((match = pattern.exec(filter)) !== null) {
|
while ((match = pattern.exec(filter)) !== null) {
|
||||||
|
@ -99,3 +104,48 @@ export function transformFilterStringForApi(
|
||||||
|
|
||||||
return filter
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ export const useLabelStore = defineStore('label', () => {
|
||||||
const getLabelsByIds = computed(() => {
|
const getLabelsByIds = computed(() => {
|
||||||
return (ids: ILabel['id'][]) => Object.values(labels.value).filter(({id}) => ids.includes(id))
|
return (ids: ILabel['id'][]) => Object.values(labels.value).filter(({id}) => ids.includes(id))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getLabelById = computed(() => {
|
||||||
|
return (labelId: ILabel['id']) => Object.values(labels.value).find(({id}) => id === labelId)
|
||||||
|
})
|
||||||
|
|
||||||
// **
|
// **
|
||||||
// * Checks if a project of labels is available in the store and filters them then query
|
// * Checks if a project of labels is available in the store and filters them then query
|
||||||
|
@ -138,6 +142,7 @@ export const useLabelStore = defineStore('label', () => {
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
||||||
getLabelsByIds,
|
getLabelsByIds,
|
||||||
|
getLabelById,
|
||||||
filterLabelsByQuery,
|
filterLabelsByQuery,
|
||||||
getLabelsByExactTitles,
|
getLabelsByExactTitles,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue