Description
UI to configure import value mappings for discrete datatypes like boolean or enum.
Implements
Example
Methods
|
onSplittingToggle
|
onSplittingToggle()
|
|
|
Rebuild form when user toggles splitting option.
Attempts to preserve existing mappings where possible.
|
|
data
|
Type : unknown
|
Default value : inject<MappingDialogData>(MAT_DIALOG_DATA)
|
|
|
import {
Component,
OnInit,
inject,
ChangeDetectionStrategy,
} from "@angular/core";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import {
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
FormsModule,
} from "@angular/forms";
import { MatFormFieldModule } from "@angular/material/form-field";
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
import { EntitySchemaService } from "../../../entity/schema/entity-schema.service";
import { MappingDialogData } from "app/core/import/import-column-mapping/mapping-dialog-data";
import { splitArrayValue } from "app/core/import/split-array-value";
import { EntitySchemaField } from "../../../entity/schema/entity-schema-field";
import { KeyValuePipe } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
import { ConfigurableEnumService } from "../../configurable-enum/configurable-enum.service";
import { DynamicEditComponent } from "../../../entity/entity-field-edit/dynamic-edit/dynamic-edit.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { HelpButtonComponent } from "../../../common-components/help-button/help-button.component";
import { DiscreteColumnMappingAdditional } from "../discrete.datatype";
/**
* UI to configure import value mappings for discrete datatypes like boolean or enum.
*/
@DynamicComponent("DiscreteImportDialog")
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-discrete-import-dialog",
templateUrl: "./discrete-import-dialog.component.html",
styleUrls: ["./discrete-import-dialog.component.scss"],
imports: [
MatDialogModule,
MatFormFieldModule,
KeyValuePipe,
MatButtonModule,
ReactiveFormsModule,
DynamicEditComponent,
HintBoxComponent,
MatCheckboxModule,
FormsModule,
HelpButtonComponent,
],
})
export class DiscreteImportDialogComponent implements OnInit {
data = inject<MappingDialogData>(MAT_DIALOG_DATA);
private readonly fb = inject(FormBuilder);
private readonly dialog = inject<MatDialogRef<any>>(MatDialogRef);
private readonly confirmation = inject(ConfirmationDialogService);
private readonly schemaService = inject(EntitySchemaService);
private readonly configurableEnumService = inject(ConfigurableEnumService);
form: FormGroup;
component: string;
schema: EntitySchemaField;
enableSplitting: boolean;
separator: string;
ngOnInit() {
this.schema = this.data.entityType.schema.get(this.data.col.propertyName);
this.component = this.schemaService.getComponent(this.schema, "edit");
this.separator = this.data.additionalSettings?.multiValueSeparator ?? ",";
const discreteAdditional = this.data.col
.additional as DiscreteColumnMappingAdditional;
// For array fields: default to splitting (but can be disabled)
// For single-select: never split
if (this.schema?.isArray) {
this.enableSplitting = discreteAdditional?.enableSplitting ?? true;
} else {
this.enableSplitting = false;
}
this.buildForm();
}
/**
* Rebuild form when user toggles splitting option.
* Attempts to preserve existing mappings where possible.
*/
onSplittingToggle() {
// Save current mappings before rebuilding
const currentMappings = this.getValuesInDatabaseFormat(
this.form.getRawValue(),
);
this.buildForm();
// Try to restore mappings that still match
const newFormValue = {};
for (const key in this.form.controls) {
if (currentMappings[key] !== undefined) {
newFormValue[key] = this.schemaService.valueToEntityFormat(
currentMappings[key],
this.schema,
);
}
}
if (Object.keys(newFormValue).length > 0) {
this.form.patchValue(newFormValue);
}
}
/**
* Build the form with value mappings
*/
private buildForm() {
const discreteAdditional = this.data.col
.additional as DiscreteColumnMappingAdditional;
const splitValues = this.splitAndFlattenValues(this.data.values);
this.form = this.fb.group(
this.getFormValues(discreteAdditional?.values, splitValues),
);
}
/**
* Split raw values using the configured separator and return unique individual values.
*/
private splitAndFlattenValues(values: any[]): string[] {
const uniqueValues = new Set<string>();
for (const value of values) {
if (value == null || value === "") {
continue;
}
// Split values only if user enabled splitting for this column
const parts: string[] = this.enableSplitting
? splitArrayValue(value, this.separator)
: [String(value)];
parts.forEach((part) => uniqueValues.add(part));
}
return [...uniqueValues];
}
private getFormValues(additional: any, values: string[]) {
additional = additional || {};
let enumOptions = [];
if (this.schema?.additional) {
const enumEntity = this.configurableEnumService.getEnum(
this.schema.additional,
);
enumOptions = enumEntity?.values ?? [];
}
const formObj = {};
for (const value of values) {
let initialValue: string;
if (value in additional) {
initialValue = additional[value];
} else {
const matchedEnumOption = enumOptions.find(
(opt) => opt.id === value || opt.label === value,
);
initialValue = matchedEnumOption?.id ?? value;
}
formObj[value] = new FormControl(
this.schemaService.valueToEntityFormat(initialValue, this.schema),
);
}
return formObj;
}
async save() {
const rawValues = this.getValuesInDatabaseFormat(this.form.getRawValue());
const allFilled = Object.values(rawValues).every((val) => val !== null);
const confirmed =
allFilled ||
(await this.confirmation.getConfirmation(
$localize`Ignore values?`,
$localize`Some values don't have a mapping and will not be imported. Are you sure you want to keep it like this?`,
));
if (confirmed) {
// Save value mappings and splitting setting in 'additional'
const discreteAdditional: DiscreteColumnMappingAdditional = {
values: rawValues,
};
if (this.schema?.isArray) {
discreteAdditional.enableSplitting = this.enableSplitting;
}
this.data.col.additional = discreteAdditional;
this.dialog.close();
}
}
/**
* Transform object property values into their database format values to be stored.
* @private
*/
private getValuesInDatabaseFormat(rawValues: any) {
for (const k in rawValues) {
const value = this.schemaService.valueToDatabaseFormat(
rawValues[k],
this.schema,
);
// mark a value the user did not assign explicitly as null, because an empty value is
// dropped whenever the mapping is serialized (e.g. stored with the import history),
// which loses the information that this value should not be imported at all
rawValues[k] = this.isAssigned(value) ? value : null;
}
return rawValues;
}
/**
* Whether the given value is a target value the user picked,
* which includes deliberately empty values like `false` for an unchecked checkbox.
*/
private isAssigned(value: any): boolean {
if (Array.isArray(value)) {
return value.some((v) => this.isAssigned(v));
}
return value !== undefined && value !== null;
}
}
<mat-dialog-content>
<app-hint-box i18n
>For each unique value from your imported data (left), select the category
value from the system's data structure into which it will be transformed.
The values you do not assign here will be ignored (remain empty) during
import.</app-hint-box
>
@if (schema?.isArray) {
<div style="margin-bottom: 16px">
<mat-checkbox
[(ngModel)]="enableSplitting"
(change)="onSplittingToggle()"
>
<span i18n>Split values at separator ({{ separator }})</span>
</mat-checkbox>
<app-help-button
text="Enable this to split values containing the separator into multiple entries. Disable for values that contain the separator as part of the text (e.g., 'media (article, ad, tv)')."
i18n-text="import discrete config split toggle help"
>
</app-help-button>
</div>
}
<table class="mapping-table">
<tr>
<th i18n="Import mapping header" class="first-column">Imported values</th>
<th i18n="Import mapping header">Assigned values</th>
</tr>
@for (ctrl of form.controls | keyvalue; track ctrl.key) {
<tr>
<td class="first-column">{{ ctrl.key }}:</td>
<td>
<mat-form-field>
<app-dynamic-edit
[formControl]="$any(ctrl.value)"
[formFieldConfig]="{
id: ctrl.key,
editComponent: component,
dataType: schema.dataType,
additional: schema.additional,
label: ctrl.key,
}"
></app-dynamic-edit>
</mat-form-field>
</td>
</tr>
}
</table>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button color="accent" (click)="save()" i18n>
Save & Close
</button>
<button mat-stroked-button matDialogClose i18n>Cancel</button>
</mat-dialog-actions>
.mapping-table {
width: 100%;
}
.mapping-table th {
text-align: left;
}
.first-column {
width: 40%;
max-width: 400px;
padding-right: 20px;
}
:host ::ng-deep mat-form-field {
width: 100%;
}
Legend
Html element with directive