src/app/features/inherited-field/automated-field-update/automated-field-mapping/automated-field-mapping.component.ts

Description

Dialog to configure additional details for the "inherited-field" default value strategy, working in combination with the admin components.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Constructor

constructor()

Methods

save
save()
Returns : void

Properties

addToExisting
Type : unknown
Default value : signal(false)

If the user enabled adding new values to the existing values of this field instead of replacing them

Readonly isAddToExistingAvailable
Type : boolean

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.

isInvalidMapping
Type : boolean
Default value : false
mappingEnabled
Type : unknown
Default value : signal(false)

If the user explicitly enabled the optional value mapping functionality

selectedSourceValueField
Type : WritableSignal<string | null>

The currently selected "sourceValueField" on the related entity

sourceValueEntityType
Type : EntityConstructor

The entity type of the related entity that triggers the updates

sourceValueFieldSchema
Type : Signal<EntitySchemaField | undefined>
Default value : computed( () => { const fieldId = this.selectedSourceValueField(); if (!fieldId) return; return this.sourceValueEntityType.schema.get(fieldId); }, )
targetFieldConfig
Type : FormFieldConfig

The full schema of the field for which this default value is configured

value
Type : DefaultValueConfigInheritedField
valueMappingOptions
Type : Signal<literal type[]>
Default value : 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>, }; }); })

The possible values of the selected sourceValueField that can be mapped to custom target values. Currently mapping only supported for ConfigurableEnum fields.

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>

./automated-field-mapping.component.scss

.mapping-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: center;
  margin-top: 20px;

  .header-row {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 12px;
  }

  .option-label {
    font-weight: 500;
    padding: 8px 0;
  }
}

:host ::ng-deep .mat-mdc-form-field {
  width: 100%;
}

.mapping-header {
  font-weight: bold;
}

.no-options-message {
  font-style: italic;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""