From 34fee07f622715c5a3bc4f7c4960bb1e006fdf8c Mon Sep 17 00:00:00 2001 From: Schottkyc137 <45085299+Schottkyc137@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:06:55 +0200 Subject: [PATCH] core(*): dynamic default values for form fields (#1796) schemaField.defaultValue allows to define a value that is automatically set in UI form fields for new entities. The default values support dynamic placeholders for current time or user. Replace hard-coded defaults in factory methods and use current date and user in the property schema closes #1671, closes #1672 --------- Co-authored-by: Lukas Scheller Co-authored-by: Simon Co-authored-by: Sebastian Leidig --- .../aser/aser-component/aser.component.ts | 1 - .../children/aser/model/aser.ts | 4 +- .../educational-material.component.ts | 1 - .../model/educational-material.ts | 1 + src/app/child-dev-project/notes/model/note.ts | 3 + .../notes-manager/notes-manager.component.ts | 2 - .../notes-related-to-entity.component.spec.ts | 10 +--- .../notes-related-to-entity.component.ts | 9 --- .../entity-form/entity-form.service.spec.ts | 60 +++++++++++++++++++ .../entity-form/entity-form.service.ts | 36 ++++++++++- .../core/entity/schema/entity-schema-field.ts | 13 +++- .../entity/schema/entity-schema.service.ts | 15 +---- .../primary-action.component.ts | 7 +-- .../historical-data.component.ts | 1 - .../model/historical-entity-data.ts | 6 +- src/app/features/todos/model/todo.ts | 2 + .../todos/todo-list/todo-list.component.ts | 4 +- .../todos-related-to-entity.component.ts | 5 +- 18 files changed, 128 insertions(+), 52 deletions(-) diff --git a/src/app/child-dev-project/children/aser/aser-component/aser.component.ts b/src/app/child-dev-project/children/aser/aser-component/aser.component.ts index a2c399caf1..2a1269fb74 100644 --- a/src/app/child-dev-project/children/aser/aser-component/aser.component.ts +++ b/src/app/child-dev-project/children/aser/aser-component/aser.component.ts @@ -51,7 +51,6 @@ export class AserComponent implements OnInit { generateNewRecordFactory() { return () => { const newAtt = new Aser(Date.now().toString()); - newAtt.date = new Date(); newAtt.child = this.entity.getId(); return newAtt; }; diff --git a/src/app/child-dev-project/children/aser/model/aser.ts b/src/app/child-dev-project/children/aser/model/aser.ts index a5e54673c7..f869db517a 100644 --- a/src/app/child-dev-project/children/aser/model/aser.ts +++ b/src/app/child-dev-project/children/aser/model/aser.ts @@ -21,14 +21,16 @@ import { DatabaseEntity } from "../../../../core/entity/database-entity.decorato import { SkillLevel } from "./skill-levels"; import { WarningLevel } from "../../../../core/entity/model/warning-level"; import { ConfigurableEnumDatatype } from "../../../../core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype"; +import { PLACEHOLDERS } from "../../../../core/entity/schema/entity-schema-field"; @DatabaseEntity("Aser") export class Aser extends Entity { @DatabaseField() child: string; // id of Child entity @DatabaseField({ label: $localize`:Label for date of the ASER results:Date`, + defaultValue: PLACEHOLDERS.NOW, }) - date: Date = new Date(); + date: Date; @DatabaseField({ label: $localize`:Label of the Hindi ASER result:Hindi`, dataType: "configurable-enum", diff --git a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts index e5a4956a6d..160e221d1c 100644 --- a/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts +++ b/src/app/child-dev-project/children/educational-material/educational-material-component/educational-material.component.ts @@ -73,7 +73,6 @@ export class EducationalMaterialComponent implements OnInit { // use last entered date as default, otherwise today's date newAtt.date = this.records.length > 0 ? this.records[0].date : new Date(); newAtt.child = this.entity.getId(); - newAtt.materialAmount = 1; return newAtt; }; diff --git a/src/app/child-dev-project/children/educational-material/model/educational-material.ts b/src/app/child-dev-project/children/educational-material/model/educational-material.ts index b96b200d46..2128843a64 100644 --- a/src/app/child-dev-project/children/educational-material/model/educational-material.ts +++ b/src/app/child-dev-project/children/educational-material/model/educational-material.ts @@ -42,6 +42,7 @@ export class EducationalMaterial extends Entity { materialType: ConfigurableEnumValue; @DatabaseField({ label: $localize`:The amount of the material which has been borrowed:Amount`, + defaultValue: 1, validators: { required: true, }, diff --git a/src/app/child-dev-project/notes/model/note.ts b/src/app/child-dev-project/notes/model/note.ts index 010700ea47..0521ea9be0 100644 --- a/src/app/child-dev-project/notes/model/note.ts +++ b/src/app/child-dev-project/notes/model/note.ts @@ -35,6 +35,7 @@ import { } from "../../../core/entity/model/warning-level"; import { School } from "../../schools/model/school"; import { Ordering } from "../../../core/configurable-enum/configurable-enum-ordering"; +import { PLACEHOLDERS } from "../../../core/entity/schema/entity-schema-field"; @DatabaseEntity("Note") export class Note extends Entity { @@ -93,6 +94,7 @@ export class Note extends Entity { @DatabaseField({ label: $localize`:Label for the date of a note:Date`, dataType: "date-only", + defaultValue: PLACEHOLDERS.NOW, }) date: Date; @DatabaseField({ label: $localize`:Label for the subject of a note:Subject` }) @@ -107,6 +109,7 @@ export class Note extends Entity { label: $localize`:Label for the social worker(s) who created the note:SW`, dataType: "entity-array", additional: User.ENTITY_TYPE, + defaultValue: PLACEHOLDERS.CURRENT_USER, }) authors: string[] = []; diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts index 2e1fc36362..aba86c325a 100644 --- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts +++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts @@ -183,8 +183,6 @@ export class NotesManagerComponent implements OnInit { addNoteClick() { const newNote = new Note(Date.now().toString()); - newNote.date = new Date(); - newNote.authors = [this.sessionService.getCurrentUser().name]; this.showDetails(newNote); } 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 58732aa1b9..b39df063e9 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 @@ -8,10 +8,7 @@ import { import { ChildrenService } from "../../children/children.service"; import { Note } from "../model/note"; import { Child } from "../../children/model/child"; -import { - MockedTestingModule, - TEST_USER, -} from "../../../utils/mocked-testing.module"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { Entity } from "../../../core/entity/model/entity"; import { School } from "../../schools/model/school"; import { User } from "../../../core/user/user"; @@ -62,21 +59,18 @@ describe("NotesRelatedToEntityComponent", () => { component.ngOnInit(); let note = component.generateNewRecordFactory()(); expect(note.children).toEqual([entity.getId()]); - expect(note.authors).toEqual([TEST_USER]); entity = new School(); component.entity = entity; component.ngOnInit(); note = component.generateNewRecordFactory()(); expect(note.schools).toEqual([entity.getId()]); - expect(note.authors).toEqual([TEST_USER]); entity = new User(); component.entity = entity; component.ngOnInit(); note = component.generateNewRecordFactory()(); - // adding a note for a User does not make that User an author of the note! - expect(note.authors).toEqual([TEST_USER]); + expect(note.relatedEntities).toEqual([entity.getId(true)]); entity = new ChildSchoolRelation(); entity["childId"] = "someChild"; 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 6ca45d495f..0e060b7c09 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 @@ -3,7 +3,6 @@ import { Note } from "../model/note"; import { NoteDetailsComponent } from "../note-details/note-details.component"; import { ChildrenService } from "../../children/children.service"; import moment from "moment"; -import { SessionService } from "../../../core/session/session-service/session.service"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator"; import { Entity } from "../../../core/entity/model/entity"; @@ -50,7 +49,6 @@ export class NotesRelatedToEntityComponent implements OnInit { constructor( private childrenService: ChildrenService, - private sessionService: SessionService, private formDialog: FormDialogService, private filterService: FilterService ) {} @@ -80,11 +78,8 @@ export class NotesRelatedToEntityComponent implements OnInit { } generateNewRecordFactory() { - const user = this.sessionService.getCurrentUser().name; - return () => { const newNote = new Note(Date.now().toString()); - newNote.date = new Date(); //TODO: generalize this code - possibly by only using relatedEntities to link other records here? see #1501 if (this.entity.getType() === Child.ENTITY_TYPE) { @@ -99,10 +94,6 @@ export class NotesRelatedToEntityComponent implements OnInit { newNote.relatedEntities.push(this.entity.getId(true)); } - if (!newNote.authors.includes(user)) { - // TODO: should we keep authors completely separate of also add them into the relatedEntities as well? - newNote.authors.push(user); - } this.filterService.alignEntityWithFilter(newNote, this.filter); return newNote; diff --git a/src/app/core/entity-components/entity-form/entity-form.service.spec.ts b/src/app/core/entity-components/entity-form/entity-form.service.spec.ts index 2bf9f0c50c..be53457682 100644 --- a/src/app/core/entity-components/entity-form/entity-form.service.spec.ts +++ b/src/app/core/entity-components/entity-form/entity-form.service.spec.ts @@ -18,6 +18,14 @@ import { MatDialogModule } from "@angular/material/dialog"; import { UnsavedChangesService } from "../entity-details/form/unsaved-changes.service"; import { Router } from "@angular/router"; import { NotFoundComponent } from "../../view/dynamic-routing/not-found/not-found.component"; +import { SessionService } from "../../session/session-service/session.service"; +import { + EntitySchemaField, + PLACEHOLDERS, +} from "../../entity/schema/entity-schema-field"; +import { TEST_USER } from "../../../utils/mocked-testing.module"; +import { arrayEntitySchemaDatatype } from "../../entity/schema-datatypes/datatype-array"; +import { entityArrayEntitySchemaDatatype } from "../../entity/schema-datatypes/datatype-entity-array"; describe("EntityFormService", () => { let service: EntityFormService; @@ -33,6 +41,10 @@ describe("EntityFormService", () => { EntitySchemaService, { provide: EntityMapperService, useValue: mockEntityMapper }, EntityAbility, + { + provide: SessionService, + useValue: { getCurrentUser: () => ({ name: TEST_USER }) }, + }, ], }); service = TestBed.inject(EntityFormService); @@ -168,4 +180,52 @@ describe("EntityFormService", () => { expect(unsavedChanged.pending).toBeFalse(); }); + + it("should assign default values", () => { + const schema: EntitySchemaField = { defaultValue: 1 }; + Entity.schema.set("test", schema); + + let form = service.createFormGroup([{ id: "test" }], new Entity()); + expect(form.get("test")).toHaveValue(1); + + schema.defaultValue = PLACEHOLDERS.NOW; + form = service.createFormGroup([{ id: "test" }], new Entity()); + expect(form.get("test")).toHaveValue(new Date()); + + schema.defaultValue = PLACEHOLDERS.CURRENT_USER; + form = service.createFormGroup([{ id: "test" }], new Entity()); + expect(form.get("test")).toHaveValue(TEST_USER); + + schema.dataType = arrayEntitySchemaDatatype.name; + form = service.createFormGroup([{ id: "test" }], new Entity()); + expect(form.get("test")).toHaveValue([TEST_USER]); + + schema.dataType = entityArrayEntitySchemaDatatype.name; + form = service.createFormGroup([{ id: "test" }], new Entity()); + expect(form.get("test")).toHaveValue([TEST_USER]); + + Entity.schema.delete("test"); + }); + + it("should not assign default values to existing entities", () => { + Entity.schema.set("test", { defaultValue: 1 }); + + const entity = new Entity(); + entity._rev = "1-existing_entity"; + const form = service.createFormGroup([{ id: "test" }], entity); + expect(form.get("test")).toHaveValue(null); + + Entity.schema.delete("test"); + }); + + it("should not overwrite existing values with default value", () => { + Entity.schema.set("test", { defaultValue: 1 }); + + const entity = new Entity(); + entity["test"] = 2; + const form = service.createFormGroup([{ id: "test" }], entity); + expect(form.get("test")).toHaveValue(2); + + Entity.schema.delete("test"); + }); }); diff --git a/src/app/core/entity-components/entity-form/entity-form.service.ts b/src/app/core/entity-components/entity-form/entity-form.service.ts index be650962cf..3ae8adbff1 100644 --- a/src/app/core/entity-components/entity-form/entity-form.service.ts +++ b/src/app/core/entity-components/entity-form/entity-form.service.ts @@ -12,6 +12,12 @@ import { UnsavedChangesService } from "../entity-details/form/unsaved-changes.se import { ActivationStart, Router } from "@angular/router"; import { Subscription } from "rxjs"; import { filter } from "rxjs/operators"; +import { SessionService } from "../../session/session-service/session.service"; +import { + EntitySchemaField, + PLACEHOLDERS, +} from "../../entity/schema/entity-schema-field"; +import { isArrayDataType } from "../entity-utils/entity-utils"; /** * These are utility types that allow to define the type of `FormGroup` the way it is returned by `EntityFormService.create` @@ -34,6 +40,7 @@ export class EntityFormService { private dynamicValidator: DynamicValidatorsService, private ability: EntityAbility, private unsavedChanges: UnsavedChangesService, + private session: SessionService, router: Router ) { router.events @@ -115,7 +122,16 @@ export class EntityFormService { formFields .filter((formField) => entitySchema.get(formField.id)) .forEach((formField) => { - formConfig[formField.id] = [copy[formField.id]]; + const schema = entitySchema.get(formField.id); + let val = copy[formField.id]; + if ( + entity.isNew && + schema.defaultValue && + (!val || (val as []).length === 0) + ) { + val = this.getDefaultValue(schema); + } + formConfig[formField.id] = [val]; if (formField.validators) { const validators = this.dynamicValidator.buildValidators( formField.validators @@ -131,6 +147,24 @@ export class EntityFormService { return group; } + private getDefaultValue(schema: EntitySchemaField) { + let newVal; + switch (schema.defaultValue) { + case PLACEHOLDERS.NOW: + newVal = new Date(); + break; + case PLACEHOLDERS.CURRENT_USER: + newVal = this.session.getCurrentUser().name; + break; + default: + newVal = schema.defaultValue; + } + if (isArrayDataType(schema.dataType)) { + newVal = [newVal]; + } + return newVal; + } + /** * 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. diff --git a/src/app/core/entity/schema/entity-schema-field.ts b/src/app/core/entity/schema/entity-schema-field.ts index 8d89dd72a1..68622e7a21 100644 --- a/src/app/core/entity/schema/entity-schema-field.ts +++ b/src/app/core/entity/schema/entity-schema-field.ts @@ -54,9 +54,9 @@ export interface EntitySchemaField { /** * Whether the field should be initialized with a default value if undefined - * (which is then run through dataType transformation); + * Default values are applied to form fields before they are displayed to users */ - defaultValue?: any; + defaultValue?: PLACEHOLDERS | any; /** * (Optional) Assign any custom "extension" configuration you need for a specific datatype extension. @@ -105,3 +105,12 @@ export interface EntitySchemaField { /** whether to show this field in the default details view */ showInDetailsView?: boolean; } + +/** + * Available placeholder variables that can be used to configure a dynamic default value. + * (e.g. "$now" to set to current date) + */ +export enum PLACEHOLDERS { + NOW = "$now", + CURRENT_USER = "$current_user", +} diff --git a/src/app/core/entity/schema/entity-schema.service.ts b/src/app/core/entity/schema/entity-schema.service.ts index 3783fd6af8..7d69f5c5fa 100644 --- a/src/app/core/entity/schema/entity-schema.service.ts +++ b/src/app/core/entity/schema/entity-schema.service.ts @@ -107,12 +107,7 @@ export class EntitySchemaService { const schemaField: EntitySchemaField = schema.get(key); if (data[key] === undefined || data[key] === null) { - if (schemaField.defaultValue !== undefined) { - data[key] = schemaField.defaultValue; - } else { - // skip and keep undefined - continue; - } + continue; } const newValue = this.getDatatypeOrDefault( @@ -163,12 +158,8 @@ export class EntitySchemaService { const schemaField: EntitySchemaField = schema.get(key); if (value === undefined || value === null) { - if (schemaField.defaultValue !== undefined) { - value = schemaField.defaultValue; - } else { - // skip and keep undefined - continue; - } + // skip and keep undefined + continue; } try { diff --git a/src/app/core/ui/primary-action/primary-action.component.ts b/src/app/core/ui/primary-action/primary-action.component.ts index 4eb7258b77..358cdd6209 100644 --- a/src/app/core/ui/primary-action/primary-action.component.ts +++ b/src/app/core/ui/primary-action/primary-action.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; import { Note } from "../../../child-dev-project/notes/model/note"; -import { SessionService } from "../../session/session-service/session.service"; import { NoteDetailsComponent } from "../../../child-dev-project/notes/note-details/note-details.component"; import { FormDialogService } from "../../form-dialog/form-dialog.service"; import { MatButtonModule } from "@angular/material/button"; @@ -31,7 +30,6 @@ export class PrimaryActionComponent { noteConstructor = Note; constructor( - private sessionService: SessionService, private formDialog: FormDialogService ) {} @@ -47,9 +45,6 @@ export class PrimaryActionComponent { } private createNewNote() { - const newNote = new Note(Date.now().toString()); - newNote.date = new Date(); - newNote.authors = [this.sessionService.getCurrentUser().name]; - return newNote; + return new Note(Date.now().toString()); } } 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 070c4f17ed..8aef4d8fa7 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 @@ -39,7 +39,6 @@ export class HistoricalDataComponent implements OnInit { return () => { const newEntry = new HistoricalEntityData(); newEntry.relatedEntity = this.entity.getId(); - newEntry.date = new Date(); return newEntry; }; } diff --git a/src/app/features/historical-data/model/historical-entity-data.ts b/src/app/features/historical-data/model/historical-entity-data.ts index 85367cad9d..82f669510e 100644 --- a/src/app/features/historical-data/model/historical-entity-data.ts +++ b/src/app/features/historical-data/model/historical-entity-data.ts @@ -1,6 +1,7 @@ import { Entity } from "../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../core/entity/database-field.decorator"; +import { PLACEHOLDERS } from "../../../core/entity/schema/entity-schema-field"; /** * A general class that represents data that is collected for a entity over time. @@ -8,7 +9,10 @@ import { DatabaseField } from "../../../core/entity/database-field.decorator"; */ @DatabaseEntity("HistoricalEntityData") export class HistoricalEntityData extends Entity { - @DatabaseField({ label: $localize`:Label for date of historical data:Date` }) + @DatabaseField({ + label: $localize`:Label for date of historical data:Date`, + defaultValue: PLACEHOLDERS.NOW, + }) date: Date; @DatabaseField() relatedEntity: string; } diff --git a/src/app/features/todos/model/todo.ts b/src/app/features/todos/model/todo.ts index 15e40ffa5a..e050c1c657 100644 --- a/src/app/features/todos/model/todo.ts +++ b/src/app/features/todos/model/todo.ts @@ -25,6 +25,7 @@ import { RecurringActivity } from "../../../child-dev-project/attendance/model/r import { TimeInterval } from "../recurring-interval/time-interval"; import { TodoCompletion } from "./todo-completion"; import { WarningLevel } from "../../../core/entity/model/warning-level"; +import { PLACEHOLDERS } from "../../../core/entity/schema/entity-schema-field"; @DatabaseEntity("Todo") export class Todo extends Entity { @@ -68,6 +69,7 @@ export class Todo extends Entity { dataType: "entity-array", additional: User.ENTITY_TYPE, showInDetailsView: true, + defaultValue: PLACEHOLDERS.CURRENT_USER, }) assignedTo: string[] = []; diff --git a/src/app/features/todos/todo-list/todo-list.component.ts b/src/app/features/todos/todo-list/todo-list.component.ts index b4fba6026d..062f74b568 100644 --- a/src/app/features/todos/todo-list/todo-list.component.ts +++ b/src/app/features/todos/todo-list/todo-list.component.ts @@ -116,9 +116,7 @@ export class TodoListComponent implements OnInit { } createNew() { - const newEntity = new Todo(); - newEntity.assignedTo = [this.sessionService.getCurrentUser().name]; - this.showDetails(newEntity); + this.showDetails(new Todo()); } showDetails(entity: Todo) { 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 4d0753f6fa..2b39dcbbbc 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 @@ -4,7 +4,6 @@ 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/view/dynamic-components/dynamic-component.decorator"; -import { SessionService } from "../../../core/session/session-service/session.service"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; import { TodoDetailsComponent } from "../todo-details/todo-details.component"; import { DataFilter } from "../../../core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; @@ -53,8 +52,7 @@ export class TodosRelatedToEntityComponent implements OnInit { constructor( private formDialog: FormDialogService, - private dbIndexingService: DatabaseIndexingService, - private sessionService: SessionService + private dbIndexingService: DatabaseIndexingService ) { // TODO: move this generic index creation into schema this.dbIndexingService.generateIndexOnProperty( @@ -86,7 +84,6 @@ export class TodosRelatedToEntityComponent implements OnInit { return () => { const newEntry = new Todo(); newEntry.relatedEntities = [this.entity.getId(true)]; - newEntry.assignedTo = [this.sessionService.getCurrentUser().name]; return newEntry; }; }