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,