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 { 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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""