Skip to content

Commit

Permalink
feat(Admin UI): configure general settings of entity types like label…
Browse files Browse the repository at this point in the history
… and icon (#2328)

see #2120 

Co-authored-by: Sebastian Leidig <sebastian@aam-digital.com>
  • Loading branch information
Abhinegi2 and sleidig authored Apr 5, 2024
1 parent b53deb3 commit e2f0945
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<h2 i18n>General Settings of "{{ entityConstructor.label }}" Records</h2>
<p i18n>
The settings here apply to the entity type overall and take effect everywhere
the entity is displayed, including lists, forms and other views.
</p>

<form [formGroup]="form">
<mat-tab-group formGroupName="basicSettings">
<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 i18n>
Label (Plural)
<fa-icon
icon="question-circle"
matTooltip="Optionally you can define how multiple records of this entity should be called, e.g. in lists."
i18n-matTooltip
></fa-icon>
</mat-label>
<input
formControlName="labelPlural"
matInput
[placeholder]="formLabel.value"
/>
</mat-form-field>

<mat-form-field floatLabel="always">
<mat-label i18n>
Icon
<fa-icon
icon="question-circle"
matTooltip="The icon to represent this entity type, e.g. when displaying records as a small preview block. [see fontawesome.com/icons]"
i18n-matTooltip
></fa-icon>
</mat-label>
<input
formControlName="icon"
matInput
[placeholder]="formLabel.value"
/>
</mat-form-field>
</div>

<div class="entity-form-cell">
<mat-form-field floatLabel="always">
<mat-label i18n>
Generated Title of Record
<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."
i18n-matTooltip
></fa-icon>
</mat-label>
<input
formControlName="toStringAttributes"
matInput
[placeholder]="formLabel.value"
/>
</mat-form-field>
</div>
</div>
</mat-tab>

<!--
ADVANCED SETTINGS
-->
<mat-tab label="Configure PII / Anonymization" i18n-label [disabled]="true">
<div class="grid-layout margin-top-regular"></div>
</mat-tab>
</mat-tab-group>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use "../../../../../styles/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%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatInputModule } from "@angular/material/input";
import { MatTabsModule } from "@angular/material/tabs";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatTooltipModule } from "@angular/material/tooltip";
import { AdminEntityGeneralSettingsComponent } from "./admin-entity-general-settings.component";
import { Entity, EntityConstructor } from "../../../entity/model/entity";
import { FaDynamicIconComponent } from "../../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";

describe("AdminEntityGeneralSettingsComponent", () => {
let component: AdminEntityGeneralSettingsComponent;
let fixture: ComponentFixture<AdminEntityGeneralSettingsComponent>;

// Mock EntityConstructor
const mockEntityConstructor: EntityConstructor = class MockEntity extends Entity {
constructor(public id?: string) {
super(id);
}
};

mockEntityConstructor.prototype.label = "Child";
mockEntityConstructor.prototype.labelPlural = "Childrens";
mockEntityConstructor.prototype.icon = "child";
mockEntityConstructor.prototype.toStringAttributes = [
"firstname",
"lastname",
];

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
MatButtonModule,
MatInputModule,
MatTabsModule,
MatSlideToggleModule,
MatTooltipModule,
FaDynamicIconComponent,
FontAwesomeTestingModule,
ReactiveFormsModule,
FormsModule,
],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AdminEntityGeneralSettingsComponent);
component = fixture.componentInstance;
component.entityConstructor = mockEntityConstructor;
component.config = { label: "Test Label" };
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
Component,
EventEmitter,
Input,
Output,
OnChanges,
SimpleChanges,
OnInit,
} from "@angular/core";
import { EntityConstructor } from "../../../entity/model/entity";
import { MatButtonModule } from "@angular/material/button";
import { DialogCloseComponent } from "../../../common-components/dialog-close/dialog-close.component";
import { MatInputModule } from "@angular/material/input";
import { ErrorHintComponent } from "../../../common-components/error-hint/error-hint.component";
import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { NgIf } from "@angular/common";
import { MatTabsModule } from "@angular/material/tabs";
import { MatSlideToggleModule } 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 { EntityConfig } from "../../../entity/entity-config";

@Component({
selector: "app-admin-entity-general-settings",
standalone: true,
templateUrl: "./admin-entity-general-settings.component.html",
styleUrl: "./admin-entity-general-settings.component.scss",
imports: [
MatButtonModule,
DialogCloseComponent,
MatInputModule,
ErrorHintComponent,
FormsModule,
NgIf,
MatTabsModule,
MatSlideToggleModule,
ReactiveFormsModule,
FontAwesomeModule,
MatTooltipModule,
BasicAutocompleteComponent,
],
})
export class AdminEntityGeneralSettingsComponent implements OnChanges, OnInit {
@Input() entityConstructor: EntityConstructor;
@Output() generalSettingsChange: EventEmitter<EntityConfig> =
new EventEmitter<EntityConfig>();
@Input() config: EntityConfig;
form: FormGroup;
basicSettingsForm: FormGroup;

constructor(private fb: FormBuilder) {}

ngOnInit(): void {
this.init();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.config) {
this.init();
}
}

private init() {
this.basicSettingsForm = this.fb.group({
label: [this.config.label, Validators.required],
labelPlural: [this.config.labelPlural],
icon: [this.config.icon, Validators.required],
toStringAttributes: [this.config.toStringAttributes, Validators.required],
});
this.form = this.fb.group({
basicSettings: this.basicSettingsForm,
});

this.form.valueChanges.subscribe((value) => {
this.emitStaticDetails(); // Optionally, emit the initial value
});
}

emitStaticDetails() {
const toStringAttributesControl =
this.basicSettingsForm.get("toStringAttributes");
let toStringAttributesValue = toStringAttributesControl.value;
// Convert toStringAttributesValue to an array if it's a string
if (typeof toStringAttributesValue === "string") {
toStringAttributesValue = toStringAttributesValue
.split(",")
.map((item) => item.trim());
}

// Update the form control with the modified value
toStringAttributesControl.setValue(toStringAttributesValue, {
emitEvent: false,
});

// Emit the updated value
this.generalSettingsChange.emit(this.basicSettingsForm.getRawValue());
}
}
6 changes: 5 additions & 1 deletion src/app/core/admin/admin-entity/admin-entity.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@
></app-admin-entity-list>
}
@case ("general") {
<div>COMING SOON</div>
<app-admin-entity-general-settings
[entityConstructor]="entityConstructor"
(generalSettingsChange)="configEntitySettings = $event"
[config]="configEntitySettings"
></app-admin-entity-general-settings>
}
}
</div>
Expand Down
66 changes: 41 additions & 25 deletions src/app/core/admin/admin-entity/admin-entity.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe("AdminEntityComponent", () => {
@DatabaseEntity("AdminTest")
class AdminTestEntity extends Entity {
static readonly ENTITY_TYPE = "AdminTest";
static label = "Admin Test";

@DatabaseField({ label: "Name" }) name: string;
}
Expand Down Expand Up @@ -112,25 +113,31 @@ describe("AdminEntityComponent", () => {

it("should reset all entity schema changes on cancel", () => {
// simulate schema changes done through the field config popup form
AdminTestEntity.schema.set("testField", { label: "New field" });
AdminTestEntity.schema.set("testCancelField", { label: "New field" });
const existingField = AdminTestEntity.schema.get("name");
const originalLabelOfExisting = existingField.label;
existingField.label = "Changed existing field";
component.configEntitySettings = { label: "new entity label" };

component.cancel();

expect(AdminTestEntity.schema.has("testField")).toBeFalse();
expect(AdminTestEntity.schema.has("testCancelField")).toBeFalse();
expect(AdminTestEntity.schema.get("name").label).toBe(
originalLabelOfExisting,
);
expect(AdminTestEntity.label).toBe("Admin Test");
console.log(AdminTestEntity, JSON.stringify(AdminTestEntity.schema));

// cleanup
AdminTestEntity.schema.delete("testCancelField");
});

it("should save schema and view config", fakeAsync(() => {
const newSchemaField: EntitySchemaField = {
_isCustomizedField: true,
label: "New field",
};
AdminTestEntity.schema.set("testField", newSchemaField);
AdminTestEntity.schema.set("testSaveField", newSchemaField);

const newPanel: Panel = {
title: "New Panel",
Expand All @@ -139,28 +146,37 @@ describe("AdminEntityComponent", () => {
component.configDetailsView.panels.push(newPanel);

component.save();
tick();

const expectedViewConfig = {
entity: AdminTestEntity.ENTITY_TYPE,
panels: [{ title: "Tab 1", components: [] }, newPanel],
};
const expectedEntityConfig = {
attributes: jasmine.objectContaining({
testField: newSchemaField,
}),
};

const actual: Config = entityMapper.get(
Config.ENTITY_TYPE,
Config.CONFIG_KEY,
) as Config;
expect(actual.data[viewConfigId]).toEqual({
component: "EntityDetails",
config: expectedViewConfig,
fixture.whenStable().then(() => {
const expectedViewConfig = {
entity: AdminTestEntity.ENTITY_TYPE,
panels: [{ title: "Tab 1", components: [] }, newPanel],
};
const expectedEntityConfig = {
label: "AdminTest",
labelPlural: "AdminTest",
icon: "child",
toStringAttributes: ["entityId"],
attributes: jasmine.objectContaining({
testSaveField: newSchemaField,
}),
};

const actual: Config = entityMapper.get(
Config.ENTITY_TYPE,
Config.CONFIG_KEY,
) as Config;
expect(actual.data[viewConfigId]).toEqual({
component: "EntityDetails",
config: expectedViewConfig,
});
expect(actual.data[entityConfigId]).toEqual(expectedEntityConfig);
expect(component.configEntitySettings).toEqual(
component.entityConstructor,
);

// cleanup:
AdminTestEntity.schema.delete("testSaveField");
});
expect(actual.data[entityConfigId]).toEqual(expectedEntityConfig);

AdminTestEntity.schema.delete("testField");
tick();
}));
});
Loading

0 comments on commit e2f0945

Please sign in to comment.