import { EntityForm } from "#src/app/core/common-components/entity-form/entity-form";
import { EntityFieldEditComponent } from "#src/app/core/entity/entity-field-edit/entity-field-edit.component";
import { EntityFieldLabelComponent } from "#src/app/core/entity/entity-field-label/entity-field-label.component";
import {
Component,
computed,
inject,
Signal,
signal,
WritableSignal,
ChangeDetectionStrategy,
} from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatOptionModule } from "@angular/material/core";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatTooltipModule } from "@angular/material/tooltip";
import { ConfigurableEnumService } from "#src/app/core/basic-datatypes/configurable-enum/configurable-enum.service";
import { ConfigurableEnumValue } from "#src/app/core/basic-datatypes/configurable-enum/configurable-enum.types";
import { Entity, EntityConstructor } from "#src/app/core/entity/model/entity";
import { EntitySchemaService } from "#src/app/core/entity/schema/entity-schema.service";
import { DialogCloseComponent } from "../../../../core/common-components/dialog-close/dialog-close.component";
import { DefaultValueConfigInheritedField } from "../../inherited-field-config";
import { FormFieldConfig } from "#src/app/core/common-components/entity-form/FormConfig";
import { EntityFieldSelectComponent } from "#src/app/core/entity/entity-field-select/entity-field-select.component";
import { EntitySchemaField } from "#src/app/core/entity/schema/entity-schema-field";
import { MatSlideToggle } from "@angular/material/slide-toggle";
import { ConfigurableEnumDatatype } from "#src/app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype";
/**
* Dialog to configure additional details for the "inherited-field"
* default value strategy, working in combination with the admin components.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-automated-field-mapping",
imports: [
MatOptionModule,
MatFormFieldModule,
MatDialogModule,
MatButtonModule,
MatTooltipModule,
EntityFieldEditComponent,
EntityFieldLabelComponent,
DialogCloseComponent,
EntityFieldSelectComponent,
MatSlideToggle,
],
templateUrl: "./automated-field-mapping.component.html",
styleUrl: "./automated-field-mapping.component.scss",
})
export class AutomatedFieldMappingComponent {
private readonly dialogRef = inject<MatDialogRef<any>>(MatDialogRef);
private readonly configurableEnumService = inject(ConfigurableEnumService);
private readonly schemaService = inject(EntitySchemaService);
/** The full schema of the field for which this default value is configured */
targetFieldConfig: FormFieldConfig;
/** The entity type of the related entity that triggers the updates */
sourceValueEntityType: EntityConstructor;
value: DefaultValueConfigInheritedField;
/** The currently selected "sourceValueField" on the related entity */
selectedSourceValueField: WritableSignal<string | null>;
sourceValueFieldSchema: Signal<EntitySchemaField | undefined> = computed(
() => {
const fieldId = this.selectedSourceValueField();
if (!fieldId) return;
return this.sourceValueEntityType.schema.get(fieldId);
},
);
/**
* The possible values of the selected sourceValueField that can be mapped to custom target values.
* Currently mapping only supported for ConfigurableEnum fields.
*/
valueMappingOptions: Signal<
{
sourceValue: ConfigurableEnumValue;
sourceValueRaw: string;
form: EntityForm<Entity>;
}[]
> = computed(() => {
if (
this.sourceValueFieldSchema()?.dataType !==
ConfigurableEnumDatatype.dataType
) {
// only configurable-enum fields supported currently
return [];
}
// labels resolved for display; the option ids stay untouched below
const values = this.configurableEnumService.getEnumValues(
this.sourceValueFieldSchema().additional,
);
return values.map((sourceValue) => {
const sourceValueRaw = sourceValue.id; // database format of the source value
// select existing mapping value if available
let selectedMappedValue: any = this.value.valueMapping?.[sourceValueRaw];
if (selectedMappedValue) {
selectedMappedValue = this.schemaService.valueToEntityFormat(
selectedMappedValue,
this.targetFieldConfig,
);
}
const formControl = new FormControl(selectedMappedValue);
const formGroup = new FormGroup({
[this.targetFieldConfig.id]: formControl,
});
return {
sourceValue,
sourceValueRaw,
form: {
formGroup,
} as unknown as EntityForm<Entity>,
};
});
});
/**
* If the user explicitly enabled the optional value mapping functionality
*/
mappingEnabled = signal(false);
/**
* Only offered for fields allowing multiple values and for rules where the related records link to this record,
* because the inherit button for a single linked parent always replaces the value.
*/
readonly isAddToExistingAvailable: boolean;
/**
* If the user enabled adding new values to the existing values of this field instead of replacing them
*/
addToExisting = signal(false);
isInvalidMapping: boolean = false;
constructor() {
const data = inject<AutomatedFieldMappingDialogData>(MAT_DIALOG_DATA);
this.targetFieldConfig = {
...data.currentField,
id: "targetValue", // simulate a faked full entity-form to use as basis for value mapping edit components
} as FormFieldConfig;
this.value = data.value;
this.sourceValueEntityType = data.sourceValueEntityType;
this.selectedSourceValueField = signal(
this.value?.sourceValueField ?? null,
);
if (this.value?.valueMapping) {
this.mappingEnabled.set(true);
}
this.isAddToExistingAvailable =
!!this.value?.sourceReferenceEntity && !!data.currentField?.isArray;
this.addToExisting.set(this.value?.aggregation === "add");
}
save() {
const selectedMappings = {};
if (this.mappingEnabled()) {
this.isInvalidMapping = this.valueMappingOptions().some((v) => {
v.form.formGroup.markAllAsTouched();
return v.form.formGroup.invalid;
});
if (this.isInvalidMapping) return;
this.valueMappingOptions().forEach(({ sourceValueRaw, form }) => {
const value = form.formGroup.get(this.targetFieldConfig.id)?.value;
selectedMappings[sourceValueRaw] =
this.schemaService.valueToDatabaseFormat(
value,
this.targetFieldConfig,
);
});
}
const newValue: DefaultValueConfigInheritedField = {
...this.value,
sourceValueField: this.selectedSourceValueField(),
valueMapping: selectedMappings,
};
if (Object.keys(selectedMappings).length === 0) {
// do not store empty mappings and delete any potentially existing mappings
delete newValue.valueMapping;
}
if (this.isAddToExistingAvailable && this.addToExisting()) {
newValue.aggregation = "add";
} else {
delete newValue.aggregation;
}
this.dialogRef.close(newValue);
}
}
/**
* The DialogData for the `AutomatedFieldMappingComponent`
*/
export interface AutomatedFieldMappingDialogData {
currentEntityType: EntityConstructor;
currentField: EntitySchemaField;
sourceValueEntityType: EntityConstructor;
value: DefaultValueConfigInheritedField;
}
<h2 mat-dialog-title i18n>Configure Automation Rule</h2>
<app-dialog-close mat-dialog-close></app-dialog-close>
<mat-dialog-content>
<p i18n>
You are linking this field to the value of a related
{{ sourceValueEntityType.label }} record. The system will show a dialog to
automatically update this field whenever the related record's field changes.
You can still manually adjust the value if needed. This helps you ensure
data consistency and reduces manual work.
</p>
<p>
(<span i18n>Field defining the related source record:</span>
<app-entity-field-label
[field]="value?.sourceReferenceField"
[entityType]="sourceValueEntityType"
></app-entity-field-label
>)
</p>
<mat-form-field
appearance="fill"
matTooltip="Choose the field in the related record that will trigger updates to this field when it is changed."
i18n-matTooltip
>
<mat-label i18n
>Source value field (of {{ sourceValueEntityType.label }})
</mat-label>
<app-entity-field-select
[entityType]="sourceValueEntityType"
[value]="selectedSourceValueField()"
(valueChange)="
selectedSourceValueField.set(
typeof $event === 'object' ? $event[0] : $event
)
"
></app-entity-field-select>
</mat-form-field>
@if (isAddToExistingAvailable) {
<div class="margin-bottom-regular">
<mat-slide-toggle
matTooltip="Keep the existing values of this field and add the new values of the related records. Values removed from a related record are suggested for removal unless another related record still has them."
i18n-matTooltip
[checked]="addToExisting()"
(change)="addToExisting.set($event.checked)"
i18n
>
Add to existing values instead of replacing
</mat-slide-toggle>
</div>
} @else if (targetFieldConfig.isArray) {
<p class="hint-text margin-bottom-regular" i18n>
Adding to the existing values is only possible if the related records link
to this record (option "Entity > field"), not if this record links to
them.
</p>
} @else {
<p class="hint-text margin-bottom-regular" i18n>
Adding to the existing values is only possible for fields that allow
multiple values.
</p>
}
<!--
Detailed trigger value -> target value mapping
-->
<mat-slide-toggle
[disabled]="valueMappingOptions().length === 0"
matTooltip="If the source is a dropdown field, you can optionally also define a custom mapping for specific values."
i18n-matTooltip
[checked]="mappingEnabled()"
(change)="mappingEnabled.set($event.checked)"
i18n
>
Transform value with custom mapping
</mat-slide-toggle>
@if (valueMappingOptions().length > 0 && mappingEnabled()) {
<div class="mapping-grid">
<div class="header-row">
<h3 class="mapping-header" i18n>When "trigger field" changes to</h3>
<h3 class="mapping-header" i18n>then this field is updated to</h3>
</div>
@for (v of valueMappingOptions(); track v.sourceValue.id) {
<div class="option-label">{{ v.sourceValue.label }}</div>
<div class="dropdown-select">
@if (v.form) {
<app-entity-field-edit
[field]="targetFieldConfig"
[form]="v.form"
></app-entity-field-edit>
}
</div>
}
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="flex-row flex-wrap">
<button
mat-button
(click)="save()"
[disabled]="!selectedSourceValueField()"
i18n
>
Save
</button>
<button mat-button mat-dialog-close i18n>Cancel</button>
@if (isInvalidMapping) {
<mat-error i18n>
The data is invalid, please check the fields in the "Configure mapping"
</mat-error>
}
</mat-dialog-actions>