src/app/features/matching-entities/admin-matching-entities/edit-new-match-action/edit-new-match-action.component.ts

Implements

OnDestroy

Metadata

Relationships

Depends on

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

leftEntityType
Type : string
rightEntityType
Type : string
value
Type : NewMatchAction

Outputs

value
Type : NewMatchAction

Methods

initForm
initForm()
Returns : void
onEntityTypeChange
onEntityTypeChange(newType: string | string[])

Triggered when the selected entity type changes. Updates match options and resets fields if needed.

Parameters :
Name Type Optional Description
newType string | string[] No

The newly selected entity type identifier.

Returns : void
Protected setActiveFields
setActiveFields(fields: ColumnConfig[])
Parameters :
Name Type Optional
fields ColumnConfig[] No
Returns : void

Properties

activeFields
Type : unknown
Default value : signal<ColumnConfig[]>([])
availableFields
Type : unknown
Default value : signal<ColumnConfig[]>([])
availableRelatedEntities
Type : unknown
Default value : computed(() => this.buildAvailableRelatedEntities( this.leftEntityType(), this.rightEntityType(), ), )

Entities common to both sides, each with its available left/right reference fields.

Readonly dialog
Type : unknown
Default value : inject(MatDialog)
entityConstructor
Type : unknown
Default value : signal<EntityConstructor | null>(null)

The currently selected entity constructor for generating field options.

Readonly entityRegistry
Type : unknown
Default value : inject(EntityRegistry)
Readonly entityRelationsService
Type : unknown
Default value : inject(EntityRelationsService)
Readonly fb
Type : unknown
Default value : inject(FormBuilder)
fieldsAsStrings
Type : unknown
Default value : computed(() => this.value()?.columnsToReview?.map((field) => typeof field === "string" ? field : field.id, ), )
form
Type : FormGroup
hideLeftOption
Type : unknown
Default value : () => {...}

Hide any option not part of the left-side matchPropertyLeftOptions list.

Parameters :
Name Description
option

The form field configuration to evaluate.

hideRightOption
Type : unknown
Default value : () => {...}

Hide any option not part of the right-side matchPropertyRightOptions list.

Parameters :
Name Description
option

The form field configuration to evaluate.

matchPropertyLeftOptions
Type : unknown
Default value : signal<string[]>([])

Available field IDs for left-side match property based on selected entity.

matchPropertyRightOptions
Type : unknown
Default value : signal<string[]>([])

Available field IDs for right-side match property based on selected entity.

import {
  Component,
  ChangeDetectionStrategy,
  computed,
  effect,
  inject,
  OnDestroy,
  input,
  model,
  signal,
} from "@angular/core";
import { MatFormFieldModule } from "@angular/material/form-field";
import { HelpButtonComponent } from "#src/app/core/common-components/help-button/help-button.component";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatDialog } from "@angular/material/dialog";
import {
  MatExpansionPanel,
  MatExpansionPanelHeader,
} from "@angular/material/expansion";
import { NewMatchAction } from "#src/app/features/matching-entities/matching-entities/matching-entities-config";
import { EntityConstructor } from "#src/app/core/entity/model/entity";
import {
  ColumnConfig,
  FormFieldConfig,
} from "#src/app/core/common-components/entity-form/FormConfig";
import { EntityRegistry } from "#src/app/core/entity/database-entity.decorator";
import { EntityFieldSelectComponent } from "#src/app/core/entity/entity-field-select/entity-field-select.component";
import { EntityRelationsService } from "#src/app/core/entity/entity-mapper/entity-relations.service";
import { MatOptionModule } from "@angular/material/core";
import { MatSelectModule } from "@angular/material/select";
import { EntityFieldsMenuComponent } from "#src/app/core/common-components/entity-fields-menu/entity-fields-menu.component";
import { EntityTypeSelectComponent } from "#src/app/core/entity/entity-type-select/entity-type-select.component";
import { Subject, takeUntil } from "rxjs";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-new-match-action",
  imports: [
    FontAwesomeModule,
    MatFormFieldModule,
    MatButtonModule,
    MatTooltipModule,
    HelpButtonComponent,
    ReactiveFormsModule,
    MatExpansionPanel,
    MatExpansionPanelHeader,
    EntityFieldSelectComponent,
    MatOptionModule,
    MatSelectModule,
    EntityFieldsMenuComponent,
    EntityTypeSelectComponent,
  ],
  templateUrl: "./edit-new-match-action.component.html",
  styleUrl: "./edit-new-match-action.component.scss",
})
export class EditNewMatchActionComponent implements OnDestroy {
  readonly fb = inject(FormBuilder);
  readonly entityRegistry = inject(EntityRegistry);
  readonly dialog = inject(MatDialog);
  readonly entityRelationsService = inject(EntityRelationsService);
  private readonly destroyed$ = new Subject<void>();

  value = model<NewMatchAction>();
  leftEntityType = input<string>();
  rightEntityType = input<string>();

  form: FormGroup;

  /**
   * Entities common to both sides, each with its available left/right reference fields.
   */
  availableRelatedEntities = computed(() =>
    this.buildAvailableRelatedEntities(
      this.leftEntityType(),
      this.rightEntityType(),
    ),
  );
  availableFields = signal<ColumnConfig[]>([]);

  /** Available field IDs for left-side match property based on selected entity. */
  matchPropertyLeftOptions = signal<string[]>([]);
  /** Available field IDs for right-side match property based on selected entity. */
  matchPropertyRightOptions = signal<string[]>([]);

  activeFields = signal<ColumnConfig[]>([]);

  protected setActiveFields(fields: ColumnConfig[]): void {
    this.activeFields.set(fields);
    if (this.form?.value) {
      this.value.set({
        ...this.value(),
        ...this.form.value,
        columnsToReview: fields,
      });
    }
  }

  /** The currently selected entity constructor for generating field options. */
  entityConstructor = signal<EntityConstructor | null>(null);

  constructor() {
    effect(() => {
      const value = this.value();
      // Also read entity type signals to recompute options when they change
      this.leftEntityType();
      this.rightEntityType();

      if (!value) {
        return;
      }

      if (!this.form) {
        this.initForm();
        this.form.valueChanges
          .pipe(takeUntil(this.destroyed$))
          .subscribe((formValues) => {
            this.value.set({
              ...this.value(),
              ...formValues,
              columnsToReview: this.activeFields(),
            });
          });
      } else {
        this.form.patchValue(
          {
            newEntityType: value.newEntityType ?? "",
            newEntityMatchPropertyLeft: value.newEntityMatchPropertyLeft ?? "",
            newEntityMatchPropertyRight:
              value.newEntityMatchPropertyRight ?? "",
          },
          { emitEvent: false },
        );
      }

      this.activeFields.set(value.columnsToReview ?? []);
      this.updateMatchOptions(value.newEntityType);
    });
  }

  initForm() {
    const value = this.value();
    this.form = this.fb.group({
      newEntityType: [value?.newEntityType ?? ""],
      newEntityMatchPropertyLeft: [value?.newEntityMatchPropertyLeft ?? ""],
      newEntityMatchPropertyRight: [value?.newEntityMatchPropertyRight ?? ""],
    });
  }

  /**
   * Builds the list of related entities common to both sides.
   */
  private buildAvailableRelatedEntities(leftType: string, rightType: string) {
    if (!leftType || !rightType) {
      return [];
    }
    const left = this.entityRelationsService
      .getEntityTypesReferencingType(leftType)
      .map((refType) => ({
        label: refType.entityType.label || refType.entityType.ENTITY_TYPE,
        entityType: refType.entityType.ENTITY_TYPE,
        relatedReferenceFields: refType.referencingProperties.map((p) => p.id),
      }));
    const right = this.entityRelationsService
      .getEntityTypesReferencingType(rightType)
      .map((refType) => ({
        label: refType.entityType.label || refType.entityType.ENTITY_TYPE,
        entityType: refType.entityType.ENTITY_TYPE,
        relatedReferenceFields: refType.referencingProperties.map((p) => p.id),
      }));
    return left
      .map((l) => {
        const r = right.find((r) => r.entityType === l.entityType);
        return r
          ? {
              label: l.label,
              entityType: l.entityType,
              leftReferenceFields: l.relatedReferenceFields,
              rightReferenceFields: r.relatedReferenceFields,
            }
          : null;
      })
      .filter((e): e is NonNullable<typeof e> => !!e);
  }

  /**
   * Hide any option not part of the left-side matchPropertyLeftOptions list.
   * @param option The form field configuration to evaluate.
   */
  hideLeftOption = (option: FormFieldConfig): boolean => {
    return (
      this.matchPropertyLeftOptions().length > 0 &&
      !this.matchPropertyLeftOptions().includes(option.id)
    );
  };

  /**
   * Hide any option not part of the right-side matchPropertyRightOptions list.
   * @param option The form field configuration to evaluate.
   */
  hideRightOption = (option: FormFieldConfig): boolean => {
    return (
      this.matchPropertyRightOptions().length > 0 &&
      !this.matchPropertyRightOptions().includes(option.id)
    );
  };

  /**
   * Triggered when the selected entity type changes.
   * Updates match options and resets fields if needed.
   * @param newType The newly selected entity type identifier.
   */
  onEntityTypeChange(newType: string | string[]) {
    const selectedEntityType = newType as string;
    if (selectedEntityType === this.value()?.newEntityType) {
      return;
    }
    this.updateMatchOptions(selectedEntityType, true);
    if (!this.value()) {
      return;
    }
    this.value.set({
      ...this.value(),
      newEntityType: selectedEntityType,
    });
  }

  /**
   * Update entityConstructor and match property options.
   * @param entityType The entity type to configure.
   * @param clearExisting Whether to clear existing selections.
   */
  private updateMatchOptions(
    entityType: string,
    clearExisting: boolean = false,
  ): void {
    if (!entityType) {
      this.entityConstructor.set(null);
      this.availableFields.set(Array.from(new Set(this.activeFields() ?? [])));
      this.matchPropertyLeftOptions.set([]);
      this.matchPropertyRightOptions.set([]);
      if (clearExisting) {
        this.form.patchValue({
          newEntityMatchPropertyLeft: "",
          newEntityMatchPropertyRight: "",
        });
        this.setActiveFields([]);
      }
      return;
    }

    try {
      this.entityConstructor.set(this.entityRegistry.get(entityType) ?? null);
    } catch {
      this.entityConstructor.set(null);
    }

    const targetEntitySchemaFields = Array.from(
      this.entityConstructor()?.schema.keys() ?? [],
    );
    this.availableFields.set(
      Array.from(
        new Set([...(this.activeFields() ?? []), ...targetEntitySchemaFields]),
      ),
    );
    if (clearExisting) {
      this.form.patchValue({
        newEntityMatchPropertyLeft: "",
        newEntityMatchPropertyRight: "",
      });
      this.setActiveFields([]);
    }
    const selected = this.availableRelatedEntities().find(
      (e) => e.entityType === entityType,
    );
    this.matchPropertyLeftOptions.set(selected?.leftReferenceFields ?? []);
    this.matchPropertyRightOptions.set(selected?.rightReferenceFields ?? []);
    this.applySingleOptionDefaults();
  }

  /**
   * Auto-selects matchProperty controls when only a single option exists.
   */
  private applySingleOptionDefaults(): void {
    if (
      this.matchPropertyLeftOptions().length === 1 &&
      this.form.get("newEntityMatchPropertyLeft")?.value !==
        this.matchPropertyLeftOptions()[0]
    ) {
      this.form.patchValue({
        newEntityMatchPropertyLeft: this.matchPropertyLeftOptions()[0],
      });
    }
    if (
      this.matchPropertyRightOptions().length === 1 &&
      this.form.get("newEntityMatchPropertyRight")?.value !==
        this.matchPropertyRightOptions()[0]
    ) {
      this.form.patchValue({
        newEntityMatchPropertyRight: this.matchPropertyRightOptions()[0],
      });
    }
  }

  fieldsAsStrings = computed(() =>
    this.value()?.columnsToReview?.map((field) =>
      typeof field === "string" ? field : field.id,
    ),
  );

  ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
<mat-expansion-panel class="padding-regular">
  <mat-expansion-panel-header style="height: fit-content">
    <h2 i18n>Match Settings</h2>
  </mat-expansion-panel-header>

  <div class="flex-column gap-regular">
    <div
      class="flex-row align-baseline gap-regular margin-right-large full-width"
    >
      <p i18n>
        Configure the details of the record that is created as a match. (e.g. A
        "school enrollment" record that is created upon matching a student with
        a school).
      </p>
    </div>

    <form [formGroup]="form">
      <div class="flex-row">
        <mat-form-field class="full-width">
          <mat-label i18n>Record created as a match:</mat-label>
          <app-entity-type-select
            formControlName="newEntityType"
            (valueChange)="onEntityTypeChange($event)"
          ></app-entity-type-select>
          <mat-hint i18n
            >Only types that have a reference field to both selected record
            types above are available here.</mat-hint
          >
        </mat-form-field>
      </div>

      <div class="flex-row gap-regular margin-top-regular">
        <mat-form-field class="full-width">
          <mat-label i18n>Left Match Property</mat-label>
          <app-entity-field-select
            [entityType]="entityConstructor()"
            formControlName="newEntityMatchPropertyLeft"
            [hideOption]="hideLeftOption"
          ></app-entity-field-select>
        </mat-form-field>
        <app-help-button
          i18n-text
          text="Select the field from the record that will be used as the left-side key in matching comparison."
        ></app-help-button>

        <mat-form-field class="full-width">
          <mat-label i18n>Right Match Property</mat-label>
          <app-entity-field-select
            [entityType]="entityConstructor()"
            formControlName="newEntityMatchPropertyRight"
            [hideOption]="hideRightOption"
          ></app-entity-field-select>
        </mat-form-field>
        <app-help-button
          i18n-text
          text="Select the field from the record that will be used as the right-side key in matching comparison."
        ></app-help-button>
      </div>

      <div class="flex-column">
        <div i18n>
          Fields shown in dialog for confirmation or editing when creating a
          match:
        </div>
        @if (!(activeFields()?.length > 0)) {
          <div class="color-error" i18n>
            You should select at least one field to be shown in the confirmation
            dialog.
          </div>
        }
        <app-entity-fields-menu
          [entityType]="entityConstructor()"
          [availableFields]="availableFields()"
          [activeFields]="fieldsAsStrings()"
          (activeFieldsChange)="setActiveFields($event)"
          class="full-with-field"
        ></app-entity-fields-menu>
      </div>
    </form>
  </div>
</mat-expansion-panel>

./edit-new-match-action.component.scss

.full-width-field {
  width: 100%;
  display: inline-block;

  ::ng-deep mat-form-field {
    width: 100% !important;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""