Refactor, actual operator selections

This commit is contained in:
shamoon 2024-09-03 09:17:41 -07:00
parent 67831a6216
commit f13e8667f8
6 changed files with 273 additions and 142 deletions

View File

@ -42,13 +42,13 @@
bindValue="id"
></ng-select>
<select class="w-25 form-select" [(ngModel)]="query.operator" [disabled]="disabled">
<option *ngFor="let operator of getOperatorsForField(query.field)" [ngValue]="operator">{{operator}}</option>
<option *ngFor="let operator of getOperatorsForField(query.field)" [ngValue]="operator.value">{{operator.label}}</option>
</select>
@switch (query.operator) {
@case ('exists') {
@case (CustomFieldQueryOperator.Exists) {
<select class="w-25 form-select rounded-end" [(ngModel)]="query.value" [disabled]="disabled">
<option value="true" i18n>true</option>
<option value="false" i18n>false</option>
<option value="true" i18n>True</option>
<option value="false" i18n>False</option>
</select>
}
@default {

View File

@ -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<CustomFieldQueryElement>
constructor(type: CustomFieldQueryElementType) {
this.type = type
this.changed = new Subject<CustomFieldQueryElement>()
}
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<CustomFieldQueryAtom | CustomFieldQueryExpression> = []
@ -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],
}))
}
}

View File

@ -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'

View File

@ -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<CustomFieldQueryElement>
constructor(type: CustomFieldQueryElementType) {
this.type = type
this.changed = new Subject<CustomFieldQueryElement>()
}
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)
)
}
}

View File

@ -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[] = [

View File

@ -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,