Browse Source

Refactor filter dialog components: typed forms, modern Angular patterns, UI improvements

- Migrate filter-dialog, key-filter-list, user-filter-dialog to typed reactive forms
- Replace mat-expansion-panel with tb-form-panel stroked layout in key-filter-list
- Move AND/OR toggle-select into key-filter-list via ng-content projection
- Replace editable slide-toggle with tb-rounded-btn toggle button
- Migrate to @if/@for/@switch control flow and takeUntilDestroyed
- Remove unnecessary ErrorStateMatcher/submitted from dialog components
- Simplify keyFiltersToText operation parameter and alias-controller filter resolution
- Fix filter-text ngOnChanges to check value equality
- Fix user-filter-dialog: pass label to tb-datetime, fix tbTruncateWithTooltip in slide-toggle
pull/15394/head
Vladyslav_Prykhodko 6 months ago
parent
commit
ae58a58a6a
  1. 10
      ui-ngx/src/app/core/api/alias-controller.ts
  2. 1
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 81
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  4. 11
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss
  5. 81
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts
  6. 56
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts
  7. 144
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  8. 51
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss
  9. 122
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  10. 77
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  11. 6
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss
  12. 68
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts
  13. 2
      ui-ngx/src/app/shared/components/time/datetime.component.ts
  14. 3
      ui-ngx/src/app/shared/models/query/query.models.ts

10
ui-ngx/src/app/core/api/alias-controller.ts

@ -314,10 +314,14 @@ export class AliasController implements IAliasController {
|| newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) { || newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
const filterInfo = this.getFilterInfo(newDatasource.filterId); const filterInfo = this.getFilterInfo(newDatasource.filterId);
if (filterInfo?.keyFiltersOperation) { if (filterInfo) {
newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation; newDatasource.keyFilters = filterInfoToKeyFilters(filterInfo);
if (filterInfo.keyFiltersOperation) {
newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation;
}
} else {
newDatasource.keyFilters = [];
} }
} }
if (newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.type === DatasourceType.alarmCount) {

1
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -358,7 +358,6 @@ export class EntityDataSubscription {
this.subscriber = new TelemetrySubscriber(this.telemetryService); this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd(); this.dataCommand = new EntityDataCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters; let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) { if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) { if (keyFilters) {

81
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -16,64 +16,61 @@
--> -->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;"> <form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary" class="justify-between">
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2> <h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <div mat-dialog-content class="flex flex-col">
</mat-progress-bar> <div class="flex flex-row items-start gap-2">
<div mat-dialog-content> <mat-form-field class="mat-block flex-1" appearance="outline">
<fieldset [disabled]="isLoading$ | async"> <mat-label translate>filter.name</mat-label>
<div class="flex flex-1 flex-col"> <input matInput formControlName="filter" required>
<div class="flex flex-row"> @if (filterFormGroup.controls.filter.hasError('required')) {
<mat-form-field class="mat-block flex-1"> <mat-error>{{ 'filter.name-required' | translate }}</mat-error>
<mat-label translate>filter.name</mat-label> }
<input matInput formControlName="filter" required> @if (filterFormGroup.controls.filter.hasError('duplicateFilterName')) {
<mat-error *ngIf="filterFormGroup.get('filter').hasError('required')"> <mat-error>{{ 'filter.duplicate-filter' | translate }}</mat-error>
{{ 'filter.name-required' | translate }} }
</mat-error> </mat-form-field>
<mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')"> <button mat-stroked-button
{{ 'filter.duplicate-filter' | translate }} type="button"
</mat-error> class="tb-rounded-btn mt-2"
</mat-form-field> color="primary"
<section class="tb-editable-switch flex flex-col items-center justify-start"> [class.active]="filterFormGroup.get('editable').value"
<label class="tb-small editable-label" translate>filter.editable</label> (click)="onEditableChange($event)">
<mat-slide-toggle class="editable-switch" <mat-icon [class.invisible]="!filterFormGroup.get('editable').value">checkmark</mat-icon>
formControlName="editable"> <span translate>filter.editable</span>
</mat-slide-toggle> </button>
</section> </div>
</div> <tb-key-filter-list
<div class="flex flex-row items-center justify-between" style="margin-top: 8px;"> formControlName="keyFilters"
<div class="tb-form-panel-title" translate>filter.key-filters</div> [operation]="filterFormGroup.get('keyFiltersOperation').value">
<tb-toggle-select formControlName="keyFiltersOperation" <tb-toggle-select keyFilterHeaderActions
[matTooltip]="!allowKeyFiltersOrConditions ? ('filter.or-conditions-disabled' | translate) : ''" formControlName="keyFiltersOperation"
selectMediaBreakpoint="xs"> [matTooltip]="!allowKeyFiltersOrConditions ? ('filter.or-conditions-disabled' | translate) : ''"
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option> selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option> <tb-toggle-option
</tb-toggle-select> [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate | titlecase }}
</div> </tb-toggle-option>
<tb-key-filter-list <tb-toggle-option
formControlName="keyFilters" [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate | titlecase }}
[operation]="filterFormGroup.get('keyFiltersOperation').value"> </tb-toggle-option>
</tb-key-filter-list> </tb-toggle-select>
</div> </tb-key-filter-list>
</fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> [disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }} {{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
</div> </div>

11
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss

@ -14,15 +14,4 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-editable-switch {
padding-left: 10px;
.editable-switch {
margin: 0;
}
.editable-label {
margin: 5px 0;
}
}
} }

81
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts

@ -14,26 +14,23 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
Validators
} from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import {
import { ComplexOperation, complexOperationTranslationMap, Filter, Filters } from '@shared/models/query/query.models'; ComplexOperation,
complexOperationTranslationMap,
Filter,
FilterInfo,
Filters
} from '@shared/models/query/query.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { FormControlsFrom } from '@shared/models/tenant.model';
export interface FilterDialogData { export interface FilterDialogData {
isAdd: boolean; isAdd: boolean;
@ -42,36 +39,30 @@ export interface FilterDialogData {
} }
@Component({ @Component({
selector: 'tb-filter-dialog', selector: 'tb-filter-dialog',
templateUrl: './filter-dialog.component.html', templateUrl: './filter-dialog.component.html',
providers: [{ provide: ErrorStateMatcher, useExisting: FilterDialogComponent }], styleUrls: ['./filter-dialog.component.scss'],
styleUrls: ['./filter-dialog.component.scss'], standalone: false
standalone: false
}) })
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> {
implements OnInit, ErrorStateMatcher {
isAdd: boolean; isAdd: boolean;
filters: Array<Filter>;
filter: Filter;
filterFormGroup: UntypedFormGroup;
submitted = false; filterFormGroup: FormGroup<FormControlsFrom<FilterInfo>>;
ComplexOperation = ComplexOperation; ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = complexOperationTranslationMap;
allowKeyFiltersOrConditions: boolean; allowKeyFiltersOrConditions: boolean;
private readonly filter: Filter;
private filters: Array<Filter>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: FilterDialogData, @Inject(MAT_DIALOG_DATA) public data: FilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, protected dialogRef: MatDialogRef<FilterDialogComponent, Filter>,
public dialogRef: MatDialogRef<FilterDialogComponent, Filter>, private fb: FormBuilder,
private fb: UntypedFormBuilder, private utils: UtilsService) {
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.isAdd = data.isAdd; this.isAdd = data.isAdd;
if (Array.isArray(data.filters)) { if (Array.isArray(data.filters)) {
@ -102,15 +93,15 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
}); });
if (!this.allowKeyFiltersOrConditions) { if (!this.allowKeyFiltersOrConditions) {
if (this.filter.keyFiltersOperation === ComplexOperation.OR) { if (this.filter.keyFiltersOperation === ComplexOperation.OR) {
this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND); this.filterFormGroup.controls.keyFiltersOperation.setValue(ComplexOperation.AND);
this.filterFormGroup.markAsDirty(); this.filterFormGroup.markAsDirty();
} }
this.filterFormGroup.get('keyFiltersOperation').disable(); this.filterFormGroup.controls.keyFiltersOperation.disable();
} }
} }
validateDuplicateFilterName(): ValidatorFn { validateDuplicateFilterName(): ValidatorFn {
return (c: UntypedFormControl) => { return (c: FormControl<string>) => {
const newFilter = c.value.trim(); const newFilter = c.value.trim();
const found = this.filters.find((filter) => filter.filter === newFilter); const found = this.filters.find((filter) => filter.filter === newFilter);
if (found) { if (found) {
@ -126,13 +117,11 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
}; };
} }
ngOnInit(): void { onEditableChange(event: MouseEvent): void {
} event.stopPropagation();
const editableControl = this.filterFormGroup.controls.editable;
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { editableControl.setValue(!editableControl.value);
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); editableControl.markAsDirty();
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
} }
cancel(): void { cancel(): void {
@ -140,11 +129,11 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
} }
save(): void { save(): void {
this.submitted = true; const {filter, editable, keyFilters, keyFiltersOperation} = this.filterFormGroup.getRawValue();
this.filter.filter = this.filterFormGroup.get('filter').value.trim(); this.filter.filter = filter.trim();
this.filter.editable = this.filterFormGroup.get('editable').value; this.filter.editable = editable;
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value; this.filter.keyFilters = keyFilters;
this.filter.keyFiltersOperation = this.filterFormGroup.get('keyFiltersOperation').value; this.filter.keyFiltersOperation = keyFiltersOperation;
if (this.isAdd) { if (this.isAdd) {
this.filter.id = this.utils.guid(); this.filter.id = this.utils.guid();
} }

56
ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts

@ -14,37 +14,31 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ComplexOperation, KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; import { ComplexOperation, KeyFilter, keyFiltersToText } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-filter-text', selector: 'tb-filter-text',
templateUrl: './filter-text.component.html', templateUrl: './filter-text.component.html',
styleUrls: ['./filter-text.component.scss'], styleUrls: ['./filter-text.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterTextComponent), useExisting: forwardRef(() => FilterTextComponent),
multi: true multi: true
} }
], ],
standalone: false standalone: false
}) })
export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChanges { export class FilterTextComponent implements ControlValueAccessor, OnChanges {
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input() @Input()
set required(value: boolean) { @coerceBoolean()
this.requiredValue = coerceBooleanProperty(value); required = false;
}
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -66,26 +60,20 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChan
public filterText: string; public filterText: string;
private currentValue: Array<KeyFilter>; private currentValue: Array<KeyFilter>;
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog, constructor(private translate: TranslateService,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private datePipe: DatePipe) { private datePipe: DatePipe) {
} }
registerOnChange(fn: any): void { registerOnChange(_fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
} }
ngOnInit() { registerOnTouched(_fn: any): void {
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes.operation && !changes.operation.firstChange) { if (changes.operation && !changes.operation.firstChange
&& changes.operation.currentValue !== changes.operation.previousValue) {
this.updateFilterText(this.currentValue); this.updateFilterText(this.currentValue);
} }
} }

144
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html

@ -15,78 +15,86 @@
limitations under the License. limitations under the License.
--> -->
<section class="flex flex-col" [formGroup]="keyFilterListFormGroup"> <section class="tb-form-panel no-padding no-border">
<mat-accordion [multi]="true"> <div class="tb-form-panel stroked">
<mat-expansion-panel [expanded]="true"> <div class="flex flex-row items-center justify-between">
<mat-expansion-panel-header> <div class="tb-form-panel-title" translate>filter.key-filters</div>
<mat-panel-title> <ng-content select="[keyFilterHeaderActions]"></ng-content>
<div translate>filter.key-filters</div> </div>
</mat-panel-title> @if (keyFiltersFormArray.length) {
</mat-expansion-panel-header> <section class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row"> <div class="flex flex-row">
<span class="max-w-8% flex-full"></span> <span class="min-w-16"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start"> <div class="flex flex-1 flex-row items-center justify-start">
<label translate class="tb-title no-padding flex-1">filter.key-name</label> <div translate class="filter-title flex-1">filter.key-name</div>
<label translate class="tb-title no-padding flex-1">filter.key-type.key-type</label> <div translate class="filter-title flex-1">filter.key-type.key-type</div>
<span [class.!hidden]="disabled" style="min-width: 80px;">&nbsp;</span> <span [class.!hidden]="disabled" style="min-width: 96px;">&nbsp;</span>
<span [class.!hidden]="!disabled" style="min-width: 40px;">&nbsp;</span> <span [class.!hidden]="!disabled" style="min-width: 48px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="key-filter-list">
<div class="flex flex-row items-stretch justify-start" style="max-height: 76px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray.controls; let $index = index">
<div class="filters-operation max-w-8% flex-full">
<span *ngIf="$index > 0">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div> </div>
<div class="flex max-w-92% flex-full flex-col"> </div>
<div class="flex flex-row items-center justify-start"> <mat-divider></mat-divider>
<div class="flex-1">{{ keyFilterControl.value.key.key }}</div> <div class="filter-list">
<div class="flex-1">{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}</div> @for (keyFilterControl of keyFiltersFormArray.controls; track keyFilterControl; let index = $index) {
<button mat-icon-button color="primary" <div class="flex flex-row items-stretch justify-start"
type="button" [class.filter-list-divider]="index">
(click)="editKeyFilter($index)" <div class="filters-operation min-w-16">
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}" @if (index) {
matTooltipPosition="above"> <div class="filters-operation-container">
<mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon> <span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</button> </div>
<button mat-icon-button color="primary" }
[class.!hidden]="disabled" </div>
type="button" <div class="flex flex-1 flex-col">
(click)="removeKeyFilter($index)" <div class="flex flex-row items-center justify-start">
matTooltip="{{ 'filter.remove-key-filter' | translate }}" <div class="filters-text flex-1" tbTruncateWithTooltip>{{ keyFilterControl.value.key.key }}</div>
matTooltipPosition="above"> <div class="filters-text flex-1" tbTruncateWithTooltip>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}</div>
<mat-icon>close</mat-icon> <button mat-icon-button color="primary"
</button> type="button"
(click)="editKeyFilter(index)"
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>{{disabled ? 'visibility' : 'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
[class.!hidden]="disabled"
type="button"
(click)="removeKeyFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div> </div>
<mat-divider></mat-divider> }
</div>
</div> </div>
<span [class.!hidden]="!!keyFiltersFormArray.length" </section>
[class.disabled]="disabled" } @else {
class="no-data-found flex items-center justify-center" translate>filter.no-key-filters</span> <span class="no-data-found tb-prompt flex items-center justify-center"
</div> [class.required]="!disabled"
<div style="margin-top: 16px;"> translate>filter.no-key-filters</span>
<button mat-button mat-raised-button color="primary" }
[class.!hidden]="disabled" <div>
(click)="addKeyFilter()" <button mat-button mat-stroked-button color="primary"
type="button" [class.!hidden]="disabled"
matTooltip="{{ 'filter.add-key-filter' | translate }}" (click)="addKeyFilter()"
matTooltipPosition="above"> type="button">
{{ 'filter.add-key-filter' | translate }} {{ 'filter.add-key-filter' | translate }}
</button> </button>
</div> </div>
</mat-expansion-panel> </div>
<mat-expansion-panel [expanded]="true"> <div class="tb-form-panel no-gap stroked">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> {{ 'filter.preview' | translate }}
<div translate>filter.preview</div>
</mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="tb-filter-preview"> <ng-template matExpansionPanelContent>
<tb-filter-text [formControl]="keyFiltersControl" [operation]="operation"></tb-filter-text> @if (keyFiltersFormArray.length) {
</div> <tb-filter-text [formControl]="keyFiltersControl" [operation]="operation"></tb-filter-text>
} @else {
<span class="tb-prompt justify-center text-base" translate>filter.no-filter-text</span>
}
</ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </div>
</section> </section>

51
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss

@ -13,30 +13,45 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../scss/constants";
:host { :host {
.key-filter-list { .filter-title {
padding: 12px 0;
font-size: 14px;
font-weight: 500;
}
.no-data-found {
height: 50px;
font-size: 16px;
}
.filter-list {
overflow: auto; overflow: auto;
max-height: 300px; max-height: 300px;
.no-data-found {
height: 50px; &-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.filters-text {
font-size: 14px;
} }
} }
.filters-operation { .filters-operation {
margin-top: -18px; display: flex;
color: #666; justify-content: center;
font-weight: 500; margin-top: -14px;
} &-container {
.tb-filter-preview { background-color: white;
padding: 8px; }
border: 1px groove rgba(0, 0, 0, .25); &-label {
border-radius: 4px; font-size: 15px;
} font-weight: 400;
} color: $tb-primary-color;
padding: 0 8px;
:host ::ng-deep { border-radius: 4px;
.tb-filter-preview { border: 1px solid rgba($tb-primary-color, 0.32);
.tb-filter-text { background-color: rgba($tb-primary-color, 0.04);
max-height: 200px;
} }
} }
} }

122
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -14,26 +14,26 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { import {
AbstractControl,
ControlValueAccessor, ControlValueAccessor,
UntypedFormArray, FormArray,
UntypedFormBuilder, FormBuilder,
UntypedFormControl, FormControl,
UntypedFormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ValidationErrors, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, Subject } from 'rxjs'; import { Observable } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
ComplexOperation, ComplexOperation,
complexOperationTranslationMap, complexOperationTranslationMap,
EntityKeyType, EntityKeyType,
entityKeyTypeTranslationMap, entityKeyTypeTranslationMap,
KeyFilter,
KeyFilterInfo, KeyFilterInfo,
keyFilterInfosToKeyFilters keyFilterInfosToKeyFilters
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
@ -41,27 +41,26 @@ import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-key-filter-list', selector: 'tb-key-filter-list',
templateUrl: './key-filter-list.component.html', templateUrl: './key-filter-list.component.html',
styleUrls: ['./key-filter-list.component.scss'], styleUrls: ['./key-filter-list.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent), useExisting: forwardRef(() => KeyFilterListComponent),
multi: true multi: true
}, },
{ {
provide: NG_VALIDATORS, provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyFilterListComponent), useExisting: forwardRef(() => KeyFilterListComponent),
multi: true multi: true
} }
], ],
standalone: false standalone: false
}) })
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -77,39 +76,28 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = complexOperationTranslationMap;
keyFilterListFormGroup: UntypedFormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap; entityKeyTypeTranslations = entityKeyTypeTranslationMap;
keyFiltersControl: UntypedFormControl; keyFiltersFormArray: FormArray<FormControl<KeyFilterInfo>>;
keyFiltersControl: FormControl<Array<KeyFilter>>;
private destroy$ = new Subject<void>();
private propagateChange = null; private propagateChange = null;
constructor(private fb: UntypedFormBuilder, constructor(private fb: FormBuilder,
private dialog: MatDialog) { private dialog: MatDialog,
private destroyRef: DestroyRef) {
} }
ngOnInit(): void { ngOnInit(): void {
this.keyFilterListFormGroup = this.fb.group({ this.keyFiltersFormArray = this.fb.array<FormControl<KeyFilterInfo>>([]);
keyFilters: this.fb.array([])
});
this.keyFiltersControl = this.fb.control(null); this.keyFiltersControl = this.fb.control(null);
this.keyFilterListFormGroup.valueChanges.pipe( this.keyFiltersFormArray.valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel()); ).subscribe(() => this.updateModel());
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get keyFiltersFormArray(): UntypedFormArray {
return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -120,61 +108,61 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
setDisabledState?(isDisabled: boolean): void { setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false}); this.keyFiltersFormArray.disable({emitEvent: false});
this.keyFiltersControl.disable({emitEvent: false}); this.keyFiltersControl.disable({emitEvent: false});
} else { } else {
this.keyFilterListFormGroup.enable({emitEvent: false}); this.keyFiltersFormArray.enable({emitEvent: false});
this.keyFiltersControl.enable({emitEvent: false}); this.keyFiltersControl.enable({emitEvent: false});
} }
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : { return this.keyFiltersFormArray.valid && this.keyFiltersControl.valid ? null : {
keyFilterList: {valid: false} keyFilterList: {valid: false}
}; };
} }
writeValue(keyFilters: Array<KeyFilterInfo>): void { writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (keyFilters.length === this.keyFiltersFormArray.length) { if (keyFilters?.length === this.keyFiltersFormArray.length) {
this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false}); this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false});
} else { } else {
const keyFilterControls: Array<AbstractControl> = []; this.keyFiltersFormArray.clear({emitEvent: false});
if (keyFilters) { if (keyFilters) {
for (const keyFilter of keyFilters) { for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); this.keyFiltersFormArray.push(
this.fb.control(keyFilter, [Validators.required]),
{emitEvent: false}
);
} }
} }
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls), {emitEvent: false});
if (this.disabled) { if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false}); this.keyFiltersFormArray.disable({emitEvent: false});
} else { } else {
this.keyFilterListFormGroup.enable({emitEvent: false}); this.keyFiltersFormArray.enable({emitEvent: false});
} }
} }
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});
} }
public removeKeyFilter(index: number) { removeKeyFilter(index: number) {
(this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).removeAt(index); this.keyFiltersFormArray.removeAt(index);
} }
public addKeyFilter() { addKeyFilter() {
const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
this.openKeyFilterDialog(null).subscribe((result) => { this.openKeyFilterDialog(null).subscribe((result) => {
if (result) { if (result) {
keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); this.keyFiltersFormArray.push(this.fb.control(result, [Validators.required]));
} }
}); });
} }
public editKeyFilter(index: number) { editKeyFilter(index: number) {
const keyFilter: KeyFilterInfo = const keyFilter = this.keyFiltersFormArray.at(index).value;
(this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).value;
this.openKeyFilterDialog(keyFilter).subscribe( this.openKeyFilterDialog(keyFilter).subscribe(
(result) => { (result) => {
if (result) { if (result) {
(this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).patchValue(result); this.keyFiltersFormArray.at(index).patchValue(result);
} }
} }
); );
@ -210,12 +198,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
} }
private updateModel() { private updateModel() {
const keyFilters: Array<KeyFilterInfo> = this.keyFilterListFormGroup.getRawValue().keyFilters; const keyFilters = this.keyFiltersFormArray.getRawValue();
if (keyFilters.length) { this.propagateChange(keyFilters.length ? keyFilters : null);
this.propagateChange(keyFilters);
} else {
this.propagateChange(null);
}
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});
} }

77
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -15,64 +15,61 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog"> <form (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
<mat-toolbar color="primary"> <mat-toolbar color="primary" class="justify-between">
<h2>{{ filter.filter | customTranslate }}</h2> <h2>{{ filter.filter | customTranslate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> @for (userInputControl of userInputsFormArray.controls; track userInputControl) {
<div formArrayName="userInputs" @switch (userInputControl.controls.valueType.value) {
*ngFor="let userInputControl of userInputsFormArray().controls; let $index = index"> @case (valueTypeEnum.STRING) {
<div [ngSwitch]="userInputControl.get('valueType').value"> <mat-form-field class="mat-block" appearance="outline">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING"> <mat-label>{{ userInputControl.controls.label.value | customTranslate }}</mat-label>
<mat-form-field class="mat-block" appearance="outline"> <input matInput [formControl]="userInputControl.controls.value">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label> </mat-form-field>
<input matInput [formControl]="userInputControl.get('value')"> }
</mat-form-field> @case (valueTypeEnum.NUMERIC) {
</ng-template> <mat-form-field class="mat-block numeric-field" appearance="outline">
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC"> <mat-label>{{ userInputControl.controls.label.value | customTranslate }}</mat-label>
<mat-form-field class="mat-block" appearance="outline"> <input required type="number" matInput [formControl]="userInputControl.controls.value">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label> @if (userInputControl.controls.unitSymbol.value) {
<input required type="number" matInput [formControl]="userInputControl.get('value')"> <div matSuffix class="mr-2">
<div matSuffix class="mr-2" {{ userInputControl.controls.unitSymbol.value }}
[class.!hidden]="!userInputControl.get('unitSymbol').value">
{{ userInputControl.get('unitSymbol').value }}
</div> </div>
</mat-form-field> }
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> }
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value | customTranslate }}</label> @case (valueTypeEnum.DATE_TIME) {
<tb-datetime [formControl]="userInputControl.get('value')" <tb-datetime [formControl]="userInputControl.controls.value"
dateText="{{ 'filter.date' | translate }}" [fieldClass]="'mat-block'"
required [showLabel]="false"></tb-datetime> [dateText]="userInputControl.controls.label.value | customTranslate"
</ng-template> appearance="outline"
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> required></tb-datetime>
<mat-checkbox labelPosition="before" [formControl]="userInputControl.get('value')"> }
{{ userInputControl.get('label').value | customTranslate }} @case (valueTypeEnum.BOOLEAN) {
</mat-checkbox> <mat-slide-toggle [formControl]="userInputControl.controls.value">
</ng-template> <span class="inline-block max-w-72" tbTruncateWithTooltip>
</div> {{ userInputControl.controls.label.value | customTranslate }}
</div> </span>
</fieldset> </mat-slide-toggle>
}
}
}
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || userFilterFormGroup.invalid || !userFilterFormGroup.dirty"> [disabled]="userInputsFormArray.invalid || !userInputsFormArray.dirty">
{{ 'action.update' | translate }} {{ 'action.update' | translate }}
</button> </button>
</div> </div>

6
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss

@ -14,14 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { :host ::ng-deep {
.user-filter-dialog { .numeric-field {
.mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder { &.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder {
.mat-mdc-form-field-icon-suffix { .mat-mdc-form-field-icon-suffix {
place-self: center; place-self: center;
} }
} }
.mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix { &.mat-mdc-form-field-has-icon-suffix {
.mat-mdc-form-field-icon-suffix { .mat-mdc-form-field-icon-suffix {
place-self: baseline; place-self: baseline;
} }

68
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts

@ -14,12 +14,11 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core'; import { Component, DestroyRef, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormArray, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -39,29 +38,31 @@ export interface UserFilterDialogData {
filter: Filter; filter: Filter;
} }
interface UserInputForm {
label: FormControl<string>;
valueType: FormControl<EntityKeyValueType>;
unitSymbol: FormControl<string>;
value: FormControl<string | number | boolean>;
}
@Component({ @Component({
selector: 'tb-user-filter-dialog', selector: 'tb-user-filter-dialog',
templateUrl: './user-filter-dialog.component.html', templateUrl: './user-filter-dialog.component.html',
providers: [{ provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent }],
styleUrls: ['./user-filter-dialog.component.scss'], styleUrls: ['./user-filter-dialog.component.scss'],
standalone: false standalone: false
}) })
export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter> export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter> {
implements ErrorStateMatcher {
filter: Filter; filter: Filter;
userFilterFormGroup: FormGroup; userInputsFormArray: FormArray<FormGroup<UserInputForm>>;
valueTypeEnum = EntityKeyValueType; valueTypeEnum = EntityKeyValueType;
submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData, @Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, protected dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
public dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
private fb: FormBuilder, private fb: FormBuilder,
private translate: TranslateService, private translate: TranslateService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
@ -70,19 +71,13 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
this.filter = data.filter; this.filter = data.filter;
const userInputs = filterToUserFilterInfoList(this.filter, this.translate); const userInputs = filterToUserFilterInfoList(this.filter, this.translate);
const userInputControls: Array<FormGroup> = []; const userInputControls = userInputs.map(input => this.createUserInputFormControl(input));
for (const userInput of userInputs) { this.userInputsFormArray = this.fb.array(userInputControls);
userInputControls.push(this.createUserInputFormControl(userInput));
}
this.userFilterFormGroup = this.fb.group({
userInputs: this.fb.array(userInputControls)
});
} }
private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup { private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup<UserInputForm> {
const predicateValue: FilterPredicateValue<string | number | boolean> = (userInput.info.keyFilterPredicate as any).value; const predicateValue: FilterPredicateValue<string | number | boolean> = (userInput.info.keyFilterPredicate as any).value;
let value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; let value: string | number | boolean = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue;
let unitSymbol = ''; let unitSymbol = '';
let valueConvertor: TbUnitConverter; let valueConvertor: TbUnitConverter;
if (userInput.valueType === EntityKeyValueType.NUMERIC) { if (userInput.valueType === EntityKeyValueType.NUMERIC) {
@ -91,42 +86,31 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
value = this.unitService.convertUnitValue(value as number, userInput.unit); value = this.unitService.convertUnitValue(value as number, userInput.unit);
valueConvertor = this.unitService.geUnitConverter(unitSymbol, sourceUnit); valueConvertor = this.unitService.geUnitConverter(unitSymbol, sourceUnit);
} }
const userInputControl = this.fb.group({ const userInputControl = this.fb.group<UserInputForm>({
label: [userInput.label], label: this.fb.control(userInput.label),
valueType: [userInput.valueType], valueType: this.fb.control(userInput.valueType),
unitSymbol: [unitSymbol], unitSymbol: this.fb.control(unitSymbol),
value: [value, value: this.fb.control(value,
userInput.valueType === EntityKeyValueType.NUMERIC || userInput.valueType === EntityKeyValueType.NUMERIC ||
userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []] userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : [])
}); });
userInputControl.get('value').valueChanges.pipe( userInputControl.controls.value.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(userValue => { ).subscribe(userValue => {
let value = userValue; let val = userValue;
if (valueConvertor) { if (valueConvertor) {
value = valueConvertor(value as number); val = valueConvertor(val as number);
} }
(userInput.info.keyFilterPredicate as any).value.userValue = value; (userInput.info.keyFilterPredicate as any).value.userValue = val;
}); });
return userInputControl; return userInputControl;
} }
userInputsFormArray(): FormArray {
return this.userFilterFormGroup.get('userInputs') as FormArray;
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save(): void { save(): void {
this.submitted = true;
this.dialogRef.close(this.filter); this.dialogRef.close(this.filter);
} }
} }

2
ui-ngx/src/app/shared/components/time/datetime.component.ts

@ -40,7 +40,7 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
return this.requiredValue; return this.requiredValue;
} }
@Input() @Input()
set required(value: boolean) { set required(value: unknown) {
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }

3
ui-ngx/src/app/shared/models/query/query.models.ts

@ -411,8 +411,7 @@ export interface FiltersInfo {
export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<KeyFilter>, export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<KeyFilter>,
operation: ComplexOperation = ComplexOperation.AND): string { operation: ComplexOperation = ComplexOperation.AND): string {
const filtersText = keyFilters.map(keyFilter => const filtersText = keyFilters.map(keyFilter =>
keyFilterToText(translate, datePipe, keyFilter, keyFilterToText(translate, datePipe, keyFilter, operation));
keyFilters.length > 1 ? operation : undefined));
let result: string; let result: string;
if (filtersText.length > 1) { if (filtersText.length > 1) {
const opText = translate.instant(complexOperationTranslationMap.get(operation)); const opText = translate.instant(complexOperationTranslationMap.get(operation));

Loading…
Cancel
Save