src/app/core/admin/admin-entity/admin-entity-general-settings/admin-entity-general-settings.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-admin-entity-general-settings |
| standalone | true |
| imports |
MatButtonModule
MatInputModule
MatTabsModule
MatTooltipModule
MatCheckboxModule
MatTableModule
MatOptionModule
MatSelectModule
MatExpansionModule
|
| styleUrls | ./admin-entity-general-settings.component.scss |
| templateUrl | ./admin-entity-general-settings.component.html |
MatButtonModule
MatInputModule
FormsModule
MatTabsModule
ReactiveFormsModule
MatTooltipModule
BasicAutocompleteComponent
MatCheckboxModule
MatTableModule
MatOptionModule
MatSelectModule
MatExpansionModule
HelpButtonComponent
AnonymizeOptionsComponent
FaIconComponent
IconComponent
ConditionalColorConfigComponent
HintBoxComponent
EntityFieldSelectComponent
Properties |
Methods |
Inputs |
Outputs |
constructor()
|
| entityConstructor | |
Type : EntityConstructor
|
|
| Required : true | |
| generalSettings | |
Type : EntityConfig
|
|
| Required : true | |
| showPIIDetailsInput | |
Type : boolean
|
|
Default value : false
|
|
| generalSettingsChange | |
Type : EntityConfig
|
|
| changeFieldAnonymization | |||||||||
changeFieldAnonymization(fieldSchema: EntitySchemaField, newAnonymizationValue: unknown)
|
|||||||||
|
Parameters :
Returns :
void
|
| clearToBlockAttributes |
clearToBlockAttributes()
|
|
Returns :
void
|
| isFormValid |
isFormValid()
|
|
Returns :
boolean
|
| toggleAnonymizationTable | ||||||
toggleAnonymizationTable(event: MatCheckboxChange)
|
||||||
|
Parameters :
Returns :
void
|
| 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,
)
|
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>
<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>
<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>
<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>
<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>
<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>
<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;
}