Big refactor of layout etc

This commit is contained in:
shamoon 2024-09-04 14:27:31 -07:00
parent 30fb5a96ce
commit b6a3c7b58b
3 changed files with 113 additions and 90 deletions

View File

@ -1,20 +1,12 @@
<div class="btn-group w-100" ngbDropdown role="group" #dropdown="ngbDropdown"> <div class="btn-group w-100" ngbDropdown role="group" #dropdown="ngbDropdown" (openChange)="onOpenChange($event)">
<button class="btn btn-sm btn-outline-primary" id="dropdown_{{name}}" ngbDropdownToggle [disabled]="disabled"> <button class="btn btn-sm btn-outline-primary" id="dropdown_{{name}}" ngbDropdownToggle [disabled]="disabled">
<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>
@if (selectionModel.queries.length > 0) { @if (isActive) {
<pngx-clearable-badge [selected]="selectionModel.queries.length > 0" (cleared)="reset()"></pngx-clearable-badge> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
} }
</button> </button>
<div class="dropdown-menu px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> <div class="dropdown-menu px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="btn-group my-2 w-100">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addAtom()" [disabled]="disabled">
<i-bs name="node-plus"></i-bs>&nbsp;Add query
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="addExpression()" [disabled]="disabled">
<i-bs name="braces"></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 px-0 d-flex flex-nowrap"> <div class="list-group-item px-0 d-flex flex-nowrap">
@ -61,36 +53,46 @@
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="query.value" [disabled]="disabled"> <input class="w-25 form-control rounded-end" type="text" [(ngModel)]="query.value" [disabled]="disabled">
} }
} }
<button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeComponent(query)" [disabled]="disabled"> <button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeElement(query)" [disabled]="disabled">
<i-bs name="x-circle"></i-bs> <i-bs name="x-circle"></i-bs>
</button> </button>
</div> </div>
</ng-template> </ng-template>
<ng-template #queryExpression let-query="query"> <ng-template #queryExpression let-query="query">
<div class="d-flex flex-column border border-primary rounded px-2 pt-2 w-100"> <div class="d-flex w-100">
<div class="btn-group btn-group-xs" role="group"> <div class="d-flex flex-grow-1 flex-column">
<input [(ngModel)]="query.operator" type="radio" class="btn-check" id="logicalOperatorAnd_{{query.field}}" name="logicalOperatorAnd_{{query.field}}" value="AND"> <div class="btn-group btn-group-xs" role="group">
<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">
<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>Any</label>
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{query.field}}" i18n>Or</label> <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>All</label>
</div>
<div class="list-group list-group-flush mb-n2">
@for (subquery of query.value; track subquery; let i = $index) {
<div class="list-group-item px-0 d-flex flex-nowrap">
@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>
}
}
</div>
}
</div>
</div> </div>
<div class="list-group list-group-flush"> <div class="btn-group-vertical ms-2 ps-2 border-start" role="group" aria-label="Vertical button group">
@for (subquery of query.value; track subquery; let i = $index) { <button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add query" i18n-title (click)="addAtom(query)" [disabled]="disabled">
<div class="list-group-item px-0 d-flex flex-nowrap"> <i-bs name="node-plus"></i-bs>
@switch (subquery.type) { </button>
@case (CustomFieldQueryComponentType.Atom) { <button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add expression" i18n-title (click)="addExpression(query)" [disabled]="disabled">
<ng-container *ngTemplateOutlet="queryAtom; context: { query: subquery }"></ng-container> <i-bs name="braces"></i-bs>
} </button>
@case (CustomFieldQueryComponentType.Expression) { <button type="button" class="btn btn-sm btn-outline-secondary text-danger" (click)="removeElement(query)" [disabled]="disabled">
<ng-container *ngTemplateOutlet="queryExpression; context: { query: subquery }"></ng-container> <i-bs name="x-circle"></i-bs>
} </button>
}
</div>
}
</div> </div>
</div> </div>
<button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeComponent(query)" [disabled]="disabled">
<i-bs name="x-circle"></i-bs>
</button>
</ng-template> </ng-template>

View File

@ -10,11 +10,12 @@ import {
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP, CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
CustomFieldQueryOperatorGroups, CustomFieldQueryOperatorGroups,
CUSTOM_FIELD_QUERY_OPERATOR_LABELS, CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
CustomFieldQueryElement,
} from 'src/app/data/custom-field-query' } from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
export class CustomFieldQueriesModel { export class CustomFieldQueriesModel {
public queries: Array<CustomFieldQueryAtom | CustomFieldQueryExpression> = [] public queries: CustomFieldQueryElement[] = []
public readonly changed = new Subject<CustomFieldQueriesModel>() public readonly changed = new Subject<CustomFieldQueriesModel>()
@ -25,22 +26,11 @@ export class CustomFieldQueriesModel {
} }
} }
public addQuery( public addQuery(query: CustomFieldQueryAtom) {
query: CustomFieldQueryAtom = new CustomFieldQueryAtom([ if (this.queries.length === 0) {
null, this.addExpression()
CustomFieldQueryOperator.Exists,
'true',
])
) {
if (this.queries.length > 0) {
if (this.queries[0].type === CustomFieldQueryElementType.Expression) {
;(this.queries[0].value as Array<any>).push(query)
} else {
this.queries.push(query)
}
} else {
this.queries.push(query)
} }
;(this.queries[0].value as CustomFieldQueryElement[]).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)
@ -52,15 +42,10 @@ export class CustomFieldQueriesModel {
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression() expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
) { ) {
if (this.queries.length > 0) { if (this.queries.length > 0) {
if (this.queries[0].type === CustomFieldQueryElementType.Atom) { ;(
expression.value = this.queries as CustomFieldQueryAtom[] (this.queries[0] as CustomFieldQueryExpression)
this.queries = [] .value as CustomFieldQueryElement[]
this.queries.push(expression) ).push(expression)
} else {
;((this.queries[0] as CustomFieldQueryExpression).value as any[]).push(
expression
)
}
} else { } else {
this.queries.push(expression) this.queries.push(expression)
} }
@ -69,20 +54,12 @@ export class CustomFieldQueriesModel {
}) })
} }
private findComponent( private findElement(queryElement: CustomFieldQueryElement, elements: any[]) {
queryComponent: CustomFieldQueryAtom | CustomFieldQueryExpression, for (let i = 0; i < elements.length; i++) {
components: any[] if (elements[i] === queryElement) {
) { return elements.splice(i, 1)[0]
for (let i = 0; i < components.length; i++) { } else if (elements[i].type === CustomFieldQueryElementType.Expression) {
if (components[i] === queryComponent) { let found = this.findElement(queryElement, elements[i].value as any[])
return components.splice(i, 1)[0]
} else if (
components[i].type === CustomFieldQueryElementType.Expression
) {
let found = this.findComponent(
queryComponent,
components[i].value as any[]
)
if (found !== undefined) { if (found !== undefined) {
return found return found
} }
@ -91,17 +68,15 @@ export class CustomFieldQueriesModel {
return undefined return undefined
} }
public removeComponent( public removeElement(queryElement: CustomFieldQueryElement) {
queryComponent: CustomFieldQueryAtom | CustomFieldQueryExpression
) {
let foundComponent let foundComponent
for (let i = 0; i < this.queries.length; i++) { for (let i = 0; i < this.queries.length; i++) {
let query = this.queries[i] let query = this.queries[i]
if (query === queryComponent) { if (query === queryElement) {
foundComponent = this.queries.splice(i, 1)[0] foundComponent = this.queries.splice(i, 1)[0]
break break
} else if (query.type === CustomFieldQueryElementType.Expression) { } else if (query.type === CustomFieldQueryElementType.Expression) {
let found = this.findComponent(queryComponent, query.value as any[]) let found = this.findElement(queryElement, query.value as any[])
if (found !== undefined) { if (found !== undefined) {
foundComponent = found foundComponent = found
} }
@ -149,10 +124,14 @@ export class CustomFieldsQueryDropdownComponent {
@Input() @Input()
disabled: boolean = false disabled: boolean = false
_selectionModel: CustomFieldQueriesModel = new CustomFieldQueriesModel() private _selectionModel: CustomFieldQueriesModel =
new CustomFieldQueriesModel()
@Input() @Input()
set selectionModel(model: CustomFieldQueriesModel) { set selectionModel(model: CustomFieldQueriesModel) {
if (this._selectionModel) {
this._selectionModel.changed.complete()
}
model.changed.subscribe((updatedModel) => { model.changed.subscribe((updatedModel) => {
this.selectionModelChange.next(updatedModel) this.selectionModelChange.next(updatedModel)
}) })
@ -172,6 +151,7 @@ export class CustomFieldsQueryDropdownComponent {
constructor(protected customFieldsService: CustomFieldsService) { constructor(protected customFieldsService: CustomFieldsService) {
this.getFields() this.getFields()
this.reset()
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -179,6 +159,19 @@ export class CustomFieldsQueryDropdownComponent {
this.unsubscribeNotifier.complete() this.unsubscribeNotifier.complete()
} }
public onOpenChange(open: boolean) {
if (open && this.selectionModel.queries.length === 0) {
this.selectionModel.addExpression()
}
}
public get isActive(): boolean {
return (
(this.selectionModel.queries[0] as CustomFieldQueryExpression)?.value
?.length > 0
)
}
private getFields() { private getFields() {
this.customFieldsService this.customFieldsService
.listAll() .listAll()
@ -188,22 +181,23 @@ export class CustomFieldsQueryDropdownComponent {
}) })
} }
public addAtom() { public addAtom(expression: CustomFieldQueryExpression) {
this.selectionModel.addQuery() expression.addAtom()
} }
public addExpression() { public addExpression(expression: CustomFieldQueryExpression) {
this.selectionModel.addExpression() expression.addExpression()
} }
public removeComponent( public removeComponent(
component: CustomFieldQueryAtom | CustomFieldQueryExpression component: CustomFieldQueryAtom | CustomFieldQueryExpression
) { ) {
this.selectionModel.removeComponent(component) this.selectionModel.removeElement(component)
} }
public reset() { public reset() {
this.selectionModel.clear() this.selectionModel.clear(false)
this.selectionModel.changed.next(this.selectionModel)
} }
getOperatorsForField( getOperatorsForField(

View File

@ -44,6 +44,7 @@ export enum CustomFieldQueryOperatorGroups {
String = 'string', String = 'string',
Arithmetic = 'arithmetic', Arithmetic = 'arithmetic',
Containment = 'containment', Containment = 'containment',
Date = 'date',
} }
export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = { export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = {
@ -63,11 +64,15 @@ export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = {
CustomFieldQueryOperator.GreaterThanOrEqual, CustomFieldQueryOperator.GreaterThanOrEqual,
CustomFieldQueryOperator.LessThan, CustomFieldQueryOperator.LessThan,
CustomFieldQueryOperator.LessThanOrEqual, CustomFieldQueryOperator.LessThanOrEqual,
CustomFieldQueryOperator.Range, // CustomFieldQueryOperator.Range,
], ],
[CustomFieldQueryOperatorGroups.Containment]: [ [CustomFieldQueryOperatorGroups.Containment]: [
CustomFieldQueryOperator.Contains, CustomFieldQueryOperator.Contains,
], ],
[CustomFieldQueryOperatorGroups.Date]: [
CustomFieldQueryOperator.GreaterThanOrEqual,
CustomFieldQueryOperator.LessThanOrEqual,
],
} }
// filters.py > SUPPORTED_EXPR_CATEGORIES // filters.py > SUPPORTED_EXPR_CATEGORIES
@ -82,7 +87,7 @@ export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = {
], ],
[CustomFieldDataType.Date]: [ [CustomFieldDataType.Date]: [
CustomFieldQueryOperatorGroups.Basic, CustomFieldQueryOperatorGroups.Basic,
CustomFieldQueryOperatorGroups.Arithmetic, CustomFieldQueryOperatorGroups.Date,
], ],
[CustomFieldDataType.Boolean]: [CustomFieldQueryOperatorGroups.Basic], [CustomFieldDataType.Boolean]: [CustomFieldQueryOperatorGroups.Basic],
[CustomFieldDataType.Integer]: [ [CustomFieldDataType.Integer]: [
@ -232,13 +237,13 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
export class CustomFieldQueryExpression extends CustomFieldQueryElement { export class CustomFieldQueryExpression extends CustomFieldQueryElement {
constructor( constructor(
expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [ expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [
CustomFieldQueryLogicalOperator.And, CustomFieldQueryLogicalOperator.Or,
null, null,
] ]
) { ) {
super(CustomFieldQueryElementType.Expression) super(CustomFieldQueryElementType.Expression)
;[this._operator] = expressionArray let values
let values = expressionArray[1] ;[this._operator, values] = expressionArray
if (!values) { if (!values) {
this._value = [] this._value = []
} else if (values?.length > 0 && values[0] instanceof Array) { } else if (values?.length > 0 && values[0] instanceof Array) {
@ -279,4 +284,26 @@ export class CustomFieldQueryExpression extends CustomFieldQueryElement {
(this._value as any[]).every((v) => v.isValid) (this._value as any[]).every((v) => v.isValid)
) )
} }
public addAtom(
atom: CustomFieldQueryAtom = new CustomFieldQueryAtom([
null,
CustomFieldQueryOperator.Exists,
'true',
])
) {
;(this._value as CustomFieldQueryElement[]).push(atom)
atom.changed.subscribe(() => {
this.changed.next(this)
})
}
public addExpression(
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
) {
;(this._value as CustomFieldQueryElement[]).push(expression)
expression.changed.subscribe(() => {
this.changed.next(this)
})
}
} }