From f13e8667f877281adf98b8e557a8b4af70cc33b3 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:17:41 -0700 Subject: [PATCH] Refactor, actual operator selections --- ...ustom-fields-query-dropdown.component.html | 8 +- .../custom-fields-query-dropdown.component.ts | 158 +++--------- .../filter-editor/filter-editor.component.ts | 7 +- src-ui/src/app/data/custom-field-query.ts | 227 ++++++++++++++++++ src-ui/src/app/utils/query-params.spec.ts | 2 +- src-ui/src/app/utils/query-params.ts | 13 +- 6 files changed, 273 insertions(+), 142 deletions(-) create mode 100644 src-ui/src/app/data/custom-field-query.ts 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,