src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts
Allows configuration of the schema of a single Entity field, like its dataType and labels.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-admin-entity-field |
| standalone | true |
| imports |
MatDialogModule
MatButtonModule
MatInputModule
MatTabsModule
MatSlideToggleModule
FontAwesomeModule
MatTooltipModule
MatCheckbox
|
| styleUrls | ./admin-entity-field.component.scss, |
| templateUrl | ./admin-entity-field.component.html |
No results matching.
MatDialogModule
MatButtonModule
DialogCloseComponent
MatInputModule
FormsModule
MatTabsModule
MatSlideToggleModule
ReactiveFormsModule
FontAwesomeModule
MatTooltipModule
BasicAutocompleteComponent
ConfigureEntityFieldValidatorComponent
AnonymizeOptionsComponent
MatCheckbox
AdminDefaultValueComponent
EntityTypeSelectComponent
AdminSearchableCheckboxComponent
OnInit
Properties |
Methods |
| entityFieldValidatorChanges | ||||||
entityFieldValidatorChanges(validatorData: FormValidatorConfig)
|
||||||
|
Parameters :
Returns :
void
|
| Async onEntityAdditionalSelectionModeChange | ||||||
onEntityAdditionalSelectionModeChange(change: MatSlideToggleChange)
|
||||||
|
Parameters :
Returns :
any
|
| onEntitySchemaFieldChanges | ||||||
onEntitySchemaFieldChanges(changes: Partial<EntitySchemaField>)
|
||||||
|
Parameters :
Returns :
void
|
| openEnumOptions | ||||||
openEnumOptions(event: Event)
|
||||||
|
Parameters :
Returns :
void
|
| resetToBaseFieldSettings |
resetToBaseFieldSettings()
|
|
Returns :
void
|
| Async save |
save()
|
|
Returns :
any
|
| supportsMultiValue | ||||||
supportsMultiValue(dataType: string)
|
||||||
|
Whether the user can choose to hold multiple values in one field of the given dataType, i.e. whether the "allow multiple values" option is offered.
Parameters :
Returns :
boolean
|
| additionalForm |
Type : FormControl
|
| attendanceParticipantTypesForm |
Type : unknown
|
Default value : new FormControl<string[]>([])
|
| createNewAdditionalOption |
Type : function
|
| createNewAdditionalOptionAsync |
Type : unknown
|
Default value : () => {...}
|
| data |
Type : unknown
|
Default value : inject<AdminEntityFieldData>(MAT_DIALOG_DATA)
|
| dataTypes |
Type : SimpleDropdownValue[]
|
Default value : []
|
| entityAdditionalMultiSelect |
Type : WritableSignal<boolean>
|
Default value : signal(false)
|
| entityType |
Type : EntityConstructor
|
| fieldId |
Type : string
|
| fieldIdForm |
Type : FormControl
|
| form |
Type : FormGroup
|
| objectToLabel |
Type : unknown
|
Default value : () => {...}
|
| objectToValue |
Type : unknown
|
Default value : () => {...}
|
| schemaFieldsForm |
Type : FormGroup
|
|
form group of all fields in EntitySchemaField (i.e. without fieldId) |
| typeAdditionalOptions |
Type : SimpleDropdownValue[]
|
Default value : []
|
import {
Component,
DestroyRef,
inject,
OnInit,
signal,
viewChild,
WritableSignal,
ChangeDetectionStrategy,
} from "@angular/core";
import { Entity, EntityConstructor } from "../../../entity/model/entity";
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { MatButtonModule } from "@angular/material/button";
import { DialogCloseComponent } from "../../../common-components/dialog-close/dialog-close.component";
import { MatInputModule } from "@angular/material/input";
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { EntitySchemaField } from "../../../entity/schema/entity-schema-field";
import { MatTabsModule } from "@angular/material/tabs";
import {
MatSlideToggleModule,
MatSlideToggleChange,
} from "@angular/material/slide-toggle";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { BasicAutocompleteComponent } from "../../../common-components/basic-autocomplete/basic-autocomplete.component";
import { DefaultDatatype } from "../../../entity/default-datatype/default.datatype";
import { ConfigurableEnumDatatype } from "../../../basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype";
import { EntityDatatype } from "../../../basic-datatypes/entity/entity.datatype";
import { ConfigurableEnumService } from "../../../basic-datatypes/configurable-enum/configurable-enum.service";
import { EntityRegistry } from "../../../entity/database-entity.decorator";
import { ConfigureEnumPopupComponent } from "../../../basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component";
import { ConfigurableEnum } from "../../../basic-datatypes/configurable-enum/configurable-enum";
import { generateIdFromLabel } from "../../../../utils/generate-id-from-label/generate-id-from-label";
import { merge } from "rxjs";
import { filter } from "rxjs/operators";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { uniquePropertyValidator } from "app/core/common-components/entity-form/unique-property-validator/unique-property-validator";
import { ConfigureEntityFieldValidatorComponent } from "./configure-entity-field-validator/configure-entity-field-validator.component";
import { FormValidatorConfig } from "app/core/common-components/entity-form/dynamic-form-validators/form-validator-config";
import { AnonymizeOptionsComponent } from "./anonymize-options/anonymize-options.component";
import { MatCheckbox } from "@angular/material/checkbox";
import { AdminDefaultValueComponent } from "../../../default-values/admin-default-value/admin-default-value.component";
import { EntityTypeSelectComponent } from "app/core/entity/entity-type-select/entity-type-select.component";
import { AdminSearchableCheckboxComponent } from "./admin-searchable-checkbox/admin-searchable-checkbox.component";
import { SimpleDropdownValue } from "app/core/common-components/basic-autocomplete/simple-dropdown-value.interface";
import { ConfirmationDialogService } from "app/core/common-components/confirmation-dialog/confirmation-dialog.service";
import { YesNoButtons } from "app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
import { AttendanceDatatype } from "#src/app/features/attendance/model/attendance.datatype";
/**
* Dialog data for AdminEntityFieldComponent
*/
export interface AdminEntityFieldData {
/** current state of the field being edited */
entitySchemaField: EntitySchemaField;
/**
* Entity type this field is part of,
* to use as context information.
* The entityType is not changed by this component.
*/
entityType: EntityConstructor;
/**
* Whether the field is changed only for a single view instead of globally for the entity type.
* Use to prevent changes to config that are required to be consistent across all uses of the field.
*/
overwriteLocally: boolean;
}
/**
* Allows configuration of the schema of a single Entity field, like its dataType and labels.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-entity-field",
templateUrl: "./admin-entity-field.component.html",
styleUrls: [
"./admin-entity-field.component.scss",
"../../../common-components/entity-form/entity-form/entity-form.component.scss",
],
imports: [
MatDialogModule,
MatButtonModule,
DialogCloseComponent,
MatInputModule,
FormsModule,
MatTabsModule,
MatSlideToggleModule,
ReactiveFormsModule,
FontAwesomeModule,
MatTooltipModule,
BasicAutocompleteComponent,
ConfigureEntityFieldValidatorComponent,
AnonymizeOptionsComponent,
MatCheckbox,
AdminDefaultValueComponent,
EntityTypeSelectComponent,
AdminSearchableCheckboxComponent,
],
})
export class AdminEntityFieldComponent implements OnInit {
data = inject<AdminEntityFieldData>(MAT_DIALOG_DATA);
private dialogRef = inject<MatDialogRef<any>>(MatDialogRef);
private fb = inject(FormBuilder);
private allDataTypes = inject(DefaultDatatype);
private configurableEnumService = inject(ConfigurableEnumService);
private entityRegistry = inject(EntityRegistry);
private dialog = inject(MatDialog);
private readonly confirmationDialog = inject(ConfirmationDialogService);
private readonly destroyRef = inject(DestroyRef);
private readonly validatorConfig = viewChild(
ConfigureEntityFieldValidatorComponent,
);
fieldId: string;
entityType: EntityConstructor;
form: FormGroup;
fieldIdForm: FormControl;
/** form group of all fields in EntitySchemaField (i.e. without fieldId) */
schemaFieldsForm: FormGroup;
additionalForm: FormControl;
typeAdditionalOptions: SimpleDropdownValue[] = [];
dataTypes: SimpleDropdownValue[] = [];
entityAdditionalMultiSelect: WritableSignal<boolean> = signal(false);
attendanceParticipantTypesForm = new FormControl<string[]>([]);
/**
* dataTypes for which a field can hold multiple values (`isArray`),
* so that the "allow multiple values" option is offered to the user.
*/
private static readonly MULTI_VALUE_DATATYPES: string[] = [
ConfigurableEnumDatatype.dataType,
EntityDatatype.dataType,
];
ngOnInit() {
this.entityType = this.data.entityType;
this.initSettings();
if (this.data.overwriteLocally) {
this.lockGlobalFields();
}
// Auto-generate ID if not yet set
if (!this.data.entitySchemaField.id) {
this.autoGenerateId();
}
this.initAvailableDatatypes(
this.allDataTypes as unknown as DefaultDatatype<any, any>[],
);
}
/**
* Disable editing of those fields that have to be consistent across all uses of the field.
* @private
*/
private lockGlobalFields() {
["dataType", "additional", "isArray"].forEach((ctrlName) => {
const control = this.schemaFieldsForm.get(ctrlName);
if (control?.value) {
control.disable();
}
});
}
private initSettings() {
this.fieldIdForm = this.fb.control(this.data.entitySchemaField.id, {
validators: [
Validators.required,
Validators.pattern(/^[a-zA-Z0-9][a-zA-Z0-9_]*$/),
],
asyncValidators: [
uniquePropertyValidator({
getExistingValues: async () =>
Array.from(this.data.entityType.schema.keys()),
normalize: true,
fieldLabel: $localize`:field label:id`,
}),
],
});
this.additionalForm = this.fb.control(
this.data.entitySchemaField.additional,
);
const labelFormControl = this.fb.control(
this.data.entitySchemaField.label,
{
validators: [Validators.required],
asyncValidators: [
uniquePropertyValidator({
getExistingValues: async () => {
const labels: string[] = [];
for (const [
key,
field,
] of this.data.entityType.schema.entries()) {
if (key === this.data.entitySchemaField.id) continue;
if (field.label) {
labels.push(field.label);
}
}
return labels;
},
normalize: true,
fieldLabel: $localize`:field label:label`,
}),
],
},
);
this.schemaFieldsForm = this.fb.group({
id: this.fieldIdForm,
label: labelFormControl,
labelShort: [this.data.entitySchemaField.labelShort],
displayFullLengthLabel: [
this.data.entitySchemaField.displayFullLengthLabel ?? false,
],
displayFullLengthOptionLabel: [
this.data.entitySchemaField.displayFullLengthOptionLabel ?? false,
],
description: [this.data.entitySchemaField.description],
dataType: [this.data.entitySchemaField.dataType, Validators.required],
isArray: [this.data.entitySchemaField.isArray],
additional: this.additionalForm,
defaultValue: new FormControl(this.data.entitySchemaField.defaultValue),
searchable: [this.data.entitySchemaField.searchable],
anonymize: [this.data.entitySchemaField.anonymize],
viewComponent: [this.data.entitySchemaField.viewComponent],
editComponent: [this.data.entitySchemaField.editComponent],
showInDetailsView: [this.data.entitySchemaField.showInDetailsView],
generateIndex: [this.data.entitySchemaField.generateIndex],
validators: [this.data.entitySchemaField.validators],
});
this.form = this.fb.group({
id: this.fieldIdForm,
schemaFields: this.schemaFieldsForm,
});
this.schemaFieldsForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((formValues) => this.updateSchemaFieldFromForm(formValues));
this.schemaFieldsForm
.get("labelShort")
.valueChanges.pipe(
filter((v) => v === ""),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((v) => {
// labelShort should never be empty string, in that case it has to be removed so that label works as fallback
this.schemaFieldsForm.get("labelShort").setValue(null);
});
// Sync attendance participant type selection back to additionalForm in nested format
this.attendanceParticipantTypesForm.valueChanges
.pipe(
filter(
() => this.schemaFieldsForm.get("dataType").value === "attendance",
),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((types) => {
this.additionalForm.setValue(this.buildAttendanceAdditional(types));
});
this.updateDataTypeAdditional(this.schemaFieldsForm.get("dataType").value);
this.schemaFieldsForm
.get("dataType")
.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((v) => {
this.updateDataTypeAdditional(v);
this.resetIsArrayIfUnsupported(v);
});
this.updateForNewOrExistingField();
}
private updateSchemaFieldFromForm(formValues) {
if (
JSON.stringify(formValues) === JSON.stringify(this.data.entitySchemaField)
)
return;
for (const key of Object.keys(formValues)) {
if (formValues[key] !== null) {
this.data.entitySchemaField[key] = formValues[key];
} else if (this.data.entitySchemaField.hasOwnProperty(key)) {
// When field is cleared, delete the property
delete this.data.entitySchemaField[key];
}
}
}
private updateForNewOrExistingField() {
if (!!this.data.entitySchemaField.id) {
// existing fields' id is readonly
this.fieldIdForm.disable();
} else {
const autoGenerateSubscr = merge(
this.schemaFieldsForm.get("label").valueChanges,
this.schemaFieldsForm.get("labelShort").valueChanges,
)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.autoGenerateId());
// stop updating id when user manually edits
this.fieldIdForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => autoGenerateSubscr.unsubscribe());
}
}
entityFieldValidatorChanges(validatorData: FormValidatorConfig) {
this.schemaFieldsForm.get("validators").setValue(validatorData);
}
onEntitySchemaFieldChanges(changes: Partial<EntitySchemaField>) {
Object.assign(this.data.entitySchemaField, changes);
}
private autoGenerateId() {
// prefer labelShort if it exists, as this makes less verbose IDs
const label =
this.schemaFieldsForm.get("labelShort").value ??
this.schemaFieldsForm.get("label").value;
const generatedId = generateIdFromLabel(label);
this.fieldIdForm.setValue(generatedId, { emitEvent: false });
}
private initAvailableDatatypes(dataTypes: DefaultDatatype[]) {
this.dataTypes = dataTypes
.filter((d) => d.label !== DefaultDatatype.label) // hide "internal" technical dataTypes that did not define a human-readable label
.map((d) => ({
label: d.label,
value: d.dataType,
}));
}
objectToLabel = (v: SimpleDropdownValue) => v?.label;
objectToValue = (v: SimpleDropdownValue) => v?.value;
createNewAdditionalOption: (input: string) => SimpleDropdownValue;
createNewAdditionalOptionAsync = async (input) =>
this.createNewAdditionalOption(input);
/**
* Whether the user can choose to hold multiple values in one field of the given dataType,
* i.e. whether the "allow multiple values" option is offered.
*/
supportsMultiValue(dataType: string): boolean {
return AdminEntityFieldComponent.MULTI_VALUE_DATATYPES.includes(dataType);
}
/**
* Whether the dataType itself requires multiple values, independent of the user's choice
* (see {@link DefaultDatatype.normalizeSchemaField}, e.g. "attendance").
*/
private enforcesMultiValue(dataType: string): boolean {
const datatype = (
this.allDataTypes as unknown as DefaultDatatype<any, any>[]
).find((d) => d.dataType === dataType);
return !!datatype?.normalizeSchemaField({ dataType })?.isArray;
}
/**
* Remove the `isArray` flag if the newly selected dataType does not use multiple values.
*
* The "allow multiple values" checkbox is only displayed for some dataTypes.
* Without this reset a previously set `isArray: true` silently remains in the config
* (e.g. when a multi-select "configurable-enum" field is changed to "long-text"),
* making the field's value an array that the dataType's display component cannot handle.
*/
private resetIsArrayIfUnsupported(dataType: string) {
if (this.supportsMultiValue(dataType) || this.enforcesMultiValue(dataType))
return;
const isArrayControl = this.schemaFieldsForm.get("isArray");
if (!isArrayControl?.value) return;
// `null` (rather than `false`) so that the flag is removed from the schema entirely
isArrayControl.setValue(null);
}
private updateDataTypeAdditional(
dataType: string,
newAdditional: any = this.data.entitySchemaField.additional,
) {
this.resetAdditional();
if (dataType === ConfigurableEnumDatatype.dataType) {
this.initAdditionalForEnum(
typeof newAdditional === "string" ? newAdditional : undefined,
);
} else if (dataType === EntityDatatype.dataType) {
this.initAdditionalForEntityRef(newAdditional);
} else if (dataType === AttendanceDatatype.dataType) {
this.initAdditionalForAttendance(newAdditional);
}
// hasInnerType: [ArrayDatatype.dataType].includes(d.dataType),
// TODO: this mapping of having an "additional" schema should probably become part of Datatype classes
}
private initAdditionalForEnum(newAdditional?: string) {
this.typeAdditionalOptions = this.configurableEnumService
.listEnums()
.map((x) => ({
label: Entity.extractEntityIdFromId(x), // TODO: add human-readable label to configurable-enum entities
value: Entity.extractEntityIdFromId(x),
}));
this.additionalForm.addValidators(Validators.required);
this.createNewAdditionalOption = (text) => ({
value: generateIdFromLabel(text),
label: text,
});
if (newAdditional) {
this.additionalForm.setValue(newAdditional);
} else if (this.schemaFieldsForm.get("label").value) {
// when switching to enum datatype in the form, if unset generate a suggested enum-id immediately
const newOption = this.createNewAdditionalOption(
this.schemaFieldsForm.get("label").value,
);
this.typeAdditionalOptions.push(newOption);
this.additionalForm.setValue(newOption.value);
}
}
private initAdditionalForEntityRef(newAdditional?: string | string[]) {
this.additionalForm.addValidators(Validators.required);
this.entityAdditionalMultiSelect.set(Array.isArray(newAdditional));
if (Array.isArray(newAdditional)) {
const validValues = newAdditional.filter((value) =>
this.isValidEntityType(value),
);
// Use setTimeout to ensure Angular processes the multi input change before setting the value
setTimeout(() => {
this.additionalForm.setValue(validValues);
});
return;
}
if (this.isValidEntityType(newAdditional)) {
this.additionalForm.setValue(newAdditional);
}
}
private resetAdditional() {
this.additionalForm.removeValidators(Validators.required);
this.additionalForm.reset(null);
this.typeAdditionalOptions = [];
this.createNewAdditionalOption = undefined;
this.entityAdditionalMultiSelect.set(false);
this.attendanceParticipantTypesForm.reset([]);
}
private initAdditionalForAttendance(newAdditional?: any) {
// Extract participant types from the nested additional format
let participantTypes: string[] = [];
if (newAdditional?.participant?.additional) {
const raw = newAdditional.participant.additional;
participantTypes = Array.isArray(raw) ? [...raw] : [raw];
}
const validValues = participantTypes.filter((value) =>
this.isValidEntityType(value),
);
if (validValues.length > 0) {
this.additionalForm.setValue(this.buildAttendanceAdditional(validValues));
}
// Use setTimeout to ensure Angular processes the @if block and creates the EntityTypeSelectComponent before setting the value
setTimeout(() => {
this.attendanceParticipantTypesForm.setValue(validValues);
});
}
private buildAttendanceAdditional(types: string[]): object | null {
return types && types.length > 0
? { participant: { dataType: "entity", additional: types } }
: null;
}
private isValidEntityType(type: string): boolean {
return this.entityRegistry
.getEntityTypes(true)
.some((x) => x.value.ENTITY_TYPE === type);
}
async onEntityAdditionalSelectionModeChange(change: MatSlideToggleChange) {
if (
this.schemaFieldsForm.get("dataType")?.value !== EntityDatatype.dataType
) {
change.source.checked = this.entityAdditionalMultiSelect();
return;
}
const isMulti = change.checked;
if (this.entityAdditionalMultiSelect() === isMulti) {
return;
}
const currentValue = this.additionalForm.value;
if (isMulti) {
this.entityAdditionalMultiSelect.set(true);
this.additionalForm.setValue(currentValue ? [currentValue] : []);
return;
}
// Switching to single-select with 0 or 1 values - no confirmation needed
if (!Array.isArray(currentValue) || currentValue.length <= 1) {
this.entityAdditionalMultiSelect.set(false);
this.additionalForm.setValue(
Array.isArray(currentValue) ? (currentValue[0] ?? null) : null,
);
return;
}
// Switching to single-select with multiple values - ask for confirmation
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`:Entity field config switch mode title:Switch to single selection?`,
$localize`:Entity field config switch mode body:You selected multiple target record types. Switching to single selection will clear this selection. Continue?`,
YesNoButtons,
);
if (confirmed) {
this.entityAdditionalMultiSelect.set(false);
this.additionalForm.setValue(null);
} else {
// cancelled: reset the toggle and keep checked
change.source.checked = true;
}
}
async save() {
this.form.markAllAsTouched();
// Recalculates the value and validation status of the control, also updates the value and validity of its ancestors.
this.schemaFieldsForm.updateValueAndValidity();
// the validator settings sub-form is not part of `form`, so its validity is checked separately
const validatorForm = this.validatorConfig()?.validatorForm();
validatorForm?.markAllAsTouched();
if (this.form.invalid || validatorForm?.invalid) return;
this.data.entitySchemaField.id = this.fieldIdForm.getRawValue();
this.dialogRef.close(this.data.entitySchemaField);
}
openEnumOptions(event: Event) {
event.stopPropagation(); // do not open the autocomplete dropdown when clicking the settings icon
let enumEntity = this.configurableEnumService.getEnum(
this.additionalForm.value,
);
if (!enumEntity) {
// if the user makes changes, the dialog component itself is saving the new entity to the database already
enumEntity = new ConfigurableEnum(this.additionalForm.value);
}
this.dialog.open(ConfigureEnumPopupComponent, {
data: enumEntity,
disableClose: true,
});
}
resetToBaseFieldSettings() {
this.dialogRef.close(this.fieldIdForm.getRawValue());
}
}
<h2 mat-dialog-title i18n>
Configure Field "{{ data.entitySchemaField.label }}"
</h2>
<app-dialog-close mat-dialog-close></app-dialog-close>
<mat-dialog-content>
@if (data.overwriteLocally) {
<p>
<fa-icon icon="exclamation-triangle" style="color: red"></fa-icon>
<span i18n>
You are currently overwriting the field for a local view only. The field
settings here only apply to this and do not affect the main record
schema.
</span>
</p>
} @else {
<p i18n>
The field settings here apply to the record type overall and affect both
the field here in the current view as well as all other forms and lists
where this field is displayed.
</p>
}
<form [formGroup]="form">
<mat-tab-group formGroupName="schemaFields" [dynamicHeight]="true">
<mat-tab label="Basics" i18n-label>
<div class="grid-layout margin-top-regular">
<div class="entity-form-cell">
<mat-form-field>
<mat-label i18n>Label</mat-label>
<input formControlName="label" matInput #formLabel />
@if (schemaFieldsForm.get("label").hasError("uniqueProperty")) {
<mat-error>
{{ schemaFieldsForm.get("label").getError("uniqueProperty") }}
</mat-error>
}
</mat-form-field>
<mat-checkbox
formControlName="displayFullLengthLabel"
matTooltip="Normally, long labels are shortened with an ellipsis (...) to keep simple views with only single-line labels. If you enable this option, the full label will be displayed on multiple lines instead of being shortened."
i18n-matTooltip
i18n
>
Display full label (multi-line)
</mat-checkbox>
<mat-form-field floatLabel="always">
<mat-label>
<span i18n> Label (short)</span>
<fa-icon
icon="question-circle"
matTooltip="Optionally you can define an additional shorter label to be displayed in table headers and other places where space is limited."
i18n-matTooltip
></fa-icon>
</mat-label>
<input
formControlName="labelShort"
matInput
[placeholder]="formLabel.value"
/>
</mat-form-field>
<mat-form-field>
<mat-label>
<span i18n> Description</span>
<fa-icon
icon="question-circle"
matTooltip="The description provides additional explanation or context about this field. It is usually displayed as a help icon with tooltip."
i18n-matTooltip
></fa-icon>
</mat-label>
<textarea
formControlName="description"
matInput
rows="3"
></textarea>
</mat-form-field>
</div>
<div class="entity-form-cell">
<mat-form-field>
<mat-label>
<span i18n> Field ID (readonly)</span>
<fa-icon
icon="question-circle"
matTooltip="The internal ID of the field is used at a technical level in the database. The ID cannot be changed after the field has been created."
i18n-matTooltip
></fa-icon>
</mat-label>
<input [formControl]="fieldIdForm" matInput />
@if (fieldIdForm.disabled) {
<fa-icon icon="lock" matSuffix></fa-icon>
}
@if (fieldIdForm.hasError("pattern")) {
<mat-error>
<span i18n>
Invalid ID: must start with a letter or number, and may only
contain letters, numbers, and underscores.</span
>
</mat-error>
}
@if (fieldIdForm.hasError("uniqueProperty")) {
<mat-error>
{{ fieldIdForm.getError("uniqueProperty") }}
</mat-error>
}
</mat-form-field>
<mat-form-field
i18n-matTooltip
matTooltip="The type of the field cannot be changed here because these changes only apply to this form. For data consistency, you have to change the type for the field overall from the 'Details View & Fields'."
matTooltipPosition="above"
[matTooltipDisabled]="!data.overwriteLocally"
>
<mat-label>
<span i18n>Type</span>
</mat-label>
<app-basic-autocomplete
formControlName="dataType"
#formDataType
[options]="dataTypes"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
></app-basic-autocomplete>
</mat-form-field>
@if (supportsMultiValue(formDataType.value)) {
<mat-checkbox
formControlName="isArray"
matTooltip="Check this to let users select more than just one entry in this field."
i18n-matTooltip
i18n
>
allow multiple values (multi-select)
</mat-checkbox>
}
@if (formDataType.value === "configurable-enum") {
<mat-checkbox
formControlName="displayFullLengthOptionLabel"
matTooltip="Normally, long dropdown option labels are shortened with an ellipsis (...). Enable this to display full option labels across multiple lines in the dropdown list."
i18n-matTooltip
i18n
>
Display full dropdown option labels (multi-line)
</mat-checkbox>
}
<!-- "additional" for enum datatype -->
@if (formDataType.value === "configurable-enum") {
<mat-form-field>
<mat-label>
<span i18n> Type Details (dropdown options set)</span>
<fa-icon
icon="question-circle"
matTooltip="Select an existing set of options to share between multiple fields or create a new, independent list of dropdown options."
i18n-matTooltip
></fa-icon>
</mat-label>
<app-basic-autocomplete
formControlName="additional"
[options]="typeAdditionalOptions"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
[createOption]="createNewAdditionalOptionAsync"
></app-basic-autocomplete>
<button
mat-icon-button
matSuffix
(click)="openEnumOptions($event)"
>
<fa-icon icon="wrench"></fa-icon>
</button>
</mat-form-field>
}
<!-- "additional" for entity ref datatypes -->
@if (formDataType.value === "entity") {
<mat-form-field>
<mat-label>
<span i18n> Type Details (target record type)</span>
<fa-icon
icon="question-circle"
matTooltip="Select from which type of records the user can select and link to with this field."
i18n-matTooltip
></fa-icon>
</mat-label>
<app-entity-type-select
formControlName="additional"
[multi]="entityAdditionalMultiSelect()"
></app-entity-type-select>
<mat-hint>
<mat-slide-toggle
[checked]="entityAdditionalMultiSelect()"
[disabled]="additionalForm.disabled"
(change)="onEntityAdditionalSelectionModeChange($event)"
matTooltip="Check this to allow more than one record *type* to choose from. For example, configuring this field so that users can select either Participants or Organizations from the same dropdown list."
i18n-matTooltip
i18n
>
Advanced mode (mix multiple target record types)
</mat-slide-toggle>
</mat-hint>
</mat-form-field>
}
<!-- "additional" for attendance datatype -->
@if (formDataType.value === "attendance") {
<mat-form-field>
<mat-label>
<span i18n> Type Details (participant record types)</span>
<fa-icon
icon="question-circle"
matTooltip="Select which types of records can be added as participants for attendance tracking."
i18n-matTooltip
></fa-icon>
</mat-label>
<app-entity-type-select
[formControl]="attendanceParticipantTypesForm"
[multi]="true"
></app-entity-type-select>
</mat-form-field>
}
</div>
</div>
</mat-tab>
<!--
ADVANCED SETTINGS
-->
<mat-tab label="Advanced Options" i18n-label>
<div class="grid-layout-wide margin-top-regular">
<div class="entity-form-cell">
<app-admin-default-value
formControlName="defaultValue"
[entityType]="data.entityType"
[entitySchemaField]="data.entitySchemaField"
>
</app-admin-default-value>
<app-anonymize-options
[value]="data.entitySchemaField.anonymize"
(valueChange)="schemaFieldsForm.get('anonymize').setValue($event)"
>
<span i18n>Anonymize</span>
<fa-icon
icon="question-circle"
matTooltip="Optionally This will remove all personal information (PII) permanently related to this field."
i18n-matTooltip
></fa-icon>
</app-anonymize-options>
<app-admin-searchable-checkbox
formControlName="searchable"
[entityType]="entityType"
[fieldId]="fieldIdForm.value"
[dataType]="schemaFieldsForm.get('dataType').value"
></app-admin-searchable-checkbox>
</div>
</div>
</mat-tab>
<mat-tab label="Validation" i18n-label>
<div class="grid-layout-wide margin-top-regular">
<div class="entity-form-cell">
<app-configure-entity-field-validator
[entitySchemaField]="data.entitySchemaField"
(entityValidatorChanges)="entityFieldValidatorChanges($event)"
(entitySchemaFieldChanges)="onEntitySchemaFieldChanges($event)"
></app-configure-entity-field-validator>
</div>
</div>
</mat-tab>
</mat-tab-group>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="save()" i18n="Button label">Apply</button>
<button mat-button mat-dialog-close i18n="Button label">Cancel</button>
@if (data.overwriteLocally) {
<div
matTooltip="This field does not exist yet, so it has no general settings to reset to. A new field with these settings will be created on the datatype."
i18n-matTooltip="
Tooltip explaining why resetting is unavailable while creating a field
"
[matTooltipDisabled]="fieldIdForm.disabled"
>
<button
mat-button
[disabled]="fieldIdForm.enabled"
(click)="resetToBaseFieldSettings()"
i18n="Button label"
matTooltip="Discard any changes you have made to this field only for this view and reset it to the general settings"
i18n-matTooltip
>
Reset to base field settings
</button>
</div>
}
</mat-dialog-actions>
./admin-entity-field.component.scss
@use "../../../../../styles/mixins/grid-layout";
.grid-layout {
@include grid-layout.adaptive(
$min-block-width: 250px,
$max-screen-width: 414px
);
}
.grid-layout-wide {
@include grid-layout.adaptive(
$min-block-width: 450px,
$max-screen-width: 500px
);
}
../../../common-components/entity-form/entity-form/entity-form.component.scss
@use "mixins/grid-layout";
@use "variables/sizes";
.grid-layout {
@include grid-layout.adaptive(
$min-block-width: sizes.$form-group-min-width,
$max-screen-width: 414px
);
}
.entity-form-cell {
display: flex;
flex-direction: column;
/* set the width of each form field to 100% in every form component that is a descendent
of the columns-wrapper class */
mat-form-field {
width: 100%;
max-width: 864px;
}
/* We align the photo (and only tht photo) to the center of the cell if there is one.
This looks better on desktop and mobile compared to an alignment to the start of the cell
which is the default for all other elements */
> app-edit-photo {
align-self: center;
}
}
.full-width mat-form-field {
max-width: none;
}