src/app/core/import/import-review-data/import-review-data.component.ts

Implements

OnChanges

Metadata

Relationships

Used by

Depends on

Index

Properties
Methods
Inputs
Outputs

Inputs

additionalActions
Type : AdditionalImportAction[]
additionalSettings
Type : ImportAdditionalSettings
columnMapping
Type : ColumnMapping[]
entityType
Type : string
filename
Type : string
importExisting
Type : ImportExistingSettings | undefined
rawData
Type : any[]
stepIsFocused
Type : boolean
Default value : true

Indicate if the component is currently visible, so that re-calculation and popups only happen then.

Outputs

importComplete
Type : EventEmitter

Methods

Async startImport
startImport()
Returns : any

Properties

Readonly dataSource
Type : unknown
Default value : new InMemoryDataSource()
displayColumns
Type : unknown
Default value : signal<string[]>([])
entityConstructor
Type : EntityConstructor
Readonly IMPORT_STATUS_COLUMN
Type : string
Default value : "_importStatus"
isLoading
Type : unknown
Default value : signal(false)
MULTIPLE_MATCHING_ENTITIES_KEY
Type : unknown
Default value : ImportExistingService.MULTIPLE_MATCHING_ENTITIES_KEY
transformationErrorColumns
Type : unknown
Default value : signal<string[]>([])
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  inject,
  Input,
  OnChanges,
  Output,
  signal,
  SimpleChanges,
} from "@angular/core";
import { ColumnMapping } from "../column-mapping";
import { EntityConstructor } from "../../entity/model/entity";
import { ImportCellError, ImportService } from "../import.service";
import { MatDialog } from "@angular/material/dialog";
import {
  ImportConfirmSummaryComponent,
  ImportDialogData,
  ImportDialogResult,
} from "../import-confirm-summary/import-confirm-summary.component";
import { lastValueFrom } from "rxjs";
import { ImportExistingSettings, ImportMetadata } from "../import-metadata";
import { ImportAdditionalSettings } from "../import-additional-settings";
import { MatButtonModule } from "@angular/material/button";
import { HelpButtonComponent } from "../../common-components/help-button/help-button.component";
import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { MatProgressBar } from "@angular/material/progress-bar";
import {
  MatCell,
  MatCellDef,
  MatColumnDef,
  MatHeaderCell,
  MatHeaderCellDef,
} from "@angular/material/table";
import { AdditionalImportAction } from "../additional-actions/additional-import-action";
import { MatTooltip } from "@angular/material/tooltip";
import { EntityBlockComponent } from "../../basic-datatypes/entity/entity-block/entity-block.component";
import { HintBoxComponent } from "../../common-components/hint-box/hint-box.component";
import { ImportExistingService } from "../update-existing/import-existing.service";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { Logging } from "../../logging/logging.service";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { OkButton } from "../../common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
import { InMemoryDataSource } from "#src/app/core/common-components/entities-table/data-source/in-memory-data-source";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-import-review-data",
  templateUrl: "./import-review-data.component.html",
  styleUrls: ["./import-review-data.component.scss"],
  imports: [
    MatButtonModule,
    HelpButtonComponent,
    EntitiesTableComponent,
    MatProgressBar,
    MatColumnDef,
    MatCell,
    MatCellDef,
    MatHeaderCell,
    MatHeaderCellDef,
    MatTooltip,
    EntityBlockComponent,
    HintBoxComponent,
    FaIconComponent,
  ],
})
export class ImportReviewDataComponent implements OnChanges {
  private importService = inject(ImportService);
  private matDialog = inject(MatDialog);
  private entityRegistry = inject(EntityRegistry);
  private readonly confirmationDialog = inject(ConfirmationDialogService);

  readonly IMPORT_STATUS_COLUMN = "_importStatus";

  @Input() rawData: any[];

  @Input() entityType: string;
  @Input() columnMapping: ColumnMapping[];
  @Input() additionalActions: AdditionalImportAction[];
  @Input() importExisting: ImportExistingSettings | undefined;
  @Input() additionalSettings: ImportAdditionalSettings;
  @Input() filename: string;

  /**
   * Indicate if the component is currently visible,
   * so that re-calculation and popups only happen then.
   */
  @Input() stepIsFocused = true;

  entityConstructor: EntityConstructor;

  @Output() importComplete = new EventEmitter<ImportMetadata>();

  readonly dataSource = new InMemoryDataSource();

  isLoading = signal(false);
  displayColumns = signal<string[]>([]);
  transformationErrorColumns = signal<string[]>([]);
  MULTIPLE_MATCHING_ENTITIES_KEY =
    ImportExistingService.MULTIPLE_MATCHING_ENTITIES_KEY;

  private static readonly DATA_INPUT_KEYS = [
    "rawData",
    "entityType",
    "columnMapping",
    "additionalActions",
    "importExisting",
    "additionalSettings",
  ];

  private dataInputsChanged = false;

  ngOnChanges(changes: SimpleChanges) {
    this.entityConstructor = this.entityRegistry.get(this.entityType);

    const dataChanged = this.hasDataInputChanges(changes);
    if (dataChanged) {
      this.dataInputsChanged = true;
    }

    if (this.dataInputsChanged && this.stepIsFocused) {
      this.parseRawData();
      this.dataInputsChanged = false;
    }
  }

  private hasDataInputChanges(changes: SimpleChanges): boolean {
    return ImportReviewDataComponent.DATA_INPUT_KEYS.some(
      (key) => key in changes,
    );
  }

  private async parseRawData() {
    if (!this.entityType || !this.columnMapping) {
      // incomplete settings, cannot proceed
      return;
    }

    this.isLoading.set(true);
    try {
      const result = await this.importService.transformRawDataToEntities(
        this.rawData,
        {
          entityType: this.entityType,
          columnMapping: this.columnMapping,
          additionalActions: this.additionalActions,
          importExisting: this.importExisting,
          additionalSettings: this.additionalSettings,
        },
      );
      this.setTransformationErrors(result.errors);

      if (result.errors.length > 0 && this.stepIsFocused) {
        await this.showTransformationErrorDialog(result.errors);
      }

      this.dataSource.allRecords.set(
        result.entities.sort((a, b) => {
          // sort _rev (existing records being updated) first, then new records
          if (a._rev === b._rev) return 0;
          if (!!a._rev) return -1;
          return 1;
        }),
      );
    } catch (e) {
      Logging.error("Failed to transform import data", e);
      this.dataSource.allRecords.set([]);
      this.transformationErrorColumns.set([]);
    }

    this.displayColumns.set([
      this.IMPORT_STATUS_COLUMN,
      ...this.columnMapping
        // remove unmapped columns:
        .filter(({ propertyName }) => !!propertyName)
        // show multi-mapped columns only once:
        .filter(
          (c) =>
            this.columnMapping.find(
              (x) => x.propertyName === c.propertyName,
            ) === c,
        )
        .map(({ propertyName }) => propertyName),
    ]);

    this.isLoading.set(false);
  }

  async startImport() {
    const confirmationResult = await lastValueFrom(
      this.matDialog
        .open<
          ImportConfirmSummaryComponent,
          ImportDialogData,
          ImportDialogResult
        >(ImportConfirmSummaryComponent, {
          data: {
            entitiesToImport: this.dataSource.allRecords(),
            importSettings: {
              entityType: this.entityType,
              columnMapping: this.columnMapping,
              additionalActions: this.additionalActions,
              importExisting: this.importExisting,
              additionalSettings: this.additionalSettings,
              filename: this.filename,
            },
          } as ImportDialogData,
        })
        .afterClosed(),
    );

    if (confirmationResult?.errorOccured) {
      // Problem during import - maybe underlying data changed. Refresh and let user retry
      await this.parseRawData();
      return;
    }

    if (confirmationResult?.completedImport) {
      this.importComplete.emit(confirmationResult.completedImport);
    }
  }

  private showTransformationErrorDialog(
    errors: ImportCellError[],
  ): Promise<boolean | string | undefined> {
    Logging.warn("Import Cell Errors", JSON.stringify(errors));

    return this.confirmationDialog.getConfirmation(
      $localize`Problems while preparing data for import`,
      $localize`${errors.length} value(s) could not be transformed and were skipped.`,
      OkButton,
    );
  }

  private setTransformationErrors(errors: ImportCellError[]): void {
    const uniqueColumns = [...new Set(errors.map((e) => e.column))];
    this.transformationErrorColumns.set(uniqueColumns);
  }
}
@if (isLoading()) {
  <div>
    <div i18n>Preparing data for preview and import ...</div>
    <mat-progress-bar mode="indeterminate"></mat-progress-bar>
  </div>
} @else {
  <div class="flex-row gap-regular align-center">
    <span class="flex-grow" i18n>Review your mapped data to be imported:</span>

    <button (click)="startImport()" mat-raised-button color="accent" i18n>
      Start Import
    </button>

    <app-help-button
      text="The data previewed here is mapped and transformed according to the 'Column Mapping' you defined in the previous step.
        Only columns for which you selected a field there will be imported and appears here in this preview.
        If necessary, you can go back to the previous step and make changes to the mapping."
      i18n-text="import - review data - help text"
    >
    </app-help-button>
  </div>

  @let skippedRows = rawData?.length - dataSource.allRecords().length;
  @if (skippedRows > 0) {
    <app-hint-box i18n>
      {{ skippedRows }} row(s) were skipped because no mappable data was found
      based on your column mapping.
    </app-hint-box>
  }

  @if (transformationErrorColumns().length > 0) {
    <app-hint-box>
      <div class="flex-row gap-small align-center warning-red">
        <span i18n>Some values could not be transformed and were skipped.</span>
        <span i18n>
          Affected column(s): {{ transformationErrorColumns().join(", ") }}
        </span>
      </div>
    </app-hint-box>
  }

  <app-entities-table
    [entityType]="entityConstructor"
    [columnsToDisplay]="displayColumns()"
    [recordsDataSource]="dataSource"
    clickMode="none"
    [editable]="false"
  >
    <ng-container [matColumnDef]="IMPORT_STATUS_COLUMN">
      <th mat-header-cell *matHeaderCellDef style="width: 0" i18n>
        Import Status
      </th>

      <td mat-cell *matCellDef="let row" [class.highlight]="row.record._rev">
        @if (row.record._rev) {
          <em
            matTooltip="We identified an existing record in the database that matches your imported data (based on the ID fields you selected in the previous step). The data of the record will be updated for the fields shown here in the preview."
            i18n-matTooltip
            i18n
          >
            Updating
          </em>
          <app-entity-block [entity]="row.record"></app-entity-block>
        } @else {
          <em
            class="status-create"
            matTooltip="This previewed data will be added as a new record in the database."
            i18n-matTooltip
            i18n
            >Creating</em
          >
          @if (row.record[MULTIPLE_MATCHING_ENTITIES_KEY]) {
            <fa-icon
              icon="warning"
              class="warning-red"
              matTooltip="Warning: Multiple existing records were found that match this imported data based on the ID fields you selected in the previous step. Therefore, a new record will be created instead of updating an existing one."
              i18n-matTooltip
            ></fa-icon>
          }
        }
      </td>
    </ng-container>
  </app-entities-table>
}

./import-review-data.component.scss

@use "variables/colors";

.status-create {
  color: green;
}

.highlight {
  background-color: colors.$warn;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""