From 8ede7179f7d99460b26ff4d0a9123a8d1239e255 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 22 Jan 2024 09:28:33 +0100 Subject: [PATCH 1/9] fix: Sentry errors (#2185) --- .../core/session/auth/keycloak/keycloak-auth.service.ts | 7 +++---- .../todos/todos-dashboard/todos-dashboard.component.ts | 2 +- src/main.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts index ebd9eee790..81158e0435 100644 --- a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts +++ b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts @@ -104,10 +104,9 @@ export class KeycloakAuthService { }); } - getUserinfo(): Promise { - return this.keycloak - .getKeycloakInstance() - .loadUserInfo() as Promise; + async getUserinfo(): Promise { + const user = await this.keycloak.getKeycloakInstance().loadUserInfo(); + return user as KeycloakUser; } setEmail(email: string): Observable { diff --git a/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts b/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts index a296f3293d..6ae14739b1 100644 --- a/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts +++ b/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts @@ -45,7 +45,7 @@ export class TodosDashboardComponent extends DashboardWidget { filterEntries = (todo: Todo) => { return ( !todo.completed && - todo.assignedTo.includes(this.currentUser.value.getId()) && + todo.assignedTo.includes(this.currentUser.value?.getId()) && moment(todo.startDate).isSameOrBefore(moment(), "days") ); }; diff --git a/src/main.ts b/src/main.ts index e0f9205ade..778a435e01 100644 --- a/src/main.ts +++ b/src/main.ts @@ -53,7 +53,7 @@ if (appLang === DEFAULT_LANGUAGE) { function bootstrap(): Promise { // Dynamically load the main module after the language has been initialized return AppSettings.initRuntimeSettings() - .catch((err) => logger.error(err)) + .catch((err) => logger.debug(err)) .then(() => import("./app/app.module")) .then((m) => platformBrowserDynamic().bootstrapModule(m.AppModule)) .catch((err) => logger.error(err)); From 240a34d94da5f830db00a5d175ebeca6c689b9c4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 22 Jan 2024 22:51:46 +0100 Subject: [PATCH 2/9] fix: show new health an observation records immediately (#2187) fixes #2186 --- .../health-checkup.component.html | 6 -- .../health-checkup.component.ts | 73 ++++++++++--------- src/app/core/config/config-fix.ts | 18 +++-- .../historical-data.component.spec.ts | 2 +- .../historical-data.component.ts | 43 +++++++---- 5 files changed, 80 insertions(+), 62 deletions(-) delete mode 100644 src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.html diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.html b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.html deleted file mode 100644 index abc3e9a1d7..0000000000 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts index c862d46050..9212b4a6b8 100644 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts @@ -5,40 +5,53 @@ import { Child } from "../../model/child"; import { FormFieldConfig } from "../../../../core/common-components/entity-form/FormConfig"; import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator"; import { EntitiesTableComponent } from "../../../../core/common-components/entities-table/entities-table.component"; +import { RelatedEntitiesComponent } from "../../../../core/entity-details/related-entities/related-entities.component"; +import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service"; +import { EntityRegistry } from "../../../../core/entity/database-entity.decorator"; +import { ScreenWidthObserver } from "../../../../utils/media/screen-size-observer.service"; @DynamicComponent("HealthCheckup") @Component({ selector: "app-health-checkup", - templateUrl: "./health-checkup.component.html", + templateUrl: + "../../../../core/entity-details/related-entities/related-entities.component.html", imports: [EntitiesTableComponent], standalone: true, }) -export class HealthCheckupComponent implements OnInit { - records: HealthCheck[] = []; +export class HealthCheckupComponent + extends RelatedEntitiesComponent + implements OnInit +{ + @Input() entity: Child; + property = "child"; entityCtr = HealthCheck; /** - * Column Description for the SubentityRecordComponent + * Column Description * The Date-Column needs to be transformed to apply the MathFormCheck in the SubentityRecordComponent * BMI is rounded to 2 decimal digits */ - @Input() config: { columns: FormFieldConfig[] } = { - columns: [ - { id: "date" }, - { id: "height" }, - { id: "weight" }, - { - id: "bmi", - label: $localize`:Table header, Short for Body Mass Index:BMI`, - viewComponent: "ReadonlyFunction", - description: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, - additional: (entity: HealthCheck) => this.getBMI(entity), - }, - ], - }; - @Input() entity: Child; + override _columns: FormFieldConfig[] = [ + { id: "date" }, + { id: "height" }, + { id: "weight" }, + { + id: "bmi", + label: $localize`:Table header, Short for Body Mass Index:BMI`, + viewComponent: "ReadonlyFunction", + description: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, + additional: (entity: HealthCheck) => this.getBMI(entity), + }, + ]; - constructor(private childrenService: ChildrenService) {} + constructor( + private childrenService: ChildrenService, + entityMapper: EntityMapperService, + entityRegistry: EntityRegistry, + screenWidthObserver: ScreenWidthObserver, + ) { + super(entityMapper, entityRegistry, screenWidthObserver); + } private getBMI(healthCheck: HealthCheck): string { const bmi = healthCheck.bmi; @@ -49,16 +62,11 @@ export class HealthCheckupComponent implements OnInit { } } - ngOnInit() { - return this.loadData(); - } - - generateNewRecordFactory() { + override createNewRecordFactory() { return () => { - const newHC = new HealthCheck(Date.now().toString()); + const newHC = new HealthCheck(); - // use last entered date as default, otherwise today's date - newHC.date = this.records.length > 0 ? this.records[0].date : new Date(); + newHC.date = new Date(); newHC.child = this.entity.getId(); return newHC; @@ -68,11 +76,10 @@ export class HealthCheckupComponent implements OnInit { /** * implements the health check loading from the children service and is called in the onInit() */ - async loadData() { - this.records = await this.childrenService.getHealthChecksOfChild( - this.entity.getId(), - ); - this.records.sort( + override async initData() { + this.data = ( + await this.childrenService.getHealthChecksOfChild(this.entity.getId()) + ).sort( (a, b) => (b.date ? b.date.valueOf() : 0) - (a.date ? a.date.valueOf() : 0), ); diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 43439959a6..eb0268f442 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -640,14 +640,16 @@ export const defaultJsonConfig = { { "title": "", "component": "HistoricalDataComponent", - "config": [ - "date", - { "id": "isMotivatedDuringClass", "visibleFrom": "lg" }, - { "id": "isParticipatingInClass", "visibleFrom": "lg" }, - { "id": "isInteractingWithOthers", "visibleFrom": "lg" }, - { "id": "doesHomework", "visibleFrom": "lg" }, - { "id": "asksQuestions", "visibleFrom": "lg" }, - ] + "config": { + "columns": [ + "date", + { "id": "isMotivatedDuringClass", "visibleFrom": "lg" }, + { "id": "isParticipatingInClass", "visibleFrom": "lg" }, + { "id": "isInteractingWithOthers", "visibleFrom": "lg" }, + { "id": "doesHomework", "visibleFrom": "lg" }, + { "id": "asksQuestions", "visibleFrom": "lg" }, + ] + } } ] }, diff --git a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts index 8472a47f59..ace2206941 100644 --- a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts +++ b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts @@ -46,7 +46,7 @@ describe("HistoricalDataComponent", () => { await component.ngOnInit(); - expect(component.entries).toEqual([relatedData]); + expect(component.data).toEqual([relatedData]); expect(mockHistoricalDataService.getHistoricalDataFor).toHaveBeenCalledWith( component.entity.getId(), ); diff --git a/src/app/features/historical-data/historical-data/historical-data.component.ts b/src/app/features/historical-data/historical-data/historical-data.component.ts index 2904ed908f..faf99d28a1 100644 --- a/src/app/features/historical-data/historical-data/historical-data.component.ts +++ b/src/app/features/historical-data/historical-data/historical-data.component.ts @@ -2,9 +2,13 @@ import { Component, Input, OnInit } from "@angular/core"; import { HistoricalEntityData } from "../model/historical-entity-data"; import { Entity } from "../../../core/entity/model/entity"; import { HistoricalDataService } from "../historical-data.service"; -import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component"; +import { RelatedEntitiesComponent } from "../../../core/entity-details/related-entities/related-entities.component"; +import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; +import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; +import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; +import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig"; /** * A general component that can be included on a entity details page through the config. @@ -14,26 +18,37 @@ import { EntitiesTableComponent } from "../../../core/common-components/entities @DynamicComponent("HistoricalDataComponent") @Component({ selector: "app-historical-data", - template: ` `, + templateUrl: + "../../../core/entity-details/related-entities/related-entities.component.html", imports: [EntitiesTableComponent], standalone: true, }) -export class HistoricalDataComponent implements OnInit { +export class HistoricalDataComponent + extends RelatedEntitiesComponent + implements OnInit +{ @Input() entity: Entity; - @Input() config: FormFieldConfig[] = []; - entries: HistoricalEntityData[]; + property = "relatedEntity"; + entityCtr = HistoricalEntityData; - entityConstructor = HistoricalEntityData; + /** @deprecated use @Input() columns instead */ + @Input() set config(value: FormFieldConfig[]) { + if (Array.isArray(value)) { + this.columns = value; + } + } - constructor(private historicalDataService: HistoricalDataService) {} + constructor( + private historicalDataService: HistoricalDataService, + entityMapper: EntityMapperService, + entityRegistry: EntityRegistry, + screenWidthObserver: ScreenWidthObserver, + ) { + super(entityMapper, entityRegistry, screenWidthObserver); + } - async ngOnInit() { - this.entries = await this.historicalDataService.getHistoricalDataFor( + override async initData() { + this.data = await this.historicalDataService.getHistoricalDataFor( this.entity.getId(), ); } From f15fecb15b94ff22de73f46c62f435b1a42641a6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 23 Jan 2024 12:02:09 +0100 Subject: [PATCH 3/9] fix: correctly reset special fields like username when cancelling forms (#2191) closes #2189 --- .../entity-form/entity-form.service.spec.ts | 30 +++++++++++++++++++ .../entity-form/entity-form.service.ts | 10 +++---- .../form/form.component.spec.ts | 2 +- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/app/core/common-components/entity-form/entity-form.service.spec.ts b/src/app/core/common-components/entity-form/entity-form.service.spec.ts index 777745b9b0..2bfa0fdacc 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.spec.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.spec.ts @@ -145,6 +145,36 @@ describe("EntityFormService", () => { expect(unsavedChanges.pending).toBeFalse(); }); + it("should reset form on cancel, including special fields with getter", async () => { + class MockEntity extends Entity { + @DatabaseField() simpleField = "original"; + + @DatabaseField() get getterField(): string { + return this._getterValue; + } + set getterField(value) { + this._getterValue = value; + } + private _getterValue: string = "original value"; + + @DatabaseField() emptyField; + } + + const formFields = ["simpleField", "getterField", "emptyField"]; + const mockEntity = new MockEntity(); + const formGroup = service.createFormGroup(formFields, mockEntity); + + formGroup.get("simpleField").setValue("new"); + formGroup.get("getterField").setValue("new value"); + formGroup.get("emptyField").setValue("value"); + + service.resetForm(formGroup, mockEntity); + + expect(formGroup.get("simpleField").value).toBe("original"); + expect(formGroup.get("getterField").value).toBe("original value"); + expect(formGroup.get("emptyField").value).toBeUndefined(); + }); + it("should reset state once navigation happens", async () => { const router = TestBed.inject(Router); router.resetConfig([{ path: "test", component: NotFoundComponent }]); diff --git a/src/app/core/common-components/entity-form/entity-form.service.ts b/src/app/core/common-components/entity-form/entity-form.service.ts index 2cf4092961..a78ba87f6c 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.ts @@ -7,7 +7,6 @@ import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; import { DynamicValidatorsService } from "./dynamic-form-validators/dynamic-validators.service"; import { EntityAbility } from "../../permissions/ability/entity-ability"; import { InvalidFormFieldError } from "./invalid-form-field.error"; -import { omit } from "lodash-es"; import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; import { ActivationStart, Router } from "@angular/router"; import { Subscription } from "rxjs"; @@ -245,11 +244,10 @@ export class EntityFormService { } resetForm(form: EntityForm, entity: E) { - // Patch form with values from the entity - form.patchValue(entity as any); - // Clear values that are not yet present on the entity - const newKeys = Object.keys(omit(form.controls, Object.keys(entity))); - newKeys.forEach((key) => form.get(key).setValue(null)); + for (const key of Object.keys(form.controls)) { + form.get(key).setValue(entity[key]); + } + form.markAsPristine(); this.unsavedChanges.pending = false; } diff --git a/src/app/core/entity-details/form/form.component.spec.ts b/src/app/core/entity-details/form/form.component.spec.ts index ecdf23af5d..5b4de66acf 100644 --- a/src/app/core/entity-details/form/form.component.spec.ts +++ b/src/app/core/entity-details/form/form.component.spec.ts @@ -91,6 +91,6 @@ describe("FormComponent", () => { component.form.get("name").setValue("my name"); component.cancelClicked(); - expect(component.form.get("name")).toHaveValue(null); + expect(component.form.get("name").value).toBeUndefined(); }); }); From 048de5f79833bba7eacb697608634afe7c169101 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 23 Jan 2024 19:23:58 +0100 Subject: [PATCH 4/9] fix: plus button in notes / tasks of child correctly opens popup again (#2192) Closes #2188 Co-authored-by: Sebastian Leidig --- .../activities-overview.component.ts | 2 +- ...activity-attendance-section.component.html | 2 +- .../attendance-details.component.html | 2 +- .../notes-related-to-entity.component.html | 8 +-- .../notes-related-to-entity.component.spec.ts | 2 +- .../notes-related-to-entity.component.ts | 51 +++++++------------ .../entities-table.component.html | 2 +- .../entities-table.component.spec.ts | 2 +- .../entities-table.component.ts | 7 ++- .../related-entities.component.ts | 15 ++---- .../entity-list/entity-list.component.html | 2 +- .../matching-entities.component.html | 2 +- .../todos-related-to-entity.component.html | 6 +-- .../todos-related-to-entity.component.ts | 28 +++++----- 14 files changed, 58 insertions(+), 73 deletions(-) diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts index c161e5dc30..e052eefc74 100644 --- a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts +++ b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts @@ -20,7 +20,7 @@ export class ActivitiesOverviewComponent extends RelatedEntitiesComponent implements OnInit { - entityType = RecurringActivity.ENTITY_TYPE; + entityCtr = RecurringActivity; property = "linkedGroups"; titleColumn: FormFieldConfig = { diff --git a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html index fca5ed4321..48d8b8c8e2 100644 --- a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html +++ b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html @@ -22,7 +22,7 @@ [records]="records" [customColumns]="columns" clickMode="none" - (rowClick)="showDetails($event)" + (entityClick)="showDetails($event)" [getBackgroundColor]="getBackgroundColor" [editable]="false" > diff --git a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.html b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.html index 72fae5b170..3260af21b9 100644 --- a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.html +++ b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.html @@ -118,7 +118,7 @@

[records]="entity.events" [customColumns]="eventsColumns" clickMode="none" - (rowClick)="showEventDetails($event)" + (entityClick)="showEventDetails($event)" [editable]="false" > diff --git a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.html b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.html index b6c73f57d4..5d46913513 100644 --- a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.html +++ b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.html @@ -1,11 +1,11 @@ diff --git a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts index 2683bdc19f..23bdb8f412 100644 --- a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts +++ b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.spec.ts @@ -143,6 +143,6 @@ describe("NotesRelatedToEntityComponent", () => { expect(mockChildrenService.getNotesRelatedTo).toHaveBeenCalledWith( component.entity.getId(true), ); - expect(component.records).toEqual([n1, n2, n3]); + expect(component.data).toEqual([n1, n2, n3]); })); }); diff --git a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts index c0718ce383..d0e0951070 100644 --- a/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts +++ b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Note } from "../model/note"; import { NoteDetailsComponent } from "../note-details/note-details.component"; import { ChildrenService } from "../../children/children.service"; @@ -13,66 +13,62 @@ import { ChildSchoolRelation } from "../../children/model/childSchoolRelation"; import { EntityDatatype } from "../../../core/basic-datatypes/entity/entity.datatype"; import { EntityArrayDatatype } from "../../../core/basic-datatypes/entity-array/entity-array.datatype"; import { asArray } from "../../../utils/utils"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { applyUpdate } from "../../../core/entity/model/entity-update"; import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component"; import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; -import { ColumnConfig } from "../../../core/common-components/entity-form/FormConfig"; -import { DataFilter } from "../../../core/filter/filters/filters"; +import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig"; +import { RelatedEntitiesComponent } from "../../../core/entity-details/related-entities/related-entities.component"; +import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; +import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; /** * The component that is responsible for listing the Notes that are related to a certain entity. */ @DynamicComponent("NotesRelatedToEntity") @DynamicComponent("NotesOfChild") // for backward compatibility -@UntilDestroy() @Component({ selector: "app-notes-related-to-entity", templateUrl: "./notes-related-to-entity.component.html", imports: [EntitiesTableComponent], standalone: true, }) -export class NotesRelatedToEntityComponent implements OnInit { - @Input() entity: Entity; - records: Array; - - @Input() columns: ColumnConfig[] = [ +export class NotesRelatedToEntityComponent extends RelatedEntitiesComponent { + override entityCtr = Note; + override _columns: FormFieldConfig[] = [ { id: "date", visibleFrom: "xs" }, { id: "subject", visibleFrom: "xs" }, { id: "text", visibleFrom: "md" }, { id: "authors", visibleFrom: "md" }, { id: "warningLevel", visibleFrom: "md" }, ]; - @Input() filter: DataFilter = {}; /** * returns the color for a note; passed to the entity subrecord component * @param note note to get color for */ getColor = (note: Note) => note?.getColor(); - newRecordFactory: () => Note; - - entityConstructor = Note; + newRecordFactory = this.generateNewRecordFactory(); constructor( private childrenService: ChildrenService, - private entityMapper: EntityMapperService, private formDialog: FormDialogService, private filterService: FilterService, - ) {} + entityMapper: EntityMapperService, + entities: EntityRegistry, + screenWidthOberserver: ScreenWidthObserver, + ) { + super(entityMapper, entities, screenWidthOberserver); + } - ngOnInit(): void { + override ngOnInit() { if (this.entity.getType() === Child.ENTITY_TYPE) { // When displaying notes for a child, use attendance color highlighting this.getColor = (note: Note) => note?.getColorForId(this.entity.getId()); } - this.newRecordFactory = this.generateNewRecordFactory(); - this.initNotesOfEntity(); - this.listenToEntityUpdates(); + return super.ngOnInit(); } - private async initNotesOfEntity() { - this.records = await this.childrenService + override async initData() { + this.data = await this.childrenService .getNotesRelatedTo(this.entity.getId(true)) .then((notes: Note[]) => { notes.sort((a, b) => { @@ -86,15 +82,6 @@ export class NotesRelatedToEntityComponent implements OnInit { }); } - private listenToEntityUpdates() { - this.entityMapper - .receiveUpdates(this.entityConstructor) - .pipe(untilDestroyed(this)) - .subscribe((next) => { - this.records = applyUpdate(this.records, next); - }); - } - generateNewRecordFactory() { return () => { const newNote = new Note(Date.now().toString()); diff --git a/src/app/core/common-components/entities-table/entities-table.component.html b/src/app/core/common-components/entities-table/entities-table.component.html index dd5bfac392..0824ac5463 100644 --- a/src/app/core/common-components/entities-table/entities-table.component.html +++ b/src/app/core/common-components/entities-table/entities-table.component.html @@ -64,7 +64,7 @@ diff --git a/src/app/core/common-components/entities-table/entities-table.component.spec.ts b/src/app/core/common-components/entities-table/entities-table.component.spec.ts index 96f54b3d6d..8a3aecba04 100644 --- a/src/app/core/common-components/entities-table/entities-table.component.spec.ts +++ b/src/app/core/common-components/entities-table/entities-table.component.spec.ts @@ -183,7 +183,7 @@ describe("EntitiesTableComponent", () => { it("should notify when an entity is clicked", (done) => { const child = new Child(); - component.rowClick.subscribe((entity) => { + component.entityClick.subscribe((entity) => { expect(entity).toEqual(child); done(); }); diff --git a/src/app/core/common-components/entities-table/entities-table.component.ts b/src/app/core/common-components/entities-table/entities-table.component.ts index e28dfbc4ca..6cfb7034ba 100644 --- a/src/app/core/common-components/entities-table/entities-table.component.ts +++ b/src/app/core/common-components/entities-table/entities-table.component.ts @@ -202,7 +202,10 @@ export class EntitiesTableComponent implements AfterViewInit { idForSavingPagination: string; @Input() clickMode: "popup" | "navigate" | "none" = "popup"; - @Output() rowClick: EventEmitter = new EventEmitter(); + /** + * Emits the entity being clicked in the table - or the newly created entity from the "create" button. + */ + @Output() entityClick = new EventEmitter(); /** * BULK SELECT @@ -262,7 +265,7 @@ export class EntitiesTableComponent implements AfterViewInit { } this.showEntity(row.record); - this.rowClick.emit(row.record); + this.entityClick.emit(row.record); } showEntity(entity: T) { diff --git a/src/app/core/entity-details/related-entities/related-entities.component.ts b/src/app/core/entity-details/related-entities/related-entities.component.ts index f3a2bea2e4..33c242cf07 100644 --- a/src/app/core/entity-details/related-entities/related-entities.component.ts +++ b/src/app/core/entity-details/related-entities/related-entities.component.ts @@ -34,7 +34,9 @@ export class RelatedEntitiesComponent implements OnInit { @Input() entity: Entity; /** entity type of the related entities to be displayed */ - @Input() entityType: string; + @Input() set entityType(value: string) { + this.entityCtr = this.entityRegistry.get(value) as EntityConstructor; + } /** * property name of the related entities (type given in this.entityType) that holds the entity id @@ -80,17 +82,8 @@ export class RelatedEntitiesComponent implements OnInit { } protected async initData() { - this.entityCtr = this.entityRegistry.get( - this.entityType, - ) as EntityConstructor; this.isArray = isArrayProperty(this.entityCtr, this.property); - this.data = (await this.entityMapper.loadType(this.entityType)).filter( - (e) => - this.isArray - ? e[this.property]?.includes(this.entity.getId()) - : e[this.property] === this.entity.getId(), - ); this.filter = { ...this.filter, [this.property]: this.isArray @@ -98,7 +91,7 @@ export class RelatedEntitiesComponent implements OnInit { : this.entity.getId(), }; - this.data = (await this.entityMapper.loadType(this.entityType)).filter( + this.data = (await this.entityMapper.loadType(this.entityCtr)).filter( (e) => this.isArray ? e[this.property]?.includes(this.entity.getId()) diff --git a/src/app/core/entity-list/entity-list/entity-list.component.html b/src/app/core/entity-list/entity-list/entity-list.component.html index 1ad7d5965c..1ecb56c651 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.html +++ b/src/app/core/entity-list/entity-list/entity-list.component.html @@ -108,7 +108,7 @@

{{ title }}

[customColumns]="columns" [editable]="false" [clickMode]="clickMode" - (rowClick)="onRowClick($event)" + (entityClick)="onRowClick($event)" [columnsToDisplay]="columnsToDisplay" [filter]="filterObj" [sortBy]="defaultSort" diff --git a/src/app/features/matching-entities/matching-entities/matching-entities.component.html b/src/app/features/matching-entities/matching-entities/matching-entities.component.html index b8a2cad3cc..485a291c11 100644 --- a/src/app/features/matching-entities/matching-entities/matching-entities.component.html +++ b/src/app/features/matching-entities/matching-entities/matching-entities.component.html @@ -106,7 +106,7 @@ [customColumns]="side.columns" [editable]="false" clickMode="none" - (rowClick)="side.selectMatch($event)" + (entityClick)="side.selectMatch($event)" [filter]="side.filterObj" > diff --git a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.html b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.html index e504e1a8c0..9364d0c241 100644 --- a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.html +++ b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.html @@ -1,10 +1,10 @@ diff --git a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts index db781e9dd9..cc585d69d2 100644 --- a/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts +++ b/src/app/features/todos/todos-related-to-entity/todos-related-to-entity.component.ts @@ -1,6 +1,5 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig"; -import { Entity } from "../../../core/entity/model/entity"; import { Todo } from "../model/todo"; import { DatabaseIndexingService } from "../../../core/entity/database-indexing/database-indexing.service"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; @@ -10,6 +9,10 @@ import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { FormsModule } from "@angular/forms"; import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component"; import { DataFilter } from "../../../core/filter/filters/filters"; +import { RelatedEntitiesComponent } from "../../../core/entity-details/related-entities/related-entities.component"; +import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; +import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; +import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; @DynamicComponent("TodosRelatedToEntity") @Component({ @@ -19,12 +22,9 @@ import { DataFilter } from "../../../core/filter/filters/filters"; standalone: true, imports: [EntitiesTableComponent, MatSlideToggleModule, FormsModule], }) -export class TodosRelatedToEntityComponent implements OnInit { - entries: Todo[]; - entityCtr = Todo; - - @Input() entity: Entity; - @Input() columns: FormFieldConfig[] = [ +export class TodosRelatedToEntityComponent extends RelatedEntitiesComponent { + override entityCtr = Todo; + override _columns: FormFieldConfig[] = [ { id: "deadline" }, { id: "subject" }, { id: "startDate" }, @@ -38,10 +38,8 @@ export class TodosRelatedToEntityComponent implements OnInit { /** the property name of the Todo that contains the ids referencing related entities */ private referenceProperty: keyof Todo & string = "relatedEntities"; - showInactive: boolean; - // TODO: filter by current user as default in UX? --> custom filter component or some kind of variable interpolation? - filter: DataFilter = { isActive: true }; + override filter: DataFilter = { isActive: true }; backgroundColorFn = (r: Todo) => { if (!r.isActive) { return "#e0e0e0"; @@ -53,7 +51,11 @@ export class TodosRelatedToEntityComponent implements OnInit { constructor( private formDialog: FormDialogService, private dbIndexingService: DatabaseIndexingService, + entityMapper: EntityMapperService, + entities: EntityRegistry, + screenWidthOberserver: ScreenWidthObserver, ) { + super(entityMapper, entities, screenWidthOberserver); // TODO: move this generic index creation into schema this.dbIndexingService.generateIndexOnProperty( "todo_index", @@ -63,8 +65,8 @@ export class TodosRelatedToEntityComponent implements OnInit { ); } - async ngOnInit() { - this.entries = await this.loadDataFor(this.entity.getId(true)); + override async initData() { + this.data = await this.loadDataFor(this.entity.getId(true)); } private async loadDataFor(entityId: string): Promise { From 037873c555d41430fd7087ab9eeac52cc0f3ce80 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 25 Jan 2024 11:00:20 +0100 Subject: [PATCH 5/9] refactor(core): migrate navigationMenu config format (#2193) see #2066 --- src/app/core/config/config-fix.ts | 22 ++++----- src/app/core/config/config.service.spec.ts | 47 +++++++++++++++++++ src/app/core/config/config.service.ts | 31 ++++++++++++ src/app/core/ui/navigation/menu-item.ts | 25 ++++------ .../navigation-menu-config.interface.ts | 12 ----- .../navigation/navigation.component.spec.ts | 30 +++--------- .../navigation/navigation.component.ts | 22 ++++----- 7 files changed, 114 insertions(+), 75 deletions(-) delete mode 100644 src/app/core/ui/navigation/navigation-menu-config.interface.ts diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index eb0268f442..2e29bc48d1 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -13,57 +13,57 @@ export const defaultJsonConfig = { "navigationMenu": { "items": [ { - "name": $localize`:Menu item:Dashboard`, + "label": $localize`:Menu item:Dashboard`, "icon": "home", "link": "/" }, { - "name": $localize`:Menu item:Children`, + "label": $localize`:Menu item:Children`, "icon": "child", "link": "/child" }, { - "name": $localize`:Menu item:Schools`, + "label": $localize`:Menu item:Schools`, "icon": "university", "link": "/school" }, { - "name": $localize`:Menu item:Attendance`, + "label": $localize`:Menu item:Attendance`, "icon": "calendar-check", "link": "/attendance" }, { - "name": $localize`:Menu item:Notes`, + "label": $localize`:Menu item:Notes`, "icon": "file-alt", "link": "/note" }, { - "name": $localize`:Menu item:Tasks`, + "label": $localize`:Menu item:Tasks`, "icon": "tasks", "link": "/todo" }, { - "name": $localize`:Menu item:Import`, + "label": $localize`:Menu item:Import`, "icon": "file-import", "link": "/import" }, { - "name": $localize`:Menu item:Users`, + "label": $localize`:Menu item:Users`, "icon": "users", "link": "/user" }, { - "name": $localize`:Menu item:Reports`, + "label": $localize`:Menu item:Reports`, "icon": "line-chart", "link": "/report" }, { - "name": $localize`:Menu item:Help`, + "label": $localize`:Menu item:Help`, "icon": "question", "link": "/help" }, { - "name": $localize`:Menu item:Admin`, + "label": $localize`:Menu item:Admin`, "icon": "wrench", "link": "/admin" }, diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index ec1c1df199..8b28ecbc27 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -6,6 +6,7 @@ import { firstValueFrom, Subject } from "rxjs"; import { UpdatedEntity } from "../entity/model/entity-update"; import { EntityConfig } from "../entity/entity-config"; import { FieldGroup } from "../entity-details/form/field-group"; +import { NavigationMenuConfig } from "../ui/navigation/menu-item"; describe("ConfigService", () => { let service: ConfigService; @@ -280,4 +281,50 @@ describe("ConfigService", () => { expect(result2.config.columns[0]).toEqual(expectedFieldConfig); })); + + it("should migrate menu item format", fakeAsync(() => { + const config = new Config(); + const oldFormat = { + items: [ + { + name: "one", + icon: "child", + link: "/one", + }, + { + name: "two", + icon: "child", + link: "/two", + }, + ], + }; + const newFormat = { + items: [ + { + label: "one", + icon: "child", + link: "/one", + }, + { + label: "two", + icon: "child", + link: "/two", + }, + ], + }; + + config.data = { navigationMenu: oldFormat }; + updateSubject.next({ entity: config, type: "update" }); + tick(); + const actualFromOld = + service.getConfig("navigationMenu"); + expect(actualFromOld).toEqual(newFormat); + + config.data = { navigationMenu: newFormat }; + updateSubject.next({ entity: config, type: "update" }); + tick(); + const actualFromNew = + service.getConfig("navigationMenu"); + expect(actualFromNew).toEqual(newFormat); + })); }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index a4366becc4..7c6ce7558d 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -6,6 +6,7 @@ import { LatestEntityLoader } from "../entity/latest-entity-loader"; import { shareReplay } from "rxjs/operators"; import { EntitySchemaField } from "../entity/schema/entity-schema-field"; import { FieldGroup } from "../entity-details/form/field-group"; +import { MenuItem } from "../ui/navigation/menu-item"; /** * Access dynamic app configuration retrieved from the database @@ -56,6 +57,7 @@ export class ConfigService extends LatestEntityLoader { migrateEntityAttributesWithId, migrateFormHeadersIntoFieldGroups, migrateFormFieldConfigView2ViewComponent, + migrateMenuItemConfig, ]; const newConfig = JSON.parse(JSON.stringify(config), (_that, rawValue) => { @@ -157,3 +159,32 @@ const migrateFormFieldConfigView2ViewComponent: ConfigMigration = ( } return configPart; }; + +const migrateMenuItemConfig: ConfigMigration = (key, configPart) => { + if (key !== "navigationMenu") { + return configPart; + } + + const oldItems: ( + | { + name: string; + icon: string; + link: string; + } + | MenuItem + )[] = configPart.items; + + configPart.items = oldItems.map((item) => { + if (item.hasOwnProperty("name")) { + return { + label: item["name"], + icon: item.icon, + link: item.link, + }; + } else { + return item; + } + }); + + return configPart; +}; diff --git a/src/app/core/ui/navigation/menu-item.ts b/src/app/core/ui/navigation/menu-item.ts index b8ad56dee8..f3194f6f66 100644 --- a/src/app/core/ui/navigation/menu-item.ts +++ b/src/app/core/ui/navigation/menu-item.ts @@ -1,20 +1,3 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - /** * Structure for menu items to be displayed. */ @@ -32,3 +15,11 @@ export interface MenuItem { */ link: string; } + +/** + * Object specifying overall navigation menu + * as stored in the config database + */ +export interface NavigationMenuConfig { + items: MenuItem[]; +} diff --git a/src/app/core/ui/navigation/navigation-menu-config.interface.ts b/src/app/core/ui/navigation/navigation-menu-config.interface.ts deleted file mode 100644 index 4fa5762ed4..0000000000 --- a/src/app/core/ui/navigation/navigation-menu-config.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Object specifying overall navigation menu - * as stored in the config database - */ -export interface NavigationMenuConfig { - items: { - name: string; - icon: string; - link: string; - }[]; - find(arg0: (item: { link: string }) => boolean); -} diff --git a/src/app/core/ui/navigation/navigation/navigation.component.spec.ts b/src/app/core/ui/navigation/navigation/navigation.component.spec.ts index 38a8463c2f..e6dddc8d47 100644 --- a/src/app/core/ui/navigation/navigation/navigation.component.spec.ts +++ b/src/app/core/ui/navigation/navigation/navigation.component.spec.ts @@ -31,6 +31,7 @@ import { UserRoleGuard } from "../../../permissions/permission-guard/user-role.g import { Event, NavigationEnd, Router } from "@angular/router"; import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; import { EntityPermissionGuard } from "../../../permissions/permission-guard/entity-permission.guard"; +import { NavigationMenuConfig } from "../menu-item"; describe("NavigationComponent", () => { let component: NavigationComponent; @@ -73,28 +74,11 @@ describe("NavigationComponent", () => { expect(component).toBeTruthy(); }); - it("generates menu items from config", fakeAsync(() => { - const testConfig = { - items: [ - { name: "Dashboard", icon: "home", link: "/dashboard" }, - { name: "Children", icon: "child", link: "/child" }, - ], - }; - mockConfigService.getConfig.and.returnValue(testConfig); - mockConfigUpdated.next(null); - tick(); - - expect(component.menuItems).toEqual([ - { label: "Dashboard", icon: "home", link: "/dashboard" }, - { label: "Children", icon: "child", link: "/child" }, - ]); - })); - it("marks items that require admin rights", fakeAsync(() => { - const testConfig = { + const testConfig: NavigationMenuConfig = { items: [ - { name: "Dashboard", icon: "home", link: "/dashboard" }, - { name: "Children", icon: "child", link: "/child" }, + { label: "Dashboard", icon: "home", link: "/dashboard" }, + { label: "Children", icon: "child", link: "/child" }, ], }; mockRoleGuard.checkRoutePermissions.and.callFake(async (route: string) => { @@ -118,10 +102,10 @@ describe("NavigationComponent", () => { })); it("should add menu items where entity permissions are missing", fakeAsync(() => { - const testConfig = { + const testConfig: NavigationMenuConfig = { items: [ - { name: "Dashboard", icon: "home", link: "/dashboard" }, - { name: "Children", icon: "child", link: "/child" }, + { label: "Dashboard", icon: "home", link: "/dashboard" }, + { label: "Children", icon: "child", link: "/child" }, ], }; mockEntityGuard.checkRoutePermissions.and.callFake((route: string) => { diff --git a/src/app/core/ui/navigation/navigation/navigation.component.ts b/src/app/core/ui/navigation/navigation/navigation.component.ts index f756297612..b9f897da24 100644 --- a/src/app/core/ui/navigation/navigation/navigation.component.ts +++ b/src/app/core/ui/navigation/navigation/navigation.component.ts @@ -16,8 +16,7 @@ */ import { Component } from "@angular/core"; -import { MenuItem } from "../menu-item"; -import { NavigationMenuConfig } from "../navigation-menu-config.interface"; +import { MenuItem, NavigationMenuConfig } from "../menu-item"; import { ConfigService } from "../../../config/config.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NavigationEnd, Router, RouterLink } from "@angular/router"; @@ -48,8 +47,10 @@ import { RoutePermissionsService } from "../../../config/dynamic-routing/route-p export class NavigationComponent { /** The menu-item link (not the actual router link) that is currently active */ activeLink: string; + /** name of config array in the config json file */ private readonly CONFIG_ID = "navigationMenu"; + /** all menu items to be displayed */ public menuItems: MenuItem[] = []; @@ -113,16 +114,13 @@ export class NavigationComponent { * Load menu items from config file */ private async initMenuItemsFromConfig() { - const config: NavigationMenuConfig = - this.configService.getConfig(this.CONFIG_ID); - // TODO align interface {@link https://github.com/Aam-Digital/ndb-core/issues/2066} - const items: MenuItem[] = config.items.map(({ name, icon, link }) => ({ - label: name, - icon, - link, - })); - this.menuItems = - await this.routePermissionService.filterPermittedRoutes(items); + const config = this.configService.getConfig( + this.CONFIG_ID, + ); + + this.menuItems = await this.routePermissionService.filterPermittedRoutes( + config.items, + ); // re-select active menu item after menu has been fully initialized this.activeLink = this.computeActiveLink(location.pathname); From b1533b1a2bb304088908672471d97a872d19ff8d Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Thu, 25 Jan 2024 11:30:16 +0100 Subject: [PATCH 6/9] feat(permissions): filter fields in forms based on read/write permissions (#2180) closes #1912 Co-authored-by: Simon --- src/app/app.component.spec.ts | 4 +- .../entity-form/entity-form.service.spec.ts | 40 +++++++++++++++++++ .../entity-form/entity-form.service.ts | 28 +++++++++++-- .../entity-form/entity-form.component.spec.ts | 20 ++++++++++ .../entity-form/entity-form.component.ts | 28 +++++++++++++ .../entity-details/form/form.component.ts | 1 + .../ability/testing-entity-ability-factory.ts | 10 +++++ .../disable-entity-operation.directive.ts | 4 +- .../disabled-wrapper.component.ts | 35 +++++++++++----- src/app/utils/mocked-testing.module.ts | 9 +++++ 10 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 src/app/core/permissions/ability/testing-entity-ability-factory.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 950c3255ea..11b9035cba 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -41,11 +41,11 @@ describe("AppComponent", () => { fixture.detectChanges(); })); - afterEach(() => { + afterEach(waitForAsync(() => { environment.demo_mode = false; jasmine.DEFAULT_TIMEOUT_INTERVAL = intervalBefore; return TestBed.inject(Database).destroy(); - }); + })); it("should be created", () => { expect(component).toBeTruthy(); diff --git a/src/app/core/common-components/entity-form/entity-form.service.spec.ts b/src/app/core/common-components/entity-form/entity-form.service.spec.ts index 2bfa0fdacc..3a04a2b807 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.spec.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.spec.ts @@ -108,6 +108,46 @@ describe("EntityFormService", () => { expect(formGroup.valid).toBeTrue(); }); + it("should use create permissions to disable fields when creating a new entity", () => { + const formFields = [{ id: "name" }, { id: "dateOfBirth" }]; + TestBed.inject(EntityAbility).update([ + { subject: "Child", action: "read", fields: ["name", "dateOfBirth"] }, + { subject: "Child", action: "update", fields: ["name"] }, + { subject: "Child", action: "create", fields: ["dateOfBirth"] }, + ]); + + const formGroup = service.createFormGroup(formFields, new Child()); + + expect(formGroup.get("name").disabled).toBeTrue(); + expect(formGroup.get("dateOfBirth").enabled).toBeTrue(); + }); + + it("should always keep properties disabled if user does not have 'update' permissions for them", () => { + const formFields = [{ id: "name" }, { id: "dateOfBirth" }]; + TestBed.inject(EntityAbility).update([ + { subject: "Child", action: "read", fields: ["name", "dateOfBirth"] }, + { subject: "Child", action: "update", fields: ["name"] }, + ]); + + const child = new Child(); + child._rev = "foo"; // "not new" state + + const formGroup = service.createFormGroup(formFields, child); + + expect(formGroup.get("name").enabled).toBeTrue(); + expect(formGroup.get("dateOfBirth").disabled).toBeTrue(); + + formGroup.disable(); + + expect(formGroup.get("name").disabled).toBeTrue(); + expect(formGroup.get("dateOfBirth").disabled).toBeTrue(); + + formGroup.enable(); + + expect(formGroup.get("name").enabled).toBeTrue(); + expect(formGroup.get("dateOfBirth").disabled).toBeTrue(); + }); + it("should create a error if form is invalid", () => { const formFields = [{ id: "schoolId" }, { id: "start" }]; const formGroup = service.createFormGroup( diff --git a/src/app/core/common-components/entity-form/entity-form.service.ts b/src/app/core/common-components/entity-form/entity-form.service.ts index a78ba87f6c..85526a3975 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.ts @@ -112,11 +112,13 @@ export class EntityFormService { * @param formFields * @param entity * @param forTable + * @param withPermissionCheck if true, fields without 'update' permissions will stay disabled when enabling form */ public createFormGroup( formFields: ColumnConfig[], entity: T, forTable = false, + withPermissionCheck = true, ): EntityForm { const formConfig = {}; const copy = entity.copy(); @@ -130,10 +132,18 @@ export class EntityFormService { } const group = this.fb.group>(formConfig); - const sub = group.valueChanges.subscribe( + const valueChangesSubscription = group.valueChanges.subscribe( () => (this.unsavedChanges.pending = group.dirty), ); - this.subscriptions.push(sub); + this.subscriptions.push(valueChangesSubscription); + + if (withPermissionCheck) { + this.disableReadOnlyFormControls(group, entity); + const statusChangesSubscription = group.statusChanges + .pipe(filter((status) => status !== "DISABLED")) + .subscribe(() => this.disableReadOnlyFormControls(group, entity)); + this.subscriptions.push(statusChangesSubscription); + } return group; } @@ -194,6 +204,18 @@ export class EntityFormService { return newVal; } + private disableReadOnlyFormControls( + form: EntityForm, + entity: T, + ) { + const action = entity.isNew ? "create" : "update"; + Object.keys(form.controls).forEach((fieldId) => { + if (this.ability.cannot(action, entity, fieldId)) { + form.get(fieldId).disable({ onlySelf: true, emitEvent: false }); + } + }); + } + /** * This function applies the changes of the formGroup to the entity. * If the form is invalid or the entity does not pass validation after applying the changes, an error will be thrown. @@ -228,7 +250,7 @@ export class EntityFormService { } private checkFormValidity(form: EntityForm) { - // errors regarding invalid fields wont be displayed unless marked as touched + // errors regarding invalid fields won't be displayed unless marked as touched form.markAllAsTouched(); if (form.invalid) { throw new InvalidFormFieldError(); diff --git a/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts b/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts index c57994f075..b340b42772 100644 --- a/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts +++ b/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts @@ -7,6 +7,7 @@ import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; import { EntityFormService } from "../entity-form.service"; import { DateWithAge } from "../../../basic-datatypes/date-with-age/dateWithAge"; +import { EntityAbility } from "../../../permissions/ability/entity-ability"; describe("EntityFormComponent", () => { let component: EntityFormComponent; @@ -55,6 +56,25 @@ describe("EntityFormComponent", () => { expect(component).toBeTruthy(); }); + it("should remove fields without read permissions", async () => { + component.fieldGroups = [ + { fields: ["foo", "bar"] }, + { fields: ["name"] }, + { fields: ["birthday"] }, + ]; + + TestBed.inject(EntityAbility).update([ + { subject: "Child", action: "read", fields: ["foo", "name"] }, + ]); + + component.ngOnChanges({ entity: true, form: true } as any); + + expect(component.fieldGroups).toEqual([ + { fields: ["foo"] }, + { fields: ["name"] }, + ]); + }); + it("should not change anything if changed entity has same values as form", () => { return expectApplyChangesPopup( "not-shown", diff --git a/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts b/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts index 1d392e4447..81ab7eb210 100644 --- a/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts +++ b/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts @@ -16,6 +16,7 @@ import { Subscription } from "rxjs"; import moment from "moment"; import { EntityFieldEditComponent } from "../../entity-field-edit/entity-field-edit.component"; import { FieldGroup } from "../../../entity-details/form/field-group"; +import { EntityAbility } from "../../../permissions/ability/entity-ability"; /** * A general purpose form component for displaying and editing entities. @@ -60,9 +61,17 @@ export class EntityFormComponent constructor( private entityMapper: EntityMapperService, private confirmationDialog: ConfirmationDialogService, + private ability: EntityAbility, ) {} ngOnChanges(changes: SimpleChanges) { + if (this.fieldGroups) { + this.fieldGroups = this.filterFieldGroupsByPermissions( + this.fieldGroups, + this.entity, + ); + } + if (changes.entity && this.entity) { this.changesSubscription?.unsubscribe(); this.changesSubscription = this.entityMapper @@ -74,6 +83,7 @@ export class EntityFormComponent ) .subscribe(({ entity }) => this.applyChanges(entity)); } + if (changes.form && this.form) { this.initialFormValues = this.form.getRawValue(); this.disableForLockedEntity(); @@ -125,6 +135,24 @@ export class EntityFormComponent ); } + private filterFieldGroupsByPermissions( + fieldGroups: FieldGroup[], + entity: Entity, + ): FieldGroup[] { + return fieldGroups + .map((group) => { + group.fields = group.fields.filter((field) => + this.ability.can( + "read", + entity, + typeof field === "string" ? field : field.id, + ), + ); + return group; + }) + .filter((group) => group.fields.length > 0); + } + private entityEqualsFormValue(entityValue, formValue) { return ( (entityValue instanceof Date && diff --git a/src/app/core/entity-details/form/form.component.ts b/src/app/core/entity-details/form/form.component.ts index bc6e7d2da3..1071f27b70 100644 --- a/src/app/core/entity-details/form/form.component.ts +++ b/src/app/core/entity-details/form/form.component.ts @@ -52,6 +52,7 @@ export class FormComponent implements FormConfig, OnInit { [].concat(...this.fieldGroups.map((group) => group.fields)), this.entity, ); + if (!this.creatingNew) { this.form.disable(); } diff --git a/src/app/core/permissions/ability/testing-entity-ability-factory.ts b/src/app/core/permissions/ability/testing-entity-ability-factory.ts new file mode 100644 index 0000000000..5425812b3a --- /dev/null +++ b/src/app/core/permissions/ability/testing-entity-ability-factory.ts @@ -0,0 +1,10 @@ +import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; +import { EntityAbility } from "./entity-ability"; + +export const entityAbilityFactory = ( + entitySchemaService: EntitySchemaService, +) => { + let ability = new EntityAbility(entitySchemaService); + ability.update([{ subject: "all", action: "manage" }]); + return ability; +}; diff --git a/src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts b/src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts index 2548287941..458831a0be 100644 --- a/src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts +++ b/src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts @@ -26,12 +26,13 @@ export class DisableEntityOperationDirective { /** * These arguments are required to check whether the user has permissions to perform the operation. - * The operation property defines to what kind of operation a element belongs, e.g. OperationType.CREATE + * The operation property defines to what kind of operation an element belongs, e.g. OperationType.CREATE * The entity property defines for which kind of entity the operation will be performed, e.g. Child */ @Input("appDisabledEntityOperation") arguments: { operation: EntityAction; entity: EntitySubject; + field?: string; }; private wrapperComponent: ComponentRef; @@ -76,6 +77,7 @@ export class DisableEntityOperationDirective this.wrapperComponent.instance.elementDisabled = this.ability.cannot( this.arguments.operation, this.arguments.entity, + this.arguments.field, ); this.wrapperComponent.instance.ngAfterViewInit(); } diff --git a/src/app/core/permissions/permission-directive/disabled-wrapper.component.ts b/src/app/core/permissions/permission-directive/disabled-wrapper.component.ts index 359cacf3dc..b4e2599ee3 100644 --- a/src/app/core/permissions/permission-directive/disabled-wrapper.component.ts +++ b/src/app/core/permissions/permission-directive/disabled-wrapper.component.ts @@ -51,16 +51,31 @@ export class DisabledWrapperComponent implements AfterViewInit { constructor(private renderer: Renderer2) {} ngAfterViewInit() { - if (this.wrapper) { - const buttonElement = - this.wrapper.nativeElement.getElementsByTagName("button")[0]; - if (this.elementDisabled) { - this.renderer.addClass(buttonElement, "mat-button-disabled"); - this.renderer.setAttribute(buttonElement, "disabled", "true"); - } else { - this.renderer.removeAttribute(buttonElement, "disabled"); - this.renderer.removeClass(buttonElement, "mat-button-disabled"); - } + if (!this.wrapper) { + return; } + + const buttonElement = + this.wrapper.nativeElement.getElementsByTagName("button")[0]; + + if (!buttonElement) { + return; + } + + if (this.elementDisabled) { + this.disable(buttonElement); + } else { + this.enable(buttonElement); + } + } + + private enable(buttonElement: HTMLButtonElement) { + this.renderer.removeAttribute(buttonElement, "disabled"); + this.renderer.removeClass(buttonElement, "mat-button-disabled"); + } + + private disable(buttonElement: HTMLButtonElement) { + this.renderer.addClass(buttonElement, "mat-button-disabled"); + this.renderer.setAttribute(buttonElement, "disabled", "true"); } } diff --git a/src/app/utils/mocked-testing.module.ts b/src/app/utils/mocked-testing.module.ts index 2f6c6e0a6f..c5414eef6a 100644 --- a/src/app/utils/mocked-testing.module.ts +++ b/src/app/utils/mocked-testing.module.ts @@ -23,6 +23,9 @@ import { BehaviorSubject } from "rxjs"; import { CurrentUserSubject } from "../core/session/current-user-subject"; import { SessionInfo, SessionSubject } from "../core/session/auth/session-info"; import { TEST_USER } from "../core/user/demo-user-generator.service"; +import { EntityAbility } from "../core/permissions/ability/entity-ability"; +import { EntitySchemaService } from "../core/entity/schema/entity-schema.service"; +import { entityAbilityFactory } from "app/core/permissions/ability/testing-entity-ability-factory"; /** * Utility module that can be imported in test files or stories to have mock implementations of the SessionService @@ -72,9 +75,15 @@ export class MockedTestingModule { ): ModuleWithProviders { environment.session_type = SessionType.mock; const mockedEntityMapper = mockEntityMapper([...data]); + return { ngModule: MockedTestingModule, providers: [ + { + provide: EntityAbility, + useFactory: entityAbilityFactory, + deps: [EntitySchemaService], + }, { provide: EntityMapperService, useValue: mockedEntityMapper }, { provide: ConfigService, useValue: createTestingConfigService() }, { From f0fcdbcec9cd04024405b1180cf318dc82504f9f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 25 Jan 2024 17:19:35 +0100 Subject: [PATCH 7/9] fix: enabled unsafe-inline (#2194) Co-authored-by: Simon --- build/Dockerfile | 10 +++++----- build/default.conf | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index dcd783edeb..b4747058c3 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -86,11 +86,11 @@ ENV NOMINATIM_URL="https://nominatim.openstreetmap.org" # (also see Developer Documentation: https://aam-digital.github.io/ndb-core/documentation/additional-documentation/concepts/security.html) ENV CSP_REPORT_URI="https://o167951.ingest.sentry.io/api/1242399/security/" # overwrite the Content-Security-Policy rules (report-uri is added automatically) -## default includes all required whitelists for production server -## to disable any CSP blocking, set to "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'" -ENV CSP="default-src 'self' 'unsafe-eval' data: https://*.tile.openstreetmap.org/ https://matomo.aam-digital.org https://*.aam-digital.com https://api.github.com/repos/Aam-Digital/ https://sentry.io $CSP_REPORT_URI 'sha256-gtzIf+c+ujwirISvjI8lnwlaZwnMkh04eA9ZDBCd8TY='; style-src 'self' 'unsafe-inline'" -### 'sha256-gtzIf+c+ujwirISvjI8lnwlaZwnMkh04eA9ZDBCd8TY=' for index.html writing browser details -### 'unsafe-eval' required for pouchdb https://github.com/pouchdb/pouchdb/issues/7853#issuecomment-535020600 +# default includes all required whitelists for production server +# to disable any CSP blocking, set to "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'" +ENV CSP="default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob: https://*.tile.openstreetmap.org/ https://matomo.aam-digital.org https://*.aam-digital.com https://api.github.com/repos/Aam-Digital/ https://sentry.io $CSP_REPORT_URI; style-src 'self' 'unsafe-inline'" +# 'unsafe-eval' required for pouchdb https://github.com/pouchdb/pouchdb/issues/7853#issuecomment-535020600 +# TODO remove 'unsave-inline' and fix the reported issues # variables are inserted into the nginx config CMD envsubst '$$PORT $$COUCHDB_URL $$QUERY_URL $$NOMINATIM_URL $$CSP $$CSP_REPORT_URI' < /etc/nginx/templates/default.conf > /etc/nginx/conf.d/default.conf &&\ diff --git a/build/default.conf b/build/default.conf index 53f3d6c194..35e64bdcb4 100644 --- a/build/default.conf +++ b/build/default.conf @@ -10,7 +10,8 @@ server { root /usr/share/nginx/html; - add_header Content-Security-Policy-Report-Only "${CSP}; report-uri ${CSP_REPORT_URI}"; + add_header Content-Security-Policy-Report-Only "${CSP}; report-uri ${CSP_REPORT_URI}?ngsw-bypass=true"; + # ?ngsw-bypass prevents angular serviceworker to intercept and break CSP reporting (https://github.com/angular/angular/issues/31477) # TODO: consider adding `trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script';` CSP in future add_header X-Frame-Options: SAMEORIGIN; # only applies in older browsers, CSP frame-ancestors takes prevalence https://stackoverflow.com/a/40417609/1473411 From 9f8f15edf9f64cd28a2df2d79df37af0dd7336e7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 25 Jan 2024 18:00:11 +0100 Subject: [PATCH 8/9] fix(core): new unique-id validator (#2134) closes #1557 Co-authored-by: Simon --- .../admin-entity-field.component.ts | 12 +- .../string/edit-text/edit-text.component.html | 2 +- .../entity-field-edit.component.html | 2 +- .../dynamic-validators.service.spec.ts | 59 +++++-- .../dynamic-validators.service.ts | 152 ++++++++++++++---- .../form-validator-config.ts | 2 + .../entity-form/entity-form.service.ts | 16 +- .../entity-form/unique-id-validator.ts | 15 -- .../unique-id-validator.spec.ts | 41 +++++ .../unique-id-validator.ts | 25 +++ .../error-hint/error-hint.component.html | 4 +- .../error-hint/error-hint.component.ts | 3 - .../row-details/row-details.component.ts | 4 + src/app/core/user/user.ts | 2 +- 14 files changed, 259 insertions(+), 80 deletions(-) delete mode 100644 src/app/core/common-components/entity-form/unique-id-validator.ts create mode 100644 src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.spec.ts create mode 100644 src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts diff --git a/src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts b/src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts index 0ccea0ad79..c0c9152686 100644 --- a/src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts +++ b/src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.ts @@ -37,13 +37,13 @@ import { EntityDatatype } from "../../../basic-datatypes/entity/entity.datatype" import { EntityArrayDatatype } from "../../../basic-datatypes/entity-array/entity-array.datatype"; import { ConfigurableEnumService } from "../../../basic-datatypes/configurable-enum/configurable-enum.service"; import { EntityRegistry } from "../../../entity/database-entity.decorator"; -import { uniqueIdValidator } from "../../../common-components/entity-form/unique-id-validator"; import { AdminEntityService } from "../../admin-entity.service"; 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 { uniqueIdValidator } from "app/core/common-components/entity-form/unique-id-validator/unique-id-validator"; /** * Allows configuration of the schema of a single Entity field, like its dataType and labels. @@ -115,10 +115,12 @@ export class AdminEntityFieldComponent implements OnChanges { } private initSettings() { - this.fieldIdForm = this.fb.control(this.fieldId, [ - Validators.required, - uniqueIdValidator(Array.from(this.entityType.schema.keys())), - ]); + this.fieldIdForm = this.fb.control(this.fieldId, { + validators: [Validators.required], + asyncValidators: [ + uniqueIdValidator(Array.from(this.entityType.schema.keys())), + ], + }); this.additionalForm = this.fb.control(this.entitySchemaField.additional); this.schemaFieldsForm = this.fb.group({ diff --git a/src/app/core/basic-datatypes/string/edit-text/edit-text.component.html b/src/app/core/basic-datatypes/string/edit-text/edit-text.component.html index 2c096759b0..8dffdd07d0 100644 --- a/src/app/core/basic-datatypes/string/edit-text/edit-text.component.html +++ b/src/app/core/basic-datatypes/string/edit-text/edit-text.component.html @@ -1,7 +1,7 @@ {{ label }} - + diff --git a/src/app/core/common-components/entity-field-edit/entity-field-edit.component.html b/src/app/core/common-components/entity-field-edit/entity-field-edit.component.html index a6c3d3feae..5402a005bf 100644 --- a/src/app/core/common-components/entity-field-edit/entity-field-edit.component.html +++ b/src/app/core/common-components/entity-field-edit/entity-field-edit.component.html @@ -5,7 +5,7 @@ component: _field.editComponent, config: { formFieldConfig: _field, - formControl: form.get(_field.id), + formControl: form?.get(_field.id), entity: entity } }" diff --git a/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.spec.ts b/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.spec.ts index cbdda3fdbc..ff5be7cbe0 100644 --- a/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.spec.ts +++ b/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.spec.ts @@ -5,13 +5,29 @@ import { patternWithMessage, } from "./dynamic-validators.service"; import { FormValidatorConfig } from "./form-validator-config"; -import { UntypedFormControl, ValidatorFn } from "@angular/forms"; +import { + AsyncValidatorFn, + UntypedFormControl, + ValidatorFn, +} from "@angular/forms"; +import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service"; +import { User } from "../../../user/user"; describe("DynamicValidatorsService", () => { let service: DynamicValidatorsService; + let mockedEntityMapper: jasmine.SpyObj; + beforeEach(() => { - TestBed.configureTestingModule({}); + mockedEntityMapper = jasmine.createSpyObj("EntityMapperService", [ + "loadType", + ]); + + TestBed.configureTestingModule({ + providers: [ + { provide: EntityMapperService, useValue: mockedEntityMapper }, + ], + }); service = TestBed.inject(DynamicValidatorsService); }); @@ -19,21 +35,26 @@ describe("DynamicValidatorsService", () => { expect(service).toBeTruthy(); }); - function testValidator( - validator: ValidatorFn, + async function testValidator( + validator: ValidatorFn | AsyncValidatorFn, successState: any, failureState: any, ) { - const results = [successState, failureState].map((state) => { - const mockControl = new UntypedFormControl(state); - return validator(mockControl); - }); - expect(results[0]) + function dummyFormControl(state) { + const control = new UntypedFormControl(state); + control.markAsDirty(); + return control; + } + + const resultSuccess = await validator(dummyFormControl(successState)); + expect(resultSuccess) .withContext("Expected validator not to have errors") .toBeNull(); - expect(results[1]) + + const resultFailure = await validator(dummyFormControl(failureState)); + expect(resultFailure) .withContext("Expected validator to have errors") - .not.toBeNull(); + .toEqual(jasmine.any(Object)); } it("should load validators from the config", () => { @@ -41,7 +62,7 @@ describe("DynamicValidatorsService", () => { min: 9, pattern: "[a-z]*", }; - const validators = service.buildValidators(config); + const validators = service.buildValidators(config).validators; expect(validators).toHaveSize(2); testValidator(validators[0], 10, 8); testValidator(validators[1], "ab", "1"); @@ -55,7 +76,7 @@ describe("DynamicValidatorsService", () => { validEmail: true, pattern: "foo", }; - const validators = service.buildValidators(config); + const validators = service.buildValidators(config).validators; [ [10, 8], [8, 11], @@ -73,7 +94,7 @@ describe("DynamicValidatorsService", () => { message: "M", pattern: "[a-z]", }, - }); + }).validators; expect(validators).toHaveSize(1); const invalidForm = new UntypedFormControl("09"); const validationErrors = validators[0](invalidForm); @@ -83,6 +104,16 @@ describe("DynamicValidatorsService", () => { }), ); }); + + it("should build uniqueId async validator", async () => { + const config: FormValidatorConfig = { + uniqueId: "User", + }; + mockedEntityMapper.loadType.and.resolveTo([new User("existing id")]); + + const validators = service.buildValidators(config).asyncValidators; + await testValidator(validators[0], "new id", "existing id"); + }); }); describe("patternWithMessage", () => { diff --git a/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts b/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts index 9b949b73b6..0a770733fc 100644 --- a/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts +++ b/src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts @@ -1,9 +1,16 @@ import { Injectable } from "@angular/core"; import { DynamicValidator, FormValidatorConfig } from "./form-validator-config"; -import { AbstractControl, ValidatorFn, Validators } from "@angular/forms"; +import { + AbstractControl, + FormControl, + FormControlOptions, + ValidationErrors, + ValidatorFn, + Validators, +} from "@angular/forms"; import { LoggingService } from "../../../logging/logging.service"; - -type ValidatorFactory = (value: any, name: string) => ValidatorFn; +import { uniqueIdValidator } from "../unique-id-validator/unique-id-validator"; +import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service"; /** * creates a pattern validator that also carries a predefined @@ -47,23 +54,45 @@ export class DynamicValidatorsService { * given a value that serves as basis for the validation. * @private */ - private static validators: { - [key in DynamicValidator]: ValidatorFactory | null; - } = { - min: (value) => Validators.min(value as number), - max: (value) => Validators.max(value as number), - pattern: (value) => { - if (typeof value === "object") { - return patternWithMessage(value.pattern, value.message); - } else { - return Validators.pattern(value as string); + private getValidator( + key: DynamicValidator, + value: any, + ): + | { async?: false; fn: ValidatorFn } + | { + async: true; + fn: AsyncPromiseValidatorFn; } - }, - validEmail: (value) => (value ? Validators.email : null), - required: (value) => (value ? Validators.required : null), - }; + | null { + switch (key) { + case "min": + return { fn: Validators.min(value as number) }; + case "max": + return { fn: Validators.max(value as number) }; + case "pattern": + if (typeof value === "object") { + return { fn: patternWithMessage(value.pattern, value.message) }; + } else { + return { fn: Validators.pattern(value as string) }; + } + case "validEmail": + return value ? { fn: Validators.email } : null; + case "uniqueId": + return value ? this.buildUniqueIdValidator(value) : null; + case "required": + return value ? { fn: Validators.required } : null; + default: + this.loggingService.warn( + `Trying to generate validator ${key} but it does not exist`, + ); + return null; + } + } - constructor(private loggingService: LoggingService) {} + constructor( + private loggingService: LoggingService, + private entityMapper: EntityMapperService, + ) {} /** * Builds all validator functions that are part of the configuration object. @@ -76,24 +105,58 @@ export class DynamicValidatorsService { * [ Validators.required, Validators.max(5) ] * @see ValidatorFn */ - public buildValidators(config: FormValidatorConfig): ValidatorFn[] { - const validators: ValidatorFn[] = []; + public buildValidators(config: FormValidatorConfig): FormControlOptions { + const formControlOptions = { + validators: [], + asyncValidators: [], + }; + for (const key of Object.keys(config)) { - const factory = DynamicValidatorsService.validators[key]; - if (!factory) { - this.loggingService.warn( - `Trying to generate validator ${key} but it does not exist`, - ); - continue; - } - const validatorFn = factory(config[key], key); - if (validatorFn !== null) { - validators.push(validatorFn); + const validatorFn = this.getValidator( + key as DynamicValidator, + config[key], + ); + + if (validatorFn?.async) { + const validatorFnWithReadableErrors = (control) => + validatorFn + .fn(control) + .then((res) => this.addHumanReadableError(key, res)); + formControlOptions.asyncValidators.push(validatorFnWithReadableErrors); + } else if (validatorFn) { + const validatorFnWithReadableErrors = (control: FormControl) => + this.addHumanReadableError(key, validatorFn.fn(control)); + formControlOptions.validators.push(validatorFnWithReadableErrors); } - // A validator function of `null` is a legal case. For example - // { required : false } produces a `null` validator function + + // A validator function of `null` is a legal case, for which no validator function is added. + // For example `{ required : false }` produces a `null` validator function + } + + if (formControlOptions.asyncValidators.length > 0) { + (formControlOptions as FormControlOptions).updateOn = "blur"; + } + + return formControlOptions; + } + + private addHumanReadableError( + validatorType: string, + validationResult: ValidationErrors | null, + ): ValidationErrors { + if (!validationResult) { + return validationResult; } - return validators; + + validationResult[validatorType] = { + ...validationResult[validatorType], + errorMessage: this.descriptionForValidator( + validatorType, + validationResult[validatorType], + ), + }; + + return validationResult; } /** @@ -103,7 +166,7 @@ export class DynamicValidatorsService { * @param validator The validator to get the description for * @param validationValue The value associated with the validator */ - public descriptionForValidator( + private descriptionForValidator( validator: DynamicValidator | string, validationValue: any, ): string { @@ -126,6 +189,8 @@ export class DynamicValidatorsService { return $localize`Please enter a valid date`; case "isNumber": return $localize`Please enter a valid number`; + case "uniqueId": + return validationValue; default: this.loggingService.error( `No description defined for validator "${validator}": ${JSON.stringify( @@ -135,4 +200,23 @@ export class DynamicValidatorsService { throw $localize`Invalid input`; } } + + private buildUniqueIdValidator(value: string): { + async: true; + fn: AsyncPromiseValidatorFn; + } { + return { + fn: uniqueIdValidator(() => + this.entityMapper + .loadType(value) + // TODO: extend this to allow checking for any configurable property (e.g. Child.name rather than only id) + .then((entities) => entities.map((entity) => entity.getId(false))), + ), + async: true, + }; + } } + +export type AsyncPromiseValidatorFn = ( + control: FormControl, +) => Promise; diff --git a/src/app/core/common-components/entity-form/dynamic-form-validators/form-validator-config.ts b/src/app/core/common-components/entity-form/dynamic-form-validators/form-validator-config.ts index 5e05f974a5..edb1f843ee 100644 --- a/src/app/core/common-components/entity-form/dynamic-form-validators/form-validator-config.ts +++ b/src/app/core/common-components/entity-form/dynamic-form-validators/form-validator-config.ts @@ -11,6 +11,8 @@ export type DynamicValidator = | "required" /** type: boolean */ | "validEmail" + /** type: string = EntityType; check against existing ids of the entity type */ + | "uniqueId" /** type: string or regex */ | "pattern"; diff --git a/src/app/core/common-components/entity-form/entity-form.service.ts b/src/app/core/common-components/entity-form/entity-form.service.ts index 85526a3975..29e744aa17 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.ts @@ -1,5 +1,11 @@ import { Injectable } from "@angular/core"; -import { FormBuilder, FormGroup, ɵElement } from "@angular/forms"; +import { + FormBuilder, + FormControl, + FormControlOptions, + FormGroup, + ɵElement, +} from "@angular/forms"; import { ColumnConfig, FormFieldConfig, toFormFieldConfig } from "./FormConfig"; import { Entity, EntityConstructor } from "../../entity/model/entity"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; @@ -157,7 +163,7 @@ export class EntityFormService { * @private */ private addFormControlConfig( - formConfig: Object, + formConfig: { [key: string]: FormControl }, fieldConfig: ColumnConfig, entity: Entity, forTable: boolean, @@ -176,14 +182,16 @@ export class EntityFormService { ) { value = this.getDefaultValue(field); } - formConfig[field.id] = [value]; + const controlOptions: FormControlOptions = { nonNullable: true }; if (field.validators) { const validators = this.dynamicValidator.buildValidators( field.validators, ); - formConfig[field.id].push(validators); + Object.assign(controlOptions, validators); } + + formConfig[field.id] = new FormControl(value, controlOptions); } private getDefaultValue(schema: EntitySchemaField) { diff --git a/src/app/core/common-components/entity-form/unique-id-validator.ts b/src/app/core/common-components/entity-form/unique-id-validator.ts deleted file mode 100644 index 5a992bb85d..0000000000 --- a/src/app/core/common-components/entity-form/unique-id-validator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms"; - -export function uniqueIdValidator(existingIds: string[]): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value = control.value; - - if (existingIds.some((id) => id === value)) { - return { - uniqueId: $localize`:form field validation error:id already in use`, - }; - } - - return null; - }; -} diff --git a/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.spec.ts b/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.spec.ts new file mode 100644 index 0000000000..693801a199 --- /dev/null +++ b/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.spec.ts @@ -0,0 +1,41 @@ +import { waitForAsync } from "@angular/core/testing"; +import { uniqueIdValidator } from "./unique-id-validator"; +import { FormControl, ValidatorFn } from "@angular/forms"; + +describe("UniqueIdValidator", () => { + let validator: ValidatorFn; + + let demoIds; + let formControl: FormControl; + + beforeEach(waitForAsync(() => { + demoIds = ["id1", "id2", "id3"]; + formControl = new FormControl(); + validator = uniqueIdValidator(demoIds); + })); + + it("should validate new id", async () => { + formControl.setValue("new id"); + formControl.markAsDirty(); + const validationResult = await validator(formControl); + + expect(validationResult).toBeNull(); + }); + + it("should disallow already existing id", async () => { + formControl.setValue(demoIds[1]); + formControl.markAsDirty(); + const validationResult = await validator(formControl); + + expect(validationResult).toEqual({ uniqueId: jasmine.any(String) }); + }); + + it("should allow to keep unchanged value (to not refuse saving an existing entity with unchanged id)", async () => { + formControl = new FormControl(demoIds[1], { nonNullable: true }); + formControl.markAsDirty(); + const validationResult = await validator(formControl); + + expect(formControl.pristine).toBeFalse(); + expect(validationResult).toBeNull(); + }); +}); diff --git a/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts b/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts new file mode 100644 index 0000000000..90958b852c --- /dev/null +++ b/src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts @@ -0,0 +1,25 @@ +import { FormControl, ValidationErrors } from "@angular/forms"; +import { AsyncPromiseValidatorFn } from "../dynamic-form-validators/dynamic-validators.service"; + +export function uniqueIdValidator( + existingIds: string[] | (() => Promise), +): AsyncPromiseValidatorFn { + return async (control: FormControl): Promise => { + if (control.value === control.defaultValue) { + return null; + } + + const existingValues = Array.isArray(existingIds) + ? existingIds + : await existingIds(); + const value = control.value; + + if (existingValues.some((id) => id === value)) { + return { + uniqueId: $localize`:form field validation error:id already in use`, + }; + } + + return null; + }; +} diff --git a/src/app/core/common-components/error-hint/error-hint.component.html b/src/app/core/common-components/error-hint/error-hint.component.html index aeda1c0cea..3bd4b1147a 100644 --- a/src/app/core/common-components/error-hint/error-hint.component.html +++ b/src/app/core/common-components/error-hint/error-hint.component.html @@ -1,4 +1,4 @@ -
- {{ validatorService.descriptionForValidator(err.key, err.value) }} +
+ {{ err.value["errorMessage"] }}
diff --git a/src/app/core/common-components/error-hint/error-hint.component.ts b/src/app/core/common-components/error-hint/error-hint.component.ts index d1c7007a97..3612566ea0 100644 --- a/src/app/core/common-components/error-hint/error-hint.component.ts +++ b/src/app/core/common-components/error-hint/error-hint.component.ts @@ -1,6 +1,5 @@ import { Component, Input } from "@angular/core"; import { UntypedFormControl } from "@angular/forms"; -import { DynamicValidatorsService } from "../entity-form/dynamic-form-validators/dynamic-validators.service"; import { KeyValuePipe, NgForOf } from "@angular/common"; @Component({ @@ -12,6 +11,4 @@ import { KeyValuePipe, NgForOf } from "@angular/common"; }) export class ErrorHintComponent { @Input() form: UntypedFormControl; - - constructor(public validatorService: DynamicValidatorsService) {} } diff --git a/src/app/core/form-dialog/row-details/row-details.component.ts b/src/app/core/form-dialog/row-details/row-details.component.ts index dcebe47d03..2c47908c6e 100644 --- a/src/app/core/form-dialog/row-details/row-details.component.ts +++ b/src/app/core/form-dialog/row-details/row-details.component.ts @@ -65,6 +65,10 @@ export class RowDetailsComponent { @Inject(MAT_DIALOG_DATA) public data: DetailsComponentData, private formService: EntityFormService, ) { + this.init(data); + } + + private init(data: DetailsComponentData) { this.form = this.formService.createFormGroup(data.columns, data.entity); this.enableSaveWithoutChangesIfNew(data.entity); diff --git a/src/app/core/user/user.ts b/src/app/core/user/user.ts index 4c390db113..83d5e4538d 100644 --- a/src/app/core/user/user.ts +++ b/src/app/core/user/user.ts @@ -37,7 +37,7 @@ export class User extends Entity { /** username used for login and identification */ @DatabaseField({ label: $localize`:Label of username:Username`, - validators: { required: true }, + validators: { required: true, uniqueId: "User" }, }) set name(value: string) { if (this._name && value !== this._name) { From aab9dc153f2ef8d23301027b1ceec8794964eccc Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 25 Jan 2024 18:03:14 +0100 Subject: [PATCH 9/9] fix: updated translations --- src/assets/locale/messages.de.xlf | 110 ++++++++++++++-------------- src/assets/locale/messages.fr.xlf | 108 ++++++++++++++-------------- src/assets/locale/messages.it.xlf | 108 ++++++++++++++-------------- src/assets/locale/messages.xlf | 116 +++++++++++++++--------------- 4 files changed, 221 insertions(+), 221 deletions(-) diff --git a/src/assets/locale/messages.de.xlf b/src/assets/locale/messages.de.xlf index 2f16261a3f..46a143bd71 100644 --- a/src/assets/locale/messages.de.xlf +++ b/src/assets/locale/messages.de.xlf @@ -1158,7 +1158,7 @@ src/app/core/config/config-fix.ts - 739 + 741 @@ -2529,7 +2529,7 @@ src/app/core/config/config-fix.ts - 695 + 697 @@ -2735,7 +2735,7 @@ Panel title src/app/core/config/config-fix.ts - 723 + 725 @@ -2944,7 +2944,7 @@ src/app/core/config/config-fix.ts - 655 + 657 @@ -2957,7 +2957,7 @@ src/app/core/config/config-fix.ts - 789 + 791 @@ -3204,7 +3204,7 @@ src/app/core/config/config-fix.ts - 773 + 775 src/app/features/reporting/demo-report-config-generator.service.ts @@ -3312,7 +3312,7 @@ src/app/core/config/config-fix.ts - 797 + 799 @@ -3559,7 +3559,7 @@ 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 - >"/> + >"/> Beschreibung + >"/> Feld ID (schreibgeschützt) + >"/> Typ Details (Dropdown Optionen) + >"/> Typ Details (verknüpfter Datentyp) src/app/core/config/config-fix.ts - 710 + 712 src/app/features/reporting/demo-report-config-generator.service.ts @@ -4274,7 +4274,7 @@ src/app/core/config/config-fix.ts - 738 + 740 @@ -4283,7 +4283,7 @@ Table header, Short for Body Mass Index src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 32 + 40 src/app/core/config/config-fix.ts @@ -4296,7 +4296,7 @@ Tooltip for BMI info src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 34 + 42 @@ -4497,7 +4497,7 @@ 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 - >"/> + >"/> Bezeichnung (kurz) Label for the address of a child src/app/core/config/config-fix.ts - 744 + 746 src/app/core/config/config-fix.ts - 785 + 787 @@ -4554,7 +4554,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 748 + 750 @@ -4563,7 +4563,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 752 + 754 @@ -4572,7 +4572,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 756 + 758 @@ -4581,7 +4581,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 757 + 759 @@ -4590,7 +4590,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 761 + 763 @@ -4599,7 +4599,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 765 + 767 @@ -4611,7 +4611,7 @@ src/app/core/config/config-fix.ts - 777 + 779 @@ -4620,7 +4620,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 781 + 783 @@ -4629,7 +4629,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 793 + 795 @@ -4638,7 +4638,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 806 + 808 @@ -4647,7 +4647,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 807 + 809 @@ -4656,7 +4656,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 812 + 814 @@ -4665,7 +4665,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 813 + 815 @@ -4674,7 +4674,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 818 + 820 @@ -4683,7 +4683,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 819 + 821 @@ -4692,7 +4692,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 824 + 826 @@ -4701,7 +4701,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 825 + 827 @@ -4710,7 +4710,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 830 + 832 @@ -4719,7 +4719,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 831 + 833 @@ -4728,7 +4728,7 @@ Label of user phone src/app/core/config/config-fix.ts - 839 + 841 @@ -4974,7 +4974,7 @@ Muss größer als sein src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 112 + 175 @@ -4982,7 +4982,7 @@ Darf nicht größer als sein src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 114 + 177 @@ -4990,7 +4990,7 @@ Format inkorrekt src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 119 + 182 @@ -4998,7 +4998,7 @@ Dieses Feld ist erforderlich src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 122 + 185 @@ -5006,7 +5006,7 @@ Bitte eine korrekte E-Mail eingeben src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 124 + 187 @@ -5014,7 +5014,7 @@ Bitte ein korrektes Datum eingeben src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 126 + 189 @@ -5022,7 +5022,7 @@ Bitte eine gültige Zahl eintragen src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 128 + 191 @@ -5030,7 +5030,7 @@ Ungültige Eingabe src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 135 + 200 @@ -5038,7 +5038,7 @@ Sie haben nicht die nötigen Berechtigungen um die Änderungen zu speicher src/app/core/common-components/entity-form/entity-form.service.ts - 216 + 245 @@ -5046,7 +5046,7 @@ Speichern von fehlgeschlagen: src/app/core/common-components/entity-form/entity-form.service.ts - 227 + 256 @@ -5778,7 +5778,7 @@ Create a new record - Erstelle einen neuen Eintrag + Erstelle einen neuen Eintrag src/app/core/common-components/entity-create-button/entity-create-button.component.html 15 @@ -5957,7 +5957,7 @@ Missing permission src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts - 38 + 39 @@ -6290,7 +6290,7 @@ Änderungen laden? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 102 + 112 @@ -6298,17 +6298,17 @@ Lokale Änderungen überlappen mit aktualisierten Daten, die vom Server synchronisiert wurden. Wollen Sie die lokalen Änderungen mit den aktuellsten Daten überschreiben? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 103 + 113 id already in use bereits vergeben + form field validation error - src/app/core/common-components/entity-form/unique-id-validator.ts - 9 + src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts + 19 - form field validation error Email diff --git a/src/assets/locale/messages.fr.xlf b/src/assets/locale/messages.fr.xlf index a132941a27..877a0655b6 100644 --- a/src/assets/locale/messages.fr.xlf +++ b/src/assets/locale/messages.fr.xlf @@ -85,7 +85,7 @@ src/app/core/config/config-fix.ts - 797 + 799 @@ -902,7 +902,7 @@ src/app/core/config/config-fix.ts - 710 + 712 src/app/features/reporting/demo-report-config-generator.service.ts @@ -1072,7 +1072,7 @@ src/app/core/config/config-fix.ts - 655 + 657 @@ -1195,7 +1195,7 @@ src/app/core/config/config-fix.ts - 773 + 775 src/app/features/reporting/demo-report-config-generator.service.ts @@ -1328,7 +1328,7 @@ src/app/core/config/config-fix.ts - 789 + 791 @@ -1341,7 +1341,7 @@ src/app/core/config/config-fix.ts - 738 + 740 @@ -1640,7 +1640,7 @@ Table header, Short for Body Mass Index src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 32 + 40 src/app/core/config/config-fix.ts @@ -1653,7 +1653,7 @@ Tooltip for BMI info src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 34 + 42 @@ -2195,7 +2195,7 @@ src/app/core/config/config-fix.ts - 739 + 741 @@ -2464,7 +2464,7 @@ 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 - >"/> + >"/> Label (short) + >"/> Description + >"/> Field ID (readonly) + >"/> Type Details (dropdown options set) + >"/> Type Details (target record type) src/app/core/config/config-fix.ts - 777 + 779 @@ -3776,7 +3776,7 @@ src/app/core/config/config-fix.ts - 695 + 697 @@ -3999,7 +3999,7 @@ Panel title src/app/core/config/config-fix.ts - 723 + 725 @@ -4143,11 +4143,11 @@ Label for the address of a child src/app/core/config/config-fix.ts - 744 + 746 src/app/core/config/config-fix.ts - 785 + 787 @@ -4156,7 +4156,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 748 + 750 @@ -4165,7 +4165,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 752 + 754 @@ -4174,7 +4174,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 756 + 758 @@ -4183,7 +4183,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 757 + 759 @@ -4192,7 +4192,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 761 + 763 @@ -4201,7 +4201,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 765 + 767 @@ -4210,7 +4210,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 781 + 783 @@ -4219,7 +4219,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 793 + 795 @@ -4228,7 +4228,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 806 + 808 @@ -4237,7 +4237,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 807 + 809 @@ -4246,7 +4246,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 812 + 814 @@ -4255,7 +4255,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 813 + 815 @@ -4264,7 +4264,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 818 + 820 @@ -4273,7 +4273,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 819 + 821 @@ -4282,7 +4282,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 824 + 826 @@ -4291,7 +4291,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 825 + 827 @@ -4300,7 +4300,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 830 + 832 @@ -4309,7 +4309,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 831 + 833 @@ -4318,7 +4318,7 @@ Label of user phone src/app/core/config/config-fix.ts - 839 + 841 @@ -4700,7 +4700,7 @@ Doit être supérieur à src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 112 + 175 @@ -4708,7 +4708,7 @@ Ne peut pas être supérieur à src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 114 + 177 @@ -4716,7 +4716,7 @@ Veuillez entrer un modèle valide src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 119 + 182 @@ -4724,7 +4724,7 @@ Ce champ doit être rempli src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 122 + 185 @@ -4732,7 +4732,7 @@ Veuillez entrer une adresse électronique valide src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 124 + 187 @@ -4740,7 +4740,7 @@ Please enter a valid date src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 126 + 189 @@ -4748,7 +4748,7 @@ Please enter a valid number src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 128 + 191 @@ -4756,7 +4756,7 @@ Invalid input src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 135 + 200 @@ -4764,7 +4764,7 @@ Current user is not permitted to save these changes src/app/core/common-components/entity-form/entity-form.service.ts - 216 + 245 @@ -4772,7 +4772,7 @@ Echec pour sauvegarder : src/app/core/common-components/entity-form/entity-form.service.ts - 227 + 256 @@ -5550,7 +5550,7 @@ Missing permission src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts - 38 + 39 @@ -5943,7 +5943,7 @@ Load changes? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 102 + 112 @@ -5951,17 +5951,17 @@ Local changes are in conflict with updated values synced from the server. Do you want the local changes to be overwritten with the latest values? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 103 + 113 id already in use id already in use + form field validation error - src/app/core/common-components/entity-form/unique-id-validator.ts - 9 + src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts + 19 - form field validation error Email diff --git a/src/assets/locale/messages.it.xlf b/src/assets/locale/messages.it.xlf index 6fff4cd245..ea59d6b543 100644 --- a/src/assets/locale/messages.it.xlf +++ b/src/assets/locale/messages.it.xlf @@ -695,7 +695,7 @@ src/app/core/config/config-fix.ts - 710 + 712 src/app/features/reporting/demo-report-config-generator.service.ts @@ -816,7 +816,7 @@ src/app/core/config/config-fix.ts - 797 + 799 @@ -1057,7 +1057,7 @@ src/app/core/config/config-fix.ts - 655 + 657 @@ -1343,7 +1343,7 @@ Table header, Short for Body Mass Index src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 32 + 40 src/app/core/config/config-fix.ts @@ -1356,7 +1356,7 @@ Tooltip for BMI info src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 34 + 42 @@ -1404,7 +1404,7 @@ src/app/core/config/config-fix.ts - 773 + 775 src/app/features/reporting/demo-report-config-generator.service.ts @@ -1537,7 +1537,7 @@ src/app/core/config/config-fix.ts - 789 + 791 @@ -1550,7 +1550,7 @@ src/app/core/config/config-fix.ts - 738 + 740 @@ -2208,7 +2208,7 @@ src/app/core/config/config-fix.ts - 739 + 741 @@ -2541,7 +2541,7 @@ 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 - >"/> + >"/> Description + >"/> Field ID (readonly) + >"/> Type Details (dropdown options set) + >"/> Type Details (target record type) src/app/core/config/config-fix.ts - 695 + 697 @@ -3462,7 +3462,7 @@ Panel title src/app/core/config/config-fix.ts - 723 + 725 @@ -3675,11 +3675,11 @@ Label for the address of a child src/app/core/config/config-fix.ts - 744 + 746 src/app/core/config/config-fix.ts - 785 + 787 @@ -3688,7 +3688,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 748 + 750 @@ -3697,7 +3697,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 752 + 754 @@ -3706,7 +3706,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 756 + 758 @@ -3715,7 +3715,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 757 + 759 @@ -3724,7 +3724,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 761 + 763 @@ -3733,7 +3733,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 765 + 767 @@ -3745,7 +3745,7 @@ src/app/core/config/config-fix.ts - 777 + 779 @@ -3754,7 +3754,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 781 + 783 @@ -3763,7 +3763,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 793 + 795 @@ -3772,7 +3772,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 806 + 808 @@ -3781,7 +3781,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 807 + 809 @@ -3790,7 +3790,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 812 + 814 @@ -3799,7 +3799,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 813 + 815 @@ -3808,7 +3808,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 818 + 820 @@ -3817,7 +3817,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 819 + 821 @@ -3826,7 +3826,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 824 + 826 @@ -3835,7 +3835,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 825 + 827 @@ -3844,7 +3844,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 830 + 832 @@ -3853,7 +3853,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 831 + 833 @@ -3862,7 +3862,7 @@ Label of user phone src/app/core/config/config-fix.ts - 839 + 841 @@ -4171,7 +4171,7 @@ Must be greater than src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 112 + 175 @@ -4179,7 +4179,7 @@ Cannot be greater than src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 114 + 177 @@ -4187,7 +4187,7 @@ Please enter a valid pattern src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 119 + 182 @@ -4195,7 +4195,7 @@ This field is required src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 122 + 185 @@ -4203,7 +4203,7 @@ Please enter a valid email src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 124 + 187 @@ -4211,7 +4211,7 @@ Please enter a valid date src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 126 + 189 @@ -4219,7 +4219,7 @@ Please enter a valid number src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 128 + 191 @@ -4227,7 +4227,7 @@ Invalid input src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 135 + 200 @@ -4235,7 +4235,7 @@ Current user is not permitted to save these changes src/app/core/common-components/entity-form/entity-form.service.ts - 216 + 245 @@ -4243,7 +4243,7 @@ Could not save : src/app/core/common-components/entity-form/entity-form.service.ts - 227 + 256 @@ -5108,7 +5108,7 @@ Missing permission src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts - 38 + 39 @@ -5621,7 +5621,7 @@ Load changes? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 102 + 112 @@ -5629,17 +5629,17 @@ Local changes are in conflict with updated values synced from the server. Do you want the local changes to be overwritten with the latest values? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 103 + 113 id already in use id already in use + form field validation error - src/app/core/common-components/entity-form/unique-id-validator.ts - 9 + src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts + 19 - form field validation error Email @@ -6957,7 +6957,7 @@ 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 - >"/> + >"/> Label (short) src/app/core/config/config-fix.ts - 710 + 712 src/app/features/reporting/demo-report-config-generator.service.ts @@ -760,7 +760,7 @@ src/app/core/config/config-fix.ts - 797 + 799 Label for the remarks of a ASER result @@ -861,7 +861,7 @@ src/app/core/config/config-fix.ts - 655 + 657 Child status @@ -1170,7 +1170,7 @@ BMI src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 32 + 40 src/app/core/config/config-fix.ts @@ -1182,7 +1182,7 @@ This is calculated using the height and the weight measure src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 34 + 42 Tooltip for BMI info @@ -1226,7 +1226,7 @@ src/app/core/config/config-fix.ts - 773 + 775 src/app/features/reporting/demo-report-config-generator.service.ts @@ -1346,7 +1346,7 @@ src/app/core/config/config-fix.ts - 789 + 791 Label for the phone number of a child @@ -1358,7 +1358,7 @@ src/app/core/config/config-fix.ts - 738 + 740 Label for the child of a relation @@ -1918,7 +1918,7 @@ src/app/core/config/config-fix.ts - 739 + 741 Label for the children of a note @@ -2136,10 +2136,10 @@ 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 - >"/> + >"/> src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.html - 22,28 + 22,29 @@ -2154,10 +2154,10 @@ 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 - >"/> + >"/> src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.html - 38,44 + 38,45 @@ -2172,10 +2172,10 @@ 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 - >"/> + >"/> src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.html - 56,62 + 56,63 @@ -2190,10 +2190,10 @@ 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 - >"/> + >"/> src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.html - 93,99 + 93,100 @@ -2208,10 +2208,10 @@ icon="question-circle" matTooltip="Select from which type of records the user can select and link to with this field." i18n-matTooltip - >"/> + >"/> src/app/core/admin/admin-entity-details/admin-entity-field/admin-entity-field.component.html - 126,132 + 126,133 @@ -3005,91 +3005,91 @@ Must be greater than src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 112 + 175 Cannot be greater than src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 114 + 177 Please enter a valid pattern src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 119 + 182 This field is required src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 122 + 185 Please enter a valid email src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 124 + 187 Please enter a valid date src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 126 + 189 Please enter a valid number src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 128 + 191 Invalid input src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts - 135 + 200 Current user is not permitted to save these changes src/app/core/common-components/entity-form/entity-form.service.ts - 216 + 245 Could not save : src/app/core/common-components/entity-form/entity-form.service.ts - 227 + 256 Load changes? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 102 + 112 Local changes are in conflict with updated values synced from the server. Do you want the local changes to be overwritten with the latest values? src/app/core/common-components/entity-form/entity-form/entity-form.component.ts - 103 + 113 id already in use - src/app/core/common-components/entity-form/unique-id-validator.ts - 9 + src/app/core/common-components/entity-form/unique-id-validator/unique-id-validator.ts + 19 form field validation error @@ -3328,7 +3328,7 @@ src/app/core/config/config-fix.ts - 777 + 779 @@ -3343,7 +3343,7 @@ src/app/core/config/config-fix.ts - 695 + 697 Panel title @@ -3519,7 +3519,7 @@ Events & Attendance src/app/core/config/config-fix.ts - 723 + 725 Panel title @@ -3527,11 +3527,11 @@ Address src/app/core/config/config-fix.ts - 744 + 746 src/app/core/config/config-fix.ts - 785 + 787 Label for the address of a child @@ -3539,7 +3539,7 @@ Blood Group src/app/core/config/config-fix.ts - 748 + 750 Label for a child attribute @@ -3547,7 +3547,7 @@ Religion src/app/core/config/config-fix.ts - 752 + 754 Label for the religion of a child @@ -3555,7 +3555,7 @@ Mother Tongue src/app/core/config/config-fix.ts - 756 + 758 Label for the mother tongue of a child @@ -3563,7 +3563,7 @@ The primary language spoken at home src/app/core/config/config-fix.ts - 757 + 759 Tooltip description for the mother tongue of a child @@ -3571,7 +3571,7 @@ Last Dental Check-Up src/app/core/config/config-fix.ts - 761 + 763 Label for a child attribute @@ -3579,7 +3579,7 @@ Birth certificate src/app/core/config/config-fix.ts - 765 + 767 Label for a child attribute @@ -3587,7 +3587,7 @@ Language src/app/core/config/config-fix.ts - 781 + 783 Label for the language of a school @@ -3595,7 +3595,7 @@ School Timing src/app/core/config/config-fix.ts - 793 + 795 Label for the timing of a school @@ -3603,7 +3603,7 @@ Motivated src/app/core/config/config-fix.ts - 806 + 808 Label for a child attribute @@ -3611,7 +3611,7 @@ The child is motivated during the class. src/app/core/config/config-fix.ts - 807 + 809 Description for a child attribute @@ -3619,7 +3619,7 @@ Participating src/app/core/config/config-fix.ts - 812 + 814 Label for a child attribute @@ -3627,7 +3627,7 @@ The child is actively participating in the class. src/app/core/config/config-fix.ts - 813 + 815 Description for a child attribute @@ -3635,7 +3635,7 @@ Interacting src/app/core/config/config-fix.ts - 818 + 820 Label for a child attribute @@ -3643,7 +3643,7 @@ The child interacts with other students during the class. src/app/core/config/config-fix.ts - 819 + 821 Description for a child attribute @@ -3651,7 +3651,7 @@ Homework src/app/core/config/config-fix.ts - 824 + 826 Label for a child attribute @@ -3659,7 +3659,7 @@ The child does its homework. src/app/core/config/config-fix.ts - 825 + 827 Description for a child attribute @@ -3667,7 +3667,7 @@ Asking Questions src/app/core/config/config-fix.ts - 830 + 832 Label for a child attribute @@ -3675,7 +3675,7 @@ The child is asking questions during the class. src/app/core/config/config-fix.ts - 831 + 833 Description for a child attribute @@ -3683,7 +3683,7 @@ Contact src/app/core/config/config-fix.ts - 839 + 841 Label of user phone @@ -4844,7 +4844,7 @@ Your account does not have the required permission for this action. src/app/core/permissions/permission-directive/disable-entity-operation.directive.ts - 38 + 39 Missing permission