(null)
-onClickOutside(popup, () => close())
+onClickOutside(popup, (event) => {
+ const target = event.target as HTMLElement
+ // Check if the click target has any of the ignored classes
+ if (target?.classList && props.ignoreClickClasses.some(className => target.classList.contains(className))) {
+ return
+ }
+ close()
+})
@@ -388,28 +507,5 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
diff --git a/frontend/src/components/project/partials/Filters.vue b/frontend/src/components/project/partials/Filters.vue
index 8067c298e..39972d57a 100644
--- a/frontend/src/components/project/partials/Filters.vue
+++ b/frontend/src/components/project/partials/Filters.vue
@@ -8,9 +8,8 @@
v-model="filterQuery"
:project-id="projectId"
@update:modelValue="() => change('modelValue')"
- @blur="() => change('blur')"
/>
-
+ {{ filterQuery }}
import {computed, ref, watch} from 'vue'
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
-import FilterInput from '@/components/project/partials/FilterInput.vue'
import {useRoute} from 'vue-router'
import type {TaskFilterParams} from '@/services/taskCollection'
import {useLabelStore} from '@/stores/labels'
@@ -68,9 +66,9 @@ import {useProjectStore} from '@/stores/projects'
import {
hasFilterQuery,
transformFilterStringForApi,
- transformFilterStringFromApi,
} from '@/helpers/filters'
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
+import FilterInput from '@/components/input/filter/FilterInput.vue'
const props = withDefaults(defineProps<{
modelValue: TaskFilterParams,
@@ -124,13 +122,7 @@ const projectStore = useProjectStore()
watch(
() => props.modelValue,
(value: TaskFilterParams) => {
- const val = {...value}
- val.filter = transformFilterStringFromApi(
- val?.filter || '',
- labelId => labelStore.getLabelById(labelId)?.title || null,
- projectId => projectStore.projects[projectId]?.title || null,
- )
- params.value = val
+ params.value = {...value}
},
{
immediate: true,
diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts
index 739362f2f..f581d790d 100644
--- a/frontend/src/helpers/filters.ts
+++ b/frontend/src/helpers/filters.ts
@@ -58,14 +58,14 @@ export const FILTER_JOIN_OPERATOR = [
')',
]
-export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|not in|in)'
+export const FILTER_OPERATORS_REGEX = '('+FILTER_OPERATORS.map(op => op.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')+')'
export function hasFilterQuery(filter: string): boolean {
return FILTER_OPERATORS.find(o => filter.includes(o)) || false
}
export function getFilterFieldRegexPattern(field: string): RegExp {
- return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()<]+\\1?)?', 'ig')
+ return new RegExp('(' + field + ')\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"&|()<]+?)(?=\\s*(?:&&|\\|\\||$))', 'ig')
}
export function transformFilterStringForApi(
@@ -98,14 +98,14 @@ export function transformFilterStringForApi(
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [matched, prefix, operator, space, keyword] = match
+ const [matched, fieldName, operator, quotes, keyword] = match
if (!keyword) {
continue
}
let keywords = [keyword.trim()]
- if (operator === 'in' || operator === '?=' || operator === 'not in' || operator === '?!=') {
- keywords = keyword.trim().split(',').map(k => k.trim())
+ if (isMultiValueOperator(operator)) {
+ keywords = keyword.trim().split(',').map(k => trimQuotes(k))
}
let replaced = keyword
@@ -116,8 +116,29 @@ export function transformFilterStringForApi(
replaced = replaced.replace(k, String(id))
}
})
+
+ // Join the transformed keywords back together
+ if (isMultiValueOperator(operator)) {
+ replaced = transformedKeywords.join(', ')
+ } else {
+ replaced = transformedKeywords[0] || keyword
+ }
- const actualKeywordStart = (match?.index || 0) + prefix.length
+ replaced = replaced.replaceAll('"', '').replaceAll('\'', '')
+
+ // Reconstruct the entire match with the replaced value
+ let reconstructedMatch
+ if (quotes && quotedContent) {
+ // For quoted values, remove quotes since we converted to IDs
+ reconstructedMatch = `${fieldName} ${operator} ${replaced}`
+ } else if (unquotedContent) {
+ // For unquoted values
+ reconstructedMatch = `${fieldName} ${operator} ${replaced}`
+ } else {
+ continue
+ }
+
+ const actualKeywordStart = (match?.index || 0) + matched.length - keyword.length
replacements.push({
start: actualKeywordStart,
length: keyword.length,
@@ -178,11 +199,16 @@ export function transformFilterStringFromApi(
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [matched, prefix, operator, space, keyword] = match
+ const [matched, fieldName, operator, quotes, keyword] = match
if (keyword) {
let keywords = [keyword.trim()]
- if (operator === 'in' || operator === '?=' || operator === 'not in' || operator === '?!=') {
- keywords = keyword.trim().split(',').map(k => k.trim())
+ if (isMultiValueOperator(operator)) {
+ keywords = keyword.trim().split(',').map(k => {
+ let trimmed = k.trim()
+ // Strip quotes from individual values in multi-value scenarios
+ trimmed = trimQuotes(trimmed)
+ return trimmed
+ })
}
keywords.forEach(k => {
@@ -204,3 +230,7 @@ export function transformFilterStringFromApi(
return filter
}
+
+export function isMultiValueOperator(operator: string): boolean {
+ return ['in', '?=', 'not in', '?!='].includes(operator)
+}
diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json
index 9ff722b5a..a11c44484 100644
--- a/frontend/src/i18n/lang/en.json
+++ b/frontend/src/i18n/lang/en.json
@@ -441,6 +441,7 @@
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
+ "noResults": "No results",
"fromView": "The current view has a filter set as well:",
"fromViewBoth": "It will be used in combination with what you enter here.",
"attributes": {