src/app/core/basic-datatypes/entity/entity-block/entity-block.component.ts

Index

Properties
Methods
Inputs

Inputs

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()
Returns : void

Properties

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>

./entity-block.component.scss

@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
Component
Html element with directive

results matching ""

    No results matching ""