Rudimentary nesting / expressions

This commit is contained in:
shamoon 2024-09-01 22:07:22 -07:00
parent 96546af95f
commit 9102ddb362
3 changed files with 203 additions and 65 deletions

View File

@ -3,13 +3,36 @@
<i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
</button>
<div class="dropdown-menu shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<button type="button" class="btn btn-sm btn-outline-primary ms-3" (click)="addQuery()" [disabled]="disabled">
<div class="dropdown-menu px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="btn-group mb-2 w-100">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addAtom()" [disabled]="disabled">
<i-bs name="plus"></i-bs>&nbsp;Add query
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addExpression()" [disabled]="disabled">
<i-bs name="plus"></i-bs>&nbsp;Add expression
</button>
</div>
<div class="list-group list-group-flush">
@for (query of selectionModel.queries; track query; let i = $index) {
<div class="list-group-item d-flex">
<div class="list-group-item px-0 d-flex">
@switch (query.type) {
@case (CustomFieldQueryComponentType.Atom) {
<ng-container *ngTemplateOutlet="queryAtom; context: { query: query }"></ng-container>
}
@case (CustomFieldQueryComponentType.Expression) {
<ng-container *ngTemplateOutlet="queryExpression; context: { query: query }"></ng-container>
}
}
<button class="btn btn-link btn-sm pe-0" type="button" (click)="removeQuery(i)" [disabled]="disabled">
<i-bs name="x"></i-bs>
</button>
</div>
}
</div>
</div>
</div>
<ng-template #queryAtom let-query="query">
<div class="input-group input-group-sm flex-shrink-1 flex-nowrap">
<ng-select
class="w-50"
@ -34,6 +57,29 @@
}
}
</div>
</ng-template>
<ng-template #queryExpression let-query="query">
<div class="d-flex flex-column">
<div class="btn-group btn-group-xs flex-fill" role="group">
<input [(ngModel)]="query.operator" type="radio" class="btn-check" id="logicalOperatorAnd_{{query.field}}" name="logicalOperatorAnd_{{query.field}}" value="AND">
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{query.field}}" i18n>And</label>
<input [(ngModel)]="query.operator" type="radio" class="btn-check" id="logicalOperatorOr_{{query.field}}" name="logicalOperatorOr_{{query.field}}" value="OR">
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{query.field}}" i18n>Or</label>
<input [(ngModel)]="query.operator" type="radio" class="btn-check" id="logicalOperatorNot_{{query.field}}" name="logicalOperatorNot_{{query.field}}" value="NOT">
<label class="btn btn-outline-primary" for="logicalOperatorNot_{{query.field}}" i18n>Not</label>
</div>
<div class="border border-1 border-primary rounded p-2 mt-2">
@for (subquery of query.value; track subquery; let i = $index) {
<div class="list-group-item px-0 d-flex">
@switch (subquery.type) {
@case (CustomFieldQueryComponentType.Atom) {
<ng-container *ngTemplateOutlet="queryAtom; context: { query: subquery }"></ng-container>
}
@case (CustomFieldQueryComponentType.Expression) {
<ng-container *ngTemplateOutlet="queryExpression; context: { query: subquery }"></ng-container>
}
}
<button class="btn btn-link btn-sm pe-0" type="button" (click)="removeQuery(i)" [disabled]="disabled">
<i-bs name="x"></i-bs>
</button>
@ -41,4 +87,4 @@
}
</div>
</div>
</div>
</ng-template>

View File

@ -3,19 +3,29 @@ import { Subject, first, takeUntil } from 'rxjs'
import { CustomField } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
export class CustomFieldQuery {
public changed = new Subject<CustomFieldQuery>()
export enum CustomFieldQueryLogicalOperator {
And = 'AND',
Or = 'OR',
Not = 'NOT',
}
private _field: string
set field(value: string) {
this._field = value
this.changed.next(this)
}
get field(): string {
return this._field
export enum CustomFieldQueryComponentType {
Atom = 'Atom',
Expression = 'Expression',
}
export class CustomFieldQueryComponent {
public readonly type: CustomFieldQueryComponentType
public changed: Subject<CustomFieldQueryComponent>
constructor(type: CustomFieldQueryComponentType) {
this.type = type
this.changed = new Subject<CustomFieldQueryComponent>()
}
private _operator: string
public serialize() {}
protected _operator: string
set operator(value: string) {
this._operator = value
this.changed.next(this)
@ -24,31 +34,75 @@ export class CustomFieldQuery {
return this._operator
}
private _value: string
set value(value: string) {
protected _value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression
set value(
value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression
) {
this._value = value
this.changed.next(this)
}
get value(): string {
get value(): string | CustomFieldQueryAtom[] | CustomFieldQueryExpression {
return this._value
}
}
export class CustomFieldQueryAtom extends CustomFieldQueryComponent {
protected _field: string
set field(value: string) {
this._field = value
this.changed.next(this)
}
get field(): string {
return this._field
}
constructor(queryArray: [string, string, string] = [null, null, null]) {
super(CustomFieldQueryComponentType.Atom)
;[this._field, this._operator, this._value] = queryArray
}
public serialize() {
return [this._field, this._operator, this._value]
}
}
export class CustomFieldQueryExpression extends CustomFieldQueryComponent {
constructor(
field: string = null,
operator: string = null,
value: string = null
expressionArray: [
CustomFieldQueryLogicalOperator,
(
| [string, string, string][]
| [CustomFieldQueryLogicalOperator, [string, string, string][]]
),
] = [CustomFieldQueryLogicalOperator.And, null]
) {
this.field = field
this.operator = operator
this.value = value
super(CustomFieldQueryComponentType.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) => new CustomFieldQueryAtom(value))
} 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]
}
}
export class CustomFieldQueriesModel {
// matchingModel: MatchingModel
queries: CustomFieldQuery[] = []
public queries: Array<CustomFieldQueryAtom | CustomFieldQueryExpression> = []
changed = new Subject<CustomFieldQueriesModel>()
public readonly changed = new Subject<CustomFieldQueriesModel>()
public clear(fireEvent = true) {
this.queries = []
@ -57,8 +111,14 @@ export class CustomFieldQueriesModel {
}
}
public addQuery(query: CustomFieldQuery = new CustomFieldQuery()) {
public addQuery(query: CustomFieldQueryAtom = new CustomFieldQueryAtom()) {
if (this.queries.length > 0) {
if (this.queries[0].type === CustomFieldQueryComponentType.Expression) {
;(this.queries[0].value as Array<any>).push(query)
}
} else {
this.queries.push(query)
}
query.changed.subscribe(() => {
if (query.field && query.operator && query.value) {
this.changed.next(this)
@ -71,6 +131,21 @@ export class CustomFieldQueriesModel {
query.changed.complete()
this.changed.next(this)
}
public addExpression(
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
) {
if (this.queries.length > 0) {
if (this.queries[0].type === CustomFieldQueryComponentType.Atom) {
expression.value = this.queries as CustomFieldQueryAtom[]
this.queries = []
}
}
this.queries.push(expression)
expression.changed.subscribe(() => {
this.changed.next(this)
})
}
}
@Component({
@ -79,6 +154,8 @@ export class CustomFieldQueriesModel {
styleUrls: ['./custom-fields-lookup-dropdown.component.scss'],
})
export class CustomFieldsLookupDropdownComponent {
public CustomFieldQueryComponentType = CustomFieldQueryComponentType
@Input()
title: string
@ -143,10 +220,14 @@ export class CustomFieldsLookupDropdownComponent {
})
}
public addQuery() {
public addAtom() {
this.selectionModel.addQuery()
}
public addExpression() {
this.selectionModel.addExpression()
}
public removeQuery(index: number) {
this.selectionModel.removeQuery(index)
}

View File

@ -95,7 +95,8 @@ import { CustomField } from 'src/app/data/custom-field'
import { SearchService } from 'src/app/services/rest/search.service'
import {
CustomFieldQueriesModel,
CustomFieldQuery,
CustomFieldQueryAtom,
CustomFieldQueryExpression,
} from '../../common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component'
const TEXT_FILTER_TARGET_TITLE = 'title'
@ -523,11 +524,24 @@ export class FilterEditorComponent
)
break
case FILTER_CUSTOM_FIELDS_LOOKUP:
// TODO: fully implement
try {
const query = JSON.parse(rule.value)
this.customFieldQueriesModel.addQuery(
new CustomFieldQuery(query[0], query[1], query[2])
if (Array.isArray(query)) {
if (query.length === 2) {
// expression
this.customFieldQueriesModel.addExpression(
new CustomFieldQueryExpression(query as any)
)
} else if (query.length === 3) {
// atom
this.customFieldQueriesModel.addQuery(
new CustomFieldQueryAtom(query as any)
)
}
}
} catch (e) {
// TODO: handle error?
}
break
case FILTER_HAS_CUSTOM_FIELDS_ALL:
console.log('FILTER_HAS_CUSTOM_FIELDS_ALL', rule.value)
@ -752,8 +766,8 @@ export class FilterEditorComponent
})
}
let queries = this.customFieldQueriesModel.queries
.filter((query) => query.field && query.operator)
.map((query) => [query.field, query.operator, query.value])
.filter((query) => query.value && query.operator)
.map((query) => query.serialize())
console.log(
'this.customFieldQueriesModel.queries',
this.customFieldQueriesModel.queries
@ -762,10 +776,7 @@ export class FilterEditorComponent
if (queries.length > 0) {
filterRules.push({
rule_type: FILTER_CUSTOM_FIELDS_LOOKUP,
value:
queries.length === 1
? JSON.stringify(queries[0])
: JSON.stringify(queries),
value: JSON.stringify(queries[0]),
})
}
// TODO: fully implement custom fields