src/app/core/admin/admin-entity/admin-entity-general-settings/admin-entity-general-settings.component.ts

Metadata

Relationships

Depends on

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

entityConstructor
Type : EntityConstructor
Required :  true
generalSettings
Type : EntityConfig
Required :  true
showPIIDetailsInput
Type : boolean
Default value : false

Outputs

generalSettingsChange
Type : EntityConfig

Methods

changeFieldAnonymization
changeFieldAnonymization(fieldSchema: EntitySchemaField, newAnonymizationValue: unknown)
Parameters :
Name Type Optional
fieldSchema EntitySchemaField No
newAnonymizationValue unknown No
Returns : void
clearToBlockAttributes
clearToBlockAttributes()
Returns : void
isFormValid
isFormValid()
Returns : boolean
toggleAnonymizationTable
toggleAnonymizationTable(event: MatCheckboxChange)
Parameters :
Name Type Optional
event MatCheckboxChange No
Returns : void

Properties

Readonly basicSettingsForm
Type : unknown
Default value : this.fb.group({ label: [null as string | null, Validators.required], labelPlural: [null as string | null], icon: [null as string | null], color: [null as any], toStringAttributes: [null as string[] | null], hasPII: [null as boolean | null], enableUserAccounts: [null as boolean | null], toBlockDetailsAttributes: this.fb.group({ title: [null as string | null], image: [{ value: null as string | null, disabled: true }], fields: [[] as string[]], }), })
fieldAnonymizationDataSource
Type : unknown
Default value : computed(() => { if (!this.showPIIDetails()) return undefined; const fields = Array.from(this.entityConstructor().schema.entries()) .filter(([, field]) => !field.isInternalField) .map(([key, field]) => ({ key, label: field.label, field })); return new MatTableDataSource(fields); })
hasImageFields
Type : unknown
Default value : computed(() => Array.from(this.entityConstructor().schema.values()).some( (field) => field.dataType === PhotoDatatype.dataType, ), )
hideNonTextFields
Type : unknown
Default value : () => {...}
Readonly iconControl
Type : unknown
Default value : this.basicSettingsForm.get("icon") as FormControl< string | null >
isConditionalColor
Type : unknown
Default value : linkedSignal(() => { const color = this.generalSettings().color; return Array.isArray(color) && color.length > 0; })
objectToLabel
Type : unknown
Default value : () => {...}
objectToValue
Type : unknown
Default value : () => {...}
showOnlyImageFields
Type : unknown
Default value : () => {...}
showPIIDetails
Type : unknown
Default value : linkedSignal( () => this.showPIIDetailsInput() || !!this.generalSettings()?.hasPII, )
showTooltipDetails
Type : unknown
Default value : linkedSignal( () => !!this.generalSettings().toBlockDetailsAttributes, )
toStringAttributesOptions
Type : unknown
Default value : computed<SimpleDropdownValue[]>(() => { const selectedKeys = this.selectedStringAttributes(); const allSchemaOptions = Array.from( this.entityConstructor().schema.entries(), ) .filter( ([, field]) => [ StringDatatype.dataType, NumberDatatype.dataType, ConfigurableEnumDatatype.dataType, DateOnlyDatatype.dataType, ].includes(field.dataType) && field.label, ) .map(([key, field]) => ({ value: key, label: field.label })); return [ ...selectedKeys .map((key) => allSchemaOptions.find((o) => o.value === key)) .filter(Boolean), ...allSchemaOptions.filter((o) => !selectedKeys.includes(o.value)), ]; })
import {
  Component,
  inject,
  input,
  output,
  effect,
  ChangeDetectionStrategy,
  computed,
  linkedSignal,
} from "@angular/core";
import { EntityConstructor } from "../../../entity/model/entity";
import { MatButtonModule } from "@angular/material/button";
import { MatInputModule } from "@angular/material/input";
import {
  FormBuilder,
  FormControl,
  FormsModule,
  ReactiveFormsModule,
  Validators,
} from "@angular/forms";
import { MatTabsModule } from "@angular/material/tabs";
import { MatTooltipModule } from "@angular/material/tooltip";
import { BasicAutocompleteComponent } from "../../../common-components/basic-autocomplete/basic-autocomplete.component";
import { EntityConfig } from "../../../entity/entity-config";
import { MatTableDataSource, MatTableModule } from "@angular/material/table";
import {
  MatCheckboxChange,
  MatCheckboxModule,
} from "@angular/material/checkbox";
import { MatOptionModule } from "@angular/material/core";
import { MatSelectModule } from "@angular/material/select";
import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field";
import { AdminEntityService } from "../../admin-entity.service";
import { StringDatatype } from "../../../basic-datatypes/string/string.datatype";
import { HelpButtonComponent } from "../../../common-components/help-button/help-button.component";
import { AnonymizeOptionsComponent } from "../../admin-entity-details/admin-entity-field/anonymize-options/anonymize-options.component";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { ConfigurableEnumDatatype } from "app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype";
import { DateOnlyDatatype } from "app/core/basic-datatypes/date-only/date-only.datatype";
import { IconComponent } from "#src/app/core/common-components/icon-input/icon-input.component";
import { SimpleDropdownValue } from "app/core/common-components/basic-autocomplete/simple-dropdown-value.interface";
import { PhotoDatatype } from "app/features/file/photo.datatype";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { MatExpansionModule } from "@angular/material/expansion";
import { EntityFieldSelectComponent } from "#src/app/core/entity/entity-field-select/entity-field-select.component";
import { ConditionalColorConfigComponent } from "./conditional-color-config/conditional-color-config.component";
import { NumberDatatype } from "#src/app/core/basic-datatypes/number/number.datatype";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-admin-entity-general-settings",
  templateUrl: "./admin-entity-general-settings.component.html",
  styleUrls: ["./admin-entity-general-settings.component.scss"],
  imports: [
    MatButtonModule,
    MatInputModule,
    FormsModule,
    MatTabsModule,
    ReactiveFormsModule,
    MatTooltipModule,
    BasicAutocompleteComponent,
    MatCheckboxModule,
    MatTableModule,
    MatOptionModule,
    MatSelectModule,
    MatExpansionModule,
    HelpButtonComponent,
    AnonymizeOptionsComponent,
    FaIconComponent,
    IconComponent,
    ConditionalColorConfigComponent,
    HintBoxComponent,
    EntityFieldSelectComponent,
  ],
})
export class AdminEntityGeneralSettingsComponent {
  private fb = inject(FormBuilder);
  private adminEntityService = inject(AdminEntityService);

  entityConstructor = input.required<EntityConstructor>();
  generalSettings = input.required<EntityConfig>();
  showPIIDetailsInput = input<boolean>(false);

  generalSettingsChange = output<EntityConfig>();

  hasImageFields = computed(() =>
    Array.from(this.entityConstructor().schema.values()).some(
      (field) => field.dataType === PhotoDatatype.dataType,
    ),
  );

  showTooltipDetails = linkedSignal(
    () => !!this.generalSettings().toBlockDetailsAttributes,
  );

  showPIIDetails = linkedSignal(
    () => this.showPIIDetailsInput() || !!this.generalSettings()?.hasPII,
  );

  isConditionalColor = linkedSignal(() => {
    const color = this.generalSettings().color;
    return Array.isArray(color) && color.length > 0;
  });

  private readonly selectedStringAttributes = linkedSignal<string[]>(
    () => this.generalSettings().toStringAttributes ?? [],
  );

  toStringAttributesOptions = computed<SimpleDropdownValue[]>(() => {
    const selectedKeys = this.selectedStringAttributes();
    const allSchemaOptions = Array.from(
      this.entityConstructor().schema.entries(),
    )
      .filter(
        ([, field]) =>
          [
            StringDatatype.dataType,
            NumberDatatype.dataType,
            ConfigurableEnumDatatype.dataType,
            DateOnlyDatatype.dataType,
          ].includes(field.dataType) && field.label,
      )
      .map(([key, field]) => ({ value: key, label: field.label }));

    return [
      ...selectedKeys
        .map((key) => allSchemaOptions.find((o) => o.value === key))
        .filter(Boolean),
      ...allSchemaOptions.filter((o) => !selectedKeys.includes(o.value)),
    ];
  });

  fieldAnonymizationDataSource = computed(() => {
    if (!this.showPIIDetails()) return undefined;
    const fields = Array.from(this.entityConstructor().schema.entries())
      .filter(([, field]) => !field.isInternalField)
      .map(([key, field]) => ({ key, label: field.label, field }));
    return new MatTableDataSource(fields);
  });

  readonly basicSettingsForm = this.fb.group({
    label: [null as string | null, Validators.required],
    labelPlural: [null as string | null],
    icon: [null as string | null],
    color: [null as any],
    toStringAttributes: [null as string[] | null],
    hasPII: [null as boolean | null],
    enableUserAccounts: [null as boolean | null],
    toBlockDetailsAttributes: this.fb.group({
      title: [null as string | null],
      image: [{ value: null as string | null, disabled: true }],
      fields: [[] as string[]],
    }),
  });

  readonly iconControl = this.basicSettingsForm.get("icon") as FormControl<
    string | null
  >;

  constructor() {
    effect(() => {
      const settings = this.generalSettings();
      this.basicSettingsForm.patchValue(
        {
          label: settings.label,
          labelPlural: settings.labelPlural,
          icon: settings.icon,
          color: settings.color as EntityConfig["color"] | null,
          toStringAttributes: settings.toStringAttributes,
          hasPII: settings.hasPII,
          enableUserAccounts: settings.enableUserAccounts,
          toBlockDetailsAttributes: {
            title: settings.toBlockDetailsAttributes?.title ?? null,
            image: settings.toBlockDetailsAttributes?.image ?? null,
            fields: settings.toBlockDetailsAttributes?.fields ?? [],
          },
        },
        { emitEvent: false },
      );

      const imageControl = this.basicSettingsForm.get(
        "toBlockDetailsAttributes.image",
      );
      if (this.hasImageFields()) {
        imageControl?.enable({ emitEvent: false });
      } else {
        imageControl?.disable({ emitEvent: false });
      }
    });

    effect((onCleanup) => {
      const sub = this.basicSettingsForm.valueChanges.subscribe(() => {
        const selectedKeys: string[] =
          this.basicSettingsForm.get("toStringAttributes").value ?? [];
        this.selectedStringAttributes.set(selectedKeys);
        this.generalSettingsChange.emit(
          this.basicSettingsForm.getRawValue() as unknown as EntityConfig,
        );
      });
      onCleanup(() => sub.unsubscribe());
    });
  }

  toggleAnonymizationTable(event: MatCheckboxChange) {
    this.showPIIDetails.set(event.checked);
    this.basicSettingsForm.get("hasPII").setValue(event.checked);
  }

  changeFieldAnonymization(
    fieldSchema: EntitySchemaField,
    newAnonymizationValue,
  ) {
    fieldSchema.anonymize = newAnonymizationValue;

    this.adminEntityService.updateSchemaField(
      this.entityConstructor(),
      this.fieldAnonymizationDataSource()!.data.find(
        (v) => v.field === fieldSchema,
      )!.key,
      fieldSchema,
    );
  }

  clearToBlockAttributes() {
    this.basicSettingsForm.get("toBlockDetailsAttributes").reset();
  }

  // Filter functions for app-entity-field-select
  hideNonTextFields = (field: EntitySchemaField): boolean =>
    field.dataType === "file" || field.dataType === "photo";

  showOnlyImageFields = (field: EntitySchemaField): boolean =>
    field.dataType !== PhotoDatatype.dataType;

  objectToLabel = (v: SimpleDropdownValue) => v?.label;
  objectToValue = (v: SimpleDropdownValue) => v?.value;

  isFormValid(): boolean {
    if (!this.basicSettingsForm.valid) {
      this.basicSettingsForm.markAllAsTouched();
      return false;
    }
    return true;
  }
}
<app-hint-box i18n>
  The settings here apply to the record type overall and take effect everywhere
  the record is displayed, including lists, forms and other views.
</app-hint-box>

<h2 i18n>General Settings of "{{ entityConstructor().label }}" Records</h2>

<form [formGroup]="basicSettingsForm">
  <mat-tab-group>
    <mat-tab label="Basics" i18n-label>
      <div class="grid-layout flex-grow margin-top-regular">
        <div class="entity-form-cell">
          <mat-form-field>
            <mat-label i18n>Label</mat-label>
            <input formControlName="label" matInput #formLabel />
          </mat-form-field>

          <mat-form-field floatLabel="always">
            <mat-label>
              <span i18n>Label (Plural)</span>
              &nbsp;
              <fa-icon
                icon="question-circle"
                matTooltip="Optionally you can define how multiple records of this record type should be called, e.g. in lists."
                i18n-matTooltip
              ></fa-icon>
            </mat-label>
            <input
              formControlName="labelPlural"
              matInput
              [placeholder]="formLabel.value"
            />
          </mat-form-field>

          <app-icon-input [control]="iconControl"></app-icon-input>

          <app-conditional-color-config
            formControlName="color"
            [entityConstructor]="entityConstructor()"
            [(isConditionalMode)]="isConditionalColor"
          ></app-conditional-color-config>

          <mat-checkbox formControlName="enableUserAccounts">
            <span i18n>Enable User Accounts</span>
            &nbsp;
            <fa-icon
              icon="question-circle"
              matTooltip="Check this if records of this type can have associated user accounts with login credentials. This allows assigning roles and managing access."
              i18n-matTooltip
            ></fa-icon>
          </mat-checkbox>
        </div>

        <div class="entity-form-cell">
          <mat-form-field>
            <mat-label>
              <span i18n>Generated Title of Record</span>
              &nbsp;
              <fa-icon
                icon="question-circle"
                matTooltip="Select the fields that should be used (in that order) to generate a simple name/title for a record. This generated title is used in previews, search and for form fields that allow to select a record of this type. (Only text fields can be used here)"
                i18n-matTooltip
              ></fa-icon>
            </mat-label>
            <app-basic-autocomplete
              formControlName="toStringAttributes"
              #formDataType
              [options]="toStringAttributesOptions()"
              [optionToString]="objectToLabel"
              [valueMapper]="objectToValue"
              [multi]="true"
              [reorder]="true"
            ></app-basic-autocomplete>
          </mat-form-field>

          <!-- Entity Block Tooltip Configuration Section -->
          <mat-expansion-panel
            [(expanded)]="showTooltipDetails"
            style="margin: 0.5em"
          >
            <mat-expansion-panel-header>
              <mat-panel-title>
                <span i18n>Tooltip Configuration</span>
                <fa-icon
                  icon="question-circle"
                  class="margin-left-small"
                  matTooltip="Customise the details shown when hovering over record type in lists and forms."
                  i18n-matTooltip
                ></fa-icon>
              </mat-panel-title>
            </mat-expansion-panel-header>

            <mat-form-field
              formGroupName="toBlockDetailsAttributes"
              floatLabel="always"
            >
              <mat-label>
                <span i18n>Title Field</span>
                &nbsp;
                <fa-icon
                  icon="question-circle"
                  matTooltip="Select the main field to display as the tooltip header. This should be a descriptive field like 'name' or 'title'."
                  i18n-matTooltip
                ></fa-icon>
              </mat-label>
              <app-entity-field-select
                formControlName="title"
                [entityType]="entityConstructor()"
                [hideOption]="hideNonTextFields"
              ></app-entity-field-select>
            </mat-form-field>

            <mat-form-field
              formGroupName="toBlockDetailsAttributes"
              floatLabel="always"
            >
              <mat-label>
                <span i18n>Additional Detail Fields</span>
                &nbsp;
                <fa-icon
                  icon="question-circle"
                  matTooltip="Select additional fields to display in the tooltip. These will be shown below the title and provide extra context about the record."
                  i18n-matTooltip
                ></fa-icon>
              </mat-label>
              <app-entity-field-select
                formControlName="fields"
                [entityType]="entityConstructor()"
                [multi]="true"
                [hideOption]="hideNonTextFields"
              ></app-entity-field-select>
            </mat-form-field>

            <mat-form-field
              formGroupName="toBlockDetailsAttributes"
              floatLabel="always"
            >
              <mat-label>
                <span i18n>Image Field (Optional)</span>
                &nbsp;
                <fa-icon
                  icon="question-circle"
                  [matTooltip]="
                    hasImageFields()
                      ? 'Optional: Select an image field to display a photo in the tooltip. Only fields that contain images (like profile photos) can be selected here.'
                      : 'No image fields are available for this record type.'
                  "
                  i18n-matTooltip
                ></fa-icon>
              </mat-label>
              <app-entity-field-select
                formControlName="image"
                [entityType]="entityConstructor()"
                [hideOption]="showOnlyImageFields"
                placeholder=""
              ></app-entity-field-select>
              @if (!hasImageFields()) {
                <mat-hint i18n>
                  This record type has no image fields. Add fields with dataType
                  'file' or 'photo' to enable this option.
                </mat-hint>
              }
            </mat-form-field>

            <button
              mat-stroked-button
              class="margin-top-regular"
              (click)="clearToBlockAttributes()"
              i18n
            >
              Reset tooltip settings
            </button>
          </mat-expansion-panel>
        </div>
      </div>
    </mat-tab>

    <!--
        ADVANCED SETTINGS
        -->
    <mat-tab
      label="Configure PII / Anonymization"
      i18n-label
      [disabled]="false"
    >
      <div class="margin-top-regular overflow-table">
        <div class="flex-row align-center">
          <mat-checkbox
            [checked]="showPIIDetails()"
            (change)="toggleAnonymizationTable($event)"
            i18n
            >Has personal information (PII)</mat-checkbox
          >
          <app-help-button
            text="If the fields of this record type contain personal, sensitive information you can mark this here. Checking this box enables the 'anonymization' feature. This allows users to anonymize records of this type instead of just archiving or deleting them."
            i18n-text
          ></app-help-button>
        </div>

        @if (showPIIDetails()) {
          <div>
            <p i18n>
              Configure how records of this type can be anonymized. Users can
              "anonymize" a record as an alternative to just archiving it
              (keeping all personal details) or deleting it (losing any trace of
              the record, even in reports). Select below which fields can be
              "retained" to keep some limited data for statistical reporting and
              which fields have to be removed because they contain personal
              details.
            </p>
            <mat-table [dataSource]="fieldAnonymizationDataSource()">
              <ng-container matColumnDef="label">
                <mat-cell *matCellDef="let anonymizeData">
                  {{ anonymizeData.label }}
                </mat-cell>
              </ng-container>
              <ng-container matColumnDef="field">
                <mat-cell *matCellDef="let anonymizeData">
                  <app-anonymize-options
                    [value]="anonymizeData.field.anonymize"
                    (valueChange)="
                      changeFieldAnonymization(anonymizeData.field, $event)
                    "
                  ></app-anonymize-options>
                </mat-cell>
              </ng-container>
              <mat-row
                *matRowDef="let row; columns: ['label', 'field']"
              ></mat-row>
            </mat-table>
          </div>
        }
      </div>
    </mat-tab>
  </mat-tab-group>
</form>

./admin-entity-general-settings.component.scss

@use "mixins/grid-layout";

.grid-layout {
  @include grid-layout.adaptive(
    $min-block-width: 250px,
    $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%;
  }
}
.overflow-table {
  max-height: calc(100vh - 250px);
  overflow: auto;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""