src/app/core/entity-details/entity-details/entity-details.component.ts
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.
AbstractEntityDetailsComponent
OnChanges
selector | app-entity-details |
imports |
AblePurePipe
MatButtonModule
MatMenuModule
FontAwesomeModule
Angulartics2OnModule
MatTabsModule
TabStateModule
MatTooltipModule
MatProgressBarModule
ViewTitleComponent
DynamicComponentDirective
EntityActionsMenuComponent
EntityArchivedInfoComponent
RouterLink
CommonModule
ViewActionsComponent
EntityLoadPipe
|
styleUrls | ./entity-details.component.scss |
templateUrl | ./entity-details.component.html |
Properties |
|
Methods |
|
Inputs |
panels | |
Type : Panel[]
|
|
Default value : []
|
|
The configuration for the panels on this details page. |
entity | |
Type : Entity
|
|
Inherited from
AbstractEntityDetailsComponent
|
|
Defined in
AbstractEntityDetailsComponent:40
|
entityType | |
Type : string
|
|
Inherited from
AbstractEntityDetailsComponent
|
|
Defined in
AbstractEntityDetailsComponent:36
|
id | |
Type : string
|
|
Inherited from
AbstractEntityDetailsComponent
|
|
Defined in
AbstractEntityDetailsComponent:39
|
Protected Async loadEntity |
loadEntity()
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:66
|
Returns :
any
|
Protected subscribeToEntityChanges |
subscribeToEntityChanges()
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:53
|
Returns :
void
|
Protected Readonly ability |
Default value : inject(EntityAbility)
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:29
|
Protected Readonly entities |
Default value : inject(EntityRegistry)
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:28
|
entityConstructor |
Type : EntityConstructor
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:37
|
Protected Readonly entityMapperService |
Default value : inject(EntityMapperService)
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:27
|
isLoading |
Type : boolean
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:33
|
Protected Readonly router |
Default value : inject(Router)
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:30
|
Protected Readonly unsavedChanges |
Default value : inject(UnsavedChangesService)
|
Inherited from
AbstractEntityDetailsComponent
|
Defined in
AbstractEntityDetailsComponent:31
|
import {
Component,
inject,
Input,
OnChanges,
SimpleChanges,
} from "@angular/core";
import { RouterLink } from "@angular/router";
import { Panel, PanelComponent, PanelConfig } from "../EntityDetailsConfig";
import { MatButtonModule } from "@angular/material/button";
import { MatMenuModule } from "@angular/material/menu";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { Angulartics2OnModule } from "angulartics2";
import { MatTabsModule } from "@angular/material/tabs";
import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { CommonModule } from "@angular/common";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
import { EntityActionsMenuComponent } from "../entity-actions-menu/entity-actions-menu.component";
import { EntityArchivedInfoComponent } from "../entity-archived-info/entity-archived-info.component";
import { UntilDestroy } from "@ngneat/until-destroy";
import { RouteTarget } from "../../../route-target";
import { AbstractEntityDetailsComponent } from "../abstract-entity-details/abstract-entity-details.component";
import { ViewActionsComponent } from "../../common-components/view-actions/view-actions.component";
import { AblePurePipe } from "@casl/angular";
import { SessionSubject } from "../../session/auth/session-info";
import { EntityLoadPipe } from "../../common-components/entity-load/entity-load.pipe";
/**
* 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({
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,
RouterLink,
CommonModule,
ViewActionsComponent,
EntityLoadPipe,
],
})
export class EntityDetailsComponent
extends AbstractEntityDetailsComponent
implements OnChanges
{
/**
* The configuration for the panels on this details page.
*/
@Input() panels: Panel[] = [];
private session = inject(SessionSubject);
override async ngOnChanges(changes: SimpleChanges) {
await super.ngOnChanges(changes);
if (changes.id || changes.entity || changes.panels) {
this.initPanels();
}
}
private initPanels() {
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" }),
},
],
});
}
this.panels = 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 {
let panelConfig: PanelConfig = {
entity: this.entity,
creatingNew: this.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) {
<fa-icon
[icon]="entityConstructor.icon"
class="standard-icon-with-text margin-left-regular"
></fa-icon>
}
@if (!entity?.isNew) {
{{ entity?.toString() }}
} @else {
<span i18n="Title when adding a new entity">
Adding new {{ this.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>Edit 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 panels; track panelConfig) {
<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 componentConfig;
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%;
}