src/app/features/de-duplication/bulk-merge-records/merge-fields/merge-fields.component.ts

Metadata

Relationships

Depends on

Index

Properties
Methods
Inputs

Constructor

constructor()

Inputs

control
Type : AbstractControl
Required :  true
entities
Type : Entity[]
Required :  true
field
Type : FormFieldConfig
Required :  true

Methods

selectExistingValue
selectExistingValue(entityIndex: number, checked?: boolean)
Parameters :
Name Type Optional
entityIndex number No
checked boolean Yes
Returns : void

Properties

allowsMultiValue
Type : unknown
Default value : computed(() => this.allowsMultiValueMerge())
existingSelected
Type : unknown
Default value : computed(() => this.entities().map((entity, i) => this.isDisabled()[i] ? false : this.isValueSelected(this.controlValue(), entity[this.field().id]), ), )
isDisabled
Type : unknown
Default value : computed(() => this.entities().map((entity) => !this.hasValue(entity[this.field().id])), )
import { EntityFieldViewComponent } from "#src/app/core/entity/entity-field-view/entity-field-view.component";
import {
  Component,
  input,
  computed,
  effect,
  untracked,
  ChangeDetectionStrategy,
} from "@angular/core";
import { toSignal, toObservable } from "@angular/core/rxjs-interop";
import { switchMap, startWith } from "rxjs";
import { AbstractControl } from "@angular/forms";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatRadioModule } from "@angular/material/radio";
import { FormFieldConfig } from "app/core/common-components/entity-form/FormConfig";
import { Entity } from "app/core/entity/model/entity";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-merge-fields",
  imports: [MatCheckboxModule, MatRadioModule, EntityFieldViewComponent],
  templateUrl: "./merge-fields.component.html",
  styleUrls: ["./merge-fields.component.scss"],
})
export class MergeFieldsComponent {
  field = input.required<FormFieldConfig>();
  entities = input.required<Entity[]>();
  control = input.required<AbstractControl>();

  allowsMultiValue = computed(() => this.allowsMultiValueMerge());

  isDisabled = computed(() =>
    this.entities().map((entity) => !this.hasValue(entity[this.field().id])),
  );

  private readonly controlValue = toSignal(
    toObservable(this.control).pipe(
      switchMap((ctrl) => ctrl.valueChanges.pipe(startWith(ctrl.value))),
    ),
  );

  existingSelected = computed(() =>
    this.entities().map((entity, i) =>
      this.isDisabled()[i]
        ? false
        : this.isValueSelected(this.controlValue(), entity[this.field().id]),
    ),
  );

  constructor() {
    effect(() => {
      const entities = this.entities();
      const field = this.field();
      const ctrl = this.control();
      if (!entities?.length) return;
      const [valueA, valueB] = entities.map((e) => e[field.id]);
      const smartValue = this.setSmartSelectedValue(valueA, valueB, ctrl.value);
      untracked(() => ctrl.setValue(smartValue));
    });
  }

  /**
   * Determines the best value to use when merging two entity field values.
   * If both values are identical, it returns one of them.
   * If one value is empty while the other is not, it returns the non-empty value.
   * Otherwise, it retains the current form value.
   */
  private setSmartSelectedValue(
    valueA: any,
    valueB: any,
    currentValue: any,
  ): any {
    if (this.areValuesIdentical(valueA, valueB)) {
      return valueA;
    } else if (!this.hasValue(valueA) && this.hasValue(valueB)) {
      return valueB;
    } else if (!this.hasValue(valueB) && this.hasValue(valueA)) {
      return valueA;
    } else if (Array.isArray(valueA) || Array.isArray(valueB)) {
      return [
        ...(Array.isArray(valueA) ? valueA : []),
        ...(Array.isArray(valueB) ? valueB : []),
      ];
    }
    return currentValue;
  }

  private isValueSelected(currentValue: any, entityValue: any): boolean {
    if (this.field().isArray) {
      return entityValue?.every((e: any) =>
        currentValue?.some((c: any) => JSON.stringify(c) === JSON.stringify(e)),
      );
    } else if (this.allowsMultiValue()) {
      return (currentValue ?? "").includes(entityValue);
    } else {
      return JSON.stringify(currentValue) === JSON.stringify(entityValue);
    }
  }

  private areValuesIdentical(a: any, b: any): boolean {
    return JSON.stringify(a) === JSON.stringify(b);
  }

  private hasValue(value: any): boolean {
    return !!value || (Array.isArray(value) && value.length > 0);
  }

  private allowsMultiValueMerge(): boolean {
    return (
      this.field()?.dataType === "string" ||
      this.field()?.dataType === "long-text" ||
      this.field()?.isArray
    );
  }

  /*
   * Selects the value of the entity at the given index.
   * If the field is an array, the selected value is added to the existing value.
   * If the field allows multi-value merge, the selected value is appended to the existing value.
   */
  selectExistingValue(entityIndex: number, checked?: boolean): void {
    const selectedValue = this.entities()[entityIndex][this.field().id];
    let newValue = selectedValue;
    if (this.field().isArray) {
      newValue = this.getMergedArrayValue(
        this.control().value,
        selectedValue,
        checked,
      );
    } else if (this.allowsMultiValue()) {
      newValue = this.getMergedStringValue(
        this.control().value,
        selectedValue,
        checked,
      );
    }

    this.control().setValue(newValue);
  }

  private getMergedArrayValue(
    value: any[],
    selectedValue: any[],
    checked: boolean,
  ): any[] {
    value = value ?? [];
    if (checked) {
      value = value.concat(selectedValue);
    } else {
      value = value.filter((v) => !selectedValue.includes(v));
    }
    return value;
  }

  private getMergedStringValue(
    value: string,
    selectedValue: string,
    checked: boolean,
  ): string {
    if (checked) {
      value = (value?.length > 0 ? value + ", " : "") + selectedValue;
    } else {
      value = value
        .split(",")
        .map((v) => v.trim())
        .filter((v) => v !== selectedValue)
        .join(", ");
    }
    return value;
  }
}
@for (entity of entities(); track entity.getId(); let i = $index) {
  @if (allowsMultiValue()) {
    <mat-checkbox
      [checked]="existingSelected()[i]"
      (change)="selectExistingValue(i, $event.checked)"
      [disabled]="isDisabled()[i]"
    >
      <app-entity-field-view [field]="field()" [entity]="entity">
      </app-entity-field-view>
    </mat-checkbox>
  } @else {
    <mat-radio-group>
      <mat-radio-button
        [value]="entity[field().id]"
        (click)="selectExistingValue(i)"
        [checked]="existingSelected()[i]"
        [disabled]="isDisabled()[i]"
      >
        <app-entity-field-view [field]="field()" [entity]="entity">
        </app-entity-field-view>
      </mat-radio-button>
    </mat-radio-group>
  }
}

./merge-fields.component.scss

:host {
  min-width: 0;
}

:host ::ng-deep .mdc-form-field,
:host ::ng-deep .mdc-label,
:host ::ng-deep app-entity-field-view {
  min-width: 0;
}

:host ::ng-deep .bubble-list {
  flex-wrap: wrap;
  gap: 0.25rem;
  max-width: 100%;
}

:host ::ng-deep .colored-bubble {
  margin-right: 0;
  max-width: 100%;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""