src/app/core/import/import-review-data/import-review-data.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
||
| selector | app-import-review-data |
||
| standalone | true |
||
| imports |
MatButtonModule
MatProgressBar
MatColumnDef
MatCell
MatCellDef
MatHeaderCell
MatHeaderCellDef
MatTooltip
|
||
| styleUrls | ./import-review-data.component.scss |
||
| templateVariables |
|
||
| templateUrl | ./import-review-data.component.html |
MatButtonModule
HelpButtonComponent
EntitiesTableComponent
MatProgressBar
MatColumnDef
MatCell
MatCellDef
MatHeaderCell
MatHeaderCellDef
MatTooltip
EntityBlockComponent
HintBoxComponent
FaIconComponent
OnChanges
Properties |
Methods |
|
Inputs |
Outputs |
| 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. |
|
| importComplete | |
Type : EventEmitter
|
|
| Async startImport |
startImport()
|
|
Returns :
any
|
| 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;
}