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

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

@ -14,15 +14,4 @@
* limitations under the License.
*/
: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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
Validators
} from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { ComplexOperation, complexOperationTranslationMap, Filter, Filters } from '@shared/models/query/query.models';
import {
ComplexOperation,
complexOperationTranslationMap,
Filter,
FilterInfo,
Filters
} from '@shared/models/query/query.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { FormControlsFrom } from '@shared/models/tenant.model';
export interface FilterDialogData {
isAdd: boolean;
@ -42,36 +39,30 @@ export interface FilterDialogData {
}
@Component({
selector: 'tb-filter-dialog',
templateUrl: './filter-dialog.component.html',
providers: [{ provide: ErrorStateMatcher, useExisting: FilterDialogComponent }],
styleUrls: ['./filter-dialog.component.scss'],
standalone: false
selector: 'tb-filter-dialog',
templateUrl: './filter-dialog.component.html',
styleUrls: ['./filter-dialog.component.scss'],
standalone: false
})
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter>
implements OnInit, ErrorStateMatcher {
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> {
isAdd: boolean;
filters: Array<Filter>;
filter: Filter;
filterFormGroup: UntypedFormGroup;
submitted = false;
filterFormGroup: FormGroup<FormControlsFrom<FilterInfo>>;
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
allowKeyFiltersOrConditions: boolean;
private readonly filter: Filter;
private filters: Array<Filter>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: FilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<FilterDialogComponent, Filter>,
private fb: UntypedFormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
protected dialogRef: MatDialogRef<FilterDialogComponent, Filter>,
private fb: FormBuilder,
private utils: UtilsService) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
if (Array.isArray(data.filters)) {
@ -102,15 +93,15 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
});
if (!this.allowKeyFiltersOrConditions) {
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.get('keyFiltersOperation').disable();
this.filterFormGroup.controls.keyFiltersOperation.disable();
}
}
validateDuplicateFilterName(): ValidatorFn {
return (c: UntypedFormControl) => {
return (c: FormControl<string>) => {
const newFilter = c.value.trim();
const found = this.filters.find((filter) => filter.filter === newFilter);
if (found) {
@ -126,13 +117,11 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
};
}
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
onEditableChange(event: MouseEvent): void {
event.stopPropagation();
const editableControl = this.filterFormGroup.controls.editable;
editableControl.setValue(!editableControl.value);
editableControl.markAsDirty();
}
cancel(): void {
@ -140,11 +129,11 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
}
save(): void {
this.submitted = true;
this.filter.filter = this.filterFormGroup.get('filter').value.trim();
this.filter.editable = this.filterFormGroup.get('editable').value;
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value;
this.filter.keyFiltersOperation = this.filterFormGroup.get('keyFiltersOperation').value;
const {filter, editable, keyFilters, keyFiltersOperation} = this.filterFormGroup.getRawValue();
this.filter.filter = filter.trim();
this.filter.editable = editable;
this.filter.keyFilters = keyFilters;
this.filter.keyFiltersOperation = keyFiltersOperation;
if (this.isAdd) {
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.
///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ComplexOperation, KeyFilter, keyFiltersToText } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-filter-text',
templateUrl: './filter-text.component.html',
styleUrls: ['./filter-text.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterTextComponent),
multi: true
}
],
standalone: false
selector: 'tb-filter-text',
templateUrl: './filter-text.component.html',
styleUrls: ['./filter-text.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterTextComponent),
multi: true
}
],
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()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
required = false;
@Input()
disabled: boolean;
@ -66,26 +60,20 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChan
public filterText: string;
private currentValue: Array<KeyFilter>;
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder,
private translate: TranslateService,
constructor(private translate: TranslateService,
private datePipe: DatePipe) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnChange(_fn: any): void {
}
ngOnInit() {
registerOnTouched(_fn: any): void {
}
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);
}
}

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

@ -15,78 +15,86 @@
limitations under the License.
-->
<section class="flex flex-col" [formGroup]="keyFilterListFormGroup">
<mat-accordion [multi]="true">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.key-filters</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="tb-title no-padding flex-1">filter.key-name</label>
<label translate class="tb-title no-padding flex-1">filter.key-type.key-type</label>
<span [class.!hidden]="disabled" style="min-width: 80px;">&nbsp;</span>
<span [class.!hidden]="!disabled" style="min-width: 40px;">&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>
<section class="tb-form-panel no-padding no-border">
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>filter.key-filters</div>
<ng-content select="[keyFilterHeaderActions]"></ng-content>
</div>
@if (keyFiltersFormArray.length) {
<section class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<span class="min-w-16"></span>
<div class="flex flex-1 flex-row items-center justify-start">
<div translate class="filter-title flex-1">filter.key-name</div>
<div translate class="filter-title flex-1">filter.key-type.key-type</div>
<span [class.!hidden]="disabled" style="min-width: 96px;">&nbsp;</span>
<span [class.!hidden]="!disabled" style="min-width: 48px;">&nbsp;</span>
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ keyFilterControl.value.key.key }}</div>
<div class="flex-1">{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>{{disabled ? 'more_vert' : '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>
<mat-divider></mat-divider>
<div class="filter-list">
@for (keyFilterControl of keyFiltersFormArray.controls; track keyFilterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
[class.filter-list-divider]="index">
<div class="filters-operation min-w-16">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div>
}
</div>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center justify-start">
<div class="filters-text flex-1" tbTruncateWithTooltip>{{ keyFilterControl.value.key.key }}</div>
<div class="filters-text flex-1" tbTruncateWithTooltip>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}</div>
<button mat-icon-button color="primary"
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>
<mat-divider></mat-divider>
</div>
}
</div>
<span [class.!hidden]="!!keyFiltersFormArray.length"
[class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>filter.no-key-filters</span>
</div>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addKeyFilter()"
type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
</section>
} @else {
<span class="no-data-found tb-prompt flex items-center justify-center"
[class.required]="!disabled"
translate>filter.no-key-filters</span>
}
<div>
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addKeyFilter()"
type="button">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
</div>
<div class="tb-form-panel no-gap stroked">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.preview</div>
</mat-panel-title>
{{ 'filter.preview' | translate }}
</mat-expansion-panel-header>
<div class="tb-filter-preview">
<tb-filter-text [formControl]="keyFiltersControl" [operation]="operation"></tb-filter-text>
</div>
<ng-template matExpansionPanelContent>
@if (keyFiltersFormArray.length) {
<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-accordion>
</div>
</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
* limitations under the License.
*/
@import "../scss/constants";
: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;
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 {
margin-top: -18px;
color: #666;
font-weight: 500;
}
.tb-filter-preview {
padding: 8px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
}
}
:host ::ng-deep {
.tb-filter-preview {
.tb-filter-text {
max-height: 200px;
display: flex;
justify-content: center;
margin-top: -14px;
&-container {
background-color: white;
}
&-label {
font-size: 15px;
font-weight: 400;
color: $tb-primary-color;
padding: 0 8px;
border-radius: 4px;
border: 1px solid rgba($tb-primary-color, 0.32);
background-color: rgba($tb-primary-color, 0.04);
}
}
}

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

@ -14,26 +14,26 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormArray,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { Observable } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyType,
entityKeyTypeTranslationMap,
KeyFilter,
KeyFilterInfo,
keyFilterInfosToKeyFilters
} from '@shared/models/query/query.models';
@ -41,27 +41,26 @@ import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-key-filter-list',
templateUrl: './key-filter-list.component.html',
styleUrls: ['./key-filter-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
}
],
standalone: false
selector: 'tb-key-filter-list',
templateUrl: './key-filter-list.component.html',
styleUrls: ['./key-filter-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
}
],
standalone: false
})
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -77,39 +76,28 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
complexOperationTranslationMap = complexOperationTranslationMap;
keyFilterListFormGroup: UntypedFormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
keyFiltersControl: UntypedFormControl;
keyFiltersFormArray: FormArray<FormControl<KeyFilterInfo>>;
keyFiltersControl: FormControl<Array<KeyFilter>>;
private destroy$ = new Subject<void>();
private propagateChange = null;
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog) {
constructor(private fb: FormBuilder,
private dialog: MatDialog,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.keyFilterListFormGroup = this.fb.group({
keyFilters: this.fb.array([])
});
this.keyFiltersFormArray = this.fb.array<FormControl<KeyFilterInfo>>([]);
this.keyFiltersControl = this.fb.control(null);
this.keyFilterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
this.keyFiltersFormArray.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get keyFiltersFormArray(): UntypedFormArray {
return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@ -120,61 +108,61 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
this.keyFiltersFormArray.disable({emitEvent: false});
this.keyFiltersControl.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
this.keyFiltersFormArray.enable({emitEvent: false});
this.keyFiltersControl.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : {
return this.keyFiltersFormArray.valid && this.keyFiltersControl.valid ? null : {
keyFilterList: {valid: false}
};
}
writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (keyFilters.length === this.keyFiltersFormArray.length) {
if (keyFilters?.length === this.keyFiltersFormArray.length) {
this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false});
} else {
const keyFilterControls: Array<AbstractControl> = [];
this.keyFiltersFormArray.clear({emitEvent: false});
if (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) {
this.keyFilterListFormGroup.disable({emitEvent: false});
this.keyFiltersFormArray.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
this.keyFiltersFormArray.enable({emitEvent: false});
}
}
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});
}
public removeKeyFilter(index: number) {
(this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).removeAt(index);
removeKeyFilter(index: number) {
this.keyFiltersFormArray.removeAt(index);
}
public addKeyFilter() {
const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
addKeyFilter() {
this.openKeyFilterDialog(null).subscribe((result) => {
if (result) {
keyFiltersFormArray.push(this.fb.control(result, [Validators.required]));
this.keyFiltersFormArray.push(this.fb.control(result, [Validators.required]));
}
});
}
public editKeyFilter(index: number) {
const keyFilter: KeyFilterInfo =
(this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).value;
editKeyFilter(index: number) {
const keyFilter = this.keyFiltersFormArray.at(index).value;
this.openKeyFilterDialog(keyFilter).subscribe(
(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() {
const keyFilters: Array<KeyFilterInfo> = this.keyFilterListFormGroup.getRawValue().keyFilters;
if (keyFilters.length) {
this.propagateChange(keyFilters);
} else {
this.propagateChange(null);
}
const keyFilters = this.keyFiltersFormArray.getRawValue();
this.propagateChange(keyFilters.length ? keyFilters : null);
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
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.
-->
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
<mat-toolbar color="primary">
<form (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
<mat-toolbar color="primary" class="justify-between">
<h2>{{ filter.filter | customTranslate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div formArrayName="userInputs"
*ngFor="let userInputControl of userInputsFormArray().controls; let $index = index">
<div [ngSwitch]="userInputControl.get('valueType').value">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label>
<input matInput [formControl]="userInputControl.get('value')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label>
<input required type="number" matInput [formControl]="userInputControl.get('value')">
<div matSuffix class="mr-2"
[class.!hidden]="!userInputControl.get('unitSymbol').value">
{{ userInputControl.get('unitSymbol').value }}
@for (userInputControl of userInputsFormArray.controls; track userInputControl) {
@switch (userInputControl.controls.valueType.value) {
@case (valueTypeEnum.STRING) {
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{ userInputControl.controls.label.value | customTranslate }}</mat-label>
<input matInput [formControl]="userInputControl.controls.value">
</mat-form-field>
}
@case (valueTypeEnum.NUMERIC) {
<mat-form-field class="mat-block numeric-field" appearance="outline">
<mat-label>{{ userInputControl.controls.label.value | customTranslate }}</mat-label>
<input required type="number" matInput [formControl]="userInputControl.controls.value">
@if (userInputControl.controls.unitSymbol.value) {
<div matSuffix class="mr-2">
{{ userInputControl.controls.unitSymbol.value }}
</div>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value | customTranslate }}</label>
<tb-datetime [formControl]="userInputControl.get('value')"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
<mat-checkbox labelPosition="before" [formControl]="userInputControl.get('value')">
{{ userInputControl.get('label').value | customTranslate }}
</mat-checkbox>
</ng-template>
</div>
</div>
</fieldset>
}
</mat-form-field>
}
@case (valueTypeEnum.DATE_TIME) {
<tb-datetime [formControl]="userInputControl.controls.value"
[fieldClass]="'mat-block'"
[dateText]="userInputControl.controls.label.value | customTranslate"
appearance="outline"
required></tb-datetime>
}
@case (valueTypeEnum.BOOLEAN) {
<mat-slide-toggle [formControl]="userInputControl.controls.value">
<span class="inline-block max-w-72" tbTruncateWithTooltip>
{{ userInputControl.controls.label.value | customTranslate }}
</span>
</mat-slide-toggle>
}
}
}
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || userFilterFormGroup.invalid || !userFilterFormGroup.dirty">
[disabled]="userInputsFormArray.invalid || !userInputsFormArray.dirty">
{{ 'action.update' | translate }}
</button>
</div>

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

@ -14,14 +14,14 @@
* limitations under the License.
*/
:host ::ng-deep {
.user-filter-dialog {
.mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder {
.numeric-field {
&.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder {
.mat-mdc-form-field-icon-suffix {
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 {
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.
///
import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, DestroyRef, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
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 { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
@ -39,29 +38,31 @@ export interface UserFilterDialogData {
filter: Filter;
}
interface UserInputForm {
label: FormControl<string>;
valueType: FormControl<EntityKeyValueType>;
unitSymbol: FormControl<string>;
value: FormControl<string | number | boolean>;
}
@Component({
selector: 'tb-user-filter-dialog',
templateUrl: './user-filter-dialog.component.html',
providers: [{ provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent }],
styleUrls: ['./user-filter-dialog.component.scss'],
standalone: false
})
export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter>
implements ErrorStateMatcher {
export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter> {
filter: Filter;
userFilterFormGroup: FormGroup;
userInputsFormArray: FormArray<FormGroup<UserInputForm>>;
valueTypeEnum = EntityKeyValueType;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
protected dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
private fb: FormBuilder,
private translate: TranslateService,
private destroyRef: DestroyRef,
@ -70,19 +71,13 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
this.filter = data.filter;
const userInputs = filterToUserFilterInfoList(this.filter, this.translate);
const userInputControls: Array<FormGroup> = [];
for (const userInput of userInputs) {
userInputControls.push(this.createUserInputFormControl(userInput));
}
this.userFilterFormGroup = this.fb.group({
userInputs: this.fb.array(userInputControls)
});
const userInputControls = userInputs.map(input => this.createUserInputFormControl(input));
this.userInputsFormArray = 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;
let value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue;
let value: string | number | boolean = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue;
let unitSymbol = '';
let valueConvertor: TbUnitConverter;
if (userInput.valueType === EntityKeyValueType.NUMERIC) {
@ -91,42 +86,31 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
value = this.unitService.convertUnitValue(value as number, userInput.unit);
valueConvertor = this.unitService.geUnitConverter(unitSymbol, sourceUnit);
}
const userInputControl = this.fb.group({
label: [userInput.label],
valueType: [userInput.valueType],
unitSymbol: [unitSymbol],
value: [value,
const userInputControl = this.fb.group<UserInputForm>({
label: this.fb.control(userInput.label),
valueType: this.fb.control(userInput.valueType),
unitSymbol: this.fb.control(unitSymbol),
value: this.fb.control(value,
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)
).subscribe(userValue => {
let value = userValue;
let val = userValue;
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;
}
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 {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
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;
}
@Input()
set required(value: boolean) {
set required(value: unknown) {
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>,
operation: ComplexOperation = ComplexOperation.AND): string {
const filtersText = keyFilters.map(keyFilter =>
keyFilterToText(translate, datePipe, keyFilter,
keyFilters.length > 1 ? operation : undefined));
keyFilterToText(translate, datePipe, keyFilter, operation));
let result: string;
if (filtersText.length > 1) {
const opText = translate.instant(complexOperationTranslationMap.get(operation));

Loading…
Cancel
Save