src/app/core/basic-datatypes/entity/entity-import-config/entity-import-config.component.ts

Description

Inline import configuration component for entity reference fields. Lets users select which property of the referenced entity to match against import values, and optionally configure a value transformation for that property.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs

Inputs

additionalSettings
Type : ImportAdditionalSettings
col
Type : ColumnMapping
entityType
Type : EntityConstructor
onColumnMappingChange
Type : (col: ColumnMapping) => void
otherColumnMappings
Type : ColumnMapping[]
Default value : []
rawData
Type : any[]
Default value : []

Methods

onRefFieldChange
onRefFieldChange(newRefField: string)
Parameters :
Name Type Optional
newRefField string No
Returns : void

Properties

availableProperties
Type : unknown
Default value : computed(() => { const referencedEntities = this.referencedEntities(); const isMultiType = referencedEntities.length > 1; const properties = new Map< string, { property: string; label: string; declaringTypes: string[] } >(); for (const entity of referencedEntities) { const entityLabel = entity.label ?? entity.ENTITY_TYPE; for (const [prop, schema] of entity.schema.entries()) { if (!((!!schema.label && !schema.isInternalField) || prop === "_id")) continue; const existing = properties.get(prop); if (existing) { existing.declaringTypes.push(entityLabel); continue; } properties.set(prop, { property: prop, label: schema.label ?? prop, declaringTypes: [entityLabel], }); } } return [...properties.values()].map( ({ property, label, declaringTypes }) => ({ property, label, typeHint: isMultiType ? declaringTypes.join(", ") : "", }), ); })

The union of matchable properties across every allowed referenced type, so a multi-type field still offers a full "match by" property list instead of only the first type's properties.

Properties sharing an id across several types are deliberately collapsed into one entry: matching compares that same property on candidates of every allowed type (see EntityDatatype.importMatchField), so offering it once matches against all of them. typeHint then names every type that declares it, so users can tell a shared property from one that only narrows the match to a single type. It stays empty for a single-type field, where naming the one allowed type would just be noise.

hasMultiMapping
Type : unknown
Default value : computed(() => { const col = this.col(); const otherColumnMappings = this.otherColumnMappings(); return ( col?.propertyName !== undefined && otherColumnMappings?.some( (m) => m.propertyName === col.propertyName && m.column !== col.column, ) ); })
referencedEntities
Type : unknown
Default value : computed<EntityConstructor[]>(() => { const col = this.col(); const entityType = this.entityType(); if (!col?.propertyName || !entityType) return []; const fieldSchema = entityType.schema.get(col.propertyName); return asArray(fieldSchema?.additional) .filter((entityName) => this.entityRegistry.has(entityName)) .map((entityName) => this.entityRegistry.get(entityName)); })

All entity types this column's target field allows referencing. Normally a single type, but a field can allow linking to several record types, in which case the schema's additional is an array rather than a single type name - each is resolved individually here (an unregistered type is skipped rather than throwing, since EntityRegistry.get() only accepts one registered key at a time).

referencedEntityLabel
Type : unknown
Default value : computed(() => this.referencedEntities() // a type configured without a label falls back to its id, as elsewhere .map((entity) => entity.label ?? entity.ENTITY_TYPE) .join(" / "), )

Display label for the "Match by ... property" dropdown, joining every allowed type's label.

selectedRefField
Type : unknown
Default value : computed(() => { const col = this.col(); const current = normalizeEntityAdditional(col?.additional); return current?.refField ?? ""; })
selectedRefFieldEntity
Type : unknown
Default value : computed<EntityConstructor | null>(() => { const refField = this.selectedRefField(); if (!refField) return null; return ( this.referencedEntities().find((entity) => entity.schema.has(refField)) ?? null ); })

The allowed type that declares the currently selected match property.

With several allowed types the selected property may exist on only one of them, so the value-transformation config has to be built from that type's schema rather than from an arbitrary (e.g. the first) one.

showInheritanceHint
Type : unknown
Default value : computed(() => { const col = this.col(); const entityType = this.entityType(); if (!col?.propertyName || !entityType) return false; return isInheritanceSourceReferenceField(entityType, col.propertyName); })
subFieldInlineConfig
Type : unknown
Default value : computed(() => { const refField = this.selectedRefField(); const refEntity = this.selectedRefFieldEntity(); if (!refField || !refEntity) return null; const refFieldSchema = refEntity.schema.get(refField); if (!refFieldSchema) return null; const refDatatype = this.schemaService.getDatatypeOrDefault( refFieldSchema.dataType, ); if (!refDatatype?.importConfigComponent) return null; const col = this.col(); const additional = col?.additional as EntityAdditional; // Synthetic column mapping for the sub-field's inline component const syntheticCol: ColumnMapping = { column: col.column, propertyName: refField, additional: additional?.valueMapping, }; return { component: refDatatype.importConfigComponent, config: { col: syntheticCol, rawData: this.rawData(), entityType: refEntity, otherColumnMappings: [], additionalSettings: this.additionalSettings(), onColumnMappingChange: (updatedCol: ColumnMapping) => { const current = normalizeEntityAdditional(col.additional) ?? { refField: refField, }; const updatedParentCol: ColumnMapping = { ...col, additional: { ...current, refField: refField, valueMapping: updatedCol.additional, } as EntityAdditional, }; this.onColumnMappingChange()?.(updatedParentCol); }, }, }; })

Config for the sub-field's inline component (for value transformation config)

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
} from "@angular/core";
import { ColumnMapping } from "../../../import/column-mapping";
import { EntityConstructor } from "../../../entity/model/entity";
import { ImportAdditionalSettings } from "../../../import/import-additional-settings";
import { EntityRegistry } from "../../../entity/database-entity.decorator";
import { MatSelectModule } from "@angular/material/select";
import { MatFormFieldModule } from "@angular/material/form-field";
import { FormsModule } from "@angular/forms";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { MatTooltip } from "@angular/material/tooltip";
import { HintBoxComponent } from "../../../common-components/hint-box/hint-box.component";
import { DynamicComponentDirective } from "../../../config/dynamic-components/dynamic-component.directive";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
import {
  EntityAdditional,
  normalizeEntityAdditional,
} from "../entity.datatype";
import { isInheritanceSourceReferenceField } from "../../../import/import-inheritance-warning.util";
import { EntitySchemaService } from "../../../entity/schema/entity-schema.service";
import { asArray } from "../../../../utils/asArray";

/**
 * Inline import configuration component for entity reference fields.
 * Lets users select which property of the referenced entity to match against import values,
 * and optionally configure a value transformation for that property.
 */
@DynamicComponent("EntityImportConfig")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-entity-import-config",
  templateUrl: "./entity-import-config.component.html",
  imports: [
    MatSelectModule,
    MatFormFieldModule,
    FormsModule,
    FaIconComponent,
    MatTooltip,
    HintBoxComponent,
    DynamicComponentDirective,
  ],
})
export class EntityImportConfigComponent {
  private readonly entityRegistry = inject(EntityRegistry);
  private readonly schemaService = inject(EntitySchemaService);

  col = input<ColumnMapping>();
  rawData = input<any[]>([]);
  entityType = input<EntityConstructor>();
  otherColumnMappings = input<ColumnMapping[]>([]);
  additionalSettings = input<ImportAdditionalSettings>();
  onColumnMappingChange = input<(col: ColumnMapping) => void>();

  selectedRefField = computed(() => {
    const col = this.col();
    const current = normalizeEntityAdditional(col?.additional);
    return current?.refField ?? "";
  });

  /**
   * All entity types this column's target field allows referencing.
   * Normally a single type, but a field can allow linking to several record
   * types, in which case the schema's `additional` is an array rather than a
   * single type name - each is resolved individually here (an unregistered
   * type is skipped rather than throwing, since `EntityRegistry.get()` only
   * accepts one registered key at a time).
   */
  referencedEntities = computed<EntityConstructor[]>(() => {
    const col = this.col();
    const entityType = this.entityType();
    if (!col?.propertyName || !entityType) return [];

    const fieldSchema = entityType.schema.get(col.propertyName);
    return asArray(fieldSchema?.additional)
      .filter((entityName) => this.entityRegistry.has(entityName))
      .map((entityName) => this.entityRegistry.get(entityName));
  });

  /**
   * The allowed type that declares the currently selected match property.
   *
   * With several allowed types the selected property may exist on only one of
   * them, so the value-transformation config has to be built from that type's
   * schema rather than from an arbitrary (e.g. the first) one.
   */
  selectedRefFieldEntity = computed<EntityConstructor | null>(() => {
    const refField = this.selectedRefField();
    if (!refField) return null;

    return (
      this.referencedEntities().find((entity) => entity.schema.has(refField)) ??
      null
    );
  });

  /** Display label for the "Match by ... property" dropdown, joining every allowed type's label. */
  referencedEntityLabel = computed(() =>
    this.referencedEntities()
      // a type configured without a label falls back to its id, as elsewhere
      .map((entity) => entity.label ?? entity.ENTITY_TYPE)
      .join(" / "),
  );

  /**
   * The union of matchable properties across every allowed referenced type,
   * so a multi-type field still offers a full "match by" property list
   * instead of only the first type's properties.
   *
   * Properties sharing an id across several types are deliberately collapsed
   * into one entry: matching compares that same property on candidates of
   * every allowed type (see `EntityDatatype.importMatchField`), so offering it
   * once matches against all of them. `typeHint` then names every type that
   * declares it, so users can tell a shared property from one that only
   * narrows the match to a single type. It stays empty for a single-type
   * field, where naming the one allowed type would just be noise.
   */
  availableProperties = computed(() => {
    const referencedEntities = this.referencedEntities();
    const isMultiType = referencedEntities.length > 1;
    const properties = new Map<
      string,
      { property: string; label: string; declaringTypes: string[] }
    >();

    for (const entity of referencedEntities) {
      const entityLabel = entity.label ?? entity.ENTITY_TYPE;

      for (const [prop, schema] of entity.schema.entries()) {
        if (!((!!schema.label && !schema.isInternalField) || prop === "_id"))
          continue;

        const existing = properties.get(prop);
        if (existing) {
          existing.declaringTypes.push(entityLabel);
          continue;
        }

        properties.set(prop, {
          property: prop,
          label: schema.label ?? prop,
          declaringTypes: [entityLabel],
        });
      }
    }

    return [...properties.values()].map(
      ({ property, label, declaringTypes }) => ({
        property,
        label,
        typeHint: isMultiType ? declaringTypes.join(", ") : "",
      }),
    );
  });

  showInheritanceHint = computed(() => {
    const col = this.col();
    const entityType = this.entityType();
    if (!col?.propertyName || !entityType) return false;
    return isInheritanceSourceReferenceField(entityType, col.propertyName);
  });

  hasMultiMapping = computed(() => {
    const col = this.col();
    const otherColumnMappings = this.otherColumnMappings();
    return (
      col?.propertyName !== undefined &&
      otherColumnMappings?.some(
        (m) => m.propertyName === col.propertyName && m.column !== col.column,
      )
    );
  });

  /** Config for the sub-field's inline component (for value transformation config) */
  subFieldInlineConfig = computed(() => {
    const refField = this.selectedRefField();
    const refEntity = this.selectedRefFieldEntity();
    if (!refField || !refEntity) return null;

    const refFieldSchema = refEntity.schema.get(refField);
    if (!refFieldSchema) return null;

    const refDatatype = this.schemaService.getDatatypeOrDefault(
      refFieldSchema.dataType,
    );
    if (!refDatatype?.importConfigComponent) return null;

    const col = this.col();
    const additional = col?.additional as EntityAdditional;
    // Synthetic column mapping for the sub-field's inline component
    const syntheticCol: ColumnMapping = {
      column: col.column,
      propertyName: refField,
      additional: additional?.valueMapping,
    };

    return {
      component: refDatatype.importConfigComponent,
      config: {
        col: syntheticCol,
        rawData: this.rawData(),
        entityType: refEntity,
        otherColumnMappings: [],
        additionalSettings: this.additionalSettings(),
        onColumnMappingChange: (updatedCol: ColumnMapping) => {
          const current = normalizeEntityAdditional(col.additional) ?? {
            refField: refField,
          };
          const updatedParentCol: ColumnMapping = {
            ...col,
            additional: {
              ...current,
              refField: refField,
              valueMapping: updatedCol.additional,
            } as EntityAdditional,
          };
          this.onColumnMappingChange()?.(updatedParentCol);
        },
      },
    };
  });

  onRefFieldChange(newRefField: string) {
    const col = this.col();
    // Clear valueMapping when ref field changes
    this.onColumnMappingChange()?.({
      ...col,
      additional: { refField: newRefField } as EntityAdditional,
    });
  }
}
<div class="flex-row flex-wrap align-center gap-small">
  @if (showInheritanceHint()) {
    <app-hint-box type="warning" class="margin-bottom-regular">
      <strong i18n>⚠️ Manual update required</strong>
      <div i18n>
        Selected values will be imported as is. Inherited values need to be
        updated manually.
      </div>
    </app-hint-box>
  }

  <div class="flex-row gap-regular align-center">
    <mat-form-field class="margin-start-small">
      <mat-label i18n="import - entity - match by property label">
        Match by {{ referencedEntityLabel() }} property
      </mat-label>
      <mat-select
        [ngModel]="selectedRefField()"
        (ngModelChange)="onRefFieldChange($event)"
      >
        @for (prop of availableProperties(); track prop.property) {
          <mat-option [value]="prop.property">
            {{ prop.label }}
            @if (prop.typeHint) {
              <span class="hint-text margin-start-small">{{
                prop.typeHint
              }}</span>
            }
          </mat-option>
        }
      </mat-select>
    </mat-form-field>

    @if (hasMultiMapping()) {
      <fa-icon
        icon="object-group"
        matTooltip="This field is mapped from multiple imported columns."
        i18n-matTooltip
      ></fa-icon>
    }
  </div>

  @if (subFieldInlineConfig()) {
    <ng-container [appDynamicComponent]="subFieldInlineConfig()" />
  }
</div>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""