diff --git a/docs/content/doc/usage/filters.md b/docs/content/doc/usage/filters.md index b7885efc9..2a841469a 100644 --- a/docs/content/doc/usage/filters.md +++ b/docs/content/doc/usage/filters.md @@ -46,7 +46,7 @@ The available operators for filtering include: * `<`: Less than * `<=`: Less than or equal to * `like`: Matches a pattern (using wildcard `%`) -* `in`: Matches any value in a list +* `in`: Matches any value in a comma-seperated list of values To combine multiple conditions, you can use the following logical operators: diff --git a/frontend/src/components/project/partials/FilterInput.vue b/frontend/src/components/project/partials/FilterInput.vue index cfb8bd92f..e02eb6838 100644 --- a/frontend/src/components/project/partials/FilterInput.vue +++ b/frontend/src/components/project/partials/FilterInput.vue @@ -16,7 +16,7 @@ import { AVAILABLE_FILTER_FIELDS, FILTER_JOIN_OPERATOR, FILTER_OPERATORS, - FILTER_OPERATORS_REGEX, LABEL_FIELDS, + FILTER_OPERATORS_REGEX, LABEL_FIELDS, getFilterFieldRegexPattern, } from '@/helpers/filters' const { @@ -104,15 +104,25 @@ const highlightedFilterQuery = computed(() => { }) LABEL_FIELDS .forEach(f => { - const pattern = new RegExp(f + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig') - highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => { + const pattern = getFilterFieldRegexPattern(f) + highlighted = highlighted.replaceAll(pattern, (match, prefix, operator, space, value) => { + if (typeof value === 'undefined') { value = '' } + + let labelTitles = [value] + if(operator === 'in' || operator === '?=') { + labelTitles = value.split(',').map(v => v.trim()) + } - const label = labelStore.getLabelsByExactTitles([value])[0] || undefined + const labelsHtml: string[] = [] + labelTitles.forEach(t => { + const label = labelStore.getLabelByExactTitle(t) || undefined + labelsHtml.push(`${label?.title ?? t}`) + }) - return `${f} ${token} ${label?.title ?? value}` + return `${f} ${operator} ${labelsHtml.join(', ')}` }) }) FILTER_OPERATORS @@ -184,26 +194,31 @@ function handleFieldInput() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { + let search = keyword + if(operator === 'in' || operator === '?=') { + const keywords = keyword.split(',') + search = keywords[keywords.length - 1].trim() + } if (matched.startsWith('label')) { autocompleteResultType.value = 'labels' - autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword) + autocompleteResults.value = labelStore.filterLabelsByQuery([], search) } if (matched.startsWith('assignee')) { autocompleteResultType.value = 'assignees' if (projectId) { - projectUserService.getAll({projectId}, {s: keyword}) + projectUserService.getAll({projectId}, {s: search}) .then(users => autocompleteResults.value = users.length > 1 ? users : []) } else { - userService.getAll({}, {s: keyword}) + userService.getAll({}, {s: search}) .then(users => autocompleteResults.value = users.length > 1 ? users : []) } } if (!projectId && matched.startsWith('project')) { autocompleteResultType.value = 'projects' - autocompleteResults.value = projectStore.searchProject(keyword) + autocompleteResults.value = projectStore.searchProject(search) } autocompleteMatchText.value = keyword - autocompleteMatchPosition.value = prefix.length - 1 + autocompleteMatchPosition.value = prefix.length - 1 + keyword.replace(search, '').length } } }) diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 9f720e5d9..de10f57f6 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -57,7 +57,7 @@ export const FILTER_JOIN_OPERATOR = [ export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|in)' -function getFieldPattern(field: string): RegExp { +export function getFilterFieldRegexPattern(field: string): RegExp { return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') } @@ -73,7 +73,7 @@ export function transformFilterStringForApi( // Transform labels to ids LABEL_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -96,7 +96,7 @@ export function transformFilterStringForApi( }) // Transform projects to ids PROJECT_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -143,7 +143,7 @@ export function transformFilterStringFromApi( // Transform labels to their titles LABEL_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -167,7 +167,7 @@ export function transformFilterStringFromApi( // Transform projects to ids PROJECT_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index 8516f6812..158ef586f 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -446,7 +446,7 @@ "lessThan": "Less than", "lessThanOrEqual": "Less than or equal to", "like": "Matches a pattern (using wildcard %)", - "in": "Matches any value in a list" + "in": "Matches any value in a comma-seperated list of values" }, "logicalOperators": { "intro": "To combine multiple conditions, you can use the following logical operators:", diff --git a/frontend/src/stores/labels.ts b/frontend/src/stores/labels.ts index 5d8c48221..9c3d959b0 100644 --- a/frontend/src/stores/labels.ts +++ b/frontend/src/stores/labels.ts @@ -57,6 +57,12 @@ export const useLabelStore = defineStore('label', () => { .values(labels.value) .filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase())) }) + + const getLabelByExactTitle = computed(() => { + return (labelTitle: string) => Object + .values(labels.value) + .find(l => l.title.toLowerCase() === labelTitle.toLowerCase()) + }) function setIsLoading(newIsLoading: boolean) { @@ -145,6 +151,7 @@ export const useLabelStore = defineStore('label', () => { getLabelById, filterLabelsByQuery, getLabelsByExactTitles, + getLabelByExactTitle, setLabels, setLabel,