Skip to content

Commit

Permalink
core(*): dynamic default values for form fields (#1796)
Browse files Browse the repository at this point in the history
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 <lukasscheller@mbp-von-lukas.speedport.ip>
Co-authored-by: Simon <simon@aam-digital.com>
Co-authored-by: Sebastian Leidig <sebastian.leidig@gmail.com>
  • Loading branch information
4 people authored Jul 4, 2023
1 parent ab55a4e commit 34fee07
Show file tree
Hide file tree
Showing 18 changed files with 128 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
4 changes: 3 additions & 1 deletion src/app/child-dev-project/children/aser/model/aser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
3 changes: 3 additions & 0 deletions src/app/child-dev-project/notes/model/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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` })
Expand All @@ -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[] = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -50,7 +49,6 @@ export class NotesRelatedToEntityComponent implements OnInit {

constructor(
private childrenService: ChildrenService,
private sessionService: SessionService,
private formDialog: FormDialogService,
private filterService: FilterService
) {}
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +41,10 @@ describe("EntityFormService", () => {
EntitySchemaService,
{ provide: EntityMapperService, useValue: mockEntityMapper },
EntityAbility,
{
provide: SessionService,
useValue: { getCurrentUser: () => ({ name: TEST_USER }) },
},
],
});
service = TestBed.inject(EntityFormService);
Expand Down Expand Up @@ -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");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -34,6 +40,7 @@ export class EntityFormService {
private dynamicValidator: DynamicValidatorsService,
private ability: EntityAbility,
private unsavedChanges: UnsavedChangesService,
private session: SessionService,
router: Router
) {
router.events
Expand Down Expand Up @@ -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
Expand All @@ -131,6 +147,24 @@ export class EntityFormService {
return group;
}

private getDefaultValue<T>(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.
Expand Down
13 changes: 11 additions & 2 deletions src/app/core/entity/schema/entity-schema-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
}
15 changes: 3 additions & 12 deletions src/app/core/entity/schema/entity-schema.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 1 addition & 6 deletions src/app/core/ui/primary-action/primary-action.component.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -31,7 +30,6 @@ export class PrimaryActionComponent {
noteConstructor = Note;

constructor(
private sessionService: SessionService,
private formDialog: FormDialogService
) {}

Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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.
* Further attributes can be added through the config.
*/
@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;
}
Loading

0 comments on commit 34fee07

Please sign in to comment.