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,37 +3,26 @@
<i-bs name="{{icon}}"></i-bs> <i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div> <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
</button> </button>
<div class="dropdown-menu shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> <div class="dropdown-menu px-3 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="btn-group mb-2 w-100">
<i-bs name="plus"></i-bs>&nbsp;Add query <button type="button" class="btn btn-sm btn-outline-primary" (click)="addAtom()" [disabled]="disabled">
</button> <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"> <div class="list-group list-group-flush">
@for (query of selectionModel.queries; track query; let i = $index) { @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">
<div class="input-group input-group-sm flex-shrink-1 flex-nowrap"> @switch (query.type) {
<ng-select @case (CustomFieldQueryComponentType.Atom) {
class="w-50" <ng-container *ngTemplateOutlet="queryAtom; context: { query: query }"></ng-container>
[items]="customFields"
[(ngModel)]="query.field"
[disabled]="disabled"
bindLabel="name"
bindValue="id"
></ng-select>
<select class="w-25 form-control" [(ngModel)]="query.operator" [disabled]="disabled">
<option *ngFor="let operator of getOperatorsForField(query.field)" [ngValue]="operator">{{operator}}</option>
</select>
@switch (query.operator) {
@case ('exists') {
<select class="w-25 form-control" [(ngModel)]="query.value" [disabled]="disabled">
<option value="true" i18n>true</option>
<option value="false" i18n>false</option>
</select>
}
@default {
<input class="w-25 form-control" type="text" [(ngModel)]="query.value" [disabled]="disabled">
}
} }
</div> @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"> <button class="btn btn-link btn-sm pe-0" type="button" (click)="removeQuery(i)" [disabled]="disabled">
<i-bs name="x"></i-bs> <i-bs name="x"></i-bs>
</button> </button>
@ -42,3 +31,60 @@
</div> </div>
</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"
[items]="customFields"
[(ngModel)]="query.field"
[disabled]="disabled"
bindLabel="name"
bindValue="id"
></ng-select>
<select class="w-25 form-control" [(ngModel)]="query.operator" [disabled]="disabled">
<option *ngFor="let operator of getOperatorsForField(query.field)" [ngValue]="operator">{{operator}}</option>
</select>
@switch (query.operator) {
@case ('exists') {
<select class="w-25 form-control" [(ngModel)]="query.value" [disabled]="disabled">
<option value="true" i18n>true</option>
<option value="false" i18n>false</option>
</select>
}
@default {
<input class="w-25 form-control" type="text" [(ngModel)]="query.value" [disabled]="disabled">
}
}
</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>
</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 { CustomField } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
export class CustomFieldQuery { export enum CustomFieldQueryLogicalOperator {
public changed = new Subject<CustomFieldQuery>() And = 'AND',
Or = 'OR',
Not = 'NOT',
}
private _field: string export enum CustomFieldQueryComponentType {
set field(value: string) { Atom = 'Atom',
this._field = value Expression = 'Expression',
this.changed.next(this) }
}
get field(): string { export class CustomFieldQueryComponent {
return this._field 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) { set operator(value: string) {
this._operator = value this._operator = value
this.changed.next(this) this.changed.next(this)
@ -24,31 +34,75 @@ export class CustomFieldQuery {
return this._operator return this._operator
} }
private _value: string protected _value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression
set value(value: string) { set value(
value: string | CustomFieldQueryAtom[] | CustomFieldQueryExpression
) {
this._value = value this._value = value
this.changed.next(this) this.changed.next(this)
} }
get value(): string { get value(): string | CustomFieldQueryAtom[] | CustomFieldQueryExpression {
return this._value 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( constructor(
field: string = null, expressionArray: [
operator: string = null, CustomFieldQueryLogicalOperator,
value: string = null (
| [string, string, string][]
| [CustomFieldQueryLogicalOperator, [string, string, string][]]
),
] = [CustomFieldQueryLogicalOperator.And, null]
) { ) {
this.field = field super(CustomFieldQueryComponentType.Expression)
this.operator = operator ;[this._operator] = expressionArray
this.value = value 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 { export class CustomFieldQueriesModel {
// matchingModel: MatchingModel public queries: Array<CustomFieldQueryAtom | CustomFieldQueryExpression> = []
queries: CustomFieldQuery[] = []
changed = new Subject<CustomFieldQueriesModel>() public readonly changed = new Subject<CustomFieldQueriesModel>()
public clear(fireEvent = true) { public clear(fireEvent = true) {
this.queries = [] this.queries = []
@ -57,8 +111,14 @@ export class CustomFieldQueriesModel {
} }
} }
public addQuery(query: CustomFieldQuery = new CustomFieldQuery()) { public addQuery(query: CustomFieldQueryAtom = new CustomFieldQueryAtom()) {
this.queries.push(query) 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(() => { query.changed.subscribe(() => {
if (query.field && query.operator && query.value) { if (query.field && query.operator && query.value) {
this.changed.next(this) this.changed.next(this)
@ -71,6 +131,21 @@ export class CustomFieldQueriesModel {
query.changed.complete() query.changed.complete()
this.changed.next(this) 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({ @Component({
@ -79,6 +154,8 @@ export class CustomFieldQueriesModel {
styleUrls: ['./custom-fields-lookup-dropdown.component.scss'], styleUrls: ['./custom-fields-lookup-dropdown.component.scss'],
}) })
export class CustomFieldsLookupDropdownComponent { export class CustomFieldsLookupDropdownComponent {
public CustomFieldQueryComponentType = CustomFieldQueryComponentType
@Input() @Input()
title: string title: string
@ -143,10 +220,14 @@ export class CustomFieldsLookupDropdownComponent {
}) })
} }
public addQuery() { public addAtom() {
this.selectionModel.addQuery() this.selectionModel.addQuery()
} }
public addExpression() {
this.selectionModel.addExpression()
}
public removeQuery(index: number) { public removeQuery(index: number) {
this.selectionModel.removeQuery(index) 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 { SearchService } from 'src/app/services/rest/search.service'
import { import {
CustomFieldQueriesModel, CustomFieldQueriesModel,
CustomFieldQuery, CustomFieldQueryAtom,
CustomFieldQueryExpression,
} from '../../common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component' } from '../../common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component'
const TEXT_FILTER_TARGET_TITLE = 'title' const TEXT_FILTER_TARGET_TITLE = 'title'
@ -523,11 +524,24 @@ export class FilterEditorComponent
) )
break break
case FILTER_CUSTOM_FIELDS_LOOKUP: case FILTER_CUSTOM_FIELDS_LOOKUP:
// TODO: fully implement try {
const query = JSON.parse(rule.value) const query = JSON.parse(rule.value)
this.customFieldQueriesModel.addQuery( if (Array.isArray(query)) {
new CustomFieldQuery(query[0], query[1], query[2]) 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 break
case FILTER_HAS_CUSTOM_FIELDS_ALL: case FILTER_HAS_CUSTOM_FIELDS_ALL:
console.log('FILTER_HAS_CUSTOM_FIELDS_ALL', rule.value) console.log('FILTER_HAS_CUSTOM_FIELDS_ALL', rule.value)
@ -752,8 +766,8 @@ export class FilterEditorComponent
}) })
} }
let queries = this.customFieldQueriesModel.queries let queries = this.customFieldQueriesModel.queries
.filter((query) => query.field && query.operator) .filter((query) => query.value && query.operator)
.map((query) => [query.field, query.operator, query.value]) .map((query) => query.serialize())
console.log( console.log(
'this.customFieldQueriesModel.queries', 'this.customFieldQueriesModel.queries',
this.customFieldQueriesModel.queries this.customFieldQueriesModel.queries
@ -762,10 +776,7 @@ export class FilterEditorComponent
if (queries.length > 0) { if (queries.length > 0) {
filterRules.push({ filterRules.push({
rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, rule_type: FILTER_CUSTOM_FIELDS_LOOKUP,
value: value: JSON.stringify(queries[0]),
queries.length === 1
? JSON.stringify(queries[0])
: JSON.stringify(queries),
}) })
} }
// TODO: fully implement custom fields // TODO: fully implement custom fields