Skip to content

Commit f0a6a3c

Browse files
authored
fix(Admin UI): prevent editing of unsupported views (#2569)
like NoteDetails, which breaks the config fixes #2466
1 parent 904d20c commit f0a6a3c

File tree

6 files changed

+93
-55
lines changed

6 files changed

+93
-55
lines changed

src/app/core/admin/admin-entity/admin-entity.component.html

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,26 @@
4646
<div class="config-component flex-grow mat-elevation-z1">
4747
@switch (mode) {
4848
@case ("details") {
49-
<app-admin-entity-details
50-
[entityConstructor]="entityConstructor"
51-
[config]="configDetailsView"
52-
></app-admin-entity-details>
49+
@switch (configDetailsView.component) {
50+
@case ("EntityDetails") {
51+
<app-admin-entity-details
52+
[entityConstructor]="entityConstructor"
53+
[config]="configDetailsView.config"
54+
></app-admin-entity-details>
55+
}
56+
@default {
57+
<p class="admin-ui-not-supported-info" i18n>
58+
This component ({{ configDetailsView.component }}) cannot be
59+
edited by users in the Admin UI yet. Please contact your
60+
technical support team to make changes to this.
61+
</p>
62+
}
63+
}
5364
}
5465
@case ("list") {
5566
<app-admin-entity-list
5667
[entityConstructor]="entityConstructor"
57-
[config]="configListView"
68+
[config]="configListView.config"
5869
></app-admin-entity-list>
5970
}
6071
@case ("general") {

src/app/core/admin/admin-entity/admin-entity.component.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@
2727
color: colors.$muted;
2828
}
2929
}
30+
31+
.admin-ui-not-supported-info {
32+
padding: 20px;
33+
text-align: center;
34+
}

src/app/core/admin/admin-entity/admin-entity.component.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { EntityActionsService } from "../../entity/entity-actions/entity-actions
2626
import { ActivatedRoute } from "@angular/router";
2727
import { of } from "rxjs";
2828
import { EntitySchemaField } from "../../entity/schema/entity-schema-field";
29+
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
2930

3031
describe("AdminEntityComponent", () => {
3132
let component: AdminEntityComponent;
@@ -35,7 +36,7 @@ describe("AdminEntityComponent", () => {
3536
let entityMapper: MockEntityMapperService;
3637

3738
let config;
38-
let viewConfig: EntityDetailsConfig;
39+
let viewConfig: DynamicComponentConfig<EntityDetailsConfig>;
3940
let viewConfigId, entityConfigId;
4041

4142
@DatabaseEntity("AdminTest")
@@ -50,11 +51,14 @@ describe("AdminEntityComponent", () => {
5051
viewConfigId = `view:${AdminTestEntity.route.substring(1)}/:id`;
5152
entityConfigId = `entity:${AdminTestEntity.ENTITY_TYPE}`;
5253
viewConfig = {
53-
entityType: AdminTestEntity.ENTITY_TYPE,
54-
panels: [{ title: "Tab 1", components: [] }],
54+
component: "EntityDetails",
55+
config: {
56+
entityType: AdminTestEntity.ENTITY_TYPE,
57+
panels: [{ title: "Tab 1", components: [] }],
58+
},
5559
};
5660
config = {
57-
[viewConfigId]: { component: "EntityDetails", config: viewConfig },
61+
[viewConfigId]: viewConfig,
5862
[entityConfigId]: {},
5963
};
6064
const mockActivatedRoute = {
@@ -137,7 +141,7 @@ describe("AdminEntityComponent", () => {
137141
title: "New Panel",
138142
components: [],
139143
};
140-
component.configDetailsView.panels.push(newPanel);
144+
component.configDetailsView.config.panels.push(newPanel);
141145

142146
await component.save();
143147

src/app/core/admin/admin-entity/admin-entity.component.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { EntityRegistry } from "../../entity/database-entity.decorator";
1010
import { ConfigService } from "../../config/config.service";
1111
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
1212
import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service";
13-
import { ViewConfig } from "../../config/dynamic-routing/view-config.interface";
1413
import { EntityDetailsConfig } from "../../entity-details/EntityDetailsConfig";
1514
import { EntityConfigService } from "../../entity/entity-config.service";
1615
import { Config } from "../../config/config";
@@ -33,6 +32,7 @@ import { MatListItem, MatNavList } from "@angular/material/list";
3332
import { AdminEntityDetailsComponent } from "../admin-entity-details/admin-entity-details/admin-entity-details.component";
3433
import { AdminEntityGeneralSettingsComponent } from "./admin-entity-general-settings/admin-entity-general-settings.component";
3534
import { BetaFeatureComponent } from "../../../features/coming-soon/beta-feature/beta-feature.component";
35+
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
3636

3737
@Component({
3838
selector: "app-admin-entity",
@@ -63,8 +63,8 @@ export class AdminEntityComponent implements OnInit {
6363
entityConstructor: EntityConstructor;
6464
private originalEntitySchemaFields: [string, EntitySchemaField][];
6565

66-
configDetailsView: EntityDetailsConfig;
67-
configListView: EntityListConfig;
66+
configDetailsView: DynamicComponentConfig<EntityDetailsConfig>;
67+
configListView: DynamicComponentConfig<EntityListConfig>;
6868
configEntitySettings: EntityConfig;
6969
protected mode: "details" | "list" | "general" = "details";
7070

@@ -93,23 +93,43 @@ export class AdminEntityComponent implements OnInit {
9393
);
9494

9595
this.configDetailsView = this.loadViewConfig(
96-
EntityConfigService.getDetailsViewId(this.entityConstructor),
97-
) ?? { entityType: this.entityType, panels: [] };
98-
this.configListView = this.loadViewConfig(
99-
EntityConfigService.getListViewId(this.entityConstructor),
100-
) ?? { entityType: this.entityType };
96+
this.entityConstructor,
97+
"details",
98+
);
99+
this.configListView = this.loadViewConfig(this.entityConstructor, "list");
100+
101101
this.configEntitySettings = this.entityConstructor;
102102
}
103103

104-
private loadViewConfig<
105-
C = EntityDetailsConfig | EntityListConfig | undefined,
106-
>(viewId: string): C | undefined {
107-
const viewConfig: ViewConfig<C> = this.configService.getConfig(viewId);
104+
private loadViewConfig(
105+
entityType: EntityConstructor,
106+
viewType: "details" | "list",
107+
): DynamicComponentConfig {
108+
const viewId =
109+
viewType === "details"
110+
? EntityConfigService.getDetailsViewId(entityType)
111+
: EntityConfigService.getListViewId(entityType);
112+
const viewConfig: DynamicComponentConfig =
113+
this.configService.getConfig(viewId);
114+
115+
if (!viewConfig) {
116+
// return default view config
117+
if (viewType === "details") {
118+
return {
119+
component: "EntityDetails",
120+
config: { entityType: this.entityType, panels: [] },
121+
};
122+
} else {
123+
return {
124+
component: "EntityList",
125+
config: { entityType: this.entityType },
126+
};
127+
}
128+
}
108129

130+
viewConfig.config = viewConfig.config ?? { entityType: this.entityType };
109131
// work on a deep copy as we are editing in place (for titles, sections, etc.)
110-
return viewConfig?.config
111-
? JSON.parse(JSON.stringify(viewConfig.config))
112-
: undefined;
132+
return JSON.parse(JSON.stringify(viewConfig));
113133
}
114134

115135
cancel() {
@@ -124,18 +144,11 @@ export class AdminEntityComponent implements OnInit {
124144
);
125145
const newConfig = originalConfig.copy();
126146

127-
this.setViewConfig(
128-
newConfig,
129-
EntityConfigService.getDetailsViewId(this.entityConstructor),
130-
this.configDetailsView,
131-
"EntityDetails",
132-
);
133-
this.setViewConfig(
134-
newConfig,
135-
EntityConfigService.getListViewId(this.entityConstructor),
136-
this.configListView,
137-
"EntityList",
138-
);
147+
newConfig.data[
148+
EntityConfigService.getDetailsViewId(this.entityConstructor)
149+
] = this.configDetailsView;
150+
newConfig.data[EntityConfigService.getListViewId(this.entityConstructor)] =
151+
this.configListView;
139152
this.setEntityConfig(newConfig);
140153

141154
await this.entityMapper.save(newConfig);
@@ -171,22 +184,4 @@ export class AdminEntityComponent implements OnInit {
171184
entitySchemaConfig.hasPII = this.configEntitySettings.hasPII;
172185
}
173186
}
174-
175-
private setViewConfig(
176-
targetConfig,
177-
detailsViewId: string,
178-
viewConfig: EntityDetailsConfig | EntityListConfig,
179-
componentForNewConfig: string,
180-
) {
181-
if (targetConfig.data[detailsViewId]) {
182-
targetConfig.data[detailsViewId].config = viewConfig;
183-
} else {
184-
// create new config
185-
viewConfig.entityType = this.entityType;
186-
targetConfig.data[detailsViewId] = {
187-
component: componentForNewConfig,
188-
config: viewConfig,
189-
} as ViewConfig<EntityDetailsConfig | EntityListConfig>;
190-
}
191-
}
192187
}

src/app/core/config/config.service.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ describe("ConfigService", () => {
123123
it("should create export config string", fakeAsync(() => {
124124
const config = new Config();
125125
config.data = { first: "foo", second: "bar" };
126+
// @ts-ignore disable migrations for this test
127+
spyOn(service, "applyMigrations").and.callFake((c) => c);
128+
126129
const expected = JSON.stringify(config.data);
127130
updateSubject.next({ entity: config, type: "update" });
128131
tick();

src/app/core/config/config.service.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class ConfigService extends LatestEntityLoader<Config> {
7272
migratePhotoDatatype,
7373
migratePercentageDatatype,
7474
migrateEntityBlock,
75+
addDefaultNoteDetailsConfig,
7576
];
7677

7778
// TODO: execute this on server via ndb-admin
@@ -375,3 +376,22 @@ const migrateEntityBlock: ConfigMigration = (key, configPart) => {
375376

376377
return configPart;
377378
};
379+
380+
/**
381+
* Add default view:note/:id NoteDetails config
382+
* to avoid breaking note details with a default config from AdminModule
383+
*/
384+
const addDefaultNoteDetailsConfig: ConfigMigration = (key, configPart) => {
385+
if (
386+
// add at top-level of config
387+
configPart?.["_id"] === "Config:CONFIG_ENTITY" &&
388+
!configPart?.["data"]["view:note/:id"]
389+
) {
390+
configPart["data"]["view:note/:id"] = {
391+
component: "NoteDetails",
392+
config: {},
393+
};
394+
}
395+
396+
return configPart;
397+
};

0 commit comments

Comments
 (0)