src/app/features/inherited-field/automated-field-update/affected-entity-value/affected-entity-value.component.ts

Description

The editable value of one affected entity in the "Update related records" dialog, with its hints.

It owns the form of the value, so that what the user entered stays while the dialog calculates the suggestions again and passes in new rows.

Implements

OnInit

Example

Metadata

Relationships

Index

Properties
Inputs
Outputs

Constructor

constructor()

Inputs

row
Type : DialogRow
Required :  true

Outputs

useSuggestion
Type : void

The user wants the suggested value instead of the entered one

valueChange
Type : unknown

The value the user entered (database format)

Properties

Readonly field
Type : unknown
Default value : computed(() => this.entityFormService.extendFormFieldConfig( this.fieldId(), this.entityType(), ), )

Only created again for another field, not for every new row: a new config rebuilds the edit component, which would e.g. close the dropdown in which the user is selecting values.

Readonly form
Type : unknown
Default value : signal<EntityForm<Entity> | undefined>(undefined)
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  DestroyRef,
  effect,
  inject,
  input,
  OnInit,
  output,
  signal,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { MatButton } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";
import { EntityForm } from "#src/app/core/common-components/entity-form/entity-form";
import { EntityFormService } from "#src/app/core/common-components/entity-form/entity-form.service";
import { FaDynamicIconComponent } from "#src/app/core/common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { EntityFieldEditComponent } from "#src/app/core/entity/entity-field-edit/entity-field-edit.component";
import { EntityFieldViewComponent } from "#src/app/core/entity/entity-field-view/entity-field-view.component";
import { Entity } from "#src/app/core/entity/model/entity";
import { EntitySchemaService } from "#src/app/core/entity/schema/entity-schema.service";
import { Logging } from "#src/app/core/logging/logging.service";
import { isSameValue } from "../../get-common-value";
import { NoCommonValueHintComponent } from "../../no-common-value-hint/no-common-value-hint.component";
import { DialogRow } from "../automated-field-update.types";

/**
 * The editable value of one affected entity in the "Update related records" dialog, with its hints.
 *
 * It owns the form of the value, so that what the user entered stays while the dialog calculates
 * the suggestions again and passes in new rows.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-affected-entity-value",
  imports: [
    EntityFieldEditComponent,
    EntityFieldViewComponent,
    FaDynamicIconComponent,
    HintBoxComponent,
    MatButton,
    MatTooltipModule,
    NoCommonValueHintComponent,
  ],
  templateUrl: "./affected-entity-value.component.html",
  styleUrl: "./affected-entity-value.component.scss",
})
export class AffectedEntityValueComponent implements OnInit {
  private readonly entityFormService = inject(EntityFormService);
  private readonly entitySchemaService = inject(EntitySchemaService);
  private readonly destroyRef = inject(DestroyRef);

  readonly row = input.required<DialogRow>();

  /** The value the user entered (database format) */
  readonly valueChange = output<unknown>();

  /** The user wants the suggested value instead of the entered one */
  readonly useSuggestion = output<void>();

  readonly form = signal<EntityForm<Entity> | undefined>(undefined);

  private readonly fieldId = computed(() => this.row().targetFieldId);
  private readonly entityType = computed(() => this.row().targetEntityType);

  /**
   * Only created again for another field, not for every new row: a new config rebuilds the edit component,
   * which would e.g. close the dropdown in which the user is selecting values.
   */
  readonly field = computed(() =>
    this.entityFormService.extendFormFieldConfig(
      this.fieldId(),
      this.entityType(),
    ),
  );

  constructor() {
    // show a new suggestion (or the suggestion the user goes back to), which is not a change of the user
    effect(() => {
      const row = this.row();
      const control = this.form()?.formGroup.controls[row.targetFieldId];
      if (
        control &&
        !isSameValue(this.toDatabaseFormat(control.value), row.value)
      ) {
        control.setValue(this.toEntityFormat(row.value), { emitEvent: false });
      }
    });
  }

  ngOnInit(): void {
    this.createForm().catch((error) =>
      Logging.error(
        "AffectedEntityValueComponent could not create the form of the value",
        error,
      ),
    );
  }

  private async createForm(): Promise<void> {
    const row = this.row();
    // without default value handling: for existing records it only loads linked records
    // for the inherit button, which is not shown here (one query per row)
    const form = await this.entityFormService.createEntityForm(
      [row.targetFieldId],
      row.affectedEntity,
      this.destroyRef,
      false,
      true,
      false,
    );
    if (this.destroyRef.destroyed) return;

    const control = form.formGroup.controls[row.targetFieldId];
    control.setValue(this.toEntityFormat(this.row().value));
    control.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((value) =>
        this.valueChange.emit(this.toDatabaseFormat(value)),
      );
    this.form.set(form);
  }

  private toEntityFormat(value: unknown) {
    return this.entitySchemaService.valueToEntityFormat(value, this.field());
  }

  private toDatabaseFormat(value: unknown) {
    return this.entitySchemaService.valueToDatabaseFormat(
      value,
      this.field(),
      this.row().affectedEntity,
    );
  }
}
@if (form(); as form) {
  <app-entity-field-edit
    [field]="field()"
    [entity]="form.entity"
    [form]="form"
    [hideInheritButton]="true"
    [highlightAdded]="row().addToExisting"
  ></app-entity-field-edit>
}
@if (row().removedEntity; as removedEntity) {
  <div class="hint-text removed-values">
    <app-fa-dynamic-icon
      icon="minus"
      i18n-matTooltip="tooltip for values that are removed"
      matTooltip="Removed"
    ></app-fa-dynamic-icon>
    <span
      class="visually-hidden"
      i18n="screen reader text for values that are removed"
      >Removed:</span
    >
    <app-entity-field-view
      [field]="field()"
      [entity]="removedEntity"
    ></app-entity-field-view>
  </div>
}
@if (row().noCommonValue; as reason) {
  <app-hint-box type="warning">
    <app-no-common-value-hint [reason]="reason" />
  </app-hint-box>
}
@if (row().unlinked) {
  <div class="hint-text" i18n>No longer linked</div>
}
@if (row().suggestionChanged) {
  <div class="hint-text suggestion-changed">
    <span i18n>The suggestion changed, your value is kept.</span>
    <button type="button" mat-button (click)="useSuggestion.emit()" i18n>
      Use suggestion
    </button>
  </div>
}

./affected-entity-value.component.scss

@use "variables/colors";
@use "variables/sizes";

:host {
  display: block;
}

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

// keep a hint next to the field it explains (cancels the global bottom margin of mat-form-field)
app-entity-field-edit + .hint-text {
  margin-top: calc(#{sizes.$small} - 22px);
}

.hint-text {
  margin-bottom: sizes.$regular;
}

// values that the suggestion removes, as counterpart of the highlight of the added ones
.removed-values {
  display: flex;
  align-items: center;
  gap: sizes.$small;
  color: colors.$error;

  app-entity-field-view {
    text-decoration: line-through;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""