src/app/core/import/import-column-mapping/edit-import-column-mapping/edit-import-column-mapping.component.ts

Description

Component to edit a single imported column's mapping to an entity field (including special transformations, if applicable).

Example

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Inputs

additionalSettings
Type : ImportAdditionalSettings

Additional settings for import processing

columnMapping
Type : ColumnMapping
Required :  true
entityCtor
Type : EntityConstructor
Required :  true
otherColumnMappings
Type : ColumnMapping[]
Default value : []

Existing column mappings of other columns (e.g. to hide already mapped fields)

rawData
Type : any[]
Default value : []

the actually imported data (to let this component configure special transformations, e.g. to map values to dropdown categories)

Outputs

columnMappingChange
Type : ColumnMapping

Methods

Async onFieldSelected
onFieldSelected(propertyName: string)
Parameters :
Name Type Optional
propertyName string No
Returns : any
updateMapping
updateMapping(settingAdditional: unknown)
Parameters :
Name Type Optional Default value
settingAdditional unknown No false
Returns : void

Properties

configError
Type : unknown
Default value : computed<string | null>(() => this.configDialogs.isConfigMissing(this.columnMapping(), this.entityCtor()) ? $localize`:import column mapping - config missing:Value mapping is not configured yet. Open it and confirm to continue.` : null, )

Error message below the field while the transformation of its values still has to be configured and confirmed by the user, null once it is configured or not needed at all.

currentlyMappedDatatype
Type : unknown
Default value : computed<DefaultDatatype | null>(() => { const col = this.columnMapping(); const schema = this.entityCtor()?.schema?.get(col?.propertyName); return schema ? this.schemaService.getDatatypeOrDefault(schema.dataType) : null; })
hideOption
Type : unknown
Default value : () => {...}
inlineComponentConfig
Type : unknown
Default value : computed<DynamicComponentConfig | null>(() => { const componentName = this.currentlyMappedDatatype()?.importConfigComponent; if (!componentName) return null; return { component: componentName, config: { col: this.columnMapping(), rawData: this.rawData(), entityType: this.entityCtor(), otherColumnMappings: this.otherColumnMappings(), additionalSettings: this.additionalSettings(), onColumnMappingChange: this.onInlineComponentChange, }, }; })
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  output,
} from "@angular/core";
import { DynamicComponentConfig } from "../../../config/dynamic-components/dynamic-component-config.interface";
import { ColumnMapping } from "../../column-mapping";
import { EntityConstructor } from "../../../entity/model/entity";
import { FormFieldConfig } from "../../../common-components/entity-form/FormConfig";
import { DefaultDatatype } from "../../../entity/default-datatype/default.datatype";
import { EntitySchemaService } from "../../../entity/schema/entity-schema.service";
import { MatInputModule } from "@angular/material/input";
import { EntityFieldSelectComponent } from "../../../entity/entity-field-select/entity-field-select.component";
import { FormsModule } from "@angular/forms";
import { DynamicComponentDirective } from "../../../config/dynamic-components/dynamic-component.directive";
import { ImportAdditionalSettings } from "../../import-additional-settings";
import { ImportConfigDialogService } from "../import-config-dialog.service";

/**
 * Component to edit a single imported column's mapping to an entity field
 * (including special transformations, if applicable).
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-edit-import-column-mapping",
  templateUrl: "./edit-import-column-mapping.component.html",
  styleUrls: ["./edit-import-column-mapping.component.scss"],
  imports: [
    MatInputModule,
    EntityFieldSelectComponent,
    FormsModule,
    DynamicComponentDirective,
  ],
})
export class EditImportColumnMappingComponent {
  private readonly schemaService = inject(EntitySchemaService);
  private readonly configDialogs = inject(ImportConfigDialogService);

  columnMapping = input.required<ColumnMapping>();

  entityCtor = input.required<EntityConstructor>();

  /**
   * Existing column mappings of other columns
   * (e.g. to hide already mapped fields)
   */
  otherColumnMappings = input<ColumnMapping[]>([]);

  /**
   * the actually imported data
   * (to let this component configure special transformations, e.g. to map values to dropdown categories)
   */
  rawData = input<any[]>([]);

  /**
   * Additional settings for import processing
   */
  additionalSettings = input<ImportAdditionalSettings>();

  columnMappingChange = output<ColumnMapping>();

  /**
   * Whether this column's config dialog is currently open.
   *
   * The field select notifies twice for a single selection, this keeps the second notification
   * from opening the same dialog again on top of the first one.
   */
  private configDialogOpen = false;

  currentlyMappedDatatype = computed<DefaultDatatype | null>(() => {
    const col = this.columnMapping();
    const schema = this.entityCtor()?.schema?.get(col?.propertyName);
    return schema
      ? this.schemaService.getDatatypeOrDefault(schema.dataType)
      : null;
  });

  /**
   * Error message below the field while the transformation of its values still has to be
   * configured and confirmed by the user, `null` once it is configured or not needed at all.
   */
  configError = computed<string | null>(() =>
    this.configDialogs.isConfigMissing(this.columnMapping(), this.entityCtor())
      ? $localize`:import column mapping - config missing:Value mapping is not configured yet. Open it and confirm to continue.`
      : null,
  );

  inlineComponentConfig = computed<DynamicComponentConfig | null>(() => {
    const componentName = this.currentlyMappedDatatype()?.importConfigComponent;
    if (!componentName) return null;
    return {
      component: componentName,
      config: {
        col: this.columnMapping(),
        rawData: this.rawData(),
        entityType: this.entityCtor(),
        otherColumnMappings: this.otherColumnMappings(),
        additionalSettings: this.additionalSettings(),
        onColumnMappingChange: this.onInlineComponentChange,
      },
    };
  });

  /**
   * Callback for inline config components to propagate column mapping changes.
   * Defined as a stable reference (class field) to avoid re-rendering the dynamic component on each recomputation.
   */
  private readonly onInlineComponentChange = (updatedCol: ColumnMapping) => {
    this.columnMappingChange.emit({ ...updatedCol, manuallyUpdated: true });
  };

  hideOption = (option: FormFieldConfig) =>
    this.otherColumnMappings().some((c) => c.propertyName === option.id) &&
    !this.schemaService.getDatatypeOrDefault(option.dataType)
      .importAllowsMultiMapping &&
    option.id !== this.columnMapping()?.propertyName;

  async onFieldSelected(propertyName: string) {
    const col: ColumnMapping = {
      ...this.columnMapping(),
      propertyName,
      // the new field brings its own transformation, the previous config does not apply to it
      additional: undefined,
      manuallyUpdated: true,
    };
    this.columnMappingChange.emit(col);

    if (
      this.configDialogOpen ||
      !this.configDialogs.hasConfigDialog(col, this.entityCtor())
    ) {
      return;
    }

    // open the config right away, so that the user reviews and confirms the suggested value
    // mappings of the field they just picked instead of importing the file's values unchanged
    this.configDialogOpen = true;
    try {
      const configuredCol = await this.configDialogs.openConfigDialog(
        col,
        this.rawData(),
        this.entityCtor(),
        this.additionalSettings(),
      );
      this.columnMappingChange.emit(configuredCol);
    } finally {
      this.configDialogOpen = false;
    }
  }

  updateMapping(settingAdditional = false) {
    const col = this.columnMapping();
    const updated: ColumnMapping = { ...col, manuallyUpdated: true };
    if (!settingAdditional) {
      delete updated.additional;
    }
    this.columnMappingChange.emit(updated);
  }
}
<div class="flex-row gap-regular align-center">
  <mat-form-field>
    <mat-label>{{ columnMapping()?.column }}</mat-label>
    <app-entity-field-select
      [entityType]="entityCtor()"
      [hideOption]="hideOption"
      [ngModel]="columnMapping()?.propertyName"
      (ngModelChange)="onFieldSelected($event)"
    />
  </mat-form-field>

  @if (inlineComponentConfig()) {
    <ng-container [appDynamicComponent]="inlineComponentConfig()" />
  }
</div>

@if (configError(); as error) {
  <mat-error class="config-error">{{ error }}</mat-error>
}

./edit-import-column-mapping.component.scss

mat-form-field {
  flex-grow: 1;
  max-width: 500px;
  min-width: min(240px, 100vw);
}

.config-button {
  max-width: 240px;
}

.config-error {
  // the form field keeps space below the input for its own errors, move the message into it
  // so that it reads as belonging to the field above rather than to the next column below
  margin-top: -18px;

  // mat-error keeps a spacer for the layout inside a form field, which is not needed out here
  &::before {
    display: none;
  }
}

.lookup-warning-icon {
  color: #f57c00;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""