UI: Refactoring dashboard managment

This commit is contained in:
Vladyslav_Prykhodko 2025-08-01 19:29:55 +03:00
parent 99b4c0fbda
commit 50e0272d34
4 changed files with 192 additions and 206 deletions

View File

@ -58,7 +58,7 @@ import {
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, guid, isDefined, isDefinedAndNotNull, isEqual, isNotEmptyStr } from '@app/core/utils'; import { deepClone, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils';
import { import {
DashboardContext, DashboardContext,
DashboardPageInitData, DashboardPageInitData,
@ -119,7 +119,8 @@ import {
} from '@home/components/dashboard-page/dashboard-settings-dialog.component'; } from '@home/components/dashboard-page/dashboard-settings-dialog.component';
import { import {
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
ManageDashboardStatesDialogData ManageDashboardStatesDialogData,
ManageDashboardStatesDialogResult
} from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { AuthState } from '@app/core/auth/auth.models'; import { AuthState } from '@app/core/auth/auth.models';
@ -964,17 +965,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
$event.stopPropagation(); $event.stopPropagation();
} }
this.dialog.open<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogData, this.dialog.open<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogData,
{states: {[id: string]: DashboardState}; widgets: {[id: string]: Widget}}>(ManageDashboardStatesDialogComponent, { ManageDashboardStatesDialogResult>(ManageDashboardStatesDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
states: deepClone(this.dashboard.configuration.states), states: deepClone(this.dashboard.configuration.states),
widgets: deepClone(this.dashboard.configuration.widgets) as {[id: string]: Widget} widgets: this.dashboard.configuration.widgets as {[id: string]: Widget}
} }
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {
if (!isEqual(result.widgets, this.dashboard.configuration.widgets)) { if (result.addWidgets) {
this.dashboard.configuration.widgets = result.widgets; Object.assign(this.dashboard.configuration.widgets, result.addWidgets);
} }
if (result.states) { if (result.states) {
this.updateStates(result.states); this.updateStates(result.states);

View File

@ -15,8 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="statesFormGroup" (ngSubmit)="save()" style="width: 750px;"> <mat-toolbar color="primary">
<mat-toolbar color="primary">
<h2 translate>dashboard.manage-states</h2> <h2 translate>dashboard.manage-states</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
@ -24,13 +23,8 @@
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="manage-dashboard-states">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="manage-dashboard-states">
<div class="tb-entity-table">
<div class="tb-entity-table-content flex flex-col"> <div class="tb-entity-table-content flex flex-col">
<mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
@ -137,11 +131,8 @@
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[5, 10, 15]"></mat-paginator> [pageSizeOptions]="[5, 10, 15]"></mat-paginator>
</div> </div>
</div> </div>
</div> <div mat-dialog-actions class="flex items-center justify-end">
</fieldset>
</div>
<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)" [disabled]="(isLoading$ | async)"
@ -150,8 +141,8 @@
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || statesFormGroup.invalid || !statesFormGroup.dirty"> (click)="save()"
[disabled]="(isLoading$ | async) || !isDirty">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
</div> </div>
</form>

View File

@ -13,9 +13,14 @@
* 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 {
height: 100%;
display: grid;
grid-template-rows: min-content auto min-content;
.manage-dashboard-states { .manage-dashboard-states {
.tb-entity-table {
.tb-entity-table-content { .tb-entity-table-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -33,6 +38,13 @@
} }
} }
} }
@media #{$mat-sm} {
min-width: 470px;
}
@media #{$mat-gt-sm} {
min-width: 750px;
} }
} }

View File

@ -14,21 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { import { AfterViewInit, Component, ElementRef, Inject, OnInit, SecurityContext, ViewChild } from '@angular/core';
AfterViewInit,
Component,
ElementRef,
Inject,
OnInit,
SecurityContext,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, 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 { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } 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 { DashboardState } from '@app/shared/models/dashboard.models'; import { DashboardState } from '@app/shared/models/dashboard.models';
@ -44,7 +33,7 @@ import { fromEvent, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { deepClone, isDefined } from '@core/utils'; import { deepClone, isDefined, isEqual } from '@core/utils';
import { import {
DashboardStateDialogComponent, DashboardStateDialogComponent,
DashboardStateDialogData DashboardStateDialogData
@ -58,42 +47,42 @@ export interface ManageDashboardStatesDialogData {
widgets: {[id: string]: Widget }; widgets: {[id: string]: Widget };
} }
export interface ManageDashboardStatesDialogResult {
states: {[id: string]: DashboardState };
addWidgets?: {[id: string]: Widget };
}
@Component({ @Component({
selector: 'tb-manage-dashboard-states-dialog', selector: 'tb-manage-dashboard-states-dialog',
templateUrl: './manage-dashboard-states-dialog.component.html', templateUrl: './manage-dashboard-states-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}],
styleUrls: ['./manage-dashboard-states-dialog.component.scss'] styleUrls: ['./manage-dashboard-states-dialog.component.scss']
}) })
export class ManageDashboardStatesDialogComponent export class ManageDashboardStatesDialogComponent
extends DialogComponent<ManageDashboardStatesDialogComponent, {states: {[id: string]: DashboardState}; widgets: {[id: string]: Widget}}> extends DialogComponent<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogResult>
implements OnInit, ErrorStateMatcher, AfterViewInit { implements OnInit, AfterViewInit {
statesFormGroup: UntypedFormGroup; @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
states: {[id: string]: DashboardState }; @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
widgets: {[id: string]: Widget}; @ViewChild(MatSort, {static: false}) sort: MatSort;
isDirty = false;
displayedColumns: string[]; displayedColumns: string[];
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
dataSource: DashboardStatesDatasource; dataSource: DashboardStatesDatasource;
submitted = false; private states: {[id: string]: DashboardState };
private widgets: {[id: string]: Widget};
stateNames: Set<string> = new Set<string>(); private stateNames: Set<string> = new Set<string>();
private addWidgets: {[id: string]: Widget} = {};
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, @Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogResult>,
public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent,
{states: {[id: string]: DashboardState}; widgets: {[id: string]: Widget}}>,
private fb: UntypedFormBuilder,
private translate: TranslateService, private translate: TranslateService,
private dialogs: DialogService, private dialogs: DialogService,
private utils: UtilsService, private utils: UtilsService,
@ -103,7 +92,6 @@ export class ManageDashboardStatesDialogComponent
this.states = this.data.states; this.states = this.data.states;
this.widgets = this.data.widgets; this.widgets = this.data.widgets;
this.statesFormGroup = this.fb.group({});
Object.values(this.states).forEach(value => this.stateNames.add(value.name)); Object.values(this.states).forEach(value => this.stateNames.add(value.name));
const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC };
@ -271,9 +259,7 @@ export class ManageDashboardStatesDialogComponent
continue; continue;
} }
const originalState = state; const duplicatedState = deepClone(state);
const duplicatedState = deepClone(originalState);
const duplicatedWidgets = deepClone(this.widgets);
const mainWidgets = {}; const mainWidgets = {};
const rightWidgets = {}; const rightWidgets = {};
duplicatedState.id = candidateId; duplicatedState.id = candidateId;
@ -284,8 +270,8 @@ export class ManageDashboardStatesDialogComponent
for (const [key, value] of Object.entries(duplicatedState.layouts.main.widgets)) { for (const [key, value] of Object.entries(duplicatedState.layouts.main.widgets)) {
const guid = this.utils.guid(); const guid = this.utils.guid();
mainWidgets[guid] = value; mainWidgets[guid] = value;
duplicatedWidgets[guid] = this.widgets[key]; this.addWidgets[guid] = deepClone(this.widgets[key] ?? this.addWidgets[key]);
duplicatedWidgets[guid].id = guid; this.addWidgets[guid].id = guid;
} }
duplicatedState.layouts.main.widgets = mainWidgets; duplicatedState.layouts.main.widgets = mainWidgets;
@ -293,36 +279,32 @@ export class ManageDashboardStatesDialogComponent
for (const [key, value] of Object.entries(duplicatedState.layouts.right.widgets)) { for (const [key, value] of Object.entries(duplicatedState.layouts.right.widgets)) {
const guid = this.utils.guid(); const guid = this.utils.guid();
rightWidgets[guid] = value; rightWidgets[guid] = value;
duplicatedWidgets[guid] = this.widgets[key]; this.addWidgets[guid] = deepClone(this.widgets[key] ?? this.addWidgets[key]);
duplicatedWidgets[guid].id = guid; this.addWidgets[guid].id = guid;
} }
duplicatedState.layouts.right.widgets = rightWidgets; duplicatedState.layouts.right.widgets = rightWidgets;
} }
this.states[duplicatedState.id] = duplicatedState; this.states[duplicatedState.id] = duplicatedState;
this.widgets = duplicatedWidgets;
this.onStatesUpdated(); this.onStatesUpdated();
return; return;
} }
} }
private onStatesUpdated() { private onStatesUpdated() {
this.statesFormGroup.markAsDirty(); this.isDirty = true;
this.updateData(true); this.updateData(true);
} }
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;
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save(): void { save(): void {
this.submitted = true; const result: ManageDashboardStatesDialogResult = {states: this.states};
this.dialogRef.close({ states: this.states, widgets: this.widgets }); if (!isEqual(this.addWidgets, {})) {
result.addWidgets = this.addWidgets;
}
this.dialogRef.close(result);
} }
} }