diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
index ef63881b9..98b41e3d6 100644
--- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
+++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
@@ -42,13 +42,13 @@
bindValue="id"
>
@switch (query.operator) {
- @case ('exists') {
+ @case (CustomFieldQueryOperator.Exists) {
}
@default {
diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts
index 8efdb6a71..b4baaa79d 100644
--- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts
+++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts
@@ -1,136 +1,18 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { Subject, first, takeUntil } from 'rxjs'
import { CustomField } from 'src/app/data/custom-field'
+import {
+ CustomFieldQueryAtom,
+ CustomFieldQueryExpression,
+ CustomFieldQueryElementType,
+ CustomFieldQueryOperator,
+ CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
+ CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
+ CustomFieldQueryOperatorGroups,
+ CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
+} from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
-export enum CustomFieldQueryLogicalOperator {
- And = 'AND',
- Or = 'OR',
- Not = 'NOT',
-}
-
-export enum CustomFieldQueryElementType {
- Atom = 'Atom',
- Expression = 'Expression',
-}
-
-export class CustomFieldQueryElement {
- public readonly type: CustomFieldQueryElementType
- public changed: Subject
-
- constructor(type: CustomFieldQueryElementType) {
- this.type = type
- this.changed = new Subject()
- }
-
- public serialize() {
- throw new Error('Implemented in subclass')
- }
-
- public get isValid(): boolean {
- throw new Error('Implemented in subclass')
- }
-
- protected _operator: string = null
- set operator(value: string) {
- this._operator = value
- this.changed.next(this)
- }
- get operator(): string {
- return this._operator
- }
-
- protected _value:
- | string
- | CustomFieldQueryAtom[]
- | CustomFieldQueryExpression[] = null
- set value(
- value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression[]
- ) {
- this._value = value
- this.changed.next(this)
- }
- get value(): string | CustomFieldQueryAtom[] | CustomFieldQueryExpression[] {
- return this._value
- }
-}
-
-export class CustomFieldQueryAtom extends CustomFieldQueryElement {
- protected _field: string
- set field(value: string) {
- this._field = value
- if (this.isValid) this.changed.next(this)
- }
- get field(): string {
- return this._field
- }
-
- constructor(queryArray: [string, string, string] = [null, null, null]) {
- super(CustomFieldQueryElementType.Atom)
- ;[this._field, this._operator, this._value] = queryArray
- }
-
- public serialize() {
- return [this._field, this._operator, this._value]
- }
-
- public get isValid(): boolean {
- return !!(this._field && this._operator && this._value !== null)
- }
-}
-
-export class CustomFieldQueryExpression extends CustomFieldQueryElement {
- constructor(
- expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [
- CustomFieldQueryLogicalOperator.And,
- null,
- ]
- ) {
- super(CustomFieldQueryElementType.Expression)
- ;[this._operator] = expressionArray
- let values = expressionArray[1]
- if (!values) {
- this._value = []
- } else if (values?.length > 0 && values[0] instanceof Array) {
- this._value = values.map((value) => {
- if (value.length === 3) {
- const atom = new CustomFieldQueryAtom(value)
- atom.changed.subscribe(() => {
- this.changed.next(this)
- })
- return atom
- } else {
- const expression = new CustomFieldQueryExpression(value)
- expression.changed.subscribe(() => {
- this.changed.next(this)
- })
- return expression
- }
- })
- } else {
- this._value = [new CustomFieldQueryExpression(values as any)]
- }
- }
-
- public serialize() {
- let value
- if (this._value instanceof Array) {
- value = this._value.map((atom) => atom.serialize())
- } else {
- value = value.serialize()
- }
- return [this._operator, value]
- }
-
- public get isValid(): boolean {
- return (
- this._operator &&
- this._value.length > 0 &&
- (this._value as any[]).every((v) => v.isValid)
- )
- }
-}
-
export class CustomFieldQueriesModel {
public queries: Array = []
@@ -234,6 +116,7 @@ export class CustomFieldQueriesModel {
})
export class CustomFieldsQueryDropdownComponent {
public CustomFieldQueryComponentType = CustomFieldQueryElementType
+ public CustomFieldQueryOperator = CustomFieldQueryOperator
@Input()
title: string
@@ -317,8 +200,21 @@ export class CustomFieldsQueryDropdownComponent {
this.selectionModel.clear()
}
- getOperatorsForField(field: CustomField): string[] {
- return ['exact', 'in', 'icontains', 'isnull', 'exists']
- // TODO: implement this
+ getOperatorsForField(
+ fieldID: number
+ ): Array<{ value: string; label: string }> {
+ const field = this.customFields.find((field) => field.id === fieldID)
+ const groups: CustomFieldQueryOperatorGroups[] =
+ CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
+ if (!groups) {
+ return []
+ }
+ const operators = groups.flatMap(
+ (group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group]
+ )
+ return operators.map((operator) => ({
+ value: operator,
+ label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator],
+ }))
}
}
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
index 5a64f8ead..cf160bd19 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
@@ -94,11 +94,10 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
import { CustomField } from 'src/app/data/custom-field'
import { SearchService } from 'src/app/services/rest/search.service'
import {
- CustomFieldQueriesModel,
- CustomFieldQueryAtom,
CustomFieldQueryExpression,
- CustomFieldQueryLogicalOperator,
-} from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
+ CustomFieldQueryAtom,
+} from 'src/app/data/custom-field-query'
+import { CustomFieldQueriesModel } from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
diff --git a/src-ui/src/app/data/custom-field-query.ts b/src-ui/src/app/data/custom-field-query.ts
new file mode 100644
index 000000000..cbcada40c
--- /dev/null
+++ b/src-ui/src/app/data/custom-field-query.ts
@@ -0,0 +1,227 @@
+import { Subject } from 'rxjs'
+import { CustomFieldDataType } from './custom-field'
+
+export enum CustomFieldQueryLogicalOperator {
+ And = 'AND',
+ Or = 'OR',
+ Not = 'NOT',
+}
+
+export enum CustomFieldQueryOperator {
+ Exact = 'exact',
+ In = 'in',
+ IsNull = 'isnull',
+ Exists = 'exists',
+ Contains = 'contains',
+ IContains = 'icontains',
+ IStartsWith = 'istartswith',
+ IEndsWith = 'iendswith',
+ GreaterThan = 'gt',
+ GreaterThanOrEqual = 'gte',
+ LessThan = 'lt',
+ LessThanOrEqual = 'lte',
+ Range = 'range',
+}
+
+export const CUSTOM_FIELD_QUERY_OPERATOR_LABELS = {
+ [CustomFieldQueryOperator.Exact]: $localize`Equal to`,
+ [CustomFieldQueryOperator.In]: $localize`In`,
+ [CustomFieldQueryOperator.IsNull]: $localize`Is null`,
+ [CustomFieldQueryOperator.Exists]: $localize`Exists`,
+ [CustomFieldQueryOperator.Contains]: $localize`Contains`,
+ [CustomFieldQueryOperator.IContains]: $localize`Contains (case-insensitive)`,
+ [CustomFieldQueryOperator.IStartsWith]: $localize`Starts with (case-insensitive)`,
+ [CustomFieldQueryOperator.IEndsWith]: $localize`Ends with (case-insensitive)`,
+ [CustomFieldQueryOperator.GreaterThan]: $localize`Greater than`,
+ [CustomFieldQueryOperator.GreaterThanOrEqual]: $localize`Greater than or equal to`,
+ [CustomFieldQueryOperator.LessThan]: $localize`Less than`,
+ [CustomFieldQueryOperator.LessThanOrEqual]: $localize`Less than or equal to`,
+ [CustomFieldQueryOperator.Range]: $localize`Range`,
+}
+
+export enum CustomFieldQueryOperatorGroups {
+ Basic = 'basic',
+ String = 'string',
+ Arithmetic = 'arithmetic',
+ Containment = 'containment',
+}
+
+export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = {
+ [CustomFieldQueryOperatorGroups.Basic]: [
+ CustomFieldQueryOperator.Exact,
+ CustomFieldQueryOperator.In,
+ CustomFieldQueryOperator.IsNull,
+ CustomFieldQueryOperator.Exists,
+ ],
+ [CustomFieldQueryOperatorGroups.String]: [
+ CustomFieldQueryOperator.IContains,
+ CustomFieldQueryOperator.IStartsWith,
+ CustomFieldQueryOperator.IEndsWith,
+ ],
+ [CustomFieldQueryOperatorGroups.Arithmetic]: [
+ CustomFieldQueryOperator.GreaterThan,
+ CustomFieldQueryOperator.GreaterThanOrEqual,
+ CustomFieldQueryOperator.LessThan,
+ CustomFieldQueryOperator.LessThanOrEqual,
+ CustomFieldQueryOperator.Range,
+ ],
+ [CustomFieldQueryOperatorGroups.Containment]: [
+ CustomFieldQueryOperator.Contains,
+ ],
+}
+
+// filters.py > SUPPORTED_EXPR_CATEGORIES
+export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = {
+ [CustomFieldDataType.String]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.String,
+ ],
+ [CustomFieldDataType.Url]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.String,
+ ],
+ [CustomFieldDataType.Date]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.Arithmetic,
+ ],
+ [CustomFieldDataType.Boolean]: [CustomFieldQueryOperatorGroups.Basic],
+ [CustomFieldDataType.Integer]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.Arithmetic,
+ ],
+ [CustomFieldDataType.Float]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.Arithmetic,
+ ],
+ [CustomFieldDataType.Monetary]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.String,
+ ],
+ [CustomFieldDataType.DocumentLink]: [
+ CustomFieldQueryOperatorGroups.Basic,
+ CustomFieldQueryOperatorGroups.Containment,
+ ],
+ [CustomFieldDataType.Select]: [CustomFieldQueryOperatorGroups.Basic],
+}
+
+export enum CustomFieldQueryElementType {
+ Atom = 'Atom',
+ Expression = 'Expression',
+}
+
+export class CustomFieldQueryElement {
+ public readonly type: CustomFieldQueryElementType
+ public changed: Subject
+
+ constructor(type: CustomFieldQueryElementType) {
+ this.type = type
+ this.changed = new Subject()
+ }
+
+ public serialize() {
+ throw new Error('Implemented in subclass')
+ }
+
+ public get isValid(): boolean {
+ throw new Error('Implemented in subclass')
+ }
+
+ protected _operator: string = null
+ set operator(value: string) {
+ this._operator = value
+ this.changed.next(this)
+ }
+ get operator(): string {
+ return this._operator
+ }
+
+ protected _value:
+ | string
+ | CustomFieldQueryAtom[]
+ | CustomFieldQueryExpression[] = null
+ set value(
+ value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression[]
+ ) {
+ this._value = value
+ this.changed.next(this)
+ }
+ get value(): string | CustomFieldQueryAtom[] | CustomFieldQueryExpression[] {
+ return this._value
+ }
+}
+
+export class CustomFieldQueryAtom extends CustomFieldQueryElement {
+ protected _field: string
+ set field(value: string) {
+ this._field = value
+ if (this.isValid) this.changed.next(this)
+ }
+ get field(): string {
+ return this._field
+ }
+
+ constructor(queryArray: [string, string, string] = [null, null, null]) {
+ super(CustomFieldQueryElementType.Atom)
+ ;[this._field, this._operator, this._value] = queryArray
+ }
+
+ public serialize() {
+ return [this._field, this._operator, this._value.toString()]
+ }
+
+ public get isValid(): boolean {
+ return !!(this._field && this._operator && this._value !== null)
+ }
+}
+
+export class CustomFieldQueryExpression extends CustomFieldQueryElement {
+ constructor(
+ expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [
+ CustomFieldQueryLogicalOperator.And,
+ null,
+ ]
+ ) {
+ super(CustomFieldQueryElementType.Expression)
+ ;[this._operator] = expressionArray
+ let values = expressionArray[1]
+ if (!values) {
+ this._value = []
+ } else if (values?.length > 0 && values[0] instanceof Array) {
+ this._value = values.map((value) => {
+ if (value.length === 3) {
+ const atom = new CustomFieldQueryAtom(value)
+ atom.changed.subscribe(() => {
+ this.changed.next(this)
+ })
+ return atom
+ } else {
+ const expression = new CustomFieldQueryExpression(value)
+ expression.changed.subscribe(() => {
+ this.changed.next(this)
+ })
+ return expression
+ }
+ })
+ } else {
+ this._value = [new CustomFieldQueryExpression(values as any)]
+ }
+ }
+
+ public serialize() {
+ let value
+ if (this._value instanceof Array) {
+ value = this._value.map((atom) => atom.serialize())
+ } else {
+ value = value.serialize()
+ }
+ return [this._operator, value]
+ }
+
+ public get isValid(): boolean {
+ return (
+ this._operator &&
+ this._value.length > 0 &&
+ (this._value as any[]).every((v) => v.isValid)
+ )
+ }
+}
diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts
index 67f57423e..201770a78 100644
--- a/src-ui/src/app/utils/query-params.spec.ts
+++ b/src-ui/src/app/utils/query-params.spec.ts
@@ -12,7 +12,7 @@ import { paramsToViewState, transformLegacyFilterRules } from './query-params'
import { paramsFromViewState } from './query-params'
import { queryParamsFromFilterRules } from './query-params'
import { filterRulesFromQueryParams } from './query-params'
-import { CustomFieldQueryLogicalOperator } from '../components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
+import { CustomFieldQueryLogicalOperator } from '../data/custom-field-query'
const tags__id__all = '9'
const filterRules: FilterRule[] = [
diff --git a/src-ui/src/app/utils/query-params.ts b/src-ui/src/app/utils/query-params.ts
index ac54efd15..68009fec1 100644
--- a/src-ui/src/app/utils/query-params.ts
+++ b/src-ui/src/app/utils/query-params.ts
@@ -8,7 +8,10 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ALL,
} from '../data/filter-rule-type'
import { ListViewState } from '../services/document-list-view.service'
-import { CustomFieldQueryLogicalOperator } from '../components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
+import {
+ CustomFieldQueryLogicalOperator,
+ CustomFieldQueryOperator,
+} from '../data/custom-field-query'
const SORT_FIELD_PARAMETER = 'sort'
const SORT_REVERSE_PARAMETER = 'reverse'
@@ -71,7 +74,13 @@ export function transformLegacyFilterRules(
const valueRules = allRules.length ? allRules : anyRules
const customFieldQueryExpression = [
customFieldQueryLogicalOperator,
- [...valueRules.map((rule) => [parseInt(rule.value), 'exists', true])],
+ [
+ ...valueRules.map((rule) => [
+ parseInt(rule.value),
+ CustomFieldQueryOperator.Exists,
+ true,
+ ]),
+ ],
]
filterRules.push({
rule_type: FILTER_CUSTOM_FIELDS_LOOKUP,