UI: Prevent deleting an entity alias/filter that’s still used in map widgets

This commit is contained in:
Vladyslav_Prykhodko 2025-06-09 15:41:27 +03:00
parent 18b6a9557d
commit 429f9c7cc9
6 changed files with 74 additions and 33 deletions

View File

@ -31,7 +31,8 @@ import {
DashboardLayoutsInfo, DashboardLayoutsInfo,
DashboardState, DashboardState,
DashboardStateLayouts, DashboardStateLayouts,
GridSettings, LayoutType, GridSettings,
LayoutType,
WidgetLayout WidgetLayout
} from '@shared/models/dashboard.models'; } from '@shared/models/dashboard.models';
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
@ -61,6 +62,7 @@ import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models';
import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models'; import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models';
import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -398,6 +400,14 @@ export class DashboardUtilsService {
return datasources; return datasources;
} }
public getWidgetDatasources(widget: Widget): Datasource[] {
const widgetDefinition = findWidgetModelDefinition(widget);
if (widgetDefinition) {
return widgetDefinition.datasources(widget);
}
return this.validateAndUpdateDatasources(widget.config.datasources);
}
public createDefaultLayoutData(): DashboardLayout { public createDefaultLayoutData(): DashboardLayout {
return { return {
widgets: {}, widgets: {},

View File

@ -35,7 +35,7 @@ import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.mo
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleChainImport } from '@shared/models/rule-chain.models'; import { RuleChainImport } from '@shared/models/rule-chain.models';
import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models'; import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models';
import { getWidgetExportDefinition } from '@shared/models/widget/widget-export.models'; import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
const WIDGET_ITEM = 'widget_item'; const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference'; const WIDGET_REFERENCE = 'widget_reference';
@ -142,7 +142,7 @@ export class ItemBufferService {
} }
} }
let widgetExportInfo: any; let widgetExportInfo: any;
const exportDefinition = getWidgetExportDefinition(widget); const exportDefinition = findWidgetModelDefinition(widget);
if (exportDefinition) { if (exportDefinition) {
widgetExportInfo = exportDefinition.prepareExportInfo(dashboard, widget); widgetExportInfo = exportDefinition.prepareExportInfo(dashboard, widget);
} }
@ -270,7 +270,7 @@ export class ItemBufferService {
let callFilterUpdateFunction = false; let callFilterUpdateFunction = false;
let newEntityAliases: EntityAliases; let newEntityAliases: EntityAliases;
let newFilters: Filters; let newFilters: Filters;
const exportDefinition = getWidgetExportDefinition(widget); const exportDefinition = findWidgetModelDefinition(widget);
if (exportDefinition && widgetExportInfo || aliasesInfo) { if (exportDefinition && widgetExportInfo || aliasesInfo) {
newEntityAliases = deepClone(dashboard.configuration.entityAliases); newEntityAliases = deepClone(dashboard.configuration.entityAliases);
} }

View File

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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';
@ -60,7 +60,7 @@ export interface EntityAliasesDialogData {
styleUrls: ['./entity-aliases-dialog.component.scss'] styleUrls: ['./entity-aliases-dialog.component.scss']
}) })
export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases> export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases>
implements OnInit, ErrorStateMatcher { implements ErrorStateMatcher {
title: string; title: string;
disableAdd: boolean; disableAdd: boolean;
@ -107,8 +107,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title); this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title);
} }
} else { } else {
const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources); this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => {
datasources.forEach((datasource) => {
if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type) if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type)
&& datasource.entityAliasId) { && datasource.entityAliasId) {
this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
@ -143,7 +142,9 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
widgetsTitleList = []; widgetsTitleList = [];
this.aliasToWidgetsMap[aliasId] = widgetsTitleList; this.aliasToWidgetsMap[aliasId] = widgetsTitleList;
} }
widgetsTitleList.push(widgetTitle); if (!widgetsTitleList.includes(widgetTitle)) {
widgetsTitleList.push(widgetTitle);
}
} }
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl {
@ -166,9 +167,6 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
return this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray; return this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray;
} }
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted); const customErrorState = !!(control && control.invalid && this.submitted);

View File

@ -14,19 +14,19 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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 { import {
AbstractControl, AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -58,7 +58,7 @@ export interface FiltersDialogData {
styleUrls: ['./filters-dialog.component.scss'] styleUrls: ['./filters-dialog.component.scss']
}) })
export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters> export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters>
implements OnInit, ErrorStateMatcher { implements ErrorStateMatcher {
title: string; title: string;
disableAdd: boolean; disableAdd: boolean;
@ -96,15 +96,16 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
} }
} else { } else {
this.data.widgets.forEach((widget) => { this.data.widgets.forEach((widget) => {
const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources); this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => {
datasources.forEach((datasource) => { if (datasource.type !== DatasourceType.function && datasource.filterId) {
if (datasource.type === DatasourceType.entity && datasource.filterId) {
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId]; widgetsTitleList = this.filterToWidgetsMap[datasource.filterId];
if (!widgetsTitleList) { if (!widgetsTitleList) {
widgetsTitleList = []; widgetsTitleList = [];
this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList; this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList;
} }
widgetsTitleList.push(widget.config.title); if (!widgetsTitleList.includes(widget.config.title)) {
widgetsTitleList.push(widget.config.title);
}
} }
}); });
}); });
@ -140,9 +141,6 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
return this.filtersFormGroup.get('filters') as UntypedFormArray; return this.filtersFormGroup.get('filters') as UntypedFormArray;
} }
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted); const customErrorState = !!(control && control.invalid && this.submitted);

View File

@ -17,14 +17,15 @@
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { DatasourceType, Widget } from '@shared/models/widget.models'; import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models';
import { import {
BaseMapSettings, BaseMapSettings,
MapDataLayerSettings, MapDataLayerSettings,
MapDataSourceSettings, MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType MapType
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetExportDefinition } from '@shared/models/widget/widget-export.models'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
interface AliasFilterPair { interface AliasFilterPair {
alias?: EntityAliasInfo, alias?: EntityAliasInfo,
@ -45,7 +46,7 @@ interface MapDatasourcesInfo {
additionalDataSources?: ExportDataSourceInfo; additionalDataSources?: ExportDataSourceInfo;
} }
export const MapExportDefinition: WidgetExportDefinition<MapDatasourcesInfo> = { export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
testWidget(widget: Widget): boolean { testWidget(widget: Widget): boolean {
if (widget?.config?.settings) { if (widget?.config?.settings) {
const settings = widget.config.settings; const settings = widget.config.settings;
@ -103,6 +104,26 @@ export const MapExportDefinition: WidgetExportDefinition<MapDatasourcesInfo> = {
if (info?.additionalDataSources) { if (info?.additionalDataSources) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources); updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources);
} }
},
datasources(widget: Widget): Datasource[] {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
const datasources: Datasource[] = [];
if (settings.trips?.length) {
datasources.push(...getMapDataLayersDatasources(settings.trips));
}
if (settings.markers?.length) {
datasources.push(...getMapDataLayersDatasources(settings.markers));
}
if (settings.polygons?.length) {
datasources.push(...getMapDataLayersDatasources(settings.polygons));
}
if (settings.circles?.length) {
datasources.push(...getMapDataLayersDatasources(settings.circles));
}
if (settings.additionalDataSources?.length) {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
}
return datasources;
} }
}; };
@ -189,3 +210,16 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource
return null; return null;
} }
} }
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
datasources.push(mapDataSourceSettingsToDatasource(dsSettings));
if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) {
(dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => {
datasources.push(mapDataSourceSettingsToDatasource(ds));
});
}
});
return datasources;
};

View File

@ -14,22 +14,23 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Widget } from '@shared/models/widget.models'; import { Datasource, Widget } from '@shared/models/widget.models';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { EntityAliases } from '@shared/models/alias.models'; import { EntityAliases } from '@shared/models/alias.models';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
import { MapExportDefinition } from '@shared/models/widget/maps/map-export.models'; import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition';
export interface WidgetExportDefinition<T = any> { export interface WidgetModelDefinition<T = any> {
testWidget(widget: Widget): boolean; testWidget(widget: Widget): boolean;
prepareExportInfo(dashboard: Dashboard, widget: Widget): T; prepareExportInfo(dashboard: Dashboard, widget: Widget): T;
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void;
datasources(widget: Widget): Datasource[];
} }
const widgetExportDefinitions: WidgetExportDefinition[] = [ const widgetModelRegistry: WidgetModelDefinition[] = [
MapExportDefinition MapModelDefinition
]; ];
export const getWidgetExportDefinition = (widget: Widget): WidgetExportDefinition => { export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => {
return widgetExportDefinitions.find(def => def.testWidget(widget)); return widgetModelRegistry.find(def => def.testWidget(widget));
} }