src/app/features/inherited-field/admin-inherited-field/admin-inherited-field.component.ts
CustomFormControlDirective<DefaultValueConfigInheritedField>
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
AdminInheritedFieldComponent
|
| selector | app-admin-inherited-field |
| standalone | true |
| imports |
MatSelect
MatButton
MatTooltip
MatOption
|
| styleUrls | ./admin-inherited-field.component.scss |
| templateUrl | ./admin-inherited-field.component.html |
| styleUrl | ./admin-inherited-field.component.scss |
MatSelect
MatButton
ReactiveFormsModule
MatTooltip
MatOption
FormsModule
CustomFormControlDirective<DefaultValueConfigInheritedField>
Properties |
|
Methods |
Inputs |
Outputs |
constructor()
|
| entitySchemaField | |
Type : EntitySchemaField
|
|
| entityType | |
Type : EntityConstructor
|
|
| aria-describedby | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| disabled | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| ngControl | |
Type : any
|
|
Default value : inject(NgControl, { optional: true, self: true })
|
|
|
Inherited from
CustomFormControlDirective
|
|
| placeholder | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| required | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| value | |
Type : T
|
|
|
Inherited from
CustomFormControlDirective
|
|
| valueChange | |
Type : EventEmitter
|
|
|
Inherited from
CustomFormControlDirective
|
|
| onOptionSelected | ||||||
onOptionSelected(option: InheritanceOption)
|
||||||
|
Parameters :
Returns :
void
|
| Async openConfigDetailsDialog | ||||||||
openConfigDetailsDialog(option: InheritanceOption)
|
||||||||
|
Parameters :
Returns :
Promise<boolean>
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| writeValue | |||||||||||||||
writeValue(value: T, notifyFormControl: unknown)
|
|||||||||||||||
|
Inherited from
CustomFormControlDirective
|
|||||||||||||||
|
Implementation for Angular ControlValueAccessor interface that links the form control value to the component value
Parameters :
Returns :
void
|
| controlType |
Type : string
|
Default value : "custom-control"
|
|
Inherited from
CustomFormControlDirective
|
| elementRef |
Type : unknown
|
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
|
|
Inherited from
CustomFormControlDirective
|
| Readonly enabled |
Type : Signal<boolean>
|
Default value : computed(() => !this._disabled())
|
|
Inherited from
CustomFormControlDirective
|
|
Whether the control is currently enabled, as a signal (tracks disabled). |
| errorStateMatcher |
Type : unknown
|
Default value : inject(ErrorStateMatcher)
|
|
Inherited from
CustomFormControlDirective
|
| id |
Type : unknown
|
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
|
|
Inherited from
CustomFormControlDirective
|
| Static nextId |
Type : number
|
Default value : 0
|
|
Inherited from
CustomFormControlDirective
|
| onChange |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| onTouched |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| parentForm |
Type : unknown
|
Default value : inject(NgForm, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| parentFormGroup |
Type : unknown
|
Default value : inject(FormGroupDirective, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| stateChanges |
Type : unknown
|
Default value : new Subject<void>()
|
|
Inherited from
CustomFormControlDirective
|
| Readonly valueSignal |
Type : Signal<T>
|
Default value : computed(() => this._value())
|
|
Inherited from
CustomFormControlDirective
|
|
The current value of the control as a signal.
Authoritative in both modes: it reflects the bound |
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
input,
linkedSignal,
} from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MatButton } from "@angular/material/button";
import { MatDialog } from "@angular/material/dialog";
import { MatFormFieldControl } from "@angular/material/form-field";
import { MatOption, MatSelect } from "@angular/material/select";
import { MatTooltip } from "@angular/material/tooltip";
import { EntityRelationsService } from "app/core/entity/entity-mapper/entity-relations.service";
import { lastValueFrom } from "rxjs";
import { CustomFormControlDirective } from "../../../core/common-components/basic-autocomplete/custom-form-control.directive";
import { EntityRegistry } from "../../../core/entity/database-entity.decorator";
import { EntityConstructor } from "../../../core/entity/model/entity";
import { EntitySchemaField } from "../../../core/entity/schema/entity-schema-field";
import { EntityDatatype } from "../../../core/basic-datatypes/entity/entity.datatype";
import {
AutomatedFieldMappingComponent,
AutomatedFieldMappingDialogData,
} from "#src/app/features/inherited-field/automated-field-update/automated-field-mapping/automated-field-mapping.component";
import { DefaultValueConfigInheritedField } from "../inherited-field-config";
import { asArray } from "#src/app/utils/asArray";
interface InheritanceOption {
type: "inherit" | "automated";
label: string;
labelParts: { entityName: string; fieldName: string };
tooltip: string;
sourceReferenceField?: string;
sourceReferenceEntity?: string;
referencedEntityType?: EntityConstructor;
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-inherited-field",
imports: [
MatSelect,
MatButton,
ReactiveFormsModule,
MatTooltip,
MatOption,
FormsModule,
],
templateUrl: "./admin-inherited-field.component.html",
styleUrl: "./admin-inherited-field.component.scss",
providers: [
{
provide: MatFormFieldControl,
useExisting: AdminInheritedFieldComponent,
},
],
})
export class AdminInheritedFieldComponent extends CustomFormControlDirective<DefaultValueConfigInheritedField> {
entityType = input<EntityConstructor>();
entitySchemaField = input<EntitySchemaField>();
private readonly entityRelationsService = inject(EntityRelationsService);
private readonly entityRegistry = inject(EntityRegistry);
private readonly matDialog = inject(MatDialog);
availableOptions = computed<InheritanceOption[]>(() => {
const entityType = this.entityType();
if (!entityType) return [];
const options: InheritanceOption[] = [];
this.getInheritanceAttributes(entityType).forEach((attr) => {
const fieldConfig = entityType.schema.get(attr);
if (fieldConfig?.additional) {
for (const typeId of asArray(fieldConfig.additional)) {
const referencedEntityType = this.entityRegistry.get(typeId);
if (!referencedEntityType) continue;
const refFieldLabel = this.getFieldLabel(attr, entityType);
options.push({
type: "inherit",
label: `${refFieldLabel} > ${this.getEntityLabel(referencedEntityType)}`,
labelParts: {
entityName: this.getEntityLabel(referencedEntityType),
fieldName: refFieldLabel,
},
tooltip: $localize`Inherit value from any "${this.getEntityLabel(referencedEntityType)}" that is linked to in this record's "${refFieldLabel}" field`,
sourceReferenceField: attr,
sourceReferenceEntity: undefined,
referencedEntityType,
});
}
}
});
this.getAutomatedOptions().forEach((option) => {
option.relatedReferenceFields.forEach((refField) => {
const refFieldLabel = this.getFieldLabel(refField, option.entityType);
const refEntityType = this.entityRegistry.get(option.entityType);
options.push({
type: "automated",
label: `${option.label} > ${refFieldLabel}`,
labelParts: {
entityName: option.label,
fieldName: refFieldLabel,
},
tooltip: $localize`Inherit value from any "${this.getEntityLabel(refEntityType)}" that links to this record in its "${refFieldLabel}" field`,
sourceReferenceEntity: option.entityType,
sourceReferenceField: refField,
referencedEntityType: refEntityType,
});
});
});
return options;
});
selectedOption = linkedSignal<InheritanceOption | null>(
() =>
this.availableOptions().find(
(o) =>
o.sourceReferenceField === this.value?.sourceReferenceField &&
o.sourceReferenceEntity === this.value?.sourceReferenceEntity,
) ?? null,
);
constructor() {
super();
effect(() => {
this.entityType();
if (!this.value) {
this.value = {
sourceReferenceField: "",
sourceValueField: "",
};
}
});
}
onOptionSelected(option: InheritanceOption) {
const previousOption = this.selectedOption();
this.selectedOption.set(option);
this.openConfigDetailsDialog(option).then((confirmed) => {
if (!confirmed) {
// revert selection
this.selectedOption.set(previousOption);
}
});
}
async openConfigDetailsDialog(
option: InheritanceOption = this.selectedOption(),
): Promise<boolean> {
if (!option?.sourceReferenceField) return false;
const dialogRef = this.matDialog.open<
AutomatedFieldMappingComponent,
AutomatedFieldMappingDialogData,
DefaultValueConfigInheritedField
>(AutomatedFieldMappingComponent, {
data: {
currentEntityType: this.entityType(),
currentField: this.entitySchemaField(),
sourceValueEntityType: option.referencedEntityType,
value: {
...this.value,
sourceReferenceField: option.sourceReferenceField,
sourceReferenceEntity: option.sourceReferenceEntity,
},
},
});
const result = await lastValueFrom(dialogRef.afterClosed());
if (result?.sourceValueField) {
// successfully confirmed the dialog
this.value = result;
return true;
} else {
// dialog was cancelled
return false;
}
}
private getInheritanceAttributes(entityType: EntityConstructor): string[] {
if (!entityType?.schema) return [];
const inheritanceAttrs = Array.from(entityType.schema.entries())
.filter(
([, fieldConfig]) => fieldConfig.dataType === EntityDatatype.dataType,
)
.map(([fieldId]) => fieldId);
return inheritanceAttrs;
}
private getAutomatedOptions(): {
label: string;
entityType: string;
relatedReferenceFields: string[];
}[] {
const entityType = this.entityType();
if (!entityType) {
return [];
}
const relatedEntities =
this.entityRelationsService.getEntityTypesReferencingType(
entityType.ENTITY_TYPE,
) ?? [];
return relatedEntities
.filter(
(refType) =>
!refType.entityType.isInternalEntity && !!refType.entityType.label,
)
.map((refType) => ({
label: refType.entityType.label,
entityType: refType.entityType.ENTITY_TYPE,
relatedReferenceFields: refType.referencingProperties.map((p) => p.id),
}));
}
private getFieldLabel(
fieldId: string,
entityType: EntityConstructor | string,
): string {
if (typeof entityType === "string") {
entityType = this.entityRegistry.get(entityType);
}
if (!fieldId || !entityType?.schema) return fieldId;
const fieldConfig = entityType.schema.get(fieldId);
return fieldConfig?.label || fieldId;
}
private getEntityLabel(entityType: EntityConstructor): string {
return entityType.label || entityType.ENTITY_TYPE;
}
}
<div class="flex-row gap-small">
<mat-select
[value]="selectedOption()"
(valueChange)="onOptionSelected($event)"
class="select-box"
placeholder="Select inheritance option"
i18n-placeholder
>
@for (option of availableOptions(); track option) {
<mat-option [value]="option" [matTooltip]="option.tooltip">
<div class="flex-row gap-small align-items-center">
@if (option.type === "inherit") {
<!-- inheritance: field > entity -->
<span i18n>field</span>
<span> "{{ option.labelParts.fieldName }}"</span>
<span>></span>
<span style="font-weight: bold" class="text-primary">{{
option.labelParts.entityName
}}</span>
} @else {
<!-- automation: entity > field -->
<span style="font-weight: bold" class="text-primary">{{
option.labelParts.entityName
}}</span>
<span> > </span>
<span i18n>field</span>
<span> "{{ option.labelParts.fieldName }}"</span>
}
</div>
</mat-option>
} @empty {
<mat-option disabled>
<em i18n>No inheritance options available</em>
</mat-option>
}
</mat-select>
@if (selectedOption()?.sourceReferenceField) {
<button
mat-stroked-button
class="configure-button"
(click)="openConfigDetailsDialog()"
matTooltip="Configure the value from the other record that should be inherited (and any transformation, if necessary)."
i18n-matTooltip
i18n
>
Configure
</button>
}
</div>
./admin-inherited-field.component.scss
.configure-button {
height: auto;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.select-box {
align-content: center;
}