File

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

Description

This component can be used to display an entity in more detail. It groups subcomponents in panels. Any component that is registered (has the DynamicComponent decorator) can be used as a subcomponent. The subcomponents will be provided with the Entity object and the creating new status, as well as its static config.

Extends

AbstractEntityDetailsComponent

Metadata

Index

Properties
Methods
Inputs
Outputs

Inputs

panels
Type : Panel[]
Default value : []

The configuration for the panels on this details page.

entity
Type : Entity | null
Default value : null
entityType
Type : string
id
Type : string

Outputs

entity
Type : Entity | null

Methods

Protected Async loadEntity
loadEntity(id: string, isCancelled: () => void)
Parameters :
Name Type Optional Default value
id string No
isCancelled function No () => false
Returns : any
Protected onEntityUpdated
onEntityUpdated()

Hook called whenever the entity is updated via the live subscription (e.g. after save or anonymize). Subclasses can override this to react to entity changes beyond what markForCheck() provides.

Returns : void
Protected subscribeToEntityChanges
subscribeToEntityChanges()
Returns : void

Properties

Readonly panelsState
Type : unknown
Default value : computed<Panel[]>(() => { const entity = this.entity(); if (!entity) return []; let filteredPanels = this.panels() .filter((p) => this.hasRequiredRole({ permittedUserRoles: p?.permittedUserRoles }), ) .map((p) => ({ title: p.title, components: p.components.map((c) => ({ title: c.title, component: c.component, config: this.getPanelConfig(c), })), })); const hasUserSecurityPanel = filteredPanels.some((panel) => panel.components.some((c) => c.component === "UserSecurity"), ); if (this.entityConstructor()?.enableUserAccounts && !hasUserSecurityPanel) { filteredPanels.push({ title: $localize`:Panel title:User Account`, components: [ { title: "", component: "UserSecurity", config: this.getPanelConfig({ component: "UserSecurity" }), }, ], }); } return filteredPanels; })
Protected Readonly ability
Type : unknown
Default value : inject(EntityAbility)
Protected Readonly entities
Type : unknown
Default value : inject(EntityRegistry)
Readonly entityConstructor
Type : unknown
Default value : computed<EntityConstructor | undefined>(() => this.entityType() ? this.entities.get(this.entityType()) : undefined, )
Protected Readonly entityMapperService
Type : unknown
Default value : inject(EntityMapperService)
Readonly isLoading
Type : unknown
Default value : signal(false)
Protected Readonly router
Type : unknown
Default value : inject(Router)
Protected Readonly unsavedChanges
Type : unknown
Default value : inject(UnsavedChangesService)
import { CommonModule } from "@angular/common";
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
} from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
import { MatMenuModule } from "@angular/material/menu";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatTabsModule } from "@angular/material/tabs";
import { MatTooltipModule } from "@angular/material/tooltip";
import { RouterLink } from "@angular/router";
import { AblePurePipe } from "@casl/angular";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { UntilDestroy } from "@ngneat/until-destroy";
import { Angulartics2OnModule } from "angulartics2";
import { RouteTarget } from "../../../route-target";
import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
import { EntityLoadPipe } from "../../common-components/entity-load/entity-load.pipe";
import { FaDynamicIconComponent } from "../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { ViewActionsComponent } from "../../common-components/view-actions/view-actions.component";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
import { SessionSubject } from "../../session/auth/session-info";
import { AbstractEntityDetailsComponent } from "../abstract-entity-details/abstract-entity-details.component";
import { EntityActionsMenuComponent } from "../entity-actions-menu/entity-actions-menu.component";
import { EntityArchivedInfoComponent } from "../entity-archived-info/entity-archived-info.component";
import { Panel, PanelComponent, PanelConfig } from "../EntityDetailsConfig";

/**
 * This component can be used to display an entity in more detail.
 * It groups subcomponents in panels.
 * Any component that is registered (has the `DynamicComponent` decorator) can be used as a subcomponent.
 * The subcomponents will be provided with the Entity object and the creating new status, as well as its static config.
 */
@RouteTarget("EntityDetails")
@UntilDestroy()
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-entity-details",
  templateUrl: "./entity-details.component.html",
  styleUrls: ["./entity-details.component.scss"],
  imports: [
    AblePurePipe,
    MatButtonModule,
    MatMenuModule,
    FontAwesomeModule,
    Angulartics2OnModule,
    MatTabsModule,
    TabStateModule,
    MatTooltipModule,
    MatProgressBarModule,
    ViewTitleComponent,
    DynamicComponentDirective,
    EntityActionsMenuComponent,
    EntityArchivedInfoComponent,
    FaDynamicIconComponent,
    RouterLink,
    CommonModule,
    ViewActionsComponent,
    EntityLoadPipe,
  ],
})
export class EntityDetailsComponent extends AbstractEntityDetailsComponent {
  /**
   * The configuration for the panels on this details page.
   */
  panels = input<Panel[]>([]);

  private session = inject(SessionSubject);

  readonly panelsState = computed<Panel[]>(() => {
    const entity = this.entity();
    if (!entity) return [];

    let filteredPanels = this.panels()
      .filter((p) =>
        this.hasRequiredRole({ permittedUserRoles: p?.permittedUserRoles }),
      )
      .map((p) => ({
        title: p.title,
        components: p.components.map((c) => ({
          title: c.title,
          component: c.component,
          config: this.getPanelConfig(c),
        })),
      }));

    const hasUserSecurityPanel = filteredPanels.some((panel) =>
      panel.components.some((c) => c.component === "UserSecurity"),
    );

    if (this.entityConstructor()?.enableUserAccounts && !hasUserSecurityPanel) {
      filteredPanels.push({
        title: $localize`:Panel title:User Account`,
        components: [
          {
            title: "",
            component: "UserSecurity",
            config: this.getPanelConfig({ component: "UserSecurity" }),
          },
        ],
      });
    }

    return filteredPanels;
  });

  /**
   * Checks if the current user has access based on permitted user roles.
   * Accepts a config object containing an optional `permittedUserRoles` array.
   * Returns true if roles are not specified or if the user matches any role.
   */
  private hasRequiredRole({
    permittedUserRoles,
  }: {
    permittedUserRoles?: string[];
  }): boolean {
    if (!permittedUserRoles || permittedUserRoles.length === 0) return true;
    const userRoles = this.session.value.roles;
    return permittedUserRoles.some((role) => userRoles.includes(role));
  }

  private getPanelConfig(c: PanelComponent): PanelConfig {
    const entity = this.entity();
    let panelConfig: PanelConfig = {
      entity,
      creatingNew: entity?.isNew,
    };
    if (typeof c.config === "object" && !Array.isArray(c.config)) {
      panelConfig = { ...c.config, ...panelConfig };
    } else {
      panelConfig.config = c.config;
    }
    return panelConfig;
  }
}
<!-- Header: title + actions -->
<app-view-title>
  @if (entityConstructor()?.icon) {
    <app-fa-dynamic-icon
      [icon]="entityConstructor()!.icon"
      class="standard-icon-with-text margin-left-regular"
    ></app-fa-dynamic-icon>
  }
  @if (!entity()?.isNew) {
    {{ entity()?.toString() }}
  } @else {
    <span i18n="Title when adding a new entity">
      Adding new {{ entityConstructor()?.label }}
    </span>
  }
</app-view-title>

<app-view-actions>
  <app-entity-actions-menu
    [entity]="entity()"
    [navigateOnDelete]="true"
    [showExpanded]="true"
  >
    @if (
      ("update"
        | ablePure: ("CONFIG_ENTITY" | entityLoad: "Config" | async)
        | async) && !entityConstructor()?.isInternalEntity
    ) {
      <button
        mat-menu-item
        [routerLink]="['/admin/entity', entity()?.getType()]"
        [queryParams]="{ mode: 'details' }"
        queryParamsHandling="merge"
      >
        <fa-icon
          class="standard-icon-with-text color-accent"
          icon="tools"
        ></fa-icon>
        <span i18n>Configure Data Structure</span>
      </button>
    }
  </app-entity-actions-menu>
</app-view-actions>

<app-entity-archived-info [entity]="entity()"></app-entity-archived-info>

<!-- Content: tabbed components -->
<mat-tab-group appTabStateMemo [preserveContent]="true">
  @for (panelConfig of panelsState(); track panelConfig.title) {
    <mat-tab [disabled]="entity()?.isNew || unsavedChanges.pending()">
      <ng-template mat-tab-label>
        <span
          [matTooltipDisabled]="!entity()?.isNew"
          matTooltip="Save the new record to create it before accessing other details"
          i18n-matTooltip="
            Tooltip explaining disabled sections when creating new entity
          "
        >
          {{ panelConfig.title }}
        </span>
      </ng-template>
      <ng-template matTabContent>
        @if (isLoading()) {
          <div class="process-spinner">
            <mat-progress-bar mode="indeterminate"></mat-progress-bar>
          </div>
        }
        @for (
          componentConfig of panelConfig.components;
          track $index;
          let j = $index
        ) {
          <div class="padding-top-large">
            @if (componentConfig.title && componentConfig.title !== "") {
              <h3>
                {{ componentConfig.title }}
              </h3>
            }
            @if (componentConfig.config?.entity) {
              <ng-template
                [appDynamicComponent]="componentConfig"
              ></ng-template>
            }
            @if (j < panelConfig.components.length - 1) {
              <br />
            }
          </div>
        }
      </ng-template>
    </mat-tab>
  }
</mat-tab-group>

./entity-details.component.scss

:host {
  display: block;
  height: 100%;
}

.mat-mdc-tab-group {
  height: 100%;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""