Description
Display an inline block representing an entity.
Example
|
entity
|
Type : Entity
|
|
|
The entity to display directly. Takes precedence over entityId.
|
|
entityId
|
Type : string
|
|
|
If entity is not set, entityId (with prefix) is used to load the entity.
|
|
linkDisabled
|
Type : boolean
|
Default value : false
|
|
|
Methods
|
showDetailsPage
|
showDetailsPage()
|
|
|
|
|
|
Readonly
entityBlockConfig
|
Type : unknown
|
Default value : computed(() => {
return this.entityResource.value()?.getConstructor()
?.toBlockDetailsAttributes;
})
|
|
|
|
Readonly
entityColor
|
Type : unknown
|
Default value : computed(() => {
const entity = this.entityResource.value();
if (!entity) return undefined;
const colorConfig = entity.getConstructor().color;
if (!colorConfig) return undefined;
return Entity.getColorWithConditions(entity);
})
|
|
|
|
Readonly
entityIcon
|
Type : unknown
|
Default value : computed(() => {
return this.entityResource.value()?.getConstructor()?.icon || "diamond";
})
|
|
|
|
entityResource
|
Type : unknown
|
Default value : resourceWithRetention({
params: () => ({ entity: this.entity(), entityId: this.entityId() }),
loader: async ({ params: { entity, entityId } }) => {
if (entity) return entity;
if (!entityId) return undefined;
try {
return await this.entityMapper.load(
Entity.extractTypeFromId(entityId),
entityId,
);
} catch (e) {
Logging.debug("[DISPLAY_ENTITY] Could not find entity.", entityId, e);
return undefined;
}
},
})
|
|
|
|
initialLoading
|
Type : unknown
|
Default value : computed(
() => this.entityResource.isLoading() && !this.entityResource.value(),
)
|
|
|
True during initial loading when no entity value is available yet.
Otherwise, we want to use the previous value through the resource's retention.
|
|
Readonly
missingEntityType
|
Type : unknown
|
Default value : computed(() => {
if (!this.notFound()) {
return undefined;
}
const id = this.entityId();
if (typeof id !== "string") {
// `entityId` is typed as string, but nothing enforces that at runtime for a
// block that is rendered from user config. A non-string id makes the resource
// loader below fail and swallow the error, which in turn makes notFound() true
// and brings us here — so extractTypeFromId() would throw on every change
// detection pass, inside a computed the template reads. Degrade to the generic
// not-found display instead.
return undefined;
}
const type = Entity.extractTypeFromId(id);
return type && this.registry.has(type)
? this.registry.get(type)
: undefined;
})
|
|
|
The constructor for the id's type prefix, if registered (used for the not-found display).
|
|
notFound
|
Type : unknown
|
Default value : computed(
() =>
!!this.entityId() &&
!this.entityResource.isLoading() &&
!this.entityResource.value(),
)
|
|
|
True when an id was given but no entity could be resolved (and loading has
settled) — e.g. the referenced record was deleted.
|
|
Readonly
notFoundIcon
|
Type : unknown
|
Default value : computed(
() => this.missingEntityType()?.icon || "diamond",
)
|
|
|
Icon for the not-found block: the referenced entity's type icon (e.g. the
Child icon for a deleted child), since the entity itself is gone. Falls back
to the generic block icon for unknown types.
|
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
} from "@angular/core";
import { Router } from "@angular/router";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { DisplayImgComponent } from "../../../../features/file/display-img/display-img.component";
import { FaDynamicIconComponent } from "../../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { TemplateTooltipDirective } from "../../../common-components/template-tooltip/template-tooltip.directive";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
import { EntityFieldViewComponent } from "../../../entity/entity-field-view/entity-field-view.component";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
import {
entityRegistry,
EntityRegistry,
} from "../../../entity/database-entity.decorator";
import { getEntityRuntimeRoute } from "../../../entity/entity-config.service";
import { Entity } from "../../../entity/model/entity";
import { Logging } from "../../../logging/logging.service";
import { resourceWithRetention } from "../../../../utils/resourceWithRetention";
import { MatTooltipModule } from "@angular/material/tooltip";
/**
* Display an inline block representing an entity.
*/
@DynamicComponent("EntityBlock")
@Component({
selector: "app-entity-block",
templateUrl: "./entity-block.component.html",
styleUrls: ["./entity-block.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FaDynamicIconComponent,
TemplateTooltipDirective,
DisplayImgComponent,
EntityFieldViewComponent,
MatProgressSpinnerModule,
MatTooltipModule,
],
})
export class EntityBlockComponent {
private entityMapper = inject(EntityMapperService);
private router = inject(Router);
// optional + module-singleton fallback so this widely-reused block never
// crashes a host/test that didn't explicitly provide EntityRegistry
private readonly registry =
inject(EntityRegistry, { optional: true }) ?? entityRegistry;
/** The entity to display directly. Takes precedence over entityId. */
entity = input<Entity>();
/** If entity is not set, entityId (with prefix) is used to load the entity. */
entityId = input<string>();
linkDisabled = input(false);
entityResource = resourceWithRetention({
params: () => ({ entity: this.entity(), entityId: this.entityId() }),
loader: async ({ params: { entity, entityId } }) => {
if (entity) return entity;
if (!entityId) return undefined;
try {
return await this.entityMapper.load(
Entity.extractTypeFromId(entityId),
entityId,
);
} catch (e) {
Logging.debug("[DISPLAY_ENTITY] Could not find entity.", entityId, e);
return undefined;
}
},
});
/**
* True during initial loading when no entity value is available yet.
* Otherwise, we want to use the previous value through the resource's retention.
*/
initialLoading = computed(
() => this.entityResource.isLoading() && !this.entityResource.value(),
);
/**
* True when an id was given but no entity could be resolved (and loading has
* settled) — e.g. the referenced record was deleted.
*/
notFound = computed(
() =>
!!this.entityId() &&
!this.entityResource.isLoading() &&
!this.entityResource.value(),
);
/** The constructor for the id's type prefix, if registered (used for the not-found display). */
readonly missingEntityType = computed(() => {
if (!this.notFound()) {
return undefined;
}
const id = this.entityId();
if (typeof id !== "string") {
// `entityId` is typed as string, but nothing enforces that at runtime for a
// block that is rendered from user config. A non-string id makes the resource
// loader below fail and swallow the error, which in turn makes notFound() true
// and brings us here — so extractTypeFromId() would throw on every change
// detection pass, inside a computed the template reads. Degrade to the generic
// not-found display instead.
return undefined;
}
const type = Entity.extractTypeFromId(id);
return type && this.registry.has(type)
? this.registry.get(type)
: undefined;
});
/**
* Icon for the not-found block: the referenced entity's *type* icon (e.g. the
* Child icon for a deleted child), since the entity itself is gone. Falls back
* to the generic block icon for unknown types.
*/
readonly notFoundIcon = computed(
() => this.missingEntityType()?.icon || "diamond",
);
readonly entityBlockConfig = computed(() => {
return this.entityResource.value()?.getConstructor()
?.toBlockDetailsAttributes;
});
readonly entityIcon = computed(() => {
return this.entityResource.value()?.getConstructor()?.icon || "diamond";
});
readonly entityColor = computed(() => {
const entity = this.entityResource.value();
if (!entity) return undefined;
const colorConfig = entity.getConstructor().color;
if (!colorConfig) return undefined;
return Entity.getColorWithConditions(entity);
});
showDetailsPage() {
const entity = this.entityResource.value();
if (this.linkDisabled() || !entity) {
return;
}
this.router.navigate([
getEntityRuntimeRoute(entity.getConstructor()),
entity.getId(true),
]);
}
}
@if (initialLoading()) {
<span class="block-height">
<mat-spinner diameter="16" class="inline-spinner"></mat-spinner>
</span>
} @else if (!entityId() && !entity()) {
<span
matTooltip="no record defined"
i18n-matTooltip="No record defined tooltip"
>-</span
>
} @else if (notFound()) {
<span
class="block-height truncate-text inactive"
matTooltip="This record does not exist anymore or you do not have the required permissions to access it."
i18n-matTooltip="Entity not found tooltip"
>
<app-fa-dynamic-icon
[icon]="notFoundIcon()"
class="margin-right-small"
></app-fa-dynamic-icon>
@if (missingEntityType()) {
{{ missingEntityType()?.label }}:
}
<span i18n="Entity block not found message">not available</span>
</span>
} @else {
<span
class="block-height truncate-text"
[class.clickable]="!linkDisabled()"
[class.inactive]="entityResource.value()?.inactive"
(click)="showDetailsPage()"
[appTemplateTooltip]="tooltip"
[tooltipDisabled]="!entityBlockConfig()"
[title]="entityResource.value()?.toString()"
>
<app-fa-dynamic-icon
[icon]="entityIcon()"
[style.color]="entityColor()"
class="margin-right-small"
></app-fa-dynamic-icon>
{{ entityResource.value()?.toString() }}
</span>
}
<!-- Tooltip on Hover -->
<ng-template #tooltip>
<div class="tooltip-container">
@if (entityBlockConfig()?.image) {
<app-display-img
class="tooltip-photo"
[entity]="entityResource.value()"
[imgProperty]="entityBlockConfig()?.image"
></app-display-img>
}
<div>
<!-- Font-weight is applied as style to override the default -->
<h3 style="font-weight: bold">
@if (entityBlockConfig()?.title) {
<app-entity-field-view
[entity]="entityResource.value()"
[field]="entityBlockConfig()?.title"
></app-entity-field-view>
} @else {
{{ entityResource.value()?.toString() }}
}
</h3>
@for (field of entityBlockConfig()?.fields; track field) {
<app-entity-field-view
[entity]="entityResource.value()"
[field]="field"
></app-entity-field-view>
}
</div>
</div>
</ng-template>
@use "variables/sizes";
@use "variables/colors";
.block-height {
line-height: sizes.$icon-block;
}
.inactive {
color: colors.$inactive;
}
.inline-spinner {
display: inline-block;
vertical-align: middle;
}
.tooltip-container {
padding: 0.5em;
display: flex;
flex-direction: row;
gap: 2em;
align-items: center;
}
.tooltip-photo ::ng-deep img {
width: 80px;
height: 80px;
object-fit: cover;
overflow: hidden;
}
Legend
Html element with directive