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

Metadata

Relationships

Index

Properties
Methods
Inputs

Constructor

constructor()

Inputs

entityType
Type : string
Required :  true

Methods

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 :
Name Type Optional
entityConstructor EntityConstructor No
Returns : void
Async save
save()
Returns : any

Properties

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 { RawConfig } from "../../config/multi-lingual-config";
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 entityConfigService = inject(EntityConfigService);
  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 {
    // labels come from the raw config (not the resolved constructor statics)
    // so that all configured languages remain editable
    const rawConfig = this.entityConfigService.getRawEntityConfig(entityCtr);
    const config: RawConfig<EntityConfig> = {
      label: rawConfig?.label ?? entityCtr.label,
      labelPlural: rawConfig?.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;
    }

    // the general settings editor keeps the other languages of these texts intact,
    // but still declares them as plain strings
    return config as EntityConfig;
  }

  private loadViewConfig(
    entityType: EntityConstructor,
    viewType: "details" | "list",
  ): DynamicComponentConfig {
    const viewId =
      viewType === "details"
        ? EntityConfigService.getDetailsViewId(entityType)
        : EntityConfigService.getListViewId(entityType);
    // raw, because this editor saves the view config back to the config
    // document - resolving here would drop all other languages
    const viewConfig =
      this.configService.getRawConfig<DynamicComponentConfig>(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() },
        };
      }
    }

    // `getRawConfig` hands back the config document's own object - edit a copy
    const editableConfig: DynamicComponentConfig = JSON.parse(
      JSON.stringify(viewConfig),
    );

    editableConfig.config = editableConfig.config ?? {
      entityType: this.entityType(),
    };

    // cleanup note details config, which should not have an entity instance assigned
    if (
      editableConfig.config.entity &&
      editableConfig.component === "NoteDetails"
    ) {
      delete editableConfig.config.entity;
    }
    return editableConfig;
  }

  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-inline-end: - sizes.$margin-main-view-right;
  margin-bottom: - sizes.$margin-main-view-bottom;
  margin-inline-start: - 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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""