src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts

Description

Allows configuration of the schema of a single Entity field, like its dataType and labels.

Implements

OnInit

Example

Metadata

Relationships

Used by

No results matching.

Index

Properties
Methods

Methods

entityFieldValidatorChanges
entityFieldValidatorChanges(validatorData: FormValidatorConfig)
Parameters :
Name Type Optional
validatorData FormValidatorConfig No
Returns : void
Async onEntityAdditionalSelectionModeChange
onEntityAdditionalSelectionModeChange(change: MatSlideToggleChange)
Parameters :
Name Type Optional
change MatSlideToggleChange No
Returns : any
onEntitySchemaFieldChanges
onEntitySchemaFieldChanges(changes: Partial<EntitySchemaField>)
Parameters :
Name Type Optional
changes Partial<EntitySchemaField> No
Returns : void
openEnumOptions
openEnumOptions(event: Event)
Parameters :
Name Type Optional
event Event No
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 :
Name Type Optional
dataType string No
Returns : boolean

Properties

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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""