src/app/core/admin/admin-entity/admin-entity.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-admin-entity |
| standalone | true |
| imports |
MatButton
MatNavList
MatListItem
|
| styleUrls | ./admin-entity.component.scss |
| templateUrl | ./admin-entity.component.html |
| styleUrl | ./admin-entity.component.scss |
No results matching.
Properties |
Methods |
Inputs |
constructor()
|
| entityType | |
Type : string
|
|
| Required : true | |
| cancel |
cancel()
|
|
Returns :
void
|
| registerRelatedEntityForSave | ||||||
registerRelatedEntityForSave(entityConstructor: EntityConstructor)
|
||||||
|
Register a related entity as modified, so its schema will be saved when the main Save button is clicked. This is called from child components (e.g., panel components) when they modify related entity schemas.
Parameters :
Returns :
void
|
| Async save |
save()
|
|
Returns :
any
|
| configDetailsView |
Type : unknown
|
Default value : linkedSignal(() =>
this.loadViewConfig(this.entityConstructor(), "details"),
)
|
| configEntitySettings |
Type : unknown
|
Default value : linkedSignal(() =>
this.getEntitySettingsFromConstructor(this.entityConstructor()),
)
|
| configListView |
Type : unknown
|
Default value : linkedSignal(() =>
this.loadViewConfig(this.entityConstructor(), "list"),
)
|
| entityConstructor |
Type : unknown
|
Default value : computed(() => this.entities.get(this.entityType()))
|
| Optional generalSettingsComponent |
Type : AdminEntityGeneralSettingsComponent
|
Decorators :
@ViewChild(AdminEntityGeneralSettingsComponent)
|
| Protected mode |
Type : unknown
|
Default value : linkedSignal<"details" | "list" | "general" | "publicForm">(
() => this.queryParams()["mode"] ?? "details",
)
|
| templateRef |
Type : TemplateRef<any>
|
Decorators :
@ContentChild(TemplateRef)
|
import { Location } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
ContentChild,
TemplateRef,
ViewChild,
computed,
effect,
inject,
input,
linkedSignal,
} from "@angular/core";
import { MatButton } from "@angular/material/button";
import { MatListItem, MatNavList } from "@angular/material/list";
import { ActivatedRoute } from "@angular/router";
import { AdminNoteDetailsComponent } from "../../../child-dev-project/notes/admin-note-details/admin-note-details.component";
import { BetaFeatureComponent } from "../../../features/coming-soon/beta-feature/beta-feature.component";
import { EntityTypeLabelPipe } from "../../common-components/entity-type-label/entity-type-label.pipe";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { ConfigService } from "../../config/config.service";
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service";
import { EntityConfig } from "../../entity/entity-config";
import { EntityConfigService } from "../../entity/entity-config.service";
import { EntityConstructor } from "../../entity/model/entity";
import { EntitySchemaField } from "../../entity/schema/entity-schema-field";
import { AdminEntityDetailsComponent } from "../admin-entity-details/admin-entity-details/admin-entity-details.component";
import { AdminEntityListComponent } from "../admin-entity-list/admin-entity-list.component";
import { AdminEntityPublicFormsComponent } from "../admin-entity-public-forms/admin-entity-public-forms-component";
import { AdminEntityService } from "../admin-entity.service";
import { AdminEntityGeneralSettingsComponent } from "./admin-entity-general-settings/admin-entity-general-settings.component";
import { toSignal } from "@angular/core/rxjs-interop";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-entity",
imports: [
EntityTypeLabelPipe,
MatButton,
ViewTitleComponent,
AdminEntityListComponent,
MatNavList,
MatListItem,
AdminEntityDetailsComponent,
AdminEntityGeneralSettingsComponent,
BetaFeatureComponent,
AdminEntityPublicFormsComponent,
AdminNoteDetailsComponent,
],
templateUrl: "./admin-entity.component.html",
styleUrl: "./admin-entity.component.scss",
})
export class AdminEntityComponent {
private entities = inject(EntityRegistry);
private configService = inject(ConfigService);
private location = inject(Location);
private adminEntityService = inject(AdminEntityService);
private entityActionsService = inject(EntityActionsService);
private routes = inject(ActivatedRoute);
private readonly queryParams = toSignal(this.routes.queryParams, {
initialValue: {},
});
entityType = input.required<string>();
entityConstructor = computed(() => this.entities.get(this.entityType()));
private originalEntitySchemaFields: [string, EntitySchemaField][] = [];
/**
* Track related entity types that have been modified through the panel config dialog.
* These will need to be saved when the main Save button is clicked.
*/
private readonly modifiedRelatedEntities = new Set<EntityConstructor>();
configDetailsView = linkedSignal(() =>
this.loadViewConfig(this.entityConstructor(), "details"),
);
configListView = linkedSignal(() =>
this.loadViewConfig(this.entityConstructor(), "list"),
);
configEntitySettings = linkedSignal(() =>
this.getEntitySettingsFromConstructor(this.entityConstructor()),
);
protected mode = linkedSignal<"details" | "list" | "general" | "publicForm">(
() => this.queryParams()["mode"] ?? "details",
);
@ViewChild(AdminEntityGeneralSettingsComponent)
generalSettingsComponent?: AdminEntityGeneralSettingsComponent;
@ContentChild(TemplateRef) templateRef: TemplateRef<any>;
constructor() {
// store a copy of the original entity schema immediately (computed signals are lazy)
effect(() => {
this.originalEntitySchemaFields = JSON.parse(
JSON.stringify(Array.from(this.entityConstructor().schema.entries())),
);
});
}
private getEntitySettingsFromConstructor(
entityCtr: EntityConstructor,
): EntityConfig {
const config: EntityConfig = {
label: entityCtr.label,
labelPlural: entityCtr.labelPlural,
icon: entityCtr.icon,
color: entityCtr.color,
toStringAttributes: [...entityCtr.toStringAttributes],
hasPII: entityCtr.hasPII,
enableUserAccounts: entityCtr.enableUserAccounts,
};
// Only include toBlockDetailsAttributes if it has meaningful values
if (
entityCtr.toBlockDetailsAttributes &&
(entityCtr.toBlockDetailsAttributes.title ||
entityCtr.toBlockDetailsAttributes.image ||
(entityCtr.toBlockDetailsAttributes.fields &&
entityCtr.toBlockDetailsAttributes.fields.length > 0))
) {
config.toBlockDetailsAttributes = entityCtr.toBlockDetailsAttributes;
}
return config;
}
private loadViewConfig(
entityType: EntityConstructor,
viewType: "details" | "list",
): DynamicComponentConfig {
const viewId =
viewType === "details"
? EntityConfigService.getDetailsViewId(entityType)
: EntityConfigService.getListViewId(entityType);
const viewConfig: DynamicComponentConfig =
this.configService.getConfig(viewId);
if (!viewConfig) {
// return default view config
if (viewType === "details") {
return {
component: "EntityDetails",
config: { entityType: this.entityType(), panels: [] },
};
} else {
return {
component: "EntityList",
config: { entityType: this.entityType() },
};
}
}
viewConfig.config = viewConfig.config ?? { entityType: this.entityType() };
// cleanup note details config, which should not have an entity instance assigned
if (viewConfig.config.entity && viewConfig.component === "NoteDetails") {
delete viewConfig.config.entity;
}
// work on a deep copy as we are editing in place (for titles, sections, etc.)
return JSON.parse(JSON.stringify(viewConfig));
}
cancel() {
this.entityConstructor().schema = new Map(this.originalEntitySchemaFields);
this.location.back();
}
async save() {
if (
this.generalSettingsComponent &&
!this.generalSettingsComponent.isFormValid()
) {
return;
}
// Save the main entity configuration along with all related entities in a single transaction
const result = await this.adminEntityService.setAndSaveEntityConfig(
this.entityConstructor(),
this.configEntitySettings(),
this.configListView(),
this.configDetailsView(),
Array.from(this.modifiedRelatedEntities),
);
this.entityActionsService.showSnackbarConfirmationWithUndo(
$localize`:Save config confirmation message:Configuration updated`,
[result.previous],
);
this.location.back();
}
/**
* Register a related entity as modified, so its schema will be saved when the main Save button is clicked.
* This is called from child components (e.g., panel components) when they modify related entity schemas.
*/
registerRelatedEntityForSave(entityConstructor: EntityConstructor): void {
this.modifiedRelatedEntities.add(entityConstructor);
}
}
<div class="host-container flex-column">
<div class="header flex-row flex-wrap justify-space-between">
<app-view-title
class="admin-ui-title"
i18n="page title"
[displayInPlace]="true"
>
Configuring data structure for "{{
(entityType() | entityTypeLabel) ?? entityType()
}}" records
</app-view-title>
<div class="flex-row gap-small save-buttons">
<button mat-raised-button color="accent" (click)="save()" i18n>
Save
</button>
<button mat-stroked-button (click)="cancel()" i18n>Cancel</button>
</div>
</div>
<div class="main-container flex-row">
<mat-nav-list class="config-nav">
<mat-list-item
(click)="mode.set('details')"
[activated]="mode() === 'details'"
i18n="admin entity: nav item"
>
Details View & Fields
</mat-list-item>
<mat-list-item
(click)="mode.set('list')"
[activated]="mode() === 'list'"
i18n="admin entity: nav item"
>
List View
</mat-list-item>
<mat-list-item
(click)="mode.set('general')"
[activated]="mode() === 'general'"
i18n="admin entity: nav item"
>
General Settings
</mat-list-item>
<mat-list-item
(click)="mode.set('publicForm')"
[activated]="mode() === 'publicForm'"
i18n="admin entity: nav item"
>
Public Forms
</mat-list-item>
<app-beta-feature></app-beta-feature>
</mat-nav-list>
<div class="config-component flex-grow mat-elevation-z1">
@switch (mode()) {
@case ("details") {
@switch (configDetailsView().component) {
@case ("EntityDetails") {
<app-admin-entity-details
[entityConstructor]="entityConstructor()"
[config]="configDetailsView().config"
(relatedEntityModified)="registerRelatedEntityForSave($event)"
></app-admin-entity-details>
}
@case ("NoteDetails") {
<app-admin-note-details
[entityConstructor]="entityConstructor()"
[config]="configDetailsView().config"
(configChange)="
configDetailsView.update((v) => ({ ...v, config: $event }))
"
></app-admin-note-details>
}
@default {
<p class="admin-ui-not-supported-info" i18n>
This component ({{ configDetailsView().component }}) cannot be
edited by users in the Admin UI yet. Please contact your
technical support team to make changes to this.
</p>
}
}
}
@case ("list") {
<app-admin-entity-list
[entityConstructor]="entityConstructor()"
[config]="configListView().config"
></app-admin-entity-list>
}
@case ("general") {
<app-admin-entity-general-settings
[entityConstructor]="entityConstructor()"
[generalSettings]="configEntitySettings()"
(generalSettingsChange)="configEntitySettings.set($event)"
></app-admin-entity-general-settings>
}
@case ("publicForm") {
<app-admin-entity-public-forms-component
[entityConstructor]="entityConstructor()"
></app-admin-entity-public-forms-component>
}
}
</div>
</div>
</div>
./admin-entity.component.scss
@use "variables/colors";
@use "variables/sizes";
.host-container {
height: calc(100vh - 64px);
// undo margins of routed view
margin-top: - sizes.$margin-main-view-top;
margin-right: - sizes.$margin-main-view-right;
margin-bottom: - sizes.$margin-main-view-bottom;
margin-left: - sizes.$margin-main-view-left;
}
.header {
padding: 20px;
background-color: colors.$background-secondary;
}
.config-nav {
background-color: colors.$background-secondary;
}
.main-container {
width: 100%;
min-height: 0;
flex-grow: 1;
}
.config-component {
min-width: 0;
padding: 8px;
overflow-y: auto;
::ng-deep app-view-title {
color: colors.$muted;
}
}
.admin-ui-not-supported-info {
padding: 20px;
text-align: center;
}