src/app/features/inherited-field/automated-field-update/affected-entity-value/affected-entity-value.component.ts
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.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-affected-entity-value |
| standalone | true |
| imports |
MatButton
MatTooltipModule
|
| styleUrls | ./affected-entity-value.component.scss |
| templateUrl | ./affected-entity-value.component.html |
| styleUrl | ./affected-entity-value.component.scss |
EntityFieldEditComponent
EntityFieldViewComponent
FaDynamicIconComponent
HintBoxComponent
MatButton
MatTooltipModule
NoCommonValueHintComponent
OnInit
Properties |
Inputs |
Outputs |
constructor()
|
| row | |
Type : DialogRow
|
|
| Required : true | |
| useSuggestion | |
Type : void
|
|
|
The user wants the suggested value instead of the entered one |
|
| valueChange | |
Type : unknown
|
|
|
The value the user entered (database format) |
|
| 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;
}
}