diff --git a/package-lock.json b/package-lock.json
index c7a7518461..842945fe0f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,12 +26,12 @@
"@casl/ability": "^6.5.0",
"@casl/angular": "^8.2.3",
"@faker-js/faker": "^8.3.1",
- "@fortawesome/angular-fontawesome": "^0.14.0",
+ "@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@ngneat/until-destroy": "^10.0.0",
- "@sentry/browser": "^7.88.0",
+ "@sentry/browser": "^7.91.0",
"angulartics2": "^12.2.1",
"assert": "^2.1.0",
"crypto-es": "^2.1.0",
@@ -4191,9 +4191,9 @@
"dev": true
},
"node_modules/@fortawesome/angular-fontawesome": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.0.tgz",
- "integrity": "sha512-nB7an9t66nY0m/1MIBOIvi+vKyZaTskhtGtQwGTiMyte3Bmy9080pFpXguyox68/vxGVmLxZkRxYIgjMCvm7QQ==",
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.1.tgz",
+ "integrity": "sha512-Yb5HLiEOAxjSLEcaOM51CKIrzdfvoDafXVJERm9vufxfZkVZPZJgrZRgqwLVpejgq4/Ez6TqHZ6SqmJwdtRF6g==",
"dependencies": {
"tslib": "^2.6.2"
},
@@ -6932,87 +6932,87 @@
}
},
"node_modules/@sentry-internal/feedback": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.88.0.tgz",
- "integrity": "sha512-lbK6jgO1I0M96nZQ99mcLSZ55ebwPAP6LhEWhkmc+eAfy97VpiY+qsbmgsmOzCEPqMmEUCEcI0rEZ7fiye2v2Q==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.91.0.tgz",
+ "integrity": "sha512-SJKTSaz68F5YIwF79EttBm915M2LnacgZMYRnRumyTmMKnebGhYQLwWbZdpaDvOa1U18dgRajDX8Qed/8A3tXw==",
"dependencies": {
- "@sentry/core": "7.88.0",
- "@sentry/types": "7.88.0",
- "@sentry/utils": "7.88.0"
+ "@sentry/core": "7.91.0",
+ "@sentry/types": "7.91.0",
+ "@sentry/utils": "7.91.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/tracing": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.88.0.tgz",
- "integrity": "sha512-xXQdcYhsS+ourzJHjXNjZC9zakuc97udmpgaXRjEP7FjPYclIx+YXwgFBdHM2kzAwZLFOsEce5dr46GVXUDfZw==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.91.0.tgz",
+ "integrity": "sha512-JH5y6gs6BS0its7WF2DhySu7nkhPDfZcdpAXldxzIlJpqFkuwQKLU5nkYJpiIyZz1NHYYtW5aum2bV2oCOdDRA==",
"dependencies": {
- "@sentry/core": "7.88.0",
- "@sentry/types": "7.88.0",
- "@sentry/utils": "7.88.0"
+ "@sentry/core": "7.91.0",
+ "@sentry/types": "7.91.0",
+ "@sentry/utils": "7.91.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.88.0.tgz",
- "integrity": "sha512-il4x3PB99nuU/OJQw2RltgYYbo8vtnYoIgneOeEiw4m0ppK1nKkMkd3vDRipGL6E/0i7IUmQfYYy6U10J5Rx+g==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.91.0.tgz",
+ "integrity": "sha512-lJv3x/xekzC/biiyAsVCioq2XnKNOZhI6jY3ZzLJZClYV8eKRi7D3KCsHRvMiCdGak1d/6sVp8F4NYY+YiWy1Q==",
"dependencies": {
- "@sentry-internal/feedback": "7.88.0",
- "@sentry-internal/tracing": "7.88.0",
- "@sentry/core": "7.88.0",
- "@sentry/replay": "7.88.0",
- "@sentry/types": "7.88.0",
- "@sentry/utils": "7.88.0"
+ "@sentry-internal/feedback": "7.91.0",
+ "@sentry-internal/tracing": "7.91.0",
+ "@sentry/core": "7.91.0",
+ "@sentry/replay": "7.91.0",
+ "@sentry/types": "7.91.0",
+ "@sentry/utils": "7.91.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.88.0.tgz",
- "integrity": "sha512-Jzbb7dcwiCO7kI0a1w+32UzWxbEn2OcZWzp55QMEeAh6nZ/5CXhXwpuHi0tW7doPj+cJdmxMTMu9LqMVfdGkzQ==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.91.0.tgz",
+ "integrity": "sha512-tu+gYq4JrTdrR+YSh5IVHF0fJi/Pi9y0HZ5H9HnYy+UMcXIotxf6hIEaC6ZKGeLWkGXffz2gKpQLe/g6vy/lPA==",
"dependencies": {
- "@sentry/types": "7.88.0",
- "@sentry/utils": "7.88.0"
+ "@sentry/types": "7.91.0",
+ "@sentry/utils": "7.91.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.88.0.tgz",
- "integrity": "sha512-em5dPKLPG7c/HGDbpIj3aHrWbA4iMwqjevqTzn+++KNO1YslkOosCaGsb1whU3AL1T9c3aIFIhZ4u3rNo+DxcA==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.91.0.tgz",
+ "integrity": "sha512-XwbesnLLNtaVXKtDoyBB96GxJuhGi9zy3a662Ba/McmumCnkXrMQYpQPh08U7MgkTyDRgjDwm7PXDhiKpcb03g==",
"dependencies": {
- "@sentry-internal/tracing": "7.88.0",
- "@sentry/core": "7.88.0",
- "@sentry/types": "7.88.0",
- "@sentry/utils": "7.88.0"
+ "@sentry-internal/tracing": "7.91.0",
+ "@sentry/core": "7.91.0",
+ "@sentry/types": "7.91.0",
+ "@sentry/utils": "7.91.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry/types": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.88.0.tgz",
- "integrity": "sha512-FvwvmX1pWAZKicPj4EpKyho8Wm+C4+r5LiepbbBF8oKwSPJdD2QV1fo/LWxsrzNxWOllFIVIXF5Ed3nPYQWpTw==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.91.0.tgz",
+ "integrity": "sha512-bcQnb7J3P3equbCUc+sPuHog2Y47yGD2sCkzmnZBjvBT0Z1B4f36fI/5WjyZhTjLSiOdg3F2otwvikbMjmBDew==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
- "version": "7.88.0",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.88.0.tgz",
- "integrity": "sha512-ukminfRmdBXTzk49orwJf3Lu3hR60ZRHjE2a4IXwYhyDT6JJgJqgsq1hzGXx0AyFfyS4WhfZ6QUBy7fu3BScZQ==",
+ "version": "7.91.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.91.0.tgz",
+ "integrity": "sha512-fvxjrEbk6T6Otu++Ax9ntlQ0sGRiwSC179w68aC3u26Wr30FAIRKqHTCCdc2jyWk7Gd9uWRT/cq+g8NG/8BfSg==",
"dependencies": {
- "@sentry/types": "7.88.0"
+ "@sentry/types": "7.91.0"
},
"engines": {
"node": ">=8"
diff --git a/package.json b/package.json
index c37652e26c..795686022b 100644
--- a/package.json
+++ b/package.json
@@ -37,12 +37,12 @@
"@casl/ability": "^6.5.0",
"@casl/angular": "^8.2.3",
"@faker-js/faker": "^8.3.1",
- "@fortawesome/angular-fontawesome": "^0.14.0",
+ "@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@ngneat/until-destroy": "^10.0.0",
- "@sentry/browser": "^7.88.0",
+ "@sentry/browser": "^7.91.0",
"angulartics2": "^12.2.1",
"assert": "^2.1.0",
"crypto-es": "^2.1.0",
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 09c82acb86..c161e5dc30 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
@@ -2,9 +2,8 @@ import { Component, OnInit } from "@angular/core";
import { RecurringActivity } from "../model/recurring-activity";
import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator";
import { RelatedEntitiesComponent } from "../../../core/entity-details/related-entities/related-entities.component";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
-import { ColumnConfig } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
+import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component";
/**
* @deprecated configure a RelatedEntitiesComponent instead
@@ -14,7 +13,7 @@ import { FormFieldConfig } from "../../../core/common-components/entity-form/ent
selector: "app-activities-overview",
templateUrl:
"../../../core/entity-details/related-entities/related-entities.component.html",
- imports: [EntitySubrecordComponent],
+ imports: [EntitiesTableComponent],
standalone: true,
})
export class ActivitiesOverviewComponent
@@ -33,12 +32,12 @@ export class ActivitiesOverviewComponent
relevantValue: "",
},
};
- _columns: ColumnConfig[] = [
+ override _columns: FormFieldConfig[] = [
this.titleColumn,
- "type",
- "assignedTo",
- "linkedGroups",
- "excludedParticipants",
+ { id: "type" },
+ { id: "assignedTo" },
+ { id: "linkedGroups" },
+ { id: "excludedParticipants" },
];
async ngOnInit() {
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 5b38fa7ecd..fca5ed4321 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
@@ -17,15 +17,16 @@
>
-
-
+
{
- this.records = applyUpdate(this.records, newNotes);
+ this.records = applyUpdate(this.records, newNotes, false);
this.selectDay(this.selectedDate?.toDate());
});
}
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 7459231ef4..72fae5b170 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
@@ -113,14 +113,15 @@
-
-
+
`,
+ templateUrl:
+ "../../../../core/entity-details/related-entities/related-entities.component.html",
standalone: true,
- imports: [EntitySubrecordComponent, NgIf],
+ imports: [EntitiesTableComponent],
})
-export class AserComponent implements OnInit {
+export class AserComponent extends RelatedEntitiesComponent {
@Input() entity: Child;
- @Input() config: { columns: ColumnConfig[] } = {
- columns: [
- { id: "date", visibleFrom: "xs" },
- { id: "math", visibleFrom: "xs" },
- { id: "english", visibleFrom: "xs" },
- { id: "hindi", visibleFrom: "md" },
- { id: "bengali", visibleFrom: "md" },
- { id: "remarks", visibleFrom: "md" },
- ],
- };
- records: Aser[];
+ property = "child";
+ entityCtr = Aser;
- constructor(private childrenService: ChildrenService) {}
+ override _columns: FormFieldConfig[] = [
+ { id: "date", visibleFrom: "xs" },
+ { id: "math", visibleFrom: "xs" },
+ { id: "english", visibleFrom: "xs" },
+ { id: "hindi", visibleFrom: "md" },
+ { id: "bengali", visibleFrom: "md" },
+ { id: "remarks", visibleFrom: "md" },
+ ];
- ngOnInit() {
- return this.loadData();
+ constructor(
+ private childrenService: ChildrenService,
+ entityMapper: EntityMapperService,
+ entityRegistry: EntityRegistry,
+ screenWidthObserver: ScreenWidthObserver,
+ ) {
+ super(entityMapper, entityRegistry, screenWidthObserver);
}
- async loadData() {
- this.records = await this.childrenService.getAserResultsOfChild(
- this.entity.getId(),
- );
- this.records.sort(
+ override async initData() {
+ this.data = (
+ await this.childrenService.getAserResultsOfChild(this.entity.getId())
+ ).sort(
(a, b) =>
(b.date ? b.date.valueOf() : 0) - (a.date ? a.date.valueOf() : 0),
);
}
-
- generateNewRecordFactory() {
- return () => {
- const newAtt = new Aser(Date.now().toString());
- newAtt.child = this.entity.getId();
- return newAtt;
- };
- }
}
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
index e22dde4032..e080a43b13 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
@@ -51,7 +51,6 @@ describe("ChildrenListComponent", () => {
default: "true",
true: "Currently active children",
false: "Currently inactive children",
- all: "All children",
} as BooleanFilterConfig,
{
id: "center",
@@ -99,7 +98,6 @@ describe("ChildrenListComponent", () => {
});
it("should load children on init", async () => {
- component.isLoading = true;
const child1 = new Child("c1");
const child2 = new Child("c2");
mockChildrenService.getChildren.and.resolveTo([child1, child2]);
@@ -107,6 +105,5 @@ describe("ChildrenListComponent", () => {
expect(mockChildrenService.getChildren).toHaveBeenCalled();
expect(component.childrenList).toEqual([child1, child2]);
- expect(component.isLoading).toBeFalse();
});
});
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.ts b/src/app/child-dev-project/children/children-list/children-list.component.ts
index d417239f68..bd1c7ed4ff 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.ts
@@ -14,7 +14,6 @@ import { RouteTarget } from "../../../route-target";
`,
@@ -22,10 +21,9 @@ import { RouteTarget } from "../../../route-target";
imports: [EntityListComponent],
})
export class ChildrenListComponent implements OnInit {
- childrenList: Child[] = [];
+ childrenList: Child[];
listConfig: EntityListConfig;
childConstructor = Child;
- isLoading = true;
constructor(
private childrenService: ChildrenService,
@@ -40,6 +38,5 @@ export class ChildrenListComponent implements OnInit {
(this.listConfig = data.config),
);
this.childrenList = await this.childrenService.getChildren();
- this.isLoading = false;
}
}
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
index 49581c5a4d..abc3e9a1d7 100644
--- 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
@@ -1,5 +1,6 @@
-
+>
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 cc1cec1c36..c862d46050 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
@@ -2,19 +2,21 @@ import { Component, Input, OnInit } from "@angular/core";
import { HealthCheck } from "../model/health-check";
import { ChildrenService } from "../../children.service";
import { Child } from "../../model/child";
-import { FormFieldConfig } from "../../../../core/common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../../core/common-components/entity-form/FormConfig";
import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator";
-import { EntitySubrecordComponent } from "../../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
+import { EntitiesTableComponent } from "../../../../core/common-components/entities-table/entities-table.component";
@DynamicComponent("HealthCheckup")
@Component({
selector: "app-health-checkup",
templateUrl: "./health-checkup.component.html",
- imports: [EntitySubrecordComponent],
+ imports: [EntitiesTableComponent],
standalone: true,
})
export class HealthCheckupComponent implements OnInit {
records: HealthCheck[] = [];
+ entityCtr = HealthCheck;
+
/**
* Column Description for the SubentityRecordComponent
* The Date-Column needs to be transformed to apply the MathFormCheck in the SubentityRecordComponent
diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
index 9209cd4b07..cad8702d47 100644
--- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
+++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
@@ -1,8 +1,7 @@
{
});
it("loads initial list including EventNotes if set in config", fakeAsync(() => {
- component.isLoading = true;
const note = Note.create(new Date("2020-01-01"), "test note");
note.category = testInteractionTypes[0];
const eventNote = EventNote.create(new Date("2020-01-01"), "test event");
@@ -196,6 +195,5 @@ describe("NotesManagerComponent", () => {
flush();
expect(component.notes).toEqual([note, eventNote]);
- expect(component.isLoading).toBeFalse();
}));
});
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 972167ac4f..aebb7b3ec0 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
@@ -3,7 +3,10 @@ import { Note } from "../model/note";
import { NoteDetailsComponent } from "../note-details/note-details.component";
import { ActivatedRoute } from "@angular/router";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
-import { FilterSelectionOption } from "../../../core/filter/filters/filters";
+import {
+ DataFilter,
+ FilterSelectionOption,
+} from "../../../core/filter/filters/filters";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { LoggingService } from "../../../core/logging/logging.service";
@@ -11,7 +14,6 @@ import { EntityListComponent } from "../../../core/entity-list/entity-list/entit
import { applyUpdate } from "../../../core/entity/model/entity-update";
import { EntityListConfig } from "../../../core/entity-list/EntityListConfig";
import { EventNote } from "../../attendance/model/event-note";
-import { WarningLevel } from "../../warning-level";
import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface";
import { merge } from "rxjs";
import moment from "moment";
@@ -55,36 +57,19 @@ export class NotesManagerComponent implements OnInit {
@Input() showEventNotesToggle: boolean;
config: EntityListConfig;
- noteConstructor = Note;
- notes: Note[] = [];
- isLoading: boolean = true;
-
- private statusFS: FilterSelectionOption[] = [
- {
- key: "urgent",
- label: $localize`:Filter-option for notes:Urgent`,
- filter: { "warningLevel.id": WarningLevel.URGENT },
- },
- {
- key: "follow-up",
- label: $localize`:Filter-option for notes:Needs Follow-Up`,
- filter: {
- "warningLevel.id": { $in: [WarningLevel.URGENT, WarningLevel.WARNING] },
- },
- },
- { key: "", label: $localize`All`, filter: {} },
- ];
+ entityConstructor = Note;
+ notes: Note[];
private dateFS: FilterSelectionOption[] = [
{
key: "current-week",
label: $localize`:Filter-option for notes:This Week`,
- filter: { date: this.getWeeksFilter(0) },
+ filter: { date: this.getWeeksFilter(0) } as DataFilter,
},
{
key: "last-week",
label: $localize`:Filter-option for notes:Since Last Week`,
- filter: { date: this.getWeeksFilter(1) },
+ filter: { date: this.getWeeksFilter(1) } as DataFilter,
},
{ key: "", label: $localize`All`, filter: {} },
];
@@ -117,7 +102,6 @@ export class NotesManagerComponent implements OnInit {
const eventNotes = await this.entityMapperService.loadType(EventNote);
notes = notes.concat(eventNotes);
}
- this.isLoading = false;
return notes;
}
@@ -141,7 +125,6 @@ export class NotesManagerComponent implements OnInit {
async updateIncludeEvents() {
this.includeEventNotes = !this.includeEventNotes;
- this.isLoading = true;
this.notes = await this.loadEntities();
}
@@ -150,11 +133,6 @@ export class NotesManagerComponent implements OnInit {
(filter) => filter.type === "prebuilt",
)) {
switch (prebuiltFilter.id) {
- case "status": {
- prebuiltFilter["options"] = this.statusFS;
- prebuiltFilter["default"] = "";
- break;
- }
case "date": {
prebuiltFilter["options"] = this.dateFS;
prebuiltFilter["default"] = "current-week";
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 60bc4d9906..b6c73f57d4 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.ts b/src/app/child-dev-project/notes/notes-related-to-entity/notes-related-to-entity.component.ts
index 3cdeee1543..c0718ce383 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
@@ -6,34 +6,35 @@ import moment from "moment";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator";
import { Entity } from "../../../core/entity/model/entity";
-import {
- ColumnConfig,
- DataFilter,
-} from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { FilterService } from "../../../core/filter/filter.service";
import { Child } from "../../children/model/child";
import { School } from "../../schools/model/school";
import { ChildSchoolRelation } from "../../children/model/childSchoolRelation";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
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";
/**
* 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: [EntitySubrecordComponent],
+ imports: [EntitiesTableComponent],
standalone: true,
})
export class NotesRelatedToEntityComponent implements OnInit {
@Input() entity: Entity;
- records: Array = [];
- isLoading: boolean;
+ records: Array;
@Input() columns: ColumnConfig[] = [
{ id: "date", visibleFrom: "xs" },
@@ -51,8 +52,11 @@ export class NotesRelatedToEntityComponent implements OnInit {
getColor = (note: Note) => note?.getColor();
newRecordFactory: () => Note;
+ entityConstructor = Note;
+
constructor(
private childrenService: ChildrenService,
+ private entityMapper: EntityMapperService,
private formDialog: FormDialogService,
private filterService: FilterService,
) {}
@@ -64,11 +68,10 @@ export class NotesRelatedToEntityComponent implements OnInit {
}
this.newRecordFactory = this.generateNewRecordFactory();
this.initNotesOfEntity();
+ this.listenToEntityUpdates();
}
private async initNotesOfEntity() {
- this.isLoading = true;
-
this.records = await this.childrenService
.getNotesRelatedTo(this.entity.getId(true))
.then((notes: Note[]) => {
@@ -81,8 +84,15 @@ export class NotesRelatedToEntityComponent implements OnInit {
});
return notes;
});
+ }
- this.isLoading = false;
+ private listenToEntityUpdates() {
+ this.entityMapper
+ .receiveUpdates(this.entityConstructor)
+ .pipe(untilDestroyed(this))
+ .subscribe((next) => {
+ this.records = applyUpdate(this.records, next);
+ });
}
generateNewRecordFactory() {
diff --git a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts
index 2ac5ade5e0..36d2e72fbd 100644
--- a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts
+++ b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts
@@ -10,9 +10,12 @@ import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { FormsModule } from "@angular/forms";
import { MatTooltipModule } from "@angular/material/tooltip";
import { NgIf } from "@angular/common";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
import { PillComponent } from "../../../core/common-components/pill/pill.component";
import { RelatedTimePeriodEntitiesComponent } from "../../../core/entity-details/related-time-period-entities/related-time-period-entities.component";
+import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.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";
// TODO: once schema-generated indices are available (#262), remove this component and use its generic super class directly
@DynamicComponent("ChildSchoolOverview")
@@ -27,7 +30,7 @@ import { RelatedTimePeriodEntitiesComponent } from "../../../core/entity-details
],
imports: [
FontAwesomeModule,
- EntitySubrecordComponent,
+ EntitiesTableComponent,
MatSlideToggleModule,
FormsModule,
MatTooltipModule,
@@ -41,12 +44,17 @@ export class ChildSchoolOverviewComponent
implements OnInit
{
mode: "child" | "school" = "child";
- @Input() showInactive = false;
+ @Input() showInactive = this.mode === "child";
+ entityCtr = ChildSchoolRelation;
- constructor(private childrenService: ChildrenService) {
- super(null, null);
+ constructor(
+ private childrenService: ChildrenService,
+ entityMapper: EntityMapperService,
+ entityRegistry: EntityRegistry,
+ screenWidthObserver: ScreenWidthObserver,
+ ) {
+ super(entityMapper, entityRegistry, screenWidthObserver);
- this.entityCtr = ChildSchoolRelation;
this.columns = [
{ id: "childId" }, // schoolId/childId replaced dynamically during init
{ id: "start", visibleFrom: "md" },
@@ -60,8 +68,7 @@ export class ChildSchoolOverviewComponent
this.mode = this.inferMode(this.entity);
this.switchRelatedEntityColumnForMode();
- await this.loadData();
- super.onIsActiveFilterChange(this.showInactive);
+ await super.ngOnInit();
}
private inferMode(entity: Entity): "child" | "school" {
@@ -85,17 +92,14 @@ export class ChildSchoolOverviewComponent
}
}
- async loadData() {
+ override async initData() {
if (!this.mode) {
return;
}
- this.isLoading = true;
this.data = await this.childrenService.queryRelationsOf(
this.mode,
this.entity.getId(false),
);
-
- this.isLoading = false;
}
}
diff --git a/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.spec.ts b/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.spec.ts
index b99a191966..bd3a2ea960 100644
--- a/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.spec.ts
+++ b/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.spec.ts
@@ -15,9 +15,9 @@ import { FormGroup } from "@angular/forms";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { CdkDragDrop } from "@angular/cdk/drag-drop";
import { of } from "rxjs";
-import { ColumnConfig } from "../../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { AdminModule } from "../../admin.module";
import { FormConfig } from "../../../entity-details/form/form.component";
+import { ColumnConfig } from "../../../common-components/entity-form/FormConfig";
describe("AdminEntityFormComponent", () => {
let component: AdminEntityFormComponent;
diff --git a/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.ts b/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.ts
index ca788357f8..213e1cc421 100644
--- a/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.ts
+++ b/src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.ts
@@ -12,9 +12,9 @@ import {
} from "@angular/cdk/drag-drop";
import {
ColumnConfig,
+ FormFieldConfig,
toFormFieldConfig,
-} from "../../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
-import { FormFieldConfig } from "../../../common-components/entity-form/entity-form/FormConfig";
+} from "../../../common-components/entity-form/FormConfig";
import { AdminEntityService } from "../../admin-entity.service";
import { lastValueFrom } from "rxjs";
import { NgForOf, NgIf } from "@angular/common";
diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts
index 169ff72cbb..e78205f0f7 100644
--- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts
+++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts
@@ -30,10 +30,11 @@ export class ConfigurableEnumService {
getEnumValues(
id: string,
): T[] {
- return this.getEnum(id).values as T[];
+ const configurableEnum = this.getEnum(id);
+ return configurableEnum ? (configurableEnum.values as T[]) : [];
}
- getEnum(id: string): ConfigurableEnum {
+ getEnum(id: string): ConfigurableEnum | undefined {
if (!this.enums) {
return;
}
diff --git a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts
index bffccff20a..5f5713440c 100644
--- a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts
+++ b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts
@@ -135,5 +135,4 @@ export class ConfigureEnumPopupComponent {
});
this.newOptionInput = "";
}
- mynewFun() {}
}
diff --git a/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts
index 0a71e6821d..cdf6b647d9 100644
--- a/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts
+++ b/src/app/core/basic-datatypes/configurable-enum/enum-dropdown/enum-dropdown.component.ts
@@ -63,7 +63,7 @@ export class EnumDropdownComponent implements OnChanges {
if (changes.hasOwnProperty("enumId") || changes.hasOwnProperty("form")) {
this.invalidOptions = this.prepareInvalidOptions();
}
- this.options = [...this.enumEntity.values, ...this.invalidOptions];
+ this.options = [...this.enumEntity?.values, ...this.invalidOptions];
}
private prepareInvalidOptions(): ConfigurableEnumValue[] {
diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html
index b0bf659070..4e6944dbdb 100644
--- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html
+++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.html
@@ -18,7 +18,7 @@
(mouseenter)="preselectAllRange()"
(mouseleave)="unselectRange()"
(click)="selectRangeAndClose('all')"
- [class.selected-option]="filter.selectedOption === '_'"
+ [class.selected-option]="filter.selectedOptionValues.length === 0"
>
All
diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts
index b63dc2ae5f..8acf5cb210 100644
--- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts
+++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts
@@ -12,7 +12,8 @@ import { HarnessLoader } from "@angular/cdk/testing";
import { DateRange } from "@angular/material/datepicker";
import { MatCalendarHarness } from "@angular/material/datepicker/testing";
import moment from "moment";
-import { DateFilter } from "../../../../filter/filters/filters";
+
+import { DateFilter } from "../../../../filter/filters/dateFilter";
describe("DateRangeFilterPanelComponent", () => {
let component: DateRangeFilterPanelComponent;
@@ -22,7 +23,7 @@ describe("DateRangeFilterPanelComponent", () => {
beforeEach(async () => {
dateFilter = new DateFilter("test", "Test", defaultDateFilters);
- dateFilter.selectedOption = "1";
+ dateFilter.selectedOptionValues = ["1"];
jasmine.clock().mockDate(moment("2023-04-08").toDate());
await TestBed.configureTestingModule({
imports: [MatNativeDateModule],
@@ -85,7 +86,7 @@ describe("DateRangeFilterPanelComponent", () => {
moment("2023-04-08").startOf("day").toDate(),
);
expect(filterRange.end).toEqual(moment("2023-04-08").endOf("day").toDate());
- expect(dateFilter.selectedOption).toBe("0");
+ expect(dateFilter.selectedOptionValues).toEqual(["0"]);
});
it("should highlight the date range when hovering over a option", async () => {
@@ -114,9 +115,9 @@ describe("DateRangeFilterPanelComponent", () => {
}
});
- it("should return '_' as filter.selectedOption when 'all' option has been chosen", async () => {
+ it("should return empty array as filter.selectedOption when 'all' option has been chosen", async () => {
component.selectRangeAndClose("all");
- expect(dateFilter.selectedOption).toEqual("_");
+ expect(dateFilter.selectedOptionValues).toEqual([]);
});
it("should correctly calculate date ranges based on the config", () => {
diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts
index 1764e848b8..d4871f900c 100644
--- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts
+++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.ts
@@ -1,10 +1,10 @@
import { Component, Inject } from "@angular/core";
import {
DateRange,
+ MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
MatDatepickerModule,
MatDateSelectionModel,
MatRangeDateSelectionModel,
- MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
} from "@angular/material/datepicker";
import {
MAT_DIALOG_DATA,
@@ -16,8 +16,8 @@ import { NgForOf } from "@angular/common";
import { DateRangeFilterConfigOption } from "../../../../entity-list/EntityListConfig";
import moment from "moment";
import { FormsModule } from "@angular/forms";
-import { DateFilter } from "../../../../filter/filters/filters";
import { dateToString } from "../../../../../utils/utils";
+import { DateFilter } from "../../../../filter/filters/dateFilter";
export const defaultDateFilters: DateRangeFilterConfigOption[] = [
{
@@ -91,9 +91,9 @@ export class DateRangeFilterPanelComponent {
selectRangeAndClose(index: number | "all"): void {
if (typeof index === "number") {
- this.filter.selectedOption = index.toString();
+ this.filter.selectedOptionValues = [index.toString()];
} else {
- this.filter.selectedOption = "_";
+ this.filter.selectedOptionValues = [];
}
this.dialogRef.close();
}
@@ -102,11 +102,11 @@ export class DateRangeFilterPanelComponent {
if (!this.selectedRangeValue?.start || this.selectedRangeValue?.end) {
this.selectedRangeValue = new DateRange(selectedDate, null);
} else {
- const start = this.selectedRangeValue.start;
- this.filter.selectedOption =
+ const start: Date = this.selectedRangeValue.start;
+ this.filter.selectedOptionValues =
start < selectedDate
- ? dateToString(start) + "_" + dateToString(selectedDate)
- : dateToString(selectedDate) + "_" + dateToString(start);
+ ? [dateToString(start), dateToString(selectedDate)]
+ : [dateToString(selectedDate), dateToString(start)];
this.dialogRef.close();
}
}
diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts
index 69b266e69c..52ffd25b2a 100644
--- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts
+++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts
@@ -4,9 +4,9 @@ import { DateRangeFilterComponent } from "./date-range-filter.component";
import { MatDialog } from "@angular/material/dialog";
import { MatNativeDateModule } from "@angular/material/core";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
-import { DateFilter } from "../../../filter/filters/filters";
import { defaultDateFilters } from "./date-range-filter-panel/date-range-filter-panel.component";
import moment from "moment";
+import { DateFilter } from "../../../filter/filters/dateFilter";
describe("DateRangeFilterComponent", () => {
let component: DateRangeFilterComponent;
@@ -30,12 +30,12 @@ describe("DateRangeFilterComponent", () => {
it("should set the correct date filter when a new option is selected", () => {
const dateFilter = new DateFilter("test", "Test", defaultDateFilters);
- dateFilter.selectedOption = "9";
+ dateFilter.selectedOptionValues = ["9"];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
jasmine.clock().mockDate(moment("2023-05-18").toDate());
- dateFilter.selectedOption = "0";
+ dateFilter.selectedOptionValues = ["0"];
component.filterConfig = dateFilter;
let expectedDataFilter = {
test: {
@@ -45,7 +45,7 @@ describe("DateRangeFilterComponent", () => {
};
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter);
- dateFilter.selectedOption = "1";
+ dateFilter.selectedOptionValues = ["1"];
component.filterConfig = dateFilter;
expectedDataFilter = {
test: {
@@ -55,7 +55,7 @@ describe("DateRangeFilterComponent", () => {
};
expect(component.dateFilter.getFilter()).toEqual(expectedDataFilter);
- dateFilter.selectedOption = "_";
+ dateFilter.selectedOptionValues = [];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
jasmine.clock().uninstall();
@@ -64,15 +64,15 @@ describe("DateRangeFilterComponent", () => {
it("should set the correct date filter when inputting a specific date range via the URL", () => {
let dateFilter = new DateFilter("test", "test", []);
- dateFilter.selectedOption = "1_2_3";
+ dateFilter.selectedOptionValues = ["1", "2", "3"];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
- dateFilter.selectedOption = "_";
+ dateFilter.selectedOptionValues = [];
component.filterConfig = dateFilter;
expect(component.dateFilter.getFilter()).toEqual({});
- dateFilter.selectedOption = "2022-9-18_";
+ dateFilter.selectedOptionValues = ["2022-9-18", ""];
component.filterConfig = dateFilter;
let testFilter: { $gte?: string; $lte?: string } = { $gte: "2022-09-18" };
let expectedDateFilter = {
@@ -80,7 +80,7 @@ describe("DateRangeFilterComponent", () => {
};
expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter);
- dateFilter.selectedOption = "_2023-01-3";
+ dateFilter.selectedOptionValues = ["", "2023-01-3"];
component.filterConfig = dateFilter;
testFilter = { $lte: "2023-01-03" };
expectedDateFilter = {
@@ -88,7 +88,7 @@ describe("DateRangeFilterComponent", () => {
};
expect(component.dateFilter.getFilter()).toEqual(expectedDateFilter);
- dateFilter.selectedOption = "2022-9-18_2023-01-3";
+ dateFilter.selectedOptionValues = ["2022-9-18", "2023-01-3"];
component.filterConfig = dateFilter;
testFilter = {
$gte: "2022-09-18",
@@ -107,9 +107,10 @@ describe("DateRangeFilterComponent", () => {
component.dateChangedManually();
- expect(component.dateFilter.selectedOption).toEqual(
- "2021-10-28_2024-02-12",
- );
+ expect(component.dateFilter.selectedOptionValues).toEqual([
+ "2021-10-28",
+ "2024-02-12",
+ ]);
let expectedDataFilter = {
test: {
$gte: "2021-10-28",
diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts
index be2a4a83ef..301a1832ff 100644
--- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts
+++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.ts
@@ -1,12 +1,13 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { Entity } from "../../../entity/model/entity";
-import { DateFilter, Filter } from "../../../filter/filters/filters";
+import { Filter } from "../../../filter/filters/filters";
import { DateRangeFilterPanelComponent } from "./date-range-filter-panel/date-range-filter-panel.component";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatDatepickerModule } from "@angular/material/datepicker";
import { FormsModule } from "@angular/forms";
import { dateToString, isValidDate } from "../../../../utils/utils";
+import { DateFilter } from "../../../filter/filters/dateFilter";
@Component({
selector: "app-date-range-filter",
@@ -20,7 +21,7 @@ export class DateRangeFilterComponent {
toDate: Date;
dateFilter: DateFilter;
- @Output() selectedOptionChange = new EventEmitter();
+ @Output() selectedOptionChange = new EventEmitter();
@Input() set filterConfig(value: Filter) {
this.dateFilter = value as DateFilter;
@@ -37,16 +38,16 @@ export class DateRangeFilterComponent {
) {
this.fromDate = range.start;
this.toDate = range.end;
- this.selectedOptionChange.emit(this.dateFilter.selectedOption);
+ this.selectedOptionChange.emit(this.dateFilter.selectedOptionValues);
}
}
dateChangedManually() {
- this.dateFilter.selectedOption =
- (isValidDate(this.fromDate) ? dateToString(this.fromDate) : "") +
- "_" +
- (isValidDate(this.toDate) ? dateToString(this.toDate) : "");
- this.selectedOptionChange.emit(this.dateFilter.selectedOption);
+ this.dateFilter.selectedOptionValues = [
+ isValidDate(this.fromDate) ? dateToString(this.fromDate) : "",
+ isValidDate(this.toDate) ? dateToString(this.toDate) : "",
+ ];
+ this.selectedOptionChange.emit(this.dateFilter.selectedOptionValues);
}
openDialog(e: Event) {
diff --git a/src/app/core/basic-datatypes/entity-array/edit-entity-array/entity-reference-array.stories.ts b/src/app/core/basic-datatypes/entity-array/edit-entity-array/entity-reference-array.stories.ts
index 6c025a7ecc..8d7be739a6 100644
--- a/src/app/core/basic-datatypes/entity-array/edit-entity-array/entity-reference-array.stories.ts
+++ b/src/app/core/basic-datatypes/entity-array/edit-entity-array/entity-reference-array.stories.ts
@@ -1,5 +1,5 @@
import { applicationConfig, Meta, StoryFn } from "@storybook/angular";
-import { FormFieldConfig } from "../../../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../common-components/entity-form/FormConfig";
import {
entityFormStorybookDefaultParameters,
StorybookBaseModule,
diff --git a/src/app/core/basic-datatypes/entity/edit-single-entity/entity-reference.stories.ts b/src/app/core/basic-datatypes/entity/edit-single-entity/entity-reference.stories.ts
index 8ea71b6eaf..9602134535 100644
--- a/src/app/core/basic-datatypes/entity/edit-single-entity/entity-reference.stories.ts
+++ b/src/app/core/basic-datatypes/entity/edit-single-entity/entity-reference.stories.ts
@@ -1,5 +1,5 @@
import { applicationConfig, Meta, StoryFn } from "@storybook/angular";
-import { FormFieldConfig } from "../../../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../common-components/entity-form/FormConfig";
import {
entityFormStorybookDefaultParameters,
StorybookBaseModule,
diff --git a/src/app/core/common-components/description-only/edit-description-only/edit-description-only.component.ts b/src/app/core/common-components/description-only/edit-description-only/edit-description-only.component.ts
index d4eb424984..f8fac8ff55 100644
--- a/src/app/core/common-components/description-only/edit-description-only/edit-description-only.component.ts
+++ b/src/app/core/common-components/description-only/edit-description-only/edit-description-only.component.ts
@@ -1,6 +1,6 @@
import { Component, Input } from "@angular/core";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
-import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../entity-form/FormConfig";
@DynamicComponent("EditDescriptionOnly")
@Component({
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
new file mode 100644
index 0000000000..dd5bfac392
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entities-table.component.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss b/src/app/core/common-components/entities-table/entities-table.component.scss
similarity index 70%
rename from src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss
rename to src/app/core/common-components/entities-table/entities-table.component.scss
index cce348e38b..79afdf72c8 100644
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss
+++ b/src/app/core/common-components/entities-table/entities-table.component.scss
@@ -2,17 +2,14 @@
@use "variables/colors";
@use "@angular/material/core/style/elevation" as mat-elevation;
-.table-action-button {
- border: 1px solid lightgrey;
- border-radius: 4px;
- margin: sizes.$small;
- color: colors.$accent;
+.table-container {
+ position: relative; // anchor for further absolute positioning of child elements
}
-.mat-column-actions {
- width: 1px;
- white-space: nowrap;
- text-align: center;
+.column-menu {
+ position: absolute;
+ top: 0;
+ right: 0;
}
.table-row:hover {
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
new file mode 100644
index 0000000000..96f54b3d6d
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entities-table.component.spec.ts
@@ -0,0 +1,276 @@
+import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing";
+
+import { EntitiesTableComponent } from "./entities-table.component";
+import { Entity } from "../../entity/model/entity";
+import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface";
+import { Note } from "../../../child-dev-project/notes/model/note";
+import moment from "moment/moment";
+import { Child } from "../../../child-dev-project/children/model/child";
+import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
+import { genders } from "../../../child-dev-project/children/model/genders";
+import { DateWithAge } from "../../basic-datatypes/date-with-age/dateWithAge";
+import { EntityFormService } from "../entity-form/entity-form.service";
+import { toFormFieldConfig } from "../entity-form/FormConfig";
+import { FilterService } from "../../filter/filter.service";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { CurrentUserSubject } from "../../session/current-user-subject";
+import { of } from "rxjs";
+import { CoreTestingModule } from "../../../utils/core-testing.module";
+import { FormDialogService } from "../../form-dialog/form-dialog.service";
+import { DateDatatype } from "../../basic-datatypes/date/date.datatype";
+
+describe("EntitiesTableComponent", () => {
+ let component: EntitiesTableComponent;
+ let fixture: ComponentFixture>;
+
+ let mockFormService: jasmine.SpyObj;
+
+ beforeEach(async () => {
+ mockFormService = jasmine.createSpyObj(["extendFormFieldConfig"]);
+ mockFormService.extendFormFieldConfig.and.callFake((c) =>
+ toFormFieldConfig(c),
+ );
+
+ await TestBed.configureTestingModule({
+ imports: [
+ EntitiesTableComponent,
+ CoreTestingModule,
+ NoopAnimationsModule,
+ ],
+ providers: [
+ { provide: EntityFormService, useValue: mockFormService },
+ FilterService,
+ {
+ provide: FormDialogService,
+ useValue: jasmine.createSpyObj(["openFormPopup"]),
+ },
+ { provide: CurrentUserSubject, useValue: of(null) },
+ { provide: EntityMapperService, useValue: null },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(EntitiesTableComponent);
+ component = fixture.componentInstance;
+ component.editable = false;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should sort enums by the label", () => {
+ class Test extends Entity {
+ public enumValue: ConfigurableEnumValue;
+
+ constructor(label: string, id: string) {
+ super();
+ this.enumValue = { label: label, id: id };
+ }
+ }
+
+ const first = new Test("aaa", "first");
+ const second = new Test("aab", "second");
+ const third = new Test("c", "third");
+ component.records = [second, first, third];
+ component.customColumns = [
+ {
+ id: "enumValue",
+ label: "Test Configurable Enum",
+ viewComponent: "DisplayConfigurableEnum",
+ },
+ ];
+ fixture.detectChanges();
+
+ component.recordsDataSource.sort.direction = "";
+ component.recordsDataSource.sort.sort({
+ id: "enumValue",
+ start: "asc",
+ disableClear: false,
+ });
+
+ const sortedData = component.recordsDataSource
+ ._orderData(component.recordsDataSource.data)
+ .map((row) => row.record);
+ expect(sortedData).toEqual([first, second, third]);
+ });
+
+ it("should apply default sort on first column and order dates descending", () => {
+ component.entityType = Note;
+ component.customColumns = [
+ { id: "date", dataType: DateDatatype.dataType },
+ "subject",
+ ];
+ component.columnsToDisplay = ["date", "subject"];
+
+ const oldNote = Note.create(moment().subtract(1, "day").toDate());
+ const newNote = Note.create(new Date());
+ component.records = [oldNote, newNote];
+ fixture.detectChanges();
+
+ expect(component.recordsDataSource.sort.direction).toBe("desc");
+ expect(component.recordsDataSource.sort.active).toBe("date");
+ });
+
+ it("should use input defaultSort if defined", () => {
+ component.customColumns = ["date", "subject"];
+ component.columnsToDisplay = ["date", "subject"];
+ const n1 = Note.create(new Date(), "1");
+ const n2 = Note.create(new Date(), "2");
+ const n3 = Note.create(new Date(), "3");
+
+ component.records = [n3, n1, n2];
+
+ component.sortBy = { active: "subject", direction: "asc" };
+ fixture.detectChanges();
+
+ expect(component.recordsDataSource.sort.direction).toBe("asc");
+ expect(component.recordsDataSource.sort.active).toBe("subject");
+ });
+
+ it("should sort standard objects", () => {
+ const children = [
+ new Child("0"),
+ new Child("1"),
+ new Child("2"),
+ new Child("3"),
+ ];
+ children[0].name = "AA";
+ children[3].name = "AB";
+ children[2].name = "Z";
+ children[1].name = "C";
+ component.records = children;
+
+ component.sortBy = { active: "name", direction: "asc" };
+ fixture.detectChanges();
+
+ const sortedIds = component.recordsDataSource
+ ._orderData(component.recordsDataSource.data)
+ .map((c) => c.record.getId());
+ expect(sortedIds).toEqual(["0", "3", "1", "2"]);
+ });
+
+ it("should sort non-standard objects", () => {
+ const notes = [new Note("0"), new Note("1"), new Note("2"), new Note("3")];
+ notes[0].category = { id: "0", label: "AA", _ordinal: 3 };
+ notes[1].category = { id: "3", label: "C", _ordinal: 1 };
+ notes[2].category = { id: "2", label: "Z", _ordinal: 0 };
+ notes[3].category = { id: "1", label: "AB", _ordinal: 2 };
+ component.records = notes;
+
+ component.sortBy = { active: "category", direction: "asc" };
+ fixture.detectChanges();
+
+ const sortedIds = component.recordsDataSource
+ ._orderData(component.recordsDataSource.data)
+ .map((note) => note.record.getId());
+ expect(sortedIds).toEqual(["0", "3", "1", "2"]);
+ });
+
+ it("should sort strings ignoring case", () => {
+ const names = ["C", "A", "b"];
+ component.records = names.map((name) => Child.create(name));
+
+ component.sortBy = { active: "name", direction: "asc" };
+ fixture.detectChanges();
+
+ const sortedNames = component.recordsDataSource
+ ._orderData(component.recordsDataSource.data)
+ .map((row) => row.record["name"]);
+
+ expect(sortedNames).toEqual(["A", "b", "C"]);
+ });
+
+ it("should notify when an entity is clicked", (done) => {
+ const child = new Child();
+ component.rowClick.subscribe((entity) => {
+ expect(entity).toEqual(child);
+ done();
+ });
+
+ component.onRowClick({ record: child });
+ });
+
+ it("should filter data based on filter definition", () => {
+ const c1 = Child.create("Matching");
+ c1.dateOfBirth = new DateWithAge(moment().subtract(1, "years").toDate());
+ const c2 = Child.create("Not Matching");
+ c2.dateOfBirth = new DateWithAge(moment().subtract(2, "years").toDate());
+ const c3 = Child.create("Matching");
+ c3.dateOfBirth = new DateWithAge(moment().subtract(3, "years").toDate());
+ // get type-safety for filters
+ const childComponent = component as any as EntitiesTableComponent;
+ childComponent.records = [c1, c2, c3];
+
+ childComponent.filter = { name: "Matching" };
+
+ expect(childComponent.recordsDataSource.data).toEqual([
+ { record: c1 },
+ { record: c3 },
+ ]);
+
+ childComponent.filter = {
+ name: "Matching",
+ "dateOfBirth.age": { $gte: 2 },
+ } as any;
+
+ expect(childComponent.recordsDataSource.data).toEqual([{ record: c3 }]);
+
+ const c4 = Child.create("Matching");
+ c4.dateOfBirth = new DateWithAge(moment().subtract(4, "years").toDate());
+ const c5 = Child.create("Not Matching");
+
+ childComponent.records = [c1, c2, c3, c4, c5];
+
+ expect(childComponent.recordsDataSource.data).toEqual([
+ { record: c3 },
+ { record: c4 },
+ ]);
+ });
+
+ it("should remove an entity if it does not pass the filter anymore", fakeAsync(() => {
+ const child = new Child();
+ child.gender = genders[1];
+ component.records = [child];
+ component.filter = { "gender.id": genders[1].id } as any;
+
+ expect(component.recordsDataSource.data).toEqual([{ record: child }]);
+
+ child.gender = genders[2];
+ component.records = [child]; // parent component has to update the records Input array
+
+ expect(component.recordsDataSource.data).toEqual([]);
+ }));
+
+ it("should only show active relations by default", async () => {
+ const active1 = new Entity();
+ active1.inactive = false;
+ const inactive = new Entity();
+ inactive.inactive = true;
+
+ component.records = [active1, inactive];
+
+ expect(component.recordsDataSource.data).toEqual([{ record: active1 }]);
+ });
+
+ it("should overwrite entity schema fields with customColumn config", async () => {
+ component.entityType = Child;
+ const customField = {
+ id: "name",
+ label: "Custom Name Label",
+ };
+ component.customColumns = [customField];
+
+ expect(component._columns.find((c) => c.id === customField.id).label).toBe(
+ customField.label,
+ );
+ });
+
+ it("should set noSorting if dataType cannot be sorted properly", () => {
+ component.entityType = Note;
+
+ expect(
+ component._columns.find((c) => c.id === "children").noSorting,
+ ).toBeTrue();
+ });
+});
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
new file mode 100644
index 0000000000..e28dfbc4ca
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entities-table.component.ts
@@ -0,0 +1,376 @@
+import {
+ AfterViewInit,
+ Component,
+ EventEmitter,
+ Input,
+ Output,
+ ViewChild,
+} from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { EntityFieldEditComponent } from "../entity-field-edit/entity-field-edit.component";
+import { EntityFieldLabelComponent } from "../entity-field-label/entity-field-label.component";
+import { EntityFieldViewComponent } from "../entity-field-view/entity-field-view.component";
+import { ListPaginatorComponent } from "./list-paginator/list-paginator.component";
+import {
+ MatCheckboxChange,
+ MatCheckboxModule,
+} from "@angular/material/checkbox";
+import { MatProgressBarModule } from "@angular/material/progress-bar";
+import { MatSlideToggleModule } from "@angular/material/slide-toggle";
+import {
+ MatSort,
+ MatSortModule,
+ Sort,
+ SortDirection,
+} from "@angular/material/sort";
+import { MatTableDataSource, MatTableModule } from "@angular/material/table";
+import { Entity, EntityConstructor } from "../../entity/model/entity";
+import {
+ ColumnConfig,
+ FormFieldConfig,
+ toFormFieldConfig,
+} from "../entity-form/FormConfig";
+import {
+ EntityForm,
+ EntityFormService,
+} from "../entity-form/entity-form.service";
+import { tableSort } from "./table-sort/table-sort";
+import { UntilDestroy } from "@ngneat/until-destroy";
+import { entityFilterPredicate } from "../../filter/filter-generator/filter-predicate";
+import { FormDialogService } from "../../form-dialog/form-dialog.service";
+import { Router } from "@angular/router";
+import { FilterService } from "../../filter/filter.service";
+import { DataFilter } from "../../filter/filters/filters";
+import { EntityInlineEditActionsComponent } from "./entity-inline-edit-actions/entity-inline-edit-actions.component";
+import { EntityCreateButtonComponent } from "../entity-create-button/entity-create-button.component";
+import { DateDatatype } from "../../basic-datatypes/date/date.datatype";
+import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
+import { EntityArrayDatatype } from "../../basic-datatypes/entity-array/entity-array.datatype";
+import { EntityDatatype } from "../../basic-datatypes/entity/entity.datatype";
+
+/**
+ * A simple display component (no logic and transformations) to display a table of entities.
+ */
+@UntilDestroy()
+@Component({
+ selector: "app-entities-table",
+ standalone: true,
+ imports: [
+ CommonModule,
+ EntityFieldEditComponent,
+ EntityFieldLabelComponent,
+ EntityFieldViewComponent,
+ ListPaginatorComponent,
+ MatCheckboxModule,
+ MatProgressBarModule,
+ MatSlideToggleModule,
+ MatSortModule,
+ MatTableModule,
+ EntityInlineEditActionsComponent,
+ EntityCreateButtonComponent,
+ ],
+ templateUrl: "./entities-table.component.html",
+ styleUrl: "./entities-table.component.scss",
+})
+export class EntitiesTableComponent implements AfterViewInit {
+ @Input() set records(value: T[]) {
+ if (!value) {
+ return;
+ }
+ this._records = value;
+
+ this.updateFilteredData();
+ this.isLoading = false;
+ }
+ _records: T[] = [];
+ /** data displayed in the template's table */
+ recordsDataSource: MatTableDataSource>;
+ isLoading: boolean = true;
+
+ /**
+ * Additional or overwritten field configurations for columns
+ * @param value
+ */
+ @Input() set customColumns(value: ColumnConfig[]) {
+ this._customColumns = (value ?? []).map((c) =>
+ this._entityType
+ ? this.entityFormService.extendFormFieldConfig(c, this._entityType)
+ : toFormFieldConfig(c),
+ );
+ const entityColumns = this._entityType?.schema
+ ? [...this._entityType.schema.entries()].map(
+ ([id, field]) => ({ ...field, id }) as FormFieldConfig,
+ )
+ : [];
+
+ this._columns = [
+ ...entityColumns.filter(
+ // if there is a customColumn for a field from entity config, don't add the base schema field
+ (c) => !this._customColumns.some((customCol) => customCol.id === c.id),
+ ),
+ ...this._customColumns,
+ ];
+ this._columns.forEach((c) => this.disableSortingHeaderForAdvancedFields(c));
+
+ if (!this.columnsToDisplay) {
+ this.columnsToDisplay = this._customColumns
+ .filter((c) => !c.hideFromTable)
+ .map((c) => c.id);
+ }
+
+ this.idForSavingPagination = this._customColumns
+ .map((col) => col.id)
+ .join("");
+ }
+ _customColumns: FormFieldConfig[];
+ _columns: FormFieldConfig[] = [];
+
+ /**
+ * Manually define the columns to be shown.
+ *
+ * @param value
+ */
+ @Input() set columnsToDisplay(value: string[]) {
+ if (!value || value.length === 0) {
+ value = (this._customColumns ?? this._columns).map((c) => c.id);
+ }
+ value = value.filter((c) => !c.startsWith("__")); // remove internal action columns
+
+ const cols = [];
+ if (this._selectable) {
+ cols.push(this.ACTIONCOLUMN_SELECT);
+ }
+ if (this._editable) {
+ cols.push(this.ACTIONCOLUMN_EDIT);
+ }
+ cols.push(...value);
+ this._columnsToDisplay = cols;
+
+ if (this.sortIsInferred) {
+ this.sortBy = this.inferDefaultSort();
+ this.sortIsInferred = true;
+ }
+ }
+ _columnsToDisplay: string[];
+
+ @Input() set entityType(value: EntityConstructor) {
+ this._entityType = value;
+ this.customColumns = this._customColumns;
+ }
+ _entityType: EntityConstructor;
+
+ /** how to sort data by default during initialization */
+ @Input() set sortBy(value: Sort) {
+ if (!value) {
+ return;
+ }
+
+ this._sortBy = value;
+ this.sortIsInferred = false;
+ }
+ _sortBy: Sort;
+ @ViewChild(MatSort, { static: true }) sort: MatSort;
+ private sortIsInferred: boolean = true;
+
+ /**
+ * Adds a filter for the displayed data.
+ * Only data, that passes the filter will be shown in the table.
+ */
+ @Input() set filter(value: DataFilter) {
+ this._filter = value ?? {};
+ this.updateFilteredData();
+ }
+ _filter: DataFilter = {};
+ /** output the currently displayed records, whenever filters for the user change */
+ @Output() filteredRecordsChange = new EventEmitter(true);
+
+ private updateFilteredData() {
+ this.addActiveInactiveFilter(this._filter);
+ const filterPredicate = this.filterService.getFilterPredicate(this._filter);
+ const filteredData = this._records.filter(filterPredicate);
+ this.recordsDataSource.data = filteredData.map((record) => ({ record }));
+
+ this.filteredRecordsChange.emit(filteredData);
+ }
+
+ @Input() set filterFreetext(value: string) {
+ this.recordsDataSource.filter = value;
+ }
+
+ /** function returns the background color for each row*/
+ @Input() getBackgroundColor?: (rec: T) => string = (rec: T) => rec.getColor();
+ idForSavingPagination: string;
+
+ @Input() clickMode: "popup" | "navigate" | "none" = "popup";
+ @Output() rowClick: EventEmitter = new EventEmitter();
+
+ /**
+ * BULK SELECT
+ * User can use checkboxes to select multiple rows, so that parent components can execute bulk actions on them.
+ */
+ @Input() set selectable(v: boolean) {
+ this._selectable = v;
+ this.columnsToDisplay = this._columnsToDisplay;
+ }
+ _selectable: boolean = false;
+
+ readonly ACTIONCOLUMN_SELECT = "__select";
+
+ /**
+ * outputs an event containing an array of currently selected records (checkmarked by the user)
+ * Checkboxes to select rows are only displayed if you set "selectable" also.
+ */
+ @Output() selectedRecordsChange: EventEmitter = new EventEmitter();
+ @Input() selectedRecords: T[] = [];
+
+ selectRow(row: TableRow, event: MatCheckboxChange) {
+ if (event.checked) {
+ this.selectedRecords.push(row.record);
+ } else {
+ const index = this.selectedRecords.indexOf(row.record);
+ if (index > -1) {
+ this.selectedRecords.splice(index, 1);
+ }
+ }
+
+ this.selectedRecordsChange.emit(this.selectedRecords);
+ }
+
+ /**
+ * INLINE EDIT
+ * User can switch a row into edit mode to change and save field values directly from within the table
+ */
+ @Input() set editable(v: boolean) {
+ this._editable = v;
+ this.columnsToDisplay = this._columnsToDisplay;
+ }
+ _editable: boolean = true;
+ readonly ACTIONCOLUMN_EDIT = "__edit";
+ /**
+ * factory method to create a new instance of the displayed Entity type
+ * used when the user adds a new entity to the list.
+ */
+ @Input() newRecordFactory: () => T;
+
+ /**
+ * Show one record's details in a modal dialog (if configured).
+ * @param row The entity whose details should be displayed.
+ */
+ onRowClick(row: TableRow) {
+ if (row.formGroup && !row.formGroup.disabled) {
+ return;
+ }
+
+ this.showEntity(row.record);
+ this.rowClick.emit(row.record);
+ }
+
+ showEntity(entity: T) {
+ switch (this.clickMode) {
+ case "popup":
+ this.formDialog.openFormPopup(entity, this._customColumns);
+ break;
+ case "navigate":
+ this.router.navigate([
+ entity.getConstructor().route,
+ entity.getId(false),
+ ]);
+ break;
+ }
+ }
+
+ constructor(
+ private entityFormService: EntityFormService,
+ private formDialog: FormDialogService,
+ private router: Router,
+ private filterService: FilterService,
+ private schemaService: EntitySchemaService,
+ ) {
+ this.recordsDataSource = this.createDataSource();
+ }
+
+ ngAfterViewInit(): void {
+ this.recordsDataSource.sort = this.sort;
+ }
+
+ private createDataSource() {
+ const dataSource = new MatTableDataSource>();
+ dataSource.sortData = (data, sort) =>
+ tableSort(data, {
+ active: sort.active as keyof Entity | "",
+ direction: sort.direction,
+ });
+ dataSource.filterPredicate = (data, filter) =>
+ entityFilterPredicate(data.record, filter);
+ return dataSource;
+ }
+
+ private inferDefaultSort(): Sort {
+ // initial sorting by first column, ensure that not the 'action' column is used
+ const sortBy = (this._columnsToDisplay ?? []).filter(
+ (c) => !c.startsWith("__"),
+ )[0];
+ const sortByColumn = this._columns.find((c) => c.id === sortBy);
+
+ let sortDirection: SortDirection = "asc";
+ if (
+ sortByColumn?.viewComponent === "DisplayDate" ||
+ sortByColumn?.viewComponent === "DisplayMonth" ||
+ this.schemaService.getDatatypeOrDefault(sortByColumn?.dataType) instanceof
+ DateDatatype
+ ) {
+ // flip default sort order for dates (latest first)
+ sortDirection = "desc";
+ }
+
+ return sortBy ? { active: sortBy, direction: sortDirection } : undefined;
+ }
+
+ /**
+ * Advanced fields like entity references cannot be sorted sensibly yet - disable sort for them.
+ * @param c
+ * @private
+ */
+ private disableSortingHeaderForAdvancedFields(c: FormFieldConfig) {
+ // if no dataType is defined, these are dynamic, display-only components
+ if (
+ c.dataType === EntityArrayDatatype.dataType ||
+ c.dataType === EntityDatatype.dataType ||
+ !c.dataType
+ ) {
+ c.noSorting = true;
+ }
+ }
+
+ /**
+ * FILTER ARCHIVED RECORDS
+ * User can hide / show inactive records through a toggle
+ */
+ @Input() set showInactive(value: boolean) {
+ if (value === this._showInactive) {
+ return;
+ }
+
+ this._showInactive = value;
+ this.updateFilteredData();
+ this.showInactiveChange.emit(value);
+ }
+ _showInactive: boolean = false;
+ @Output() showInactiveChange = new EventEmitter();
+
+ addActiveInactiveFilter(filter: DataFilter) {
+ if (this._showInactive) {
+ delete filter["isActive"];
+ } else {
+ filter["isActive"] = true;
+ }
+ }
+}
+
+/**
+ * Wrapper to keep additional form data for each row of an entity, required for inline editing.
+ */
+export interface TableRow {
+ record: T;
+ formGroup?: EntityForm;
+}
diff --git a/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.html b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.html
new file mode 100644
index 0000000000..727d84af0c
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.scss b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.spec.ts b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.spec.ts
new file mode 100644
index 0000000000..ee4fa9310b
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.spec.ts
@@ -0,0 +1,128 @@
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+} from "@angular/core/testing";
+
+import { EntityInlineEditActionsComponent } from "./entity-inline-edit-actions.component";
+import { EntityAbility } from "../../../permissions/ability/entity-ability";
+import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
+import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
+import { genders } from "../../../../child-dev-project/children/model/genders";
+import { EntityFormService } from "../../entity-form/entity-form.service";
+import { AlertService } from "../../../alerts/alert.service";
+import { mockEntityMapper } from "../../../entity/entity-mapper/mock-entity-mapper-service";
+import { CurrentUserSubject } from "../../../session/current-user-subject";
+import { of } from "rxjs";
+import { EntityActionsService } from "../../../entity/entity-actions/entity-actions.service";
+import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
+import { CoreTestingModule } from "../../../../utils/core-testing.module";
+import { DatabaseEntity } from "../../../entity/database-entity.decorator";
+import { Entity } from "../../../entity/model/entity";
+import { DatabaseField } from "../../../entity/database-field.decorator";
+import { ConfigurableEnumValue } from "../../../basic-datatypes/configurable-enum/configurable-enum.interface";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+
+describe("EntityInlineEditActionsComponent", () => {
+ let component: EntityInlineEditActionsComponent;
+ let fixture: ComponentFixture<
+ EntityInlineEditActionsComponent
+ >;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ EntityInlineEditActionsComponent,
+ CoreTestingModule,
+ FontAwesomeTestingModule,
+ NoopAnimationsModule,
+ ],
+ providers: [
+ { provide: EntityMapperService, useValue: mockEntityMapper() },
+ { provide: CurrentUserSubject, useValue: of(null) },
+ { provide: EntityActionsService, useValue: null },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent<
+ EntityInlineEditActionsComponent
+ >(EntityInlineEditActionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should create a formGroup when editing a row", () => {
+ const child = new InlineEditEntity();
+ child.name = "Child Name";
+ child.projectNumber = "01";
+ component.row = { record: child };
+
+ component.edit();
+
+ const formGroup = component.row.formGroup;
+ expect(formGroup.get("name")).toHaveValue("Child Name");
+ expect(formGroup.get("projectNumber")).toHaveValue("01");
+ expect(formGroup).toBeEnabled();
+ });
+
+ it("should correctly save changes to an entity", fakeAsync(() => {
+ spyOn(TestBed.inject(EntityAbility), "can").and.returnValue(true);
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "save").and.resolveTo();
+ const fb = TestBed.inject(UntypedFormBuilder);
+ const child = new InlineEditEntity();
+ child.name = "Old Name";
+ const formGroup = fb.group({
+ name: "New Name",
+ gender: genders[2],
+ });
+ component.row = { record: child, formGroup: formGroup };
+
+ component.save();
+ tick();
+
+ expect(entityMapper.save).toHaveBeenCalledWith(component.row.record);
+ expect(component.row.record.name).toBe("New Name");
+ expect(component.row.record.gender).toBe(genders[2]);
+ expect(component.row.formGroup).toBeUndefined();
+ }));
+
+ it("should show a error message when saving fails", fakeAsync(() => {
+ const entityFormService = TestBed.inject(EntityFormService);
+ spyOn(entityFormService, "saveChanges").and.rejectWith(
+ new Error("Form invalid"),
+ );
+ const alertService = TestBed.inject(AlertService);
+ spyOn(alertService, "addDanger");
+
+ component.row = { formGroup: null, record: new InlineEditEntity() };
+ component.save();
+ tick();
+
+ expect(alertService.addDanger).toHaveBeenCalledWith("Form invalid");
+ }));
+
+ it("should clear the form group when resetting", () => {
+ component.row = {
+ record: new InlineEditEntity(),
+ formGroup: new UntypedFormGroup({}),
+ };
+
+ component.resetChanges();
+
+ expect(component.row.formGroup).toBeFalsy();
+ });
+});
+
+@DatabaseEntity("InlineEditEntity")
+class InlineEditEntity extends Entity {
+ @DatabaseField() name: string;
+ @DatabaseField() projectNumber: string;
+ @DatabaseField({ dataType: "configurable-enum", additional: "genders" })
+ gender: ConfigurableEnumValue;
+}
diff --git a/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.ts b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.ts
new file mode 100644
index 0000000000..c56160d926
--- /dev/null
+++ b/src/app/core/common-components/entities-table/entity-inline-edit-actions/entity-inline-edit-actions.component.ts
@@ -0,0 +1,78 @@
+import { Component, Input } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { Angulartics2OnModule } from "angulartics2";
+import { DisableEntityOperationDirective } from "../../../permissions/permission-directive/disable-entity-operation.directive";
+import { FaIconComponent } from "@fortawesome/angular-fontawesome";
+import { MatButtonModule } from "@angular/material/button";
+import { TableRow } from "../entities-table.component";
+import { Entity } from "../../../entity/model/entity";
+import { InvalidFormFieldError } from "../../entity-form/invalid-form-field.error";
+import { EntityFormService } from "../../entity-form/entity-form.service";
+import { AlertService } from "../../../alerts/alert.service";
+import { EntityActionsService } from "../../../entity/entity-actions/entity-actions.service";
+import { UnsavedChangesService } from "../../../entity-details/form/unsaved-changes.service";
+
+/**
+ * Buttons to edit an (entities-table) row inline, handling the necessary logic and UI buttons.
+ */
+@Component({
+ selector: "app-entity-inline-edit-actions",
+ standalone: true,
+ imports: [
+ CommonModule,
+ Angulartics2OnModule,
+ DisableEntityOperationDirective,
+ FaIconComponent,
+ MatButtonModule,
+ ],
+ templateUrl: "./entity-inline-edit-actions.component.html",
+ styleUrl: "./entity-inline-edit-actions.component.scss",
+})
+export class EntityInlineEditActionsComponent {
+ @Input() row: TableRow;
+
+ constructor(
+ private entityFormService: EntityFormService,
+ private alertService: AlertService,
+ private entityRemoveService: EntityActionsService,
+ private unsavedChanges: UnsavedChangesService,
+ ) {}
+
+ edit() {
+ this.row.formGroup = this.entityFormService.createFormGroup(
+ Array.from(this.row.record.getSchema().keys()),
+ this.row.record,
+ true,
+ );
+ this.row.formGroup.enable();
+ }
+
+ /**
+ * Save an edited record to the database (if validation succeeds).
+ */
+ async save(): Promise {
+ try {
+ this.row.record = await this.entityFormService.saveChanges(
+ this.row.formGroup,
+ this.row.record,
+ );
+ delete this.row.formGroup;
+ } catch (err) {
+ if (!(err instanceof InvalidFormFieldError)) {
+ this.alertService.addDanger(err.message);
+ }
+ }
+ }
+
+ async delete(): Promise {
+ await this.entityRemoveService.delete(this.row.record);
+ }
+
+ /**
+ * Discard any changes to the given entity and reset it to the state before the user started editing.
+ */
+ resetChanges() {
+ delete this.row.formGroup;
+ this.unsavedChanges.pending = false;
+ }
+}
diff --git a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.html b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.html
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.html
rename to src/app/core/common-components/entities-table/list-paginator/list-paginator.component.html
diff --git a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.scss b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.scss
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.scss
rename to src/app/core/common-components/entities-table/list-paginator/list-paginator.component.scss
diff --git a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts
rename to src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts
diff --git a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts
rename to src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/table-sort.spec.ts b/src/app/core/common-components/entities-table/table-sort/table-sort.spec.ts
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/entity-subrecord/table-sort.spec.ts
rename to src/app/core/common-components/entities-table/table-sort/table-sort.spec.ts
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/table-sort.ts b/src/app/core/common-components/entities-table/table-sort/table-sort.ts
similarity index 93%
rename from src/app/core/common-components/entity-subrecord/entity-subrecord/table-sort.ts
rename to src/app/core/common-components/entities-table/table-sort/table-sort.ts
index 88717421eb..5172e5fbe0 100644
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/table-sort.ts
+++ b/src/app/core/common-components/entities-table/table-sort/table-sort.ts
@@ -1,7 +1,7 @@
-import { getReadableValue } from "./value-accessor";
-import { TableRow } from "./entity-subrecord.component";
+import { getReadableValue } from "../value-accessor/value-accessor";
import { Entity } from "../../../entity/model/entity";
import { Ordering } from "../../../basic-datatypes/configurable-enum/configurable-enum-ordering";
+import { TableRow } from "../entities-table.component";
/**
* Custom sort implementation for a MatTableDataSource>
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/value-accessor.spec.ts b/src/app/core/common-components/entities-table/value-accessor/value-accessor.spec.ts
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/entity-subrecord/value-accessor.spec.ts
rename to src/app/core/common-components/entities-table/value-accessor/value-accessor.spec.ts
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/value-accessor.ts b/src/app/core/common-components/entities-table/value-accessor/value-accessor.ts
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/entity-subrecord/value-accessor.ts
rename to src/app/core/common-components/entities-table/value-accessor/value-accessor.ts
diff --git a/src/app/core/common-components/entity-create-button/entity-create-button.component.html b/src/app/core/common-components/entity-create-button/entity-create-button.component.html
new file mode 100644
index 0000000000..e1d4f5088a
--- /dev/null
+++ b/src/app/core/common-components/entity-create-button/entity-create-button.component.html
@@ -0,0 +1,25 @@
+
diff --git a/src/app/core/common-components/entity-create-button/entity-create-button.component.scss b/src/app/core/common-components/entity-create-button/entity-create-button.component.scss
new file mode 100644
index 0000000000..a9d6f86ad4
--- /dev/null
+++ b/src/app/core/common-components/entity-create-button/entity-create-button.component.scss
@@ -0,0 +1,18 @@
+@use "variables/sizes";
+@use "variables/colors";
+
+.standard-add-button {
+ background-color: white !important;
+ height: 100%;
+}
+
+.table-action-button {
+ border: 1px solid lightgrey;
+ border-radius: 4px;
+ margin: sizes.$small;
+ color: colors.$accent;
+}
+
+.icon-only {
+ vertical-align: initial;
+}
diff --git a/src/app/core/common-components/entity-create-button/entity-create-button.component.spec.ts b/src/app/core/common-components/entity-create-button/entity-create-button.component.spec.ts
new file mode 100644
index 0000000000..36110017c0
--- /dev/null
+++ b/src/app/core/common-components/entity-create-button/entity-create-button.component.spec.ts
@@ -0,0 +1,39 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { EntityCreateButtonComponent } from "./entity-create-button.component";
+import { EntityAbility } from "../../permissions/ability/entity-ability";
+import { Angulartics2Module } from "angulartics2";
+import { Entity } from "../../entity/model/entity";
+import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
+
+describe("EntityCreateButtonComponent", () => {
+ let component: EntityCreateButtonComponent;
+ let fixture: ComponentFixture;
+
+ let mockAbility: jasmine.SpyObj;
+
+ beforeEach(async () => {
+ mockAbility = jasmine.createSpyObj(["cannot", "on"]);
+ mockAbility.on.and.returnValue(() => null);
+
+ await TestBed.configureTestingModule({
+ imports: [
+ EntityCreateButtonComponent,
+ Angulartics2Module.forRoot(),
+ FontAwesomeTestingModule,
+ ],
+ providers: [{ provide: EntityAbility, useValue: mockAbility }],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(EntityCreateButtonComponent);
+ component = fixture.componentInstance;
+
+ component.entityType = Entity;
+
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/core/common-components/entity-create-button/entity-create-button.component.ts b/src/app/core/common-components/entity-create-button/entity-create-button.component.ts
new file mode 100644
index 0000000000..573a698a3b
--- /dev/null
+++ b/src/app/core/common-components/entity-create-button/entity-create-button.component.ts
@@ -0,0 +1,56 @@
+import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive";
+import { FaIconComponent } from "@fortawesome/angular-fontawesome";
+import { MatButtonModule } from "@angular/material/button";
+import { MatTableModule } from "@angular/material/table";
+import { Entity, EntityConstructor } from "../../entity/model/entity";
+import { Angulartics2OnModule } from "angulartics2";
+import { MatTooltipModule } from "@angular/material/tooltip";
+
+@Component({
+ selector: "app-entity-create-button",
+ standalone: true,
+ imports: [
+ CommonModule,
+ DisableEntityOperationDirective,
+ FaIconComponent,
+ MatButtonModule,
+ MatTableModule,
+ Angulartics2OnModule,
+ MatTooltipModule,
+ ],
+ templateUrl: "./entity-create-button.component.html",
+ styleUrl: "./entity-create-button.component.scss",
+})
+export class EntityCreateButtonComponent {
+ @Input() entityType: EntityConstructor;
+
+ /**
+ * Optional factory method to create a new entity instance with some default values.
+ * If not provided, the simple entityType constructor is used.
+ */
+ @Input() newRecordFactory?: () => T;
+
+ /**
+ * Emits a new entity instance when the user clicks the button.
+ */
+ @Output() entityCreate = new EventEmitter();
+
+ /**
+ * Whether only an icon button without text should be displayed.
+ * Default is false
+ */
+ @Input() iconOnly: boolean = false;
+
+ /**
+ * Create a new entity.
+ * The entity is only written to the database when the user saves this record which is newly added in edit mode.
+ */
+ create() {
+ const newRecord = this.newRecordFactory
+ ? this.newRecordFactory()
+ : new this.entityType();
+ this.entityCreate.emit(newRecord);
+ }
+}
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 85e740c7a2..a6c3d3feae 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
@@ -12,7 +12,10 @@
>
-
+
diff --git a/src/app/core/common-components/entity-field-edit/entity-field-edit.component.ts b/src/app/core/common-components/entity-field-edit/entity-field-edit.component.ts
index 0f191972c9..24ffbc6d20 100644
--- a/src/app/core/common-components/entity-field-edit/entity-field-edit.component.ts
+++ b/src/app/core/common-components/entity-field-edit/entity-field-edit.component.ts
@@ -6,8 +6,7 @@ import {
EntityForm,
EntityFormService,
} from "../entity-form/entity-form.service";
-import { ColumnConfig } from "../entity-subrecord/entity-subrecord/entity-subrecord-config";
-import { FormFieldConfig } from "../entity-form/entity-form/FormConfig";
+import { ColumnConfig, FormFieldConfig } from "../entity-form/FormConfig";
import { NgIf } from "@angular/common";
import { EntityFieldViewComponent } from "../entity-field-view/entity-field-view.component";
@@ -42,6 +41,11 @@ export class EntityFieldEditComponent
@Input() entity: T;
@Input() form: EntityForm;
+ /**
+ * Whether to display the field in a limited space, hiding details like the help description button.
+ */
+ @Input() compactMode: boolean;
+
constructor(private entityFormService: EntityFormService) {}
ngOnChanges(changes: SimpleChanges): void {
diff --git a/src/app/core/common-components/entity-field-label/entity-field-label.component.ts b/src/app/core/common-components/entity-field-label/entity-field-label.component.ts
index ff661a3095..476fd3aafc 100644
--- a/src/app/core/common-components/entity-field-label/entity-field-label.component.ts
+++ b/src/app/core/common-components/entity-field-label/entity-field-label.component.ts
@@ -1,8 +1,7 @@
import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
import { MatTooltipModule } from "@angular/material/tooltip";
import { EntityConstructor } from "../../entity/model/entity";
-import { ColumnConfig } from "../entity-subrecord/entity-subrecord/entity-subrecord-config";
-import { FormFieldConfig } from "../entity-form/entity-form/FormConfig";
+import { ColumnConfig, FormFieldConfig } from "../entity-form/FormConfig";
import { EntityFormService } from "../entity-form/entity-form.service";
import { NgIf } from "@angular/common";
diff --git a/src/app/core/common-components/entity-field-view/entity-field-view.component.ts b/src/app/core/common-components/entity-field-view/entity-field-view.component.ts
index 6541cc1332..bf8caae195 100644
--- a/src/app/core/common-components/entity-field-view/entity-field-view.component.ts
+++ b/src/app/core/common-components/entity-field-view/entity-field-view.component.ts
@@ -1,9 +1,8 @@
import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
import { Entity } from "../../entity/model/entity";
-import { ColumnConfig } from "../entity-subrecord/entity-subrecord/entity-subrecord-config";
import { NgIf } from "@angular/common";
import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
-import { FormFieldConfig } from "../entity-form/entity-form/FormConfig";
+import { ColumnConfig, FormFieldConfig } from "../entity-form/FormConfig";
import { EntityFormService } from "../entity-form/entity-form.service";
import { PillComponent } from "../pill/pill.component";
diff --git a/src/app/core/common-components/entity-form/entity-form/FormConfig.ts b/src/app/core/common-components/entity-form/FormConfig.ts
similarity index 80%
rename from src/app/core/common-components/entity-form/entity-form/FormConfig.ts
rename to src/app/core/common-components/entity-form/FormConfig.ts
index a84081e286..88af58ece3 100644
--- a/src/app/core/common-components/entity-form/entity-form/FormConfig.ts
+++ b/src/app/core/common-components/entity-form/FormConfig.ts
@@ -1,4 +1,4 @@
-import { EntitySchemaField } from "../../../entity/schema/entity-schema-field";
+import { EntitySchemaField } from "../../entity/schema/entity-schema-field";
/**
* The general configuration for fields in tables and forms.
@@ -44,3 +44,16 @@ export interface FormFieldConfig extends EntitySchemaField {
*/
forTable?: boolean;
}
+
+/**
+ * Type for the definition of a single column in the EntitySubrecord
+ */
+export type ColumnConfig = string | FormFieldConfig;
+
+export function toFormFieldConfig(column: ColumnConfig): FormFieldConfig {
+ if (typeof column === "string") {
+ return { id: column };
+ } else {
+ return column;
+ }
+}
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 638bdb3ade..777745b9b0 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
@@ -24,7 +24,7 @@ import { EntityArrayDatatype } from "../../basic-datatypes/entity-array/entity-a
import { Child } from "../../../child-dev-project/children/model/child";
import { DatabaseField } from "../../entity/database-field.decorator";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
-import { FormFieldConfig } from "./entity-form/FormConfig";
+import { FormFieldConfig } from "./FormConfig";
import { TEST_USER } from "../../user/demo-user-generator.service";
describe("EntityFormService", () => {
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 9a8f536b15..2cf4092961 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,6 +1,6 @@
import { Injectable } from "@angular/core";
import { FormBuilder, FormGroup, ɵElement } from "@angular/forms";
-import { FormFieldConfig } from "./entity-form/FormConfig";
+import { ColumnConfig, FormFieldConfig, toFormFieldConfig } from "./FormConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
@@ -17,10 +17,6 @@ import {
PLACEHOLDERS,
} from "../../entity/schema/entity-schema-field";
import { isArrayDataType } from "../../basic-datatypes/datatype-utils";
-import {
- ColumnConfig,
- toFormFieldConfig,
-} from "../entity-subrecord/entity-subrecord/entity-subrecord-config";
import { CurrentUserSubject } from "../../session/current-user-subject";
/**
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config.ts
deleted file mode 100644
index 7901a09c38..0000000000
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig";
-import { MongoQuery } from "@casl/ability";
-import { Entity } from "../../../entity/model/entity";
-
-/**
- * Configuration that is commonly used when working with the entity subrecord
- */
-export interface EntitySubrecordConfig {
- columns?: ColumnConfig[];
- filter?: DataFilter;
-}
-
-/**
- * Type for the definition of a single column in the EntitySubrecord
- */
-export type ColumnConfig = string | FormFieldConfig;
-
-export function toFormFieldConfig(column: ColumnConfig): FormFieldConfig {
- if (typeof column === "string") {
- return { id: column };
- } else {
- return column;
- }
-}
-
-/**
- * This filter can be used to filter an array of entities.
- * It has to follow the MongoDB Query Syntax {@link https://www.mongodb.com/docs/manual/reference/operator/query/}.
- *
- * The filter is parsed using ucast {@link https://github.com/stalniy/ucast/tree/master/packages/mongo2js}
- */
-export type DataFilter = MongoQuery;
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
deleted file mode 100644
index 046db6baa6..0000000000
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
-
- |
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts
deleted file mode 100644
index ebc80d1fd8..0000000000
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-import {
- ComponentFixture,
- fakeAsync,
- TestBed,
- tick,
- waitForAsync,
-} from "@angular/core/testing";
-
-import {
- EntitySubrecordComponent,
- TableRow,
-} from "./entity-subrecord.component";
-import { Entity } from "../../../entity/model/entity";
-import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
-import { ConfigurableEnumValue } from "../../../basic-datatypes/configurable-enum/configurable-enum.interface";
-import { Child } from "../../../../child-dev-project/children/model/child";
-import { Note } from "../../../../child-dev-project/notes/model/note";
-import { AlertService } from "../../../alerts/alert.service";
-import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
-import { EntityFormService } from "../../entity-form/entity-form.service";
-import { genders } from "../../../../child-dev-project/children/model/genders";
-import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
-import moment from "moment";
-import { Subject } from "rxjs";
-import { UpdatedEntity } from "../../../entity/model/entity-update";
-import { EntityAbility } from "../../../permissions/ability/entity-ability";
-import { ScreenWidthObserver } from "../../../../utils/media/screen-size-observer.service";
-import { WINDOW_TOKEN } from "../../../../utils/di-tokens";
-import { FormDialogService } from "../../../form-dialog/form-dialog.service";
-import { DateWithAge } from "../../../basic-datatypes/date-with-age/dateWithAge";
-import { assignInputAndTriggerOnChanges } from "../../../../utils/test-utils/mock-ng-on-changes.spec";
-
-describe("EntitySubrecordComponent", () => {
- let component: EntitySubrecordComponent;
- let fixture: ComponentFixture>;
-
- const defaultTestColumns = ["x", "name", "label"];
-
- beforeEach(waitForAsync(() => {
- TestBed.configureTestingModule({
- imports: [EntitySubrecordComponent, MockedTestingModule.withState()],
- providers: [
- { provide: WINDOW_TOKEN, useValue: window },
- {
- provide: FormDialogService,
- useValue: jasmine.createSpyObj(["openFormPopup"]),
- },
- ],
- }).compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(EntitySubrecordComponent);
- component = fixture.componentInstance;
- component.editable = false;
- component.columns = defaultTestColumns;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-
- it("should sort enums by the label", () => {
- class Test extends Entity {
- public enumValue: ConfigurableEnumValue;
-
- constructor(label: string, id: string) {
- super();
- this.enumValue = { label: label, id: id };
- }
- }
-
- const first = new Test("aaa", "first");
- const second = new Test("aab", "second");
- const third = new Test("c", "third");
- component.records = [second, first, third];
- component.columns = [
- {
- id: "enumValue",
- label: "Test Configurable Enum",
- viewComponent: "DisplayConfigurableEnum",
- },
- ];
- component.ngOnChanges({ records: undefined, columns: undefined });
- fixture.detectChanges();
-
- component.recordsDataSource.sort.direction = "";
- component.recordsDataSource.sort.sort({
- id: "enumValue",
- start: "asc",
- disableClear: false,
- });
-
- const sortedData = component.recordsDataSource
- ._orderData(component.recordsDataSource.data)
- .map((row) => row.record);
- expect(sortedData).toEqual([first, second, third]);
- });
-
- it("should apply default sort on first column and order dates descending", () => {
- component.columns = ["date", "subject"];
- component.columnsToDisplay = ["date", "subject"];
- component.records = [];
- // Trigger a change with empty columns first as this is what some components do that init data asynchronously
- component.ngOnChanges({ columns: undefined, records: undefined });
-
- const oldNote = Note.create(moment().subtract(1, "day").toDate());
- const newNote = Note.create(new Date());
- component.records = [oldNote, newNote];
- component.ngOnChanges({ records: undefined });
-
- expect(component.recordsDataSource.sort.direction).toBe("desc");
- expect(component.recordsDataSource.sort.active).toBe("date");
- });
-
- it("should use input defaultSort if defined", () => {
- component.columns = ["date", "subject"];
- component.columnsToDisplay = ["date", "subject"];
- const n1 = Note.create(new Date(), "1");
- const n2 = Note.create(new Date(), "2");
- const n3 = Note.create(new Date(), "3");
-
- component.records = [n3, n1, n2];
-
- component.defaultSort = { active: "subject", direction: "asc" };
- component.ngOnChanges({ columns: undefined, records: undefined });
-
- expect(component.recordsDataSource.sort.direction).toBe("asc");
- expect(component.recordsDataSource.sort.active).toBe("subject");
- });
-
- it("should sort standard objects", () => {
- const children = [
- new Child("0"),
- new Child("1"),
- new Child("2"),
- new Child("3"),
- ];
- children[0].name = "AA";
- children[3].name = "AB";
- children[2].name = "Z";
- children[1].name = "C";
- component.records = children;
- component.ngOnChanges({ records: undefined });
-
- component.sort.sort({ id: "name", start: "asc", disableClear: false });
- const sortedIds = component.recordsDataSource
- ._orderData(component.recordsDataSource.data)
- .map((c) => c.record.getId());
-
- expect(sortedIds).toEqual(["0", "3", "1", "2"]);
- });
-
- it("should sort non-standard objects", () => {
- const notes = [new Note("0"), new Note("1"), new Note("2"), new Note("3")];
- notes[0].category = { id: "0", label: "AA", _ordinal: 3 };
- notes[1].category = { id: "3", label: "C", _ordinal: 1 };
- notes[2].category = { id: "2", label: "Z", _ordinal: 0 };
- notes[3].category = { id: "1", label: "AB", _ordinal: 2 };
- component.records = notes;
- component.ngOnChanges({ records: undefined });
-
- component.sort.sort({ id: "category", start: "asc", disableClear: false });
- const sortedIds = component.recordsDataSource
- ._orderData(component.recordsDataSource.data)
- .map((note) => note.record.getId());
-
- expect(sortedIds).toEqual(["0", "3", "1", "2"]);
- });
-
- it("should sort strings ignoring case", () => {
- const names = ["C", "A", "b"];
- component.records = names.map((name) => Child.create(name));
- component.ngOnChanges({ records: undefined });
- component.sort.sort({ id: "resetSort", start: "asc", disableClear: false });
-
- component.sort.sort({ id: "name", start: "asc", disableClear: false });
-
- const sortedNames = component.recordsDataSource
- ._orderData(component.recordsDataSource.data)
- .map((row) => row.record["name"]);
-
- expect(sortedNames).toEqual(["A", "b", "C"]);
- });
-
- it("should create a formGroup when editing a row", () => {
- component.columns = ["name", "projectNumber"];
- assignInputAndTriggerOnChanges(component, {
- columns: ["name", "projectNumber"],
- });
-
- const child = new Child();
- child.name = "Child Name";
- child.projectNumber = "01";
- const tableRow: TableRow = { record: child };
- const media = TestBed.inject(ScreenWidthObserver);
- spyOn(media, "isDesktop").and.returnValue(true);
-
- component.edit(tableRow);
-
- const formGroup = tableRow.formGroup;
- expect(formGroup.get("name")).toHaveValue("Child Name");
- expect(formGroup.get("projectNumber")).toHaveValue("01");
- expect(formGroup).toBeEnabled();
- });
-
- it("should correctly save changes to an entity", fakeAsync(() => {
- TestBed.inject(EntityAbility).update([
- { subject: "Child", action: "create" },
- ]);
- const entityMapper = TestBed.inject(EntityMapperService);
- spyOn(entityMapper, "save").and.resolveTo();
- const fb = TestBed.inject(UntypedFormBuilder);
- const child = new Child();
- child.name = "Old Name";
- const formGroup = fb.group({
- name: "New Name",
- gender: genders[2],
- });
- const tableRow = { record: child, formGroup: formGroup };
-
- component.save(tableRow);
- tick();
-
- expect(entityMapper.save).toHaveBeenCalledWith(tableRow.record);
- expect(tableRow.record.name).toBe("New Name");
- expect(tableRow.record.gender).toBe(genders[2]);
- expect(tableRow.formGroup).not.toBeEnabled();
- }));
-
- it("should show a error message when saving fails", fakeAsync(() => {
- const entityFormService = TestBed.inject(EntityFormService);
- spyOn(entityFormService, "saveChanges").and.rejectWith(
- new Error("Form invalid"),
- );
- const alertService = TestBed.inject(AlertService);
- spyOn(alertService, "addDanger");
-
- component.save({ formGroup: null, record: new Child() });
- tick();
-
- expect(alertService.addDanger).toHaveBeenCalledWith("Form invalid");
- }));
-
- it("should clear the form group when resetting", () => {
- const row = { record: new Child(), formGroup: new UntypedFormGroup({}) };
-
- component.resetChanges(row);
-
- expect(row.formGroup).toBeFalsy();
- });
-
- it("should create new entities and call the show entity function when it is supplied", fakeAsync(() => {
- const child = new Child();
- component.newRecordFactory = () => child;
- component.columns = [{ id: "name" }, { id: "projectNumber" }];
-
- component.create();
- tick();
-
- expect(TestBed.inject(FormDialogService).openFormPopup).toHaveBeenCalled();
- }));
-
- it("should create a new entity and open a dialog on default when clicking create", () => {
- const child = new Child();
- component.newRecordFactory = () => child;
- assignInputAndTriggerOnChanges(component, {
- newRecordFactory: component.newRecordFactory,
- });
-
- const dialog = TestBed.inject(FormDialogService);
-
- component.create();
-
- expect(dialog.openFormPopup).toHaveBeenCalledWith(
- child,
- defaultTestColumns.map((x) => jasmine.objectContaining({ id: x })),
- );
- });
-
- it("should notify when an entity is clicked", (done) => {
- const child = new Child();
- component.rowClick.subscribe((entity) => {
- expect(entity).toEqual(child);
- done();
- });
-
- component.onRowClick({ record: child });
- });
-
- it("should add a new entity that was created after the initial loading to the table", () => {
- const entityUpdates = new Subject>();
- const entityMapper = TestBed.inject(EntityMapperService);
- spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
- component.newRecordFactory = () => new Entity();
- component.records = [];
- component.ngOnChanges({ records: undefined, newRecordFactory: undefined });
-
- const entity = new Entity();
- entityUpdates.next({ entity: entity, type: "new" });
-
- expect(component.recordsDataSource.data).toEqual([{ record: entity }]);
- });
-
- it("should remove a entity from the table when it has been deleted", async () => {
- const entityUpdates = new Subject>();
- const entityMapper = TestBed.inject(EntityMapperService);
- spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
- const entity = new Entity();
- component.records = [entity];
- component.ngOnChanges({ records: undefined });
-
- expect(component.recordsDataSource.data).toEqual([{ record: entity }]);
-
- entityUpdates.next({ entity: entity, type: "remove" });
-
- expect(component.recordsDataSource.data).toEqual([]);
- });
-
- it("does not change the size of it's records when not saving a new record", async () => {
- const entity = new Entity();
- component.records = [entity];
- component.ngOnChanges({ records: undefined });
-
- await component.save({ record: entity });
- expect(component.recordsDataSource.data).toHaveSize(1);
- });
-
- it("should correctly determine the entity constructor from factory", () => {
- expect(component.entityConstructor).toBeUndefined();
-
- const newRecordSpy = jasmine.createSpy().and.returnValue(new Child());
- assignInputAndTriggerOnChanges(component, {
- newRecordFactory: newRecordSpy,
- });
- expect(component.entityConstructor).toEqual(Child);
- expect(newRecordSpy).toHaveBeenCalled();
- });
-
- it("should correctly determine the entity constructor from existing record", () => {
- assignInputAndTriggerOnChanges(component, {
- newRecordFactory: undefined,
- records: [new Note()],
- });
- expect(component.entityConstructor).toEqual(Note);
- });
-
- it("should filter data based on filter definition", () => {
- const c1 = Child.create("Matching");
- c1.dateOfBirth = new DateWithAge(moment().subtract(1, "years").toDate());
- const c2 = Child.create("Not Matching");
- c2.dateOfBirth = new DateWithAge(moment().subtract(2, "years").toDate());
- const c3 = Child.create("Matching");
- c3.dateOfBirth = new DateWithAge(moment().subtract(3, "years").toDate());
- // get type-safety for filters
- const childComponent = component as any as EntitySubrecordComponent;
- childComponent.records = [c1, c2, c3];
-
- childComponent.filter = { name: "Matching" };
- childComponent.ngOnChanges({ records: undefined, filter: undefined });
-
- expect(childComponent.recordsDataSource.data).toEqual([
- { record: c1 },
- { record: c3 },
- ]);
-
- childComponent.filter = {
- name: "Matching",
- "dateOfBirth.age": { $gte: 2 },
- } as any;
- childComponent.ngOnChanges({ filter: undefined });
-
- expect(childComponent.recordsDataSource.data).toEqual([{ record: c3 }]);
-
- const c4 = Child.create("Matching");
- c4.dateOfBirth = new DateWithAge(moment().subtract(4, "years").toDate());
- const c5 = Child.create("Not Matching");
-
- childComponent.records = [c1, c2, c3, c4, c5];
- childComponent.ngOnChanges({ records: undefined });
-
- expect(childComponent.recordsDataSource.data).toEqual([
- { record: c3 },
- { record: c4 },
- ]);
- });
-
- it("should remove an entity if it does not pass the filter anymore", fakeAsync(() => {
- const entityMapper = TestBed.inject(EntityMapperService);
- const child = new Child();
- child.gender = genders[1];
- entityMapper.save(child);
- tick();
- component.records = [child];
- component.filter = { "gender.id": genders[1].id } as any;
- component.ngOnChanges({ records: undefined, filter: undefined });
-
- expect(component.recordsDataSource.data).toEqual([{ record: child }]);
-
- child.gender = genders[2];
- entityMapper.save(child);
- tick(5000);
-
- expect(component.recordsDataSource.data).toEqual([]);
- }));
-
- it("should only show active relations by default", async () => {
- const active1 = new Entity();
- active1.inactive = false;
- const inactive = new Entity();
- inactive.inactive = true;
-
- component.records = [active1, inactive];
-
- component.ngOnChanges({ records: undefined, filter: undefined });
-
- expect(component.recordsDataSource.data).toEqual([{ record: active1 }]);
- });
-});
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts
deleted file mode 100644
index 38f5301540..0000000000
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts
+++ /dev/null
@@ -1,545 +0,0 @@
-import {
- Component,
- EventEmitter,
- Input,
- OnChanges,
- Output,
- SimpleChanges,
- ViewChild,
-} from "@angular/core";
-import {
- MatSort,
- MatSortModule,
- Sort,
- SortDirection,
-} from "@angular/material/sort";
-import { MatTableDataSource, MatTableModule } from "@angular/material/table";
-import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { Entity, EntityConstructor } from "../../../entity/model/entity";
-import { AlertService } from "../../../alerts/alert.service";
-import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig";
-import {
- EntityForm,
- EntityFormService,
-} from "../../entity-form/entity-form.service";
-import { AnalyticsService } from "../../../analytics/analytics.service";
-import { EntityActionsService } from "../../../entity/entity-actions/entity-actions.service";
-import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
-import { tableSort } from "./table-sort";
-import {
- ScreenSize,
- ScreenWidthObserver,
-} from "../../../../utils/media/screen-size-observer.service";
-import { Subscription } from "rxjs";
-import { InvalidFormFieldError } from "../../entity-form/invalid-form-field.error";
-import {
- ColumnConfig,
- DataFilter,
- toFormFieldConfig,
-} from "./entity-subrecord-config";
-import { FilterService } from "../../../filter/filter.service";
-import { FormDialogService } from "../../../form-dialog/form-dialog.service";
-import { Router } from "@angular/router";
-import { NgForOf, NgIf } from "@angular/common";
-import { MatProgressBarModule } from "@angular/material/progress-bar";
-import { MatTooltipModule } from "@angular/material/tooltip";
-import { DynamicComponentDirective } from "../../../config/dynamic-components/dynamic-component.directive";
-import { MatButtonModule } from "@angular/material/button";
-import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
-import { DisableEntityOperationDirective } from "../../../permissions/permission-directive/disable-entity-operation.directive";
-import { Angulartics2Module } from "angulartics2";
-import { ListPaginatorComponent } from "../list-paginator/list-paginator.component";
-import {
- MatCheckboxChange,
- MatCheckboxModule,
-} from "@angular/material/checkbox";
-import { MatSlideToggleModule } from "@angular/material/slide-toggle";
-import { applyUpdate } from "../../../entity/model/entity-update";
-import { EntityFieldEditComponent } from "../../entity-field-edit/entity-field-edit.component";
-import { EntityFieldLabelComponent } from "../../entity-field-label/entity-field-label.component";
-import { EntityFieldViewComponent } from "../../entity-field-view/entity-field-view.component";
-
-export interface TableRow {
- record: T;
- formGroup?: EntityForm;
-}
-
-/**
- * Generically configurable component to display and edit a list of entities in a compact way
- * that can especially be used within another entity's details view to display related entities.
- *
- * For example, all Notes related to a certain Child are displayed within the Child's detail view
- * with the help of this component.
- *
- * Pagination is available, but the values are not stored. That means that every time calling
- * the component pagination starts with the initial values set in this component.
- *
- * A detailed Guide on how to use this component is available:
- * - [How to display related entities]{@link /additional-documentation/how-to-guides/display-related-entities.html}
- */
-@UntilDestroy()
-@Component({
- selector: "app-entity-subrecord",
- templateUrl: "./entity-subrecord.component.html",
- styleUrls: ["./entity-subrecord.component.scss"],
- imports: [
- NgIf,
- MatProgressBarModule,
- MatTableModule,
- MatSortModule,
- NgForOf,
- MatTooltipModule,
- DynamicComponentDirective,
- MatButtonModule,
- FontAwesomeModule,
- DisableEntityOperationDirective,
- Angulartics2Module,
- ListPaginatorComponent,
- MatCheckboxModule,
- MatSlideToggleModule,
- EntityFieldEditComponent,
- EntityFieldLabelComponent,
- EntityFieldViewComponent,
- ],
- standalone: true,
-})
-export class EntitySubrecordComponent implements OnChanges {
- @Input() isLoading: boolean;
- @Input() clickMode: "popup" | "navigate" | "none" = "popup";
-
- /**
- * outputs an event containing an array of currently selected records (checkmarked by the user)
- *
- * Checkboxes to select rows are only displayed if you set "selectable" also.
- */
- @Output() selectedRecordsChange: EventEmitter = new EventEmitter();
- @Input() selectedRecords: T[] = [];
- readonly COLUMN_ROW_SELECT = "_selectRows";
- @Input() selectable: boolean = false;
-
- @Input() showInactive = false;
- @Output() showInactiveChange = new EventEmitter();
-
- /** configuration what kind of columns to be generated for the table */
- @Input() columns: ColumnConfig[];
- /**
- * columns converted to the full, extended FormFieldConfig
- */
- _columns: FormFieldConfig[] = [];
- /**
- * columns actually displayed in the table (as some may have been passed only for the popup edit form)
- */
- filteredColumns: FormFieldConfig[] = [];
-
- /** data to be displayed, can also be used as two-way-binding */
- @Input() records: T[] = [];
-
- /** output the currently displayed records, whenever filters for the user change */
- @Output() filteredRecordsChange = new EventEmitter(true);
-
- /**
- * factory method to create a new instance of the displayed Entity type
- * used when the user adds a new entity to the list.
- */
- @Input() newRecordFactory: () => T;
-
- entityConstructor: EntityConstructor;
-
- /**
- * Whether the rows of the table are inline editable and new entries can be created through the "+" button.
- */
- @Input() editable = true;
-
- /** columns displayed in the template's table */
- @Input() columnsToDisplay: string[] = [];
-
- /** how to sort data by default during initialization */
- @Input() defaultSort: Sort;
-
- /** data displayed in the template's table */
- recordsDataSource = new MatTableDataSource>();
-
- private updateSubscription: Subscription;
- private mediaSubscription: Subscription = Subscription.EMPTY;
- private screenWidth: ScreenSize | undefined = undefined;
-
- idForSavingPagination = "startWert";
-
- @ViewChild(MatSort, { static: true }) sort: MatSort;
-
- /**
- * Event triggered when the user clicks on a row (i.e. entity).
- * This does not change the default behavior like opening popup form,
- * you may want to additionally set `clickMode` to change that.
- */
- @Output() rowClick = new EventEmitter();
-
- /**
- * Adds a filter for the displayed data.
- * Only data, that passes the filter will be shown in the table.
- * @param filter a valid MongoDB Query
- */
- @Input() filter: DataFilter;
- private predicate: (entity: T) => boolean = () => true;
-
- constructor(
- private alertService: AlertService,
- private screenWidthObserver: ScreenWidthObserver,
- private entityFormService: EntityFormService,
- private formDialog: FormDialogService,
- private router: Router,
- private analyticsService: AnalyticsService,
- public entityRemoveService: EntityActionsService,
- private entityMapper: EntityMapperService,
- private filterService: FilterService,
- ) {
- this.mediaSubscription = this.screenWidthObserver
- .shared()
- .pipe(untilDestroyed(this))
- .subscribe((change: ScreenSize) => {
- this.screenWidth = change;
- this.setupTable();
- });
- }
-
- /** function returns the background color for each row*/
- @Input() getBackgroundColor?: (rec: T) => string = (rec: T) => rec.getColor();
-
- private initDataSource() {
- this.filter = this.filter ?? ({} as DataFilter);
- this.filterActiveInactive();
- this.predicate = this.filterService.getFilterPredicate(this.filter);
-
- this.recordsDataSource.data = this.records
- .filter(this.predicate)
- .map((record) => ({ record }));
- }
-
- initEntityConstructor() {
- if (!(this.records?.length > 0) && !this.newRecordFactory) {
- this.entityConstructor = undefined;
- return;
- }
-
- const record =
- this.records?.length > 0 ? this.records[0] : this.newRecordFactory();
- this.entityConstructor = record.getConstructor();
-
- if (!this.newRecordFactory) {
- this.newRecordFactory = () => new this.entityConstructor();
- }
- }
-
- initColumns() {
- if (!this.columns) {
- return;
- }
-
- this._columns = this.columns.map((col) => {
- if (this.entityConstructor) {
- return this.entityFormService.extendFormFieldConfig(
- col,
- this.entityConstructor,
- true,
- );
- } else {
- return toFormFieldConfig(col);
- }
- });
- this.filteredColumns = this._columns.filter((col) => !col.hideFromTable);
- this.idForSavingPagination = this._columns.map((col) => col.id).join("");
- }
-
- /**
- * Update the component if any of the @Input properties were changed from outside.
- * @param changes
- */
- ngOnChanges(changes: SimpleChanges) {
- let reinitDataSource = false;
- let resetupTable = false;
- let reinitColumns = false;
-
- if (
- changes.hasOwnProperty("records") ||
- changes.hasOwnProperty("newRecordFactory")
- ) {
- this.initEntityConstructor();
- reinitColumns = true;
- }
-
- if (changes.hasOwnProperty("columns") || reinitColumns) {
- this.initColumns();
- if (this.columnsToDisplay.length < 2) {
- resetupTable = true;
- }
- }
- if (changes.hasOwnProperty("columnsToDisplay")) {
- this.mediaSubscription.unsubscribe();
- resetupTable = true;
- }
-
- if (changes.hasOwnProperty("records")) {
- if (!this.records) {
- this.records = [];
- }
- reinitDataSource = true;
-
- if (this.records.length > 0 && this.columnsToDisplay.length < 2) {
- resetupTable = true;
- }
- }
-
- if (
- (changes.hasOwnProperty("filter") && this.filter) ||
- changes.hasOwnProperty("showInactive")
- ) {
- reinitDataSource = true;
- }
- if (
- changes.hasOwnProperty("editable") ||
- changes.hasOwnProperty("selectable")
- ) {
- resetupTable = true;
- }
-
- if (reinitDataSource) {
- this.initDataSource();
- }
- if (resetupTable) {
- this.setupTable();
- }
- if (changes.hasOwnProperty("records") || reinitColumns) {
- this.sortDefault();
- }
-
- this.filteredRecordsChange.emit(
- this.recordsDataSource.filteredData.map((item) => item.record),
- );
- this.listenToEntityUpdates();
- }
-
- private sortDefault() {
- if (
- this.records.length === 0 ||
- this.filteredColumns.length === 0 ||
- this.sort.active
- ) {
- // do not overwrite existing sort
- return;
- }
-
- this.recordsDataSource.sort = this.sort;
-
- this.recordsDataSource.sortData = (data, sort) =>
- tableSort(data, {
- active: sort.active as keyof T | "",
- direction: sort.direction,
- });
-
- this.defaultSort = this.defaultSort ?? this.inferDefaultSort();
-
- this.sort.sort({
- id: this.defaultSort.active,
- start: this.defaultSort.direction,
- disableClear: false,
- });
- }
-
- private inferDefaultSort(): Sort {
- // initial sorting by first column, ensure that not the 'action' column is used
- const sortBy = this.columnsToDisplay.filter(
- (c) => c !== "actions" && c !== this.COLUMN_ROW_SELECT,
- )[0];
- const sortByColumn = this.filteredColumns.find((c) => c.id === sortBy);
-
- let sortDirection: SortDirection = "asc";
- if (
- sortByColumn?.viewComponent === "DisplayDate" ||
- sortByColumn?.viewComponent === "DisplayMonth"
- ) {
- // flip default sort order for dates (latest first)
- sortDirection = "desc";
- }
-
- return { active: sortBy, direction: sortDirection };
- }
-
- private listenToEntityUpdates() {
- if (!this.updateSubscription && this.entityConstructor) {
- this.updateSubscription = this.entityMapper
- .receiveUpdates(this.entityConstructor)
- .pipe(untilDestroyed(this))
- .subscribe((next) => {
- this.records = applyUpdate(this.records, next, true);
-
- if (this.predicate(next.entity)) {
- this.initDataSource();
- } else {
- // hide after a short delay to give a signal in the UI why records disappear by showing the changed values first
- setTimeout(() => this.initDataSource(), 5000);
- }
- });
- }
- }
-
- edit(row: TableRow) {
- if (this.screenWidthObserver.isDesktop()) {
- if (!row.formGroup) {
- row.formGroup = this.entityFormService.createFormGroup(
- this.filteredColumns,
- row.record,
- true,
- );
- }
- row.formGroup.enable();
- } else {
- this.showEntity(row.record);
- }
- }
-
- /**
- * Save an edited record to the database (if validation succeeds).
- * @param row The entity to be saved.
- */
- async save(row: TableRow): Promise {
- try {
- row.record = await this.entityFormService.saveChanges(
- row.formGroup,
- row.record,
- );
- row.formGroup.disable();
- } catch (err) {
- if (!(err instanceof InvalidFormFieldError)) {
- this.alertService.addDanger(err.message);
- }
- }
- }
-
- /**
- * Discard any changes to the given entity and reset it to the state before the user started editing.
- * @param row The entity to be reset.
- */
- resetChanges(row: TableRow) {
- row.formGroup = null;
- }
-
- /**
- * Create a new entity.
- * The entity is only written to the database when the user saves this record which is newly added in edit mode.
- */
- create() {
- const newRecord = this.newRecordFactory();
- this.showEntity(newRecord);
- this.analyticsService.eventTrack("subrecord_add_new", {
- category: newRecord.getType(),
- });
- }
-
- /**
- * Show one record's details in a modal dialog (if configured).
- * @param row The entity whose details should be displayed.
- */
- onRowClick(row: TableRow) {
- if (!row.formGroup || row.formGroup.disabled) {
- this.showEntity(row.record);
- this.analyticsService.eventTrack("subrecord_show_popup", {
- category: row.record.getType(),
- });
- }
- }
-
- private showEntity(entity: T) {
- switch (this.clickMode) {
- case "popup":
- this.formDialog.openFormPopup(entity, this._columns);
- break;
- case "navigate":
- this.router.navigate([
- entity.getConstructor().route,
- entity.getId(false),
- ]);
- break;
- }
- this.rowClick.emit(entity);
- }
-
- /**
- * resets columnsToDisplay depending on current screensize
- */
- private setupTable() {
- let columns =
- this.columnsToDisplay?.filter((c) =>
- this.filteredColumns.some((column) => column.id === c),
- ) ?? [];
-
- if (
- !(columns.length > 0) &&
- this.filteredColumns !== undefined &&
- this.screenWidth !== undefined
- ) {
- columns = [
- ...this._columns
- .filter((col) => this.isVisible(col))
- .map((col) => col.id),
- ];
- }
-
- if (this.editable) {
- columns.unshift("actions");
- }
- if (this.selectable) {
- // only show selection checkboxes if Output is used in parent
- columns.unshift(this.COLUMN_ROW_SELECT);
- }
-
- this.columnsToDisplay = [...columns];
- }
-
- /**
- * isVisible
- * compares the current screensize to the columns' property visibleFrom. screensize < visibleFrom? column not displayed
- * @param col column that is checked
- * @return returns true if column is visible
- */
- private isVisible(col: FormFieldConfig): boolean {
- if (col.hideFromTable) {
- return false;
- }
- // when `ScreenSize[col.visibleFrom]` is undefined, this returns `true`
- const numericValue = ScreenSize[col.visibleFrom];
- if (numericValue === undefined) {
- return true;
- }
- return this.screenWidthObserver.currentScreenSize() >= numericValue;
- }
-
- selectRow(row: TableRow, event: MatCheckboxChange) {
- if (event.checked) {
- this.selectedRecords.push(row.record);
- } else {
- const index = this.selectedRecords.indexOf(row.record);
- if (index > -1) {
- this.selectedRecords.splice(index, 1);
- }
- }
-
- this.selectedRecordsChange.emit(this.selectedRecords);
- }
-
- filterActiveInactive() {
- if (this.showInactive) {
- // @ts-ignore type has issues with getters
- delete this.filter.isActive;
- } else {
- this.filter["isActive"] = true;
- }
- }
-
- setActiveInactiveFilter(newValue: boolean) {
- if (newValue !== this.showInactive) {
- this.showInactive = newValue;
- this.showInactiveChange.emit(newValue);
- }
- this.initDataSource();
- }
-}
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts
deleted file mode 100644
index 32b0abe0ad..0000000000
--- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { DemoNoteGeneratorService } from "../../../../child-dev-project/notes/demo-data/demo-note-generator.service";
-import { DemoChildGenerator } from "../../../../child-dev-project/children/demo-data-generators/demo-child-generator.service";
-import { DemoUserGeneratorService } from "../../../user/demo-user-generator.service";
-
-const childGenerator = new DemoChildGenerator({ count: 10 });
-const userGenerator = new DemoUserGeneratorService();
-const data = new DemoNoteGeneratorService(
- { minNotesPerChild: 5, maxNotesPerChild: 10, groupNotes: 2 },
- childGenerator,
- userGenerator,
-).generateEntities();
-
-export default { title: "EntitySubrecord" };
-
-// TODO: fix stories for EntitySubrecord
-/*
-export default {
- title: "Core/Entities/EntitySubrecord",
- component: EntitySubrecordComponent,
- decorators: [
- moduleMetadata({
- imports: [
- EntitySubrecordComponent,
- StorybookBaseModule,
- MockedTestingModule.withState(),
- ],
- providers: [
- {
- provide: EntityMapperService,
- useValue: {
- save: () => Promise.resolve(),
- remove: () => Promise.resolve(),
- load: () =>
- Promise.resolve(
- faker.helpers.arrayElement(childGenerator.entities)
- ),
- loadType: () => Promise.resolve(childGenerator.entities),
- receiveUpdates: () => NEVER,
- },
- },
- { provide: EntitySchemaService, useValue: schemaService },
- DatePipe,
- {
- provide: ChildrenService,
- useValue: {
- getChild: () =>
- of(faker.helpers.arrayElement(childGenerator.entities)),
- },
- },
- {
- provide: AbilityService,
- useValue: { abilityUpdated: new Subject() },
- },
-
- {
- provide: EntityAbility,
- useValue: new Ability([{ subject: "all", action: "manage" }]),
- },
- ],
- }),
- ],
-} as Meta;
-
-const Template: StoryFn> = (
- args: EntitySubrecordComponent
-) => {
- EntitySubrecordComponent.prototype.newRecordFactory = () => new Note();
- return {
- component: EntitySubrecordComponent,
- props: args,
- };
-};
-
-export const Primary = Template.bind({});
-Primary.args = {
- columns: [
- { id: "date" },
- { id: "subject" },
- { id: "category" },
- { id: "children" },
- ],
- records: data,
-};
-
-export const WithAttendance = Template.bind({});
-WithAttendance.args = {
- columns: [
- { id: "date" },
- { id: "subject" },
- { id: "category" },
- { id: "children" },
- {
- id: "present",
- label: "Present",
- view: "NoteAttendanceCountBlock",
- additional: { status: AttendanceLogicalStatus.PRESENT },
- noSorting: true,
- },
- {
- id: "absent",
- label: "Absent",
- view: "NoteAttendanceCountBlock",
- additional: { status: AttendanceLogicalStatus.ABSENT },
- noSorting: true,
- },
- ],
- records: data,
-};
-*/
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index 611643676f..43439959a6 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -168,12 +168,6 @@ export const defaultJsonConfig = {
"title": $localize`:Title for notes overview:Notes & Reports`,
"includeEventNotes": false,
"showEventNotesToggle": true,
- "columns": [
- {
- "id": "children",
- "noSorting": true
- }
- ],
"columnGroups": {
"default": $localize`:Translated name of default column group:Standard`,
"mobile": $localize`:Translated name of mobile column group:Mobile`,
@@ -200,9 +194,7 @@ export const defaultJsonConfig = {
},
"filters": [
{
- "id": "status",
- "label": $localize`:Filter label:Status`,
- "type": "prebuilt"
+ "id": "warningLevel"
},
{
"id": "date",
diff --git a/src/app/core/config/default-config/default-interaction-types.ts b/src/app/core/config/default-config/default-interaction-types.ts
index ad8ddfee3f..44bc8350d9 100644
--- a/src/app/core/config/default-config/default-interaction-types.ts
+++ b/src/app/core/config/default-config/default-interaction-types.ts
@@ -1,10 +1,6 @@
import { InteractionType } from "../../../child-dev-project/notes/model/interaction-type.interface";
export const defaultInteractionTypes: InteractionType[] = [
- {
- id: "",
- label: "",
- },
{
id: "VISIT",
label: $localize`:Interaction type/Category of a Note:Home Visit`,
diff --git a/src/app/core/entity-details/form/field-group.ts b/src/app/core/entity-details/form/field-group.ts
index 9cd3b3c380..631a177671 100644
--- a/src/app/core/entity-details/form/field-group.ts
+++ b/src/app/core/entity-details/form/field-group.ts
@@ -1,4 +1,4 @@
-import { ColumnConfig } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
+import { ColumnConfig } from "../../common-components/entity-form/FormConfig";
/**
* A group of related form fields displayed within a Form component.
diff --git a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html
index c59e53e42c..8e10854001 100644
--- a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html
+++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.html
@@ -1,10 +1,10 @@
-
+>
diff --git a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts
index 2055435762..bc0cc73248 100644
--- a/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts
+++ b/src/app/core/entity-details/related-entities-with-summary/related-entities-with-summary.component.ts
@@ -2,11 +2,11 @@ import { Component, Input, OnInit } from "@angular/core";
import { NgFor, NgIf } from "@angular/common";
import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
import { RelatedEntitiesComponent } from "../related-entities/related-entities.component";
import { Entity } from "../../entity/model/entity";
import { filter } from "rxjs/operators";
import { applyUpdate } from "../../entity/model/entity-update";
+import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
/**
* Load and display a list of entity subrecords (entities related to the current entity details view)
@@ -17,7 +17,7 @@ import { applyUpdate } from "../../entity/model/entity-update";
@Component({
selector: "app-related-entities-with-summary",
templateUrl: "./related-entities-with-summary.component.html",
- imports: [EntitySubrecordComponent, NgIf, NgFor],
+ imports: [EntitiesTableComponent, NgIf, NgFor],
standalone: true,
})
export class RelatedEntitiesWithSummaryComponent
@@ -51,7 +51,7 @@ export class RelatedEntitiesWithSummaryComponent
),
)
.subscribe((update) => {
- this.data = applyUpdate(this.data, update);
+ this.data = applyUpdate(this.data, update, false);
this.updateSummary();
});
}
diff --git a/src/app/core/entity-details/related-entities/related-entities.component.html b/src/app/core/entity-details/related-entities/related-entities.component.html
index 897ca94e57..b54a0757d0 100644
--- a/src/app/core/entity-details/related-entities/related-entities.component.html
+++ b/src/app/core/entity-details/related-entities/related-entities.component.html
@@ -1,8 +1,9 @@
-
+>
diff --git a/src/app/core/entity-details/related-entities/related-entities.component.spec.ts b/src/app/core/entity-details/related-entities/related-entities.component.spec.ts
index 49a1a1f2f5..b2c98f7a22 100644
--- a/src/app/core/entity-details/related-entities/related-entities.component.spec.ts
+++ b/src/app/core/entity-details/related-entities/related-entities.component.spec.ts
@@ -1,14 +1,25 @@
-import { ComponentFixture, TestBed } from "@angular/core/testing";
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+} from "@angular/core/testing";
import { RelatedEntitiesComponent } from "./related-entities.component";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { Child } from "../../../child-dev-project/children/model/child";
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
+import { Note } from "../../../child-dev-project/notes/model/note";
+import { Subject } from "rxjs";
+import { UpdatedEntity } from "../../entity/model/entity-update";
+import { Entity } from "../../entity/model/entity";
describe("RelatedEntitiesComponent", () => {
- let component: RelatedEntitiesComponent;
- let fixture: ComponentFixture>;
+ let component: RelatedEntitiesComponent;
+ let fixture: ComponentFixture<
+ RelatedEntitiesComponent
+ >;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -51,11 +62,27 @@ describe("RelatedEntitiesComponent", () => {
component.filter = filter;
await component.ngOnInit();
- expect(component.columns).toBe(columns);
expect(component.data).toEqual([r1, r2]);
expect(component.filter).toEqual({ ...filter, childId: c1.getId() });
});
+ it("should ignore entities of the related type where the matching field is undefined instead of array", async () => {
+ const c1 = new Child();
+ const r1 = new Note();
+ r1.children = [c1.getId()];
+ const rEmpty = new Note();
+ delete rEmpty.children; // some entity types will not have a default empty array
+ const entityMapper = TestBed.inject(EntityMapperService);
+ await entityMapper.saveAll([c1, r1, rEmpty]);
+
+ component.entity = c1;
+ component.entityType = Note.ENTITY_TYPE;
+ component.property = "children";
+ await component.ngOnInit();
+
+ expect(component.data).toEqual([r1]);
+ });
+
it("should create a new entity that references the related one", async () => {
const related = new Child();
component.entity = related;
@@ -69,4 +96,33 @@ describe("RelatedEntitiesComponent", () => {
expect(newEntity instanceof ChildSchoolRelation).toBeTrue();
expect(newEntity["childId"]).toBe(related.getId());
});
+
+ it("should add a new entity that was created after the initial loading to the table", fakeAsync(() => {
+ const entityUpdates = new Subject>();
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
+ component.ngOnInit();
+ tick();
+
+ const entity = new ChildSchoolRelation();
+ entityUpdates.next({ entity: entity, type: "new" });
+ tick();
+
+ expect(component.data).toEqual([entity]);
+ }));
+
+ it("should remove an entity from the table when it has been deleted", fakeAsync(() => {
+ const entityUpdates = new Subject>();
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
+ const entity = new ChildSchoolRelation();
+ component.data = [entity];
+ component.ngOnInit();
+ tick();
+
+ entityUpdates.next({ entity: entity, type: "remove" });
+ tick();
+
+ expect(component.data).toEqual([]);
+ }));
});
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 ddefb12814..f3a2bea2e4 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
@@ -2,23 +2,32 @@ import { Component, Input, OnInit } from "@angular/core";
import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { Entity, EntityConstructor } from "../../entity/model/entity";
-import {
- ColumnConfig,
- DataFilter,
-} from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { isArrayProperty } from "../../basic-datatypes/datatype-utils";
-import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
+import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { applyUpdate } from "../../entity/model/entity-update";
+import {
+ ScreenSize,
+ ScreenWidthObserver,
+} from "../../../utils/media/screen-size-observer.service";
+import {
+ ColumnConfig,
+ FormFieldConfig,
+ toFormFieldConfig,
+} from "../../common-components/entity-form/FormConfig";
+import { DataFilter } from "../../filter/filters/filters";
/**
* Load and display a list of entity subrecords (entities related to the current entity details view).
*/
@DynamicComponent("RelatedEntities")
+@UntilDestroy()
@Component({
selector: "app-related-entities",
templateUrl: "./related-entities.component.html",
standalone: true,
- imports: [EntitySubrecordComponent],
+ imports: [EntitiesTableComponent],
})
export class RelatedEntitiesComponent implements OnInit {
/** currently viewed/main entity for which related entities are displayed in this component */
@@ -35,41 +44,51 @@ export class RelatedEntitiesComponent implements OnInit {
@Input()
public set columns(value: ColumnConfig[]) {
- this._columns = value;
- }
- public get columns(): ColumnConfig[] {
- return this._columns;
+ if (!Array.isArray(value)) {
+ return;
+ }
+
+ this._columns = value.map((c) => toFormFieldConfig(c));
+ this.updateColumnsToDisplayForScreenSize();
}
- protected _columns: ColumnConfig[];
+ protected _columns: FormFieldConfig[];
+
+ columnsToDisplay: string[];
@Input() filter?: DataFilter;
@Input() showInactive: boolean;
- data: E[] = [];
- isLoading = false;
+ data: E[];
private isArray = false;
protected entityCtr: EntityConstructor;
constructor(
protected entityMapper: EntityMapperService,
- private entities: EntityRegistry,
- ) {}
+ private entityRegistry: EntityRegistry,
+ private screenWidthObserver: ScreenWidthObserver,
+ ) {
+ this.screenWidthObserver
+ .shared()
+ .pipe(untilDestroyed(this))
+ .subscribe(() => this.updateColumnsToDisplayForScreenSize());
+ }
async ngOnInit() {
await this.initData();
+ this.listenToEntityUpdates();
}
protected async initData() {
- this.isLoading = true;
-
- this.entityCtr = this.entities.get(this.entityType) as EntityConstructor;
+ 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]?.includes(this.entity.getId())
: e[this.property] === this.entity.getId(),
);
this.filter = {
@@ -79,11 +98,25 @@ export class RelatedEntitiesComponent implements OnInit {
: this.entity.getId(),
};
+ 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(),
+ );
+
if (this.showInactive === undefined) {
this.showInactive = this.entity.anonymized;
}
+ }
- this.isLoading = false;
+ protected listenToEntityUpdates() {
+ this.entityMapper
+ .receiveUpdates(this.entityCtr)
+ .pipe(untilDestroyed(this))
+ .subscribe((next) => {
+ this.data = applyUpdate(this.data, next);
+ });
}
createNewRecordFactory() {
@@ -96,4 +129,24 @@ export class RelatedEntitiesComponent implements OnInit {
return rec;
};
}
+
+ private updateColumnsToDisplayForScreenSize() {
+ if (!this._columns) {
+ return;
+ }
+
+ this.columnsToDisplay = this._columns
+ .filter((column) => {
+ if (column?.hideFromTable) {
+ return false;
+ }
+
+ const numericValue = ScreenSize[column?.visibleFrom];
+ if (numericValue === undefined) {
+ return true;
+ }
+ return this.screenWidthObserver.currentScreenSize() >= numericValue;
+ })
+ .map((c) => c.id);
+ }
}
diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html
index 52a97c8865..14662fa85c 100644
--- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html
+++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.html
@@ -1,24 +1,19 @@
-
+
Currently there is no active entry. To add a new entry, click on the
-
+
button.
-
-
+
diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts
index 55f0654bc3..4727633e12 100644
--- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts
+++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.spec.ts
@@ -1,4 +1,10 @@
-import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+ waitForAsync,
+} from "@angular/core/testing";
import { RelatedTimePeriodEntitiesComponent } from "./related-time-period-entities.component";
import moment from "moment";
@@ -7,7 +13,6 @@ import { Child } from "../../../child-dev-project/children/model/child";
import { School } from "../../../child-dev-project/schools/model/school";
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
-import { FilterService } from "../../filter/filter.service";
describe("RelatedTimePeriodEntitiesComponent", () => {
let component: RelatedTimePeriodEntitiesComponent;
@@ -17,13 +22,6 @@ describe("RelatedTimePeriodEntitiesComponent", () => {
let entityMapper: EntityMapperService;
- function getFilteredData(comp: RelatedTimePeriodEntitiesComponent) {
- const filterPredicate = TestBed.inject(FilterService).getFilterPredicate(
- comp.filter,
- );
- return comp.data.filter(filterPredicate);
- }
-
let mainEntity: Child;
const entityType = "ChildSchoolRelation";
const property = "childId";
@@ -80,7 +78,7 @@ describe("RelatedTimePeriodEntitiesComponent", () => {
component.property = "schoolId";
await component.ngOnInit();
- expect(getFilteredData(component)).toEqual([active1]);
+ expect(component.data).toEqual([active1]);
});
it("should change columns to be displayed via config", async () => {
@@ -144,15 +142,15 @@ describe("RelatedTimePeriodEntitiesComponent", () => {
).toBeTrue();
});
- it("should show all relations if configured; with active ones being highlighted", async () => {
+ it("should show all relations if configured; with active ones being highlighted", fakeAsync(() => {
const loadType = spyOn(entityMapper, "loadType");
loadType.and.resolveTo([active1, active2, inactive]);
component.showInactive = true;
- await component.ngOnInit();
+ component.ngOnInit();
+ tick();
- expect(getFilteredData(component)).toEqual([active1, active2, inactive]);
expect(component.backgroundColorFn(active1)).not.toEqual("");
expect(component.backgroundColorFn(inactive)).toEqual("");
- });
+ }));
});
diff --git a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts
index d8bf57e977..aebec695e6 100644
--- a/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts
+++ b/src/app/core/entity-details/related-time-period-entities/related-time-period-entities.component.ts
@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from "@angular/core";
-import { FormFieldConfig } from "../../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import moment from "moment";
import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
@@ -7,7 +7,7 @@ import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { FormsModule } from "@angular/forms";
import { MatTooltipModule } from "@angular/material/tooltip";
import { NgIf } from "@angular/common";
-import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
+import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
import { PillComponent } from "../../common-components/pill/pill.component";
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
import { RelatedEntitiesComponent } from "../related-entities/related-entities.component";
@@ -28,7 +28,7 @@ import { TimePeriod } from "./time-period";
styleUrls: ["./related-time-period-entities.component.scss"],
imports: [
FontAwesomeModule,
- EntitySubrecordComponent,
+ EntitiesTableComponent,
MatSlideToggleModule,
FormsModule,
MatTooltipModule,
@@ -64,11 +64,11 @@ export class RelatedTimePeriodEntitiesComponent
hasCurrentlyActiveEntry: boolean;
async ngOnInit() {
- this.onIsActiveFilterChange(this.showInactive);
- await super.initData();
+ await super.ngOnInit();
+ this.onIsActiveFilterChange();
}
- onIsActiveFilterChange(newValue: boolean) {
+ onIsActiveFilterChange() {
this.hasCurrentlyActiveEntry = this.data.some((record) => record.isActive);
if (this.showInactive) {
@@ -83,7 +83,7 @@ export class RelatedTimePeriodEntitiesComponent
const newRelation = super.createNewRecordFactory()();
newRelation.start =
- this.data.length && this.data[0].end
+ this.data?.length && this.data[0].end
? moment(this.data[0].end).add(1, "day").toDate()
: moment().startOf("day").toDate();
diff --git a/src/app/core/entity-list/EntityListConfig.ts b/src/app/core/entity-list/EntityListConfig.ts
index 0491f51340..61e485f976 100644
--- a/src/app/core/entity-list/EntityListConfig.ts
+++ b/src/app/core/entity-list/EntityListConfig.ts
@@ -1,5 +1,5 @@
import { FilterSelectionOption } from "../filter/filters/filters";
-import { FormFieldConfig } from "../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../common-components/entity-form/FormConfig";
import { ExportColumnConfig } from "../export/data-transformation-service/export-column-config";
import { Sort } from "@angular/material/sort";
import { unitOfTime } from "moment";
@@ -17,13 +17,15 @@ export interface EntityListConfig {
entity?: string;
/**
- * The columns to be displayed in the table
+ * The columns to be displayed in the table.
+ *
+ * If any special columns aside from the entity's fields are needed, add them here.
*/
columns: (FormFieldConfig | string)[];
/**
* Optional config for which columns are displayed.
- * By default all columns are shown
+ * By default, all columns are shown
*/
columnGroups?: ColumnGroupsConfig;
@@ -55,7 +57,7 @@ export interface ColumnGroupsConfig {
default?: string;
/**
- * The name of the group group that should be selected by default on a mobile device.
+ * The name of the group that should be selected by default on a mobile device.
* Default is the name of the first group.
*/
mobile?: string;
@@ -82,7 +84,6 @@ export interface BasicFilterConfig {
export interface BooleanFilterConfig extends BasicFilterConfig {
true: string;
false: string;
- all: string;
}
export interface PrebuiltFilterConfig extends BasicFilterConfig {
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 fc0e1b6c60..1ad7d5965c 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
@@ -10,29 +10,11 @@
-
+
+
@@ -44,7 +26,7 @@
{{ title }}
-
+ [filterFreetext]="filterFreetext"
+ >
diff --git a/src/app/core/entity-list/entity-list/entity-list.component.scss b/src/app/core/entity-list/entity-list/entity-list.component.scss
index 6a3cf8efd9..620deda2f0 100644
--- a/src/app/core/entity-list/entity-list/entity-list.component.scss
+++ b/src/app/core/entity-list/entity-list/entity-list.component.scss
@@ -14,11 +14,6 @@
}
}
-.standard-add-button {
- background-color: white !important;
- height: 100%;
-}
-
.bulk-action-button {
position: fixed;
right: sizes.$large;
diff --git a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts
index f82b61e4d5..8288714b8d 100644
--- a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts
+++ b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts
@@ -20,6 +20,7 @@ import { HarnessLoader } from "@angular/cdk/testing";
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
import { MatTabGroupHarness } from "@angular/material/tabs/testing";
import { FormDialogService } from "../../form-dialog/form-dialog.service";
+import { UpdatedEntity } from "../../entity/model/entity-update";
describe("EntityListComponent", () => {
let component: EntityListComponent;
@@ -45,7 +46,7 @@ describe("EntityListComponent", () => {
groups: [
{
name: "Basic Info",
- columns: ["projectNumber", "name", "age", "gender", "religion"],
+ columns: ["projectNumber", "name", "age", "gender"],
},
{
name: "School Info",
@@ -60,14 +61,10 @@ describe("EntityListComponent", () => {
default: "true",
true: "Currently active children",
false: "Currently inactive children",
- all: "All children",
} as BooleanFilterConfig,
{
id: "center",
},
- {
- id: "religion",
- },
],
};
let mockAttendanceService: jasmine.SpyObj;
@@ -109,13 +106,7 @@ describe("EntityListComponent", () => {
createComponent();
initComponentInputs();
tick();
- expect(component.columns).toEqual([
- ...testConfig.columns,
- "projectNumber",
- "name",
- "gender",
- "religion",
- ]);
+ expect(component.columns).toEqual([...testConfig.columns]);
}));
it("should create column groups from config and set correct one", fakeAsync(() => {
@@ -153,7 +144,7 @@ describe("EntityListComponent", () => {
expect(component.columnsToDisplay).toEqual(clickedColumnGroup.columns);
});
- it("should add and initialize columns which are only mentioned in the columnGroups", fakeAsync(() => {
+ it("should allow to use entity fields which are only mentioned in the columnGroups", fakeAsync(() => {
createComponent();
initComponentInputs();
tick();
@@ -173,21 +164,17 @@ describe("EntityListComponent", () => {
},
],
columnGroups: {
- groups: [
- { name: "One", columns: ["anotherColumn"] },
- { name: "Both", columns: ["testProperty", "anotherColumn"] },
- ],
+ groups: [{ name: "Both", columns: ["testProperty", "anotherColumn"] }],
},
};
component.ngOnChanges({ listConfig: null });
tick();
- expect(
- component.columns.map((col) => (typeof col === "string" ? col : col.id)),
- ).toEqual(
- jasmine.arrayWithExactContents(["testProperty", "anotherColumn"]),
- );
+ expect(component.columnsToDisplay).toEqual([
+ "testProperty",
+ "anotherColumn",
+ ]);
}));
it("should automatically initialize values if directly referenced from config", fakeAsync(() => {
@@ -228,16 +215,49 @@ describe("EntityListComponent", () => {
expect(navigateSpy).toHaveBeenCalled();
});
+ it("should add a new entity that was created after the initial loading to the table", fakeAsync(() => {
+ const entityUpdates = new Subject>();
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
+ createComponent();
+ initComponentInputs();
+ tick();
+
+ const entity = new Child();
+ entityUpdates.next({ entity: entity, type: "new" });
+ tick();
+
+ expect(component.allEntities).toEqual([entity]);
+ }));
+
+ it("should remove an entity from the table when it has been deleted", fakeAsync(() => {
+ const entityUpdates = new Subject>();
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "receiveUpdates").and.returnValue(entityUpdates);
+ const entity = new Child();
+ createComponent();
+ initComponentInputs();
+ tick();
+
+ component.allEntities = [entity];
+ entityUpdates.next({ entity: entity, type: "remove" });
+ tick();
+
+ expect(component.allEntities).toEqual([]);
+ }));
+
function createComponent() {
fixture = TestBed.createComponent(EntityListComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
+
+ component.entityConstructor = Child;
+
fixture.detectChanges();
}
async function initComponentInputs() {
component.listConfig = testConfig;
- component.entityConstructor = Child;
await component.ngOnChanges({
allEntities: undefined,
listConfig: undefined,
diff --git a/src/app/core/entity-list/entity-list/entity-list.component.ts b/src/app/core/entity-list/entity-list/entity-list.component.ts
index eaa40dbf99..2191bd84b6 100644
--- a/src/app/core/entity-list/entity-list/entity-list.component.ts
+++ b/src/app/core/entity-list/entity-list/entity-list.component.ts
@@ -1,12 +1,10 @@
import {
- AfterViewInit,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
- ViewChild,
} from "@angular/core";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import {
@@ -16,15 +14,12 @@ import {
GroupConfig,
} from "../EntityListConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
-import { FormFieldConfig } from "../../common-components/entity-form/entity-form/FormConfig";
-import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
-import { entityFilterPredicate } from "../../filter/filter-generator/filter-predicate";
+import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { AnalyticsService } from "../../analytics/analytics.service";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { FilterOverlayComponent } from "../../filter/filter-overlay/filter-overlay.component";
import { MatDialog } from "@angular/material/dialog";
import { NgForOf, NgIf, NgStyle, NgTemplateOutlet } from "@angular/common";
@@ -46,6 +41,11 @@ import { MatTooltipModule } from "@angular/material/tooltip";
import { Sort } from "@angular/material/sort";
import { ExportColumnConfig } from "../../export/data-transformation-service/export-column-config";
import { RouteTarget } from "../../../route-target";
+import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
+import { applyUpdate } from "../../entity/model/entity-update";
+import { Subscription } from "rxjs";
+import { DataFilter } from "../../filter/filters/filters";
+import { EntityCreateButtonComponent } from "../../common-components/entity-create-button/entity-create-button.component";
/**
* This component allows to create a full-blown table with pagination, filtering, searching and grouping.
@@ -74,7 +74,7 @@ import { RouteTarget } from "../../../route-target";
NgForOf,
MatFormFieldModule,
MatInputModule,
- EntitySubrecordComponent,
+ EntitiesTableComponent,
FormsModule,
FilterComponent,
TabStateModule,
@@ -83,12 +83,13 @@ import { RouteTarget } from "../../../route-target";
DisableEntityOperationDirective,
RouterLink,
MatTooltipModule,
+ EntityCreateButtonComponent,
],
standalone: true,
})
@UntilDestroy()
export class EntityListComponent
- implements EntityListConfig, OnChanges, AfterViewInit
+ implements EntityListConfig, OnChanges
{
@Input() allEntities: T[];
@@ -105,14 +106,10 @@ export class EntityListComponent
/** initial / default state whether to include archived records in the list */
@Input() showInactive: boolean;
- @Input() isLoading: boolean;
-
@Output() elementClick = new EventEmitter();
@Output() addNewClick = new EventEmitter();
selectedRows: T[];
- @ViewChild(EntitySubrecordComponent) entityTable: EntitySubrecordComponent;
-
isDesktop: boolean;
@Input() title = "";
@@ -123,11 +120,12 @@ export class EntityListComponent
mobileColumnGroup = "";
@Input() filters: FilterConfig[] = [];
- columnsToDisplay: string[] = [];
+ columnsToDisplay: string[];
filterObj: DataFilter;
filterString = "";
filteredData = [];
+ filterFreetext: string;
get selectedColumnGroupIndex(): number {
return this.selectedColumnGroupIndex_;
@@ -187,11 +185,6 @@ export class EntityListComponent
return this.buildComponentFromConfig();
}
- ngAfterViewInit() {
- this.entityTable.recordsDataSource.filterPredicate = (data, filter) =>
- entityFilterPredicate(data.record, filter);
- }
-
private async buildComponentFromConfig() {
if (this.entity) {
this.entityConstructor = this.entities.get(
@@ -206,7 +199,6 @@ export class EntityListComponent
this.title = this.title || this.entityConstructor?.labelPlural;
- this.addColumnsFromColumnGroups();
this.initColumnGroups(this.columnGroups);
this.displayColumnGroupByName(
@@ -217,39 +209,22 @@ export class EntityListComponent
}
private async loadEntities() {
- this.isLoading = true;
-
this.allEntities = await this.entityMapperService.loadType(
this.entityConstructor,
);
-
- this.isLoading = false;
+ this.listenToEntityUpdates();
}
- private addColumnsFromColumnGroups() {
- const allColumns = [...this.columns];
- const groupColumns = (this.columnGroups?.groups ?? []).reduce(
- (accumulatedColumns: string[], currentGroup) => [
- ...accumulatedColumns,
- ...currentGroup.columns,
- ],
- [],
- );
- for (const column of groupColumns) {
- if (
- !allColumns.some((existingColumn) =>
- // Check if the column is already defined as object or string
- typeof existingColumn === "string"
- ? existingColumn === column
- : existingColumn.id === column,
- )
- ) {
- allColumns.push(column);
- }
- }
+ private updateSubscription: Subscription;
- if (allColumns.length !== this.columns.length) {
- this.columns = [...allColumns];
+ private listenToEntityUpdates() {
+ if (!this.updateSubscription && this.entityConstructor) {
+ this.updateSubscription = this.entityMapperService
+ .receiveUpdates(this.entityConstructor)
+ .pipe(untilDestroyed(this))
+ .subscribe((next) => {
+ this.allEntities = applyUpdate(this.allEntities, next);
+ });
}
}
@@ -273,12 +248,8 @@ export class EntityListComponent
applyFilter(filterValue: string) {
// TODO: turn this into one of our filter types, so that all filtering happens the same way (and we avoid accessing internal datasource of sub-component here)
- filterValue = filterValue.trim();
- filterValue = filterValue.toLowerCase(); // MatTableDataSource defaults to lowercase matches
- this.entityTable.recordsDataSource.filter = filterValue;
- this.filteredData = this.entityTable.recordsDataSource.filteredData.map(
- (x) => x.record,
- );
+ this.filterFreetext = filterValue.trim().toLowerCase();
+
this.analyticsService.eventTrack("list_filter_freetext", {
category: this.entityConstructor?.ENTITY_TYPE,
});
@@ -322,4 +293,8 @@ export class EntityListComponent
this.duplicateRecord.duplicateRecord(this.selectedRows);
this.selectedRows = undefined;
}
+
+ onRowClick(row: T) {
+ this.elementClick.emit(row);
+ }
}
diff --git a/src/app/core/entity/default-datatype/edit-component-story-utils.ts b/src/app/core/entity/default-datatype/edit-component-story-utils.ts
index 8246014748..3129ba2d1f 100644
--- a/src/app/core/entity/default-datatype/edit-component-story-utils.ts
+++ b/src/app/core/entity/default-datatype/edit-component-story-utils.ts
@@ -2,7 +2,7 @@ import { FormComponent } from "../../entity-details/form/form.component";
import { Entity, EntityConstructor } from "../model/entity";
import { DatabaseEntity } from "../database-entity.decorator";
import { DatabaseField } from "../database-field.decorator";
-import { FormFieldConfig } from "../../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { applicationConfig, Meta } from "@storybook/angular";
import {
entityFormStorybookDefaultParameters,
diff --git a/src/app/core/entity/default-datatype/edit-component.ts b/src/app/core/entity/default-datatype/edit-component.ts
index 94cf8fd975..56dfec3698 100644
--- a/src/app/core/entity/default-datatype/edit-component.ts
+++ b/src/app/core/entity/default-datatype/edit-component.ts
@@ -1,5 +1,5 @@
import { FormControl, FormGroup } from "@angular/forms";
-import { FormFieldConfig } from "../../common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { Entity } from "../model/entity";
import { Directive, Input, OnChanges, OnInit } from "@angular/core";
diff --git a/src/app/core/entity/entity-config.service.spec.ts b/src/app/core/entity/entity-config.service.spec.ts
index d4d6a4b572..8d8ec3149e 100644
--- a/src/app/core/entity/entity-config.service.spec.ts
+++ b/src/app/core/entity/entity-config.service.spec.ts
@@ -14,6 +14,7 @@ import { EntityMapperService } from "./entity-mapper/entity-mapper.service";
import { mockEntityMapper } from "./entity-mapper/mock-entity-mapper-service";
import { EntityConfig } from "./entity-config";
import { EntitySchemaField } from "./schema/entity-schema-field";
+import { Child } from "../../child-dev-project/children/model/child";
describe("EntityConfigService", () => {
let service: EntityConfigService;
@@ -82,6 +83,32 @@ describe("EntityConfigService", () => {
expect(Test2.schema).toHaveKey(ATTRIBUTE_2_NAME);
});
+ it("should reset attribute to basic class config if custom attribute disappears from config doc", () => {
+ const originalLabel = Child.schema.get("name").label;
+ const customLabel = "custom label";
+
+ const mockEntityConfigs: (EntityConfig & { _id: string })[] = [
+ {
+ _id: "entity:Child",
+ attributes: { name: { label: customLabel } },
+ },
+ ];
+ mockConfigService.getAllConfigs.and.returnValue(mockEntityConfigs);
+ service.setupEntitiesFromConfig();
+ expect(Child.schema.get("name").label).toEqual(customLabel);
+
+ mockConfigService.getAllConfigs.and.returnValue([
+ {
+ _id: "entity:Child",
+ attributes: {
+ /* undo custom label */
+ },
+ },
+ ]);
+ service.setupEntitiesFromConfig();
+ expect(Child.schema.get("name").label).toEqual(originalLabel);
+ });
+
it("should allow to configure the `.toString` method", () => {
mockConfigService.getAllConfigs.and.returnValue([
{ _id: "entity:Test", toStringAttributes: ["name", "entityId"] },
diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts
index 9bdc2acfbc..27c665c812 100644
--- a/src/app/core/entity/entity-config.service.ts
+++ b/src/app/core/entity/entity-config.service.ts
@@ -6,6 +6,8 @@ import { IconName } from "@fortawesome/fontawesome-svg-core";
import { EntityConfig } from "./entity-config";
import { addPropertySchema } from "./database-field.decorator";
import { PREFIX_VIEW_CONFIG } from "../config/dynamic-routing/view-config.interface";
+import { EntitySchemaField } from "./schema/entity-schema-field";
+import { EntitySchema } from "./schema/entity-schema";
/**
* A service that allows to work with configuration-objects
@@ -19,6 +21,9 @@ export class EntityConfigService {
/** @deprecated will become private, use the service to access the data */
static readonly PREFIX_ENTITY_CONFIG = "entity:";
+ /** original initial entity schemas without overrides from config */
+ private coreEntitySchemas = new Map();
+
static getDetailsViewId(entityConstructor: EntityConstructor) {
return (
PREFIX_VIEW_CONFIG + entityConstructor.route.replace(/^\//, "") + "/:id"
@@ -30,7 +35,21 @@ export class EntityConfigService {
constructor(
private configService: ConfigService,
private entities: EntityRegistry,
- ) {}
+ ) {
+ this.storeCoreEntitySchemas();
+ }
+
+ private storeCoreEntitySchemas() {
+ this.entities.forEach((ctr, key) => {
+ this.coreEntitySchemas.set(key, this.deepCopySchema(ctr.schema));
+ });
+ }
+
+ private deepCopySchema(schema: EntitySchema): EntitySchema {
+ return new Map(
+ JSON.parse(JSON.stringify(Array.from(schema))),
+ );
+ }
/**
* Assigns additional schema-fields to all entities that are
@@ -49,6 +68,7 @@ export class EntityConfigService {
this.createNewEntity(id, config.extends);
}
const ctor = this.entities.get(id);
+ this.setCoreSchemaAttributes(ctor, config.extends);
this.addConfigAttributes(ctor, config);
}
}
@@ -58,14 +78,37 @@ export class EntityConfigService {
? this.entities.get(parent)
: Entity;
+ const schema = this.deepCopySchema(parentClass.schema);
class DynamicClass extends parentClass {
- static schema = new Map(parentClass.schema.entries());
+ static schema = schema;
static ENTITY_TYPE = id;
}
this.entities.set(id, DynamicClass);
}
+ /**
+ * Set field definitons from the core schema to ensure undoing customized attributes is correctly applied.
+ * @param entityType
+ * @param parent
+ */
+ private setCoreSchemaAttributes(
+ entityType: EntityConstructor,
+ parent: string,
+ ) {
+ const coreEntityId = parent ?? entityType.ENTITY_TYPE;
+ const coreSchema =
+ this.coreEntitySchemas.get(coreEntityId) ?? Entity.schema;
+
+ for (const [key, value] of coreSchema.entries()) {
+ addPropertySchema(
+ entityType.prototype,
+ key,
+ JSON.parse(JSON.stringify(value)),
+ );
+ }
+ }
+
/**
* Appends the given (dynamic) attributes to the schema of the provided Entity.
* If no arguments are provided, they will be loaded from the config
diff --git a/src/app/core/entity/model/entity-update.spec.ts b/src/app/core/entity/model/entity-update.spec.ts
index e2b0a181c8..96b2a55e8d 100644
--- a/src/app/core/entity/model/entity-update.spec.ts
+++ b/src/app/core/entity/model/entity-update.spec.ts
@@ -66,10 +66,14 @@ describe("entity-update", () => {
});
it("does not change the list when an updated entity is not in the list", () => {
- const newEntities = applyUpdate(existingEntities, {
- entity: new TestEntity("n6", 1),
- type: "update",
- });
+ const newEntities = applyUpdate(
+ existingEntities,
+ {
+ entity: new TestEntity("n6", 1),
+ type: "update",
+ },
+ false,
+ );
expect(newEntities).toEqual(existingEntities);
});
diff --git a/src/app/core/entity/model/entity-update.ts b/src/app/core/entity/model/entity-update.ts
index 551548f666..51c7737945 100644
--- a/src/app/core/entity/model/entity-update.ts
+++ b/src/app/core/entity/model/entity-update.ts
@@ -28,23 +28,22 @@ export interface UpdatedEntity {
* @param next An entity that should be updated as well as the type of update. This, as well as the entity
* may be undefined or null. In this event, the entities-array is returned as is.
* @param entities The entities to update, must be defined
- * @param addIfMissing (Optional) whether to add an entity that comes through an update event but is not part of the array yet (default is to ignore)
+ * @param addIfMissing (Optional) whether to add an entity that comes through an update event but is not part of the array yet,
+ * default is to add, disable this if you do special filtering or calculations on the data
* @return An array of the given entities with the update applied
*/
export function applyUpdate(
entities: T[],
next: UpdatedEntity,
- addIfMissing: boolean = false,
+ addIfMissing: boolean = true,
): T[] {
if (!next || !next.entity || !entities) {
return entities;
}
if (
- next.type === "new" ||
- (addIfMissing &&
- next.type === "update" &&
- !entities.find((e) => e.getId() === next.entity.getId()))
+ (next.type === "new" || (addIfMissing && next.type === "update")) &&
+ !entities.find((e) => e.getId() === next.entity.getId())
) {
return [next.entity].concat(entities);
}
@@ -58,4 +57,6 @@ export function applyUpdate(
if (next.type === "remove") {
return entities.filter((e) => e.getId() !== next.entity.getId());
}
+
+ return entities;
}
diff --git a/src/app/core/entity/model/entity.ts b/src/app/core/entity/model/entity.ts
index 1a05718d53..ae0ee720fd 100644
--- a/src/app/core/entity/model/entity.ts
+++ b/src/app/core/entity/model/entity.ts
@@ -66,7 +66,7 @@ export class Entity {
/**
* True if this type's schema has been customized dynamically from the config.
*/
- static _isCustomizedType?: boolean;
+ static _isCustomizedType?: boolean; // todo should be private or renamed to "isCustomizedType"
/**
* Defining which attribute values of an entity should be shown in the `.toString()` method.
diff --git a/src/app/core/export/data-transformation-service/data-transformation.service.ts b/src/app/core/export/data-transformation-service/data-transformation.service.ts
index 0844337268..b307584c09 100644
--- a/src/app/core/export/data-transformation-service/data-transformation.service.ts
+++ b/src/app/core/export/data-transformation-service/data-transformation.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from "@angular/core";
import {
getReadableValue,
transformToReadableFormat,
-} from "../../common-components/entity-subrecord/entity-subrecord/value-accessor";
+} from "../../common-components/entities-table/value-accessor/value-accessor";
import { ExportColumnConfig } from "./export-column-config";
import { QueryService } from "../query.service";
import { groupBy } from "../../../utils/utils";
diff --git a/src/app/core/export/download-service/download.service.ts b/src/app/core/export/download-service/download.service.ts
index 2f78122c73..85012822c6 100644
--- a/src/app/core/export/download-service/download.service.ts
+++ b/src/app/core/export/download-service/download.service.ts
@@ -3,7 +3,7 @@ import { ExportColumnConfig } from "../data-transformation-service/export-column
import { ExportDataFormat } from "../export-data-directive/export-data.directive";
import { LoggingService } from "../../logging/logging.service";
import { DataTransformationService } from "../data-transformation-service/data-transformation.service";
-import { transformToReadableFormat } from "../../common-components/entity-subrecord/entity-subrecord/value-accessor";
+import { transformToReadableFormat } from "../../common-components/entities-table/value-accessor/value-accessor";
import { Papa } from "ngx-papaparse";
import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field";
diff --git a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts
index e39895f970..1b33351185 100644
--- a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts
+++ b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts
@@ -14,15 +14,12 @@ import { Child } from "../../../child-dev-project/children/model/child";
import moment from "moment";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { FilterService } from "../filter.service";
-import {
- BooleanFilter,
- ConfigurableEnumFilter,
- DateFilter,
- EntityFilter,
- FilterSelectionOption,
- SelectableFilter,
-} from "../filters/filters";
+import { FilterSelectionOption, SelectableFilter } from "../filters/filters";
import { Entity } from "../../entity/model/entity";
+import { DateFilter } from "../filters/dateFilter";
+import { BooleanFilter } from "../filters/booleanFilter";
+import { ConfigurableEnumFilter } from "../filters/configurableEnumFilter";
+import { EntityFilter } from "../filters/entityFilter";
describe("FilterGeneratorService", () => {
let service: FilterGeneratorService;
@@ -45,7 +42,6 @@ describe("FilterGeneratorService", () => {
id: "privateSchool",
true: "Private",
false: "Government",
- all: "All",
type: "boolean",
};
const schema = School.schema.get("privateSchool");
@@ -61,7 +57,6 @@ describe("FilterGeneratorService", () => {
return { key: option.key, label: option.label };
}),
).toEqual([
- { key: "all", label: "All" },
{ key: "true", label: "Private" },
{ key: "false", label: "Government" },
]);
@@ -71,9 +66,6 @@ describe("FilterGeneratorService", () => {
const interactionTypes = defaultInteractionTypes.map((it) =>
jasmine.objectContaining({ key: it.id, label: it.label }),
);
- interactionTypes.push(
- jasmine.objectContaining({ key: "all", label: "All" }),
- );
const schema = Note.schema.get("category");
let filterOptions = (
@@ -133,10 +125,9 @@ describe("FilterGeneratorService", () => {
defaultInteractionTypes[2],
];
- // indices are increased by one as first option is "all"
+ expect(filter([note], filterOptions.options[1])).toEqual([note]);
expect(filter([note], filterOptions.options[2])).toEqual([note]);
- expect(filter([note], filterOptions.options[3])).toEqual([note]);
- expect(filter([note], filterOptions.options[4])).toEqual([]);
+ expect(filter([note], filterOptions.options[3])).toEqual([]);
Note.schema.delete("otherEnum");
});
@@ -164,17 +155,12 @@ describe("FilterGeneratorService", () => {
expect(filterOptions.label).toEqual(schema.label);
expect(filterOptions.name).toEqual("schoolId");
const allRelations = [csr1, csr2, csr3, csr4];
- const allFilter = filterOptions.options.find((opt) => opt.key === "all");
- expect(allFilter.label).toEqual("All");
- expect(filter(allRelations, allFilter)).toEqual(allRelations);
- const school1Filter = filterOptions.options.find(
- (opt) => opt.key === school1.getId(),
- );
+ const school1Filter: FilterSelectionOption =
+ filterOptions.options.find((opt) => opt.key === school1.getId());
expect(school1Filter.label).toEqual(school1.name);
expect(filter(allRelations, school1Filter)).toEqual([csr1, csr4]);
- const school2Filter = filterOptions.options.find(
- (opt) => opt.key === school2.getId(),
- );
+ const school2Filter: FilterSelectionOption =
+ filterOptions.options.find((opt) => opt.key === school2.getId());
expect(school2Filter.label).toEqual(school2.name);
expect(filter(allRelations, school2Filter)).toEqual([csr2, csr3]);
});
@@ -203,7 +189,6 @@ describe("FilterGeneratorService", () => {
});
expect(comparableOptions).toEqual(
jasmine.arrayWithExactContents([
- { key: "", label: "All" },
{ key: "muslim", label: "muslim" },
{ key: "christian", label: "christian" },
]),
@@ -218,7 +203,6 @@ describe("FilterGeneratorService", () => {
label: "Date",
default: "today",
options: [
- { key: "", label: "All", filter: {} },
{
key: "today",
label: "Today",
@@ -239,15 +223,15 @@ describe("FilterGeneratorService", () => {
expect(filterOptions.label).toEqual(prebuiltFilter.label);
expect(filterOptions.name).toEqual(prebuiltFilter.id);
expect(filterOptions.options).toEqual(prebuiltFilter.options);
- expect(filterOptions.selectedOption).toEqual(prebuiltFilter.default);
+ expect(filterOptions.selectedOptionValues).toEqual([
+ prebuiltFilter.default,
+ ]);
const todayNote = new Note();
todayNote.date = new Date();
const yesterdayNote = new Note();
const notes = [todayNote, yesterdayNote];
yesterdayNote.date = moment().subtract(1, "day").toDate();
- const allFilter = filterOptions.options.find((f) => f.key === "");
- expect(filter(notes, allFilter)).toEqual(notes);
const todayFilter = filterOptions.options.find((f) => f.key === "today");
expect(filter(notes, todayFilter)).toEqual([todayNote]);
const beforeFilter = filterOptions.options.find((f) => f.key === "before");
diff --git a/src/app/core/filter/filter-generator/filter-generator.service.ts b/src/app/core/filter/filter-generator/filter-generator.service.ts
index a07e83e514..f1ae794197 100644
--- a/src/app/core/filter/filter-generator/filter-generator.service.ts
+++ b/src/app/core/filter/filter-generator/filter-generator.service.ts
@@ -1,10 +1,7 @@
import { Injectable } from "@angular/core";
import {
- BooleanFilter,
- ConfigurableEnumFilter,
- DateFilter,
- EntityFilter,
Filter,
+ FilterSelectionOption,
SelectableFilter,
} from "../filters/filters";
import {
@@ -21,6 +18,10 @@ import { FilterService } from "../filter.service";
import { defaultDateFilters } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { DateDatatype } from "../../basic-datatypes/date/date.datatype";
+import { DateFilter } from "../filters/dateFilter";
+import { BooleanFilter } from "../filters/booleanFilter";
+import { ConfigurableEnumFilter } from "../filters/configurableEnumFilter";
+import { EntityFilter } from "../filters/entityFilter";
@Injectable({
providedIn: "root",
@@ -99,7 +100,9 @@ export class FilterGeneratorService {
);
} else {
const options = [...new Set(data.map((c) => c[filterConfig.id]))];
- const fSO = SelectableFilter.generateOptions(options, filterConfig.id);
+ const fSO: FilterSelectionOption[] =
+ SelectableFilter.generateOptions(options, filterConfig.id);
+
filter = new SelectableFilter(
filterConfig.id,
fSO,
@@ -108,7 +111,7 @@ export class FilterGeneratorService {
}
if (filterConfig.hasOwnProperty("default")) {
- filter.selectedOption = filterConfig.default;
+ filter.selectedOptionValues = [filterConfig.default];
}
if (filter instanceof SelectableFilter) {
diff --git a/src/app/core/filter/filter-generator/filter-predicate.ts b/src/app/core/filter/filter-generator/filter-predicate.ts
index 7192f68faa..632ac14e16 100644
--- a/src/app/core/filter/filter-generator/filter-predicate.ts
+++ b/src/app/core/filter/filter-generator/filter-predicate.ts
@@ -1,5 +1,5 @@
import { Entity } from "../../entity/model/entity";
-import { getReadableValue } from "../../common-components/entity-subrecord/entity-subrecord/value-accessor";
+import { getReadableValue } from "../../common-components/entities-table/value-accessor/value-accessor";
export function entityFilterPredicate(data: Entity, filter: string): boolean {
return [...Object.values(data)].some((value) =>
diff --git a/src/app/core/filter/filter-overlay/filter-overlay.component.ts b/src/app/core/filter/filter-overlay/filter-overlay.component.ts
index 7a2ecd925e..63c082be66 100644
--- a/src/app/core/filter/filter-overlay/filter-overlay.component.ts
+++ b/src/app/core/filter/filter-overlay/filter-overlay.component.ts
@@ -1,10 +1,10 @@
import { Component, Inject } from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
import { Entity, EntityConstructor } from "../../entity/model/entity";
-import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { FilterConfig } from "../../entity-list/EntityListConfig";
import { FilterComponent } from "../filter/filter.component";
import { MatButtonModule } from "@angular/material/button";
+import { DataFilter } from "../filters/filters";
export interface FilterOverlayData {
filterConfig: FilterConfig[];
diff --git a/src/app/core/filter/filter.service.spec.ts b/src/app/core/filter/filter.service.spec.ts
index 3e00bf7cbd..40a675270a 100644
--- a/src/app/core/filter/filter.service.spec.ts
+++ b/src/app/core/filter/filter.service.spec.ts
@@ -2,11 +2,11 @@ import { TestBed } from "@angular/core/testing";
import { FilterService } from "./filter.service";
import { defaultInteractionTypes } from "../config/default-config/default-interaction-types";
-import { DataFilter } from "../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { Note } from "../../child-dev-project/notes/model/note";
import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service";
import { createTestingConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum-testing";
import moment from "moment";
+import { DataFilter } from "./filters/filters";
describe("FilterService", () => {
let service: FilterService;
diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts
index 178652f229..c4b9d49cd0 100644
--- a/src/app/core/filter/filter.service.ts
+++ b/src/app/core/filter/filter.service.ts
@@ -1,6 +1,5 @@
import { Injectable } from "@angular/core";
import { EntitySchemaField } from "../entity/schema/entity-schema-field";
-import { DataFilter } from "../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { Entity } from "../entity/model/entity";
import {
allInterpreters,
@@ -11,6 +10,8 @@ import {
} from "@ucast/mongo2js";
import moment from "moment";
import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service";
+import { DataFilter, Filter as EntityFilter } from "./filters/filters";
+import { MongoQuery } from "@casl/ability";
/**
* Utility service to help handling and aligning filters with entities.
@@ -27,6 +28,22 @@ export class FilterService {
constructor(private enumService: ConfigurableEnumService) {}
+ combineFilters(
+ entityFilters: EntityFilter[],
+ ): DataFilter {
+ if (entityFilters.length === 0) {
+ return {} as DataFilter;
+ }
+
+ return {
+ $and: [
+ ...entityFilters.map((value: EntityFilter): DataFilter => {
+ return value.getFilter();
+ }),
+ ],
+ } as unknown as DataFilter;
+ }
+
/**
* Builds a predicate for a given filter object.
* This predicate can be used to filter arrays of objects.
@@ -37,14 +54,14 @@ export class FilterService {
* @param filter a valid filter object, e.g. as provided by the `FilterComponent`
*/
getFilterPredicate(filter: DataFilter) {
- return this.filterFactory(filter);
+ return this.filterFactory(filter as MongoQuery);
}
/**
* Patches an entity with values required to pass the filter query.
* This patch happens in-place.
* @param entity the entity to be patched
- * @param filter the filter which the entity should pass afterwards
+ * @param filter the filter which the entity should pass afterward
*/
alignEntityWithFilter(entity: T, filter: DataFilter) {
const schema = entity.getSchema();
diff --git a/src/app/core/filter/filter/filter.component.html b/src/app/core/filter/filter/filter.component.html
index 50977eb8c8..3f3395c447 100644
--- a/src/app/core/filter/filter/filter.component.html
+++ b/src/app/core/filter/filter/filter.component.html
@@ -2,7 +2,7 @@
{
let component: FilterComponent;
let fixture: ComponentFixture;
let loader: HarnessLoader;
+ let activatedRouteMock = new ActivatedRouteMock();
+
beforeEach(async () => {
+ activatedRouteMock.snapshot = {
+ queryParams: {},
+ };
+
await TestBed.configureTestingModule({
imports: [FilterComponent, MockedTestingModule.withState()],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: activatedRouteMock,
+ },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(FilterComponent);
@@ -28,13 +47,82 @@ describe("FilterComponent", () => {
expect(component).toBeTruthy();
});
+ it("should have no filter selected when url params are empty", async () => {
+ component.entityType = Note;
+ component.useUrlQueryParams = true;
+ component.filterConfig = [{ id: "category" }];
+
+ await component.ngOnChanges({ filterConfig: true } as any);
+
+ expect(component.filterSelections.length).toBe(1);
+ expect(component.filterSelections[0].name).toBe("category");
+ expect(component.filterSelections[0].selectedOptionValues).toBeEmpty();
+ });
+
+ it("should load url params and set single filter value", async () => {
+ component.entityType = Note;
+ component.useUrlQueryParams = true;
+ component.filterConfig = [{ id: "category" }];
+
+ activatedRouteMock.snapshot = {
+ queryParams: {
+ category: "foo",
+ },
+ };
+
+ await component.ngOnChanges({ filterConfig: true } as any);
+
+ expect(component.filterSelections.length).toBe(1);
+ expect(component.filterSelections[0].name).toBe("category");
+ expect(component.filterSelections[0].selectedOptionValues.length).toBe(1);
+ expect(component.filterSelections[0].selectedOptionValues[0]).toBe("foo");
+ });
+
+ it("should load url params and set multiple filter value", async () => {
+ component.entityType = Note;
+ component.useUrlQueryParams = true;
+ component.filterConfig = [{ id: "category" }];
+
+ activatedRouteMock.snapshot = {
+ queryParams: {
+ category: "foo,bar",
+ },
+ };
+
+ await component.ngOnChanges({ filterConfig: true } as any);
+
+ expect(component.filterSelections.length).toBe(1);
+ expect(component.filterSelections[0].name).toBe("category");
+ expect(component.filterSelections[0].selectedOptionValues.length).toBe(2);
+ expect(component.filterSelections[0].selectedOptionValues[0]).toBe("foo");
+ expect(component.filterSelections[0].selectedOptionValues[1]).toBe("bar");
+ });
+
+ it("should load url params and set no filter value when empty", async () => {
+ component.entityType = Note;
+ component.useUrlQueryParams = true;
+ component.filterConfig = [{ id: "category" }];
+
+ activatedRouteMock.snapshot = {
+ queryParams: {
+ category: "",
+ },
+ };
+
+ await component.ngOnChanges({ filterConfig: true } as any);
+
+ expect(component.filterSelections.length).toBe(1);
+ expect(component.filterSelections[0].name).toBe("category");
+ expect(component.filterSelections[0].selectedOptionValues).toBeEmpty();
+ });
+
it("should set up category filter from configurable enum", async () => {
component.entityType = Note;
- const t1 = defaultInteractionTypes[1];
+ const t1 = defaultInteractionTypes[0];
const n1 = new Note();
n1.category = t1;
const n2 = new Note();
- n2.category = defaultInteractionTypes[2];
+ n2.category = defaultInteractionTypes[1];
component.entities = [n1, n2];
component.onlyShowRelevantFilterOptions = true;
component.filterConfig = [{ id: "category" }];
@@ -44,14 +132,20 @@ describe("FilterComponent", () => {
const selection = await loader.getHarness(MatSelectHarness);
await selection.open();
const options = await selection.getOptions();
- expect(options).toHaveSize(3);
+ expect(options).toHaveSize(2);
- const selectedOption = await options[1].getText();
+ const selectedOption = await options[0].getText();
expect(selectedOption).toEqual(t1.label);
- await options[1].click();
+ await options[0].click();
const selected = await selection.getValueText();
expect(selected).toEqual(t1.label);
- expect(component.filterObj).toEqual({ "category.id": t1.id } as any);
+ expect(component.filterObj).toEqual({
+ $and: [
+ {
+ $or: [{ "category.id": t1.id }],
+ },
+ ],
+ } as any);
});
});
diff --git a/src/app/core/filter/filter/filter.component.ts b/src/app/core/filter/filter/filter.component.ts
index 710298369f..7c22b90d6c 100644
--- a/src/app/core/filter/filter/filter.component.ts
+++ b/src/app/core/filter/filter/filter.component.ts
@@ -8,15 +8,15 @@ import {
} from "@angular/core";
import { FilterConfig } from "../../entity-list/EntityListConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
-import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { FilterGeneratorService } from "../filter-generator/filter-generator.service";
-import { ActivatedRoute, Params, Router } from "@angular/router";
-import { getUrlWithoutParams } from "../../../utils/utils";
+import { ActivatedRoute, Router } from "@angular/router";
import { ListFilterComponent } from "../list-filter/list-filter.component";
import { NgForOf, NgIf } from "@angular/common";
import { Angulartics2Module } from "angulartics2";
import { DateRangeFilterComponent } from "../../basic-datatypes/date/date-range-filter/date-range-filter.component";
-import { Filter } from "../filters/filters";
+import { getUrlWithoutParams } from "../../../utils/utils";
+import { FilterService } from "../filter.service";
+import { DataFilter, Filter } from "../filters/filters";
/**
* This component can be used to display filters, for example above tables.
@@ -63,7 +63,7 @@ export class FilterComponent implements OnChanges {
*/
@Input() filterObj: DataFilter;
/**
- * A event emitter that notifies about updates of the filter.
+ * An event emitter that notifies about updates of the filter.
*/
@Output() filterObjChange = new EventEmitter>();
@@ -72,6 +72,7 @@ export class FilterComponent implements OnChanges {
constructor(
private filterGenerator: FilterGeneratorService,
+ private filterService: FilterService,
private router: Router,
private route: ActivatedRoute,
) {}
@@ -89,19 +90,19 @@ export class FilterComponent implements OnChanges {
}
}
- filterOptionSelected(filter: Filter, selectedOption: string) {
- filter.selectedOption = selectedOption;
+ filterOptionSelected(filter: Filter, selectedOptions: string[]) {
+ filter.selectedOptionValues = selectedOptions;
this.applyFilterSelections();
if (this.useUrlQueryParams) {
- this.updateUrl(filter.name, selectedOption);
+ this.updateUrl(filter.name, selectedOptions.toString());
}
}
private applyFilterSelections() {
- const previousFilter = JSON.stringify(this.filterObj);
- const newFilter = this.filterSelections.reduce(
- (obj, filter) => Object.assign(obj, filter.getFilter()),
- {} as DataFilter,
+ const previousFilter: string = JSON.stringify(this.filterObj);
+
+ const newFilter: DataFilter = this.filterService.combineFilters(
+ this.filterSelections,
);
if (previousFilter === JSON.stringify(newFilter)) {
@@ -122,14 +123,16 @@ export class FilterComponent implements OnChanges {
});
}
- private loadUrlParams(parameters?: Params) {
+ private loadUrlParams() {
if (!this.useUrlQueryParams) {
return;
}
- const params = parameters || this.route.snapshot.queryParams;
+ const params = this.route.snapshot.queryParams;
this.filterSelections.forEach((f) => {
if (params.hasOwnProperty(f.name)) {
- f.selectedOption = params[f.name];
+ f.selectedOptionValues = params[f.name]
+ .split(",")
+ .filter((value) => value !== "");
}
});
}
diff --git a/src/app/core/filter/filters/booleanFilter.ts b/src/app/core/filter/filters/booleanFilter.ts
new file mode 100644
index 0000000000..da9629ebfc
--- /dev/null
+++ b/src/app/core/filter/filters/booleanFilter.ts
@@ -0,0 +1,28 @@
+import { Entity } from "../../entity/model/entity";
+import { BooleanFilterConfig } from "../../entity-list/EntityListConfig";
+import { DataFilter, SelectableFilter } from "./filters";
+
+export class BooleanFilter extends SelectableFilter {
+ constructor(name: string, label: string, config?: BooleanFilterConfig) {
+ super(
+ name,
+ [
+ {
+ key: "true",
+ label:
+ config.true ?? $localize`:Filter label default boolean true:Yes`,
+ filter: { [config.id]: true } as DataFilter,
+ },
+ {
+ key: "false",
+ label:
+ config.false ?? $localize`:Filter label default boolean true:No`,
+ filter: {
+ [config.id]: { $in: [false, undefined] },
+ } as DataFilter,
+ },
+ ],
+ label,
+ );
+ }
+}
diff --git a/src/app/core/filter/filters/configurableEnumFilter.ts b/src/app/core/filter/filters/configurableEnumFilter.ts
new file mode 100644
index 0000000000..9cf50edde4
--- /dev/null
+++ b/src/app/core/filter/filters/configurableEnumFilter.ts
@@ -0,0 +1,23 @@
+import { Entity } from "../../entity/model/entity";
+import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface";
+import { DataFilter, FilterSelectionOption, SelectableFilter } from "./filters";
+
+export class ConfigurableEnumFilter<
+ T extends Entity,
+> extends SelectableFilter {
+ constructor(
+ name: string,
+ label: string,
+ enumValues: ConfigurableEnumValue[],
+ ) {
+ const options: FilterSelectionOption[] = enumValues.map(
+ (enumValue: ConfigurableEnumValue) => ({
+ key: enumValue.id,
+ label: enumValue.label,
+ color: enumValue.color,
+ filter: { [name + ".id"]: enumValue.id } as DataFilter,
+ }),
+ );
+ super(name, options, label);
+ }
+}
diff --git a/src/app/core/filter/filters/dateFilter.ts b/src/app/core/filter/filters/dateFilter.ts
new file mode 100644
index 0000000000..a25ae6e19b
--- /dev/null
+++ b/src/app/core/filter/filters/dateFilter.ts
@@ -0,0 +1,71 @@
+import { Entity } from "../../entity/model/entity";
+import { DateRangeFilterConfigOption } from "../../entity-list/EntityListConfig";
+import { DateRange } from "@angular/material/datepicker";
+import { calculateDateRange } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component";
+import moment from "moment";
+import { DataFilter, Filter } from "./filters";
+import { isValidDate } from "../../../utils/utils";
+
+/**
+ * Represents a filter for date values.
+ * The filter can either be one of the predefined options or two manually entered dates.
+ */
+export class DateFilter extends Filter {
+ constructor(
+ public name: string,
+ public label: string = name,
+ public rangeOptions: DateRangeFilterConfigOption[],
+ ) {
+ super(name, label);
+ this.selectedOptionValues = [];
+ }
+
+ /**
+ * Returns the date range according to the selected option or dates
+ */
+ getDateRange(): DateRange {
+ const selectedOption = this.getSelectedOption();
+ if (selectedOption) {
+ return calculateDateRange(selectedOption);
+ }
+ const dates = this.selectedOptionValues;
+ if (dates?.length == 2) {
+ return this.getDateRangeFromDateStrings(dates[0], dates[1]);
+ }
+ return new DateRange(undefined, undefined);
+ }
+
+ getFilter(): DataFilter {
+ const range = this.getDateRange();
+ const filterObject: { $gte?: string; $lte?: string } = {};
+ if (range.start) {
+ filterObject.$gte = moment(range.start).format("YYYY-MM-DD");
+ }
+ if (range.end) {
+ filterObject.$lte = moment(range.end).format("YYYY-MM-DD");
+ }
+ if (filterObject.$gte || filterObject.$lte) {
+ return {
+ [this.name]: filterObject,
+ } as DataFilter;
+ }
+ return {} as DataFilter;
+ }
+
+ getSelectedOption() {
+ return this.rangeOptions[this.selectedOptionValues as any];
+ }
+
+ private getDateRangeFromDateStrings(
+ dateStr1: string,
+ dateStr2: string,
+ ): DateRange {
+ const date1 = moment(dateStr1).toDate();
+ const date2 = moment(dateStr2).toDate();
+
+ return new DateRange(
+ isValidDate(date1) ? date1 : undefined,
+ isValidDate(date2) ? date2 : undefined,
+ );
+ }
+}
diff --git a/src/app/core/filter/filters/entityFilter.ts b/src/app/core/filter/filters/entityFilter.ts
new file mode 100644
index 0000000000..d26af48246
--- /dev/null
+++ b/src/app/core/filter/filters/entityFilter.ts
@@ -0,0 +1,22 @@
+import { Entity } from "../../entity/model/entity";
+import { FilterSelectionOption, SelectableFilter } from "./filters";
+
+export class EntityFilter extends SelectableFilter {
+ constructor(name: string, label: string, filterEntities) {
+ filterEntities.sort((a, b) => a.toString().localeCompare(b.toString()));
+ const options: FilterSelectionOption[] = [];
+ options.push(
+ ...filterEntities.map((filterEntity) => ({
+ key: filterEntity.getId(),
+ label: filterEntity.toString(),
+ filter: {
+ $or: [
+ { [name]: filterEntity.getId() },
+ { [name]: { $elemMatch: { $eq: filterEntity.getId() } } },
+ ],
+ },
+ })),
+ );
+ super(name, options, label);
+ }
+}
diff --git a/src/app/core/filter/filters/filters.spec.ts b/src/app/core/filter/filters/filters.spec.ts
index 4c85f0243f..d94d74333f 100644
--- a/src/app/core/filter/filters/filters.spec.ts
+++ b/src/app/core/filter/filters/filters.spec.ts
@@ -1,11 +1,13 @@
-import { BooleanFilter, Filter, SelectableFilter } from "./filters";
+import { Filter, SelectableFilter } from "./filters";
import { FilterService } from "../filter.service";
+import { BooleanFilter } from "./booleanFilter";
+import { Entity } from "../../entity/model/entity";
describe("Filters", () => {
const filterService = new FilterService(undefined);
function testFilter(
- filterObj: Filter,
+ filterObj: Filter,
testData: any[],
expectedFilteredResult: any[],
) {
@@ -25,24 +27,24 @@ describe("Filters", () => {
});
it("init new options", () => {
- const fs = new SelectableFilter(
- "",
- [{ key: "", label: "", filter: "" }],
- "",
+ const filter = new SelectableFilter(
+ "name",
+ [{ key: "option-1", label: "op", filter: {} }],
+ "name",
);
- const keys = ["x", "y"];
- fs.options = SelectableFilter.generateOptions(keys, "category");
+ const keys: string[] = ["x", "y"];
+ filter.options = SelectableFilter.generateOptions(keys, "category");
- expect(fs.options).toHaveSize(keys.length + 1);
+ expect(filter.options).toHaveSize(keys.length);
- fs.selectedOption = "x";
+ filter.selectedOptionValues = ["x"];
const testData = [
{ id: 1, category: "x" },
{ id: 2, category: "y" },
];
- const filteredData = testFilter(fs, testData, [testData[0]]);
+ const filteredData = testFilter(filter, testData, [testData[0]]);
expect(filteredData[0].category).toBe("x");
});
@@ -53,32 +55,21 @@ describe("Filters", () => {
default: "true",
true: "is true",
false: "is not true",
- all: "All",
});
const recordTrue = { value: true };
const recordFalse = { value: false };
- const recordUndefined = {};
- filter.selectedOption = "true";
- testFilter(
- filter,
- [recordFalse, recordTrue, recordUndefined],
- [recordTrue],
- );
+ filter.selectedOptionValues = ["true"];
+ testFilter(filter, [recordFalse, recordTrue], [recordTrue]);
- filter.selectedOption = "false";
- testFilter(
- filter,
- [recordFalse, recordTrue, recordUndefined],
- [recordFalse, recordUndefined],
- );
+ filter.selectedOptionValues = ["false"];
+ testFilter(filter, [recordFalse, recordTrue], [recordFalse]);
- filter.selectedOption = "all";
- testFilter(
- filter,
- [recordFalse, recordTrue, recordUndefined],
- [recordFalse, recordTrue, recordUndefined],
- );
+ filter.selectedOptionValues = [];
+ testFilter(filter, [recordFalse, recordTrue], [recordFalse, recordTrue]);
+
+ filter.selectedOptionValues = ["true", "false"];
+ testFilter(filter, [recordFalse, recordTrue], [recordFalse, recordTrue]);
});
});
diff --git a/src/app/core/filter/filters/filters.ts b/src/app/core/filter/filters/filters.ts
index ab60a04953..9a4a6feab1 100644
--- a/src/app/core/filter/filters/filters.ts
+++ b/src/app/core/filter/filters/filters.ts
@@ -15,22 +15,21 @@
* along with ndb-core. If not, see .
*/
-import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface";
-import {
- BooleanFilterConfig,
- DateRangeFilterConfigOption,
-} from "../../entity-list/EntityListConfig";
-import { DataFilter } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { Entity } from "../../entity/model/entity";
-import { DateRange } from "@angular/material/datepicker";
-import { isValidDate } from "../../../utils/utils";
-import { calculateDateRange } from "../../basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component";
-import moment from "moment/moment";
+import { MongoQuery } from "@casl/ability";
+
+/**
+ * This filter can be used to filter an array of entities.
+ * It has to follow the MongoDB Query Syntax {@link https://www.mongodb.com/docs/manual/reference/operator/query/}.
+ *
+ * The filter is parsed using ucast {@link https://github.com/stalniy/ucast/tree/master/packages/mongo2js}
+ */
+export type DataFilter = MongoQuery | {};
export abstract class Filter {
- public selectedOption: string;
+ public selectedOptionValues: string[] = [];
- constructor(
+ protected constructor(
public name: string,
public label: string = name,
) {}
@@ -38,61 +37,6 @@ export abstract class Filter {
abstract getFilter(): DataFilter;
}
-/**
- * Represents a filter for date values.
- * The filter can either be one of the predefined options or two manually entered dates.
- */
-export class DateFilter extends Filter {
- constructor(
- public name: string,
- public label: string = name,
- public rangeOptions: DateRangeFilterConfigOption[],
- ) {
- super(name, label);
- this.selectedOption = "1";
- }
-
- /**
- * Returns the date range according to the selected option or dates
- */
- getDateRange(): DateRange {
- if (this.getSelectedOption()) {
- return calculateDateRange(this.getSelectedOption());
- }
- const dates = this.selectedOption?.split("_");
- if (dates?.length == 2) {
- const firstDate = moment(dates[0]).toDate();
- const secondDate = moment(dates[1]).toDate();
- return new DateRange(
- isValidDate(firstDate) ? firstDate : undefined,
- isValidDate(secondDate) ? secondDate : undefined,
- );
- }
- return new DateRange(undefined, undefined);
- }
-
- getFilter(): DataFilter {
- const range = this.getDateRange();
- const filterObject: { $gte?: string; $lte?: string } = {};
- if (range.start) {
- filterObject.$gte = moment(range.start).format("YYYY-MM-DD");
- }
- if (range.end) {
- filterObject.$lte = moment(range.end).format("YYYY-MM-DD");
- }
- if (filterObject.$gte || filterObject.$lte) {
- return {
- [this.name]: filterObject,
- } as DataFilter;
- }
- return {} as DataFilter;
- }
-
- getSelectedOption() {
- return this.rangeOptions[this.selectedOption as any];
- }
-}
-
/**
* Generic configuration for a filter with different selectable {@link FilterSelectionOption} options.
*
@@ -119,25 +63,13 @@ export class SelectableFilter extends Filter {
valuesToMatchAsOptions: string[],
attributeName: string,
): FilterSelectionOption[] {
- const options = [
- {
- key: "",
- label: $localize`:generic filter option showing all entries:All`,
- filter: {} as DataFilter,
- },
- ];
-
- options.push(
- ...valuesToMatchAsOptions
- .filter((k) => !!k)
- .map((k) => ({
- key: k.toLowerCase(),
- label: k.toString(),
- filter: { [attributeName]: k } as DataFilter,
- })),
- );
-
- return options;
+ return valuesToMatchAsOptions
+ .filter((k) => !!k)
+ .map((k) => ({
+ key: k.toLowerCase(),
+ label: k.toString(),
+ filter: { [attributeName]: k } as DataFilter,
+ }));
}
/**
@@ -153,18 +85,17 @@ export class SelectableFilter extends Filter {
public label: string = name,
) {
super(name, label);
- this.selectedOption = this.options[0]?.key;
+ this.selectedOptionValues = [];
}
- /** default filter will keep all items in the result */
- defaultFilter = {};
-
/**
* Get the full filter option by its key.
* @param key The identifier of the requested option
*/
- getOption(key: string): FilterSelectionOption {
- return this.options.find((option) => option.key === key);
+ getOption(key: string): FilterSelectionOption | undefined {
+ return this.options.find((option: FilterSelectionOption): boolean => {
+ return option.key === key;
+ });
}
/**
@@ -172,94 +103,19 @@ export class SelectableFilter extends Filter {
* If the given key is undefined or invalid, the returned filter matches any elements.
*/
public getFilter(): DataFilter {
- const option = this.getOption(this.selectedOption);
-
- if (!option) {
- return this.defaultFilter as DataFilter;
- } else {
- return option.filter;
+ const filters: DataFilter[] = this.selectedOptionValues
+ .map((value: string) => this.getOption(value))
+ .filter((value: FilterSelectionOption) => value !== undefined)
+ .map((previousValue: FilterSelectionOption) => {
+ return previousValue.filter as DataFilter;
+ });
+
+ if (filters.length === 0) {
+ return {} as DataFilter;
}
- }
-}
-
-export class BooleanFilter extends SelectableFilter {
- constructor(name: string, label: string, config?: BooleanFilterConfig) {
- super(
- name,
- [
- {
- key: "all",
- label: config.all ?? $localize`:Filter label:All`,
- filter: {},
- },
- {
- key: "true",
- label:
- config.true ?? $localize`:Filter label default boolean true:Yes`,
- filter: { [config.id]: true },
- },
- {
- key: "false",
- label:
- config.false ?? $localize`:Filter label default boolean true:No`,
- filter: { $or: [{ [config.id]: false }, { [config.id]: undefined }] },
- },
- ],
- label,
- );
- }
-}
-
-export class ConfigurableEnumFilter<
- T extends Entity,
-> extends SelectableFilter {
- constructor(
- name: string,
- label: string,
- enumValues: ConfigurableEnumValue[],
- ) {
- let options: FilterSelectionOption[] = [
- {
- key: "all",
- label: $localize`:Filter label:All`,
- filter: {},
- },
- ];
- options.push(
- ...enumValues.map((enumValue) => ({
- key: enumValue.id,
- label: enumValue.label,
- color: enumValue.color,
- filter: { [name + ".id"]: enumValue.id },
- })),
- );
- super(name, options, label);
- }
-}
-
-export class EntityFilter extends SelectableFilter {
- constructor(name: string, label: string, filterEntities) {
- filterEntities.sort((a, b) => a.toString().localeCompare(b.toString()));
- const options: FilterSelectionOption[] = [
- {
- key: "all",
- label: $localize`:Filter label:All`,
- filter: {},
- },
- ];
- options.push(
- ...filterEntities.map((filterEntity) => ({
- key: filterEntity.getId(),
- label: filterEntity.toString(),
- filter: {
- $or: [
- { [name]: filterEntity.getId() },
- { [name]: { $elemMatch: { $eq: filterEntity.getId() } } },
- ],
- },
- })),
- );
- super(name, options, label);
+ return {
+ $or: [...filters],
+ } as unknown as DataFilter;
}
}
@@ -280,5 +136,5 @@ export interface FilterSelectionOption {
/**
* The filter query which should be used if this filter is selected
*/
- filter: DataFilter | any;
+ filter: DataFilter;
}
diff --git a/src/app/core/filter/list-filter/list-filter.component.html b/src/app/core/filter/list-filter/list-filter.component.html
index 523a6f4081..b92ed24291 100644
--- a/src/app/core/filter/list-filter/list-filter.component.html
+++ b/src/app/core/filter/list-filter/list-filter.component.html
@@ -1,14 +1,19 @@
- {{ _filterConfig.label || _filterConfig.name }}
-
+ {{ filterConfig.label || filterConfig.name }}
+
+ @for (option of filterConfig.options; track option.key) {
{{ option.label }}
+ }
diff --git a/src/app/core/filter/list-filter/list-filter.component.spec.ts b/src/app/core/filter/list-filter/list-filter.component.spec.ts
index 701cbe4f11..a37e4a5f3b 100644
--- a/src/app/core/filter/list-filter/list-filter.component.spec.ts
+++ b/src/app/core/filter/list-filter/list-filter.component.spec.ts
@@ -1,8 +1,8 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ListFilterComponent } from "./list-filter.component";
-import { SelectableFilter } from "../filters/filters";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
+import { SelectableFilter } from "../filters/filters";
describe("ListFilterComponent", () => {
let component: ListFilterComponent;
diff --git a/src/app/core/filter/list-filter/list-filter.component.ts b/src/app/core/filter/list-filter/list-filter.component.ts
index 1c4093041e..2e561dcb6e 100644
--- a/src/app/core/filter/list-filter/list-filter.component.ts
+++ b/src/app/core/filter/list-filter/list-filter.component.ts
@@ -1,10 +1,11 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
-import { Filter, SelectableFilter } from "../filters/filters";
import { Entity } from "../../entity/model/entity";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { BorderHighlightDirective } from "../../common-components/border-highlight/border-highlight.directive";
-import { NgForOf } from "@angular/common";
+import { JsonPipe, NgForOf } from "@angular/common";
+import { ReactiveFormsModule } from "@angular/forms";
+import { SelectableFilter } from "../filters/filters";
@Component({
selector: "app-list-filter",
@@ -12,22 +13,16 @@ import { NgForOf } from "@angular/common";
imports: [
MatFormFieldModule,
MatSelectModule,
+ ReactiveFormsModule,
BorderHighlightDirective,
NgForOf,
+ JsonPipe,
],
standalone: true,
})
export class ListFilterComponent {
- @Input()
- public set filterConfig(value: Filter) {
- this._filterConfig = value as SelectableFilter;
- }
- _filterConfig: SelectableFilter;
- @Input() selectedOption: string;
- @Output() selectedOptionChange = new EventEmitter();
-
- selectOption(selectedOptionKey: string) {
- this.selectedOption = selectedOptionKey;
- this.selectedOptionChange.emit(selectedOptionKey);
- }
+ @Input({ transform: (value: any) => value as SelectableFilter })
+ filterConfig: SelectableFilter;
+ @Input() selectedOptions: string[];
+ @Output() selectedOptionChange: EventEmitter = new EventEmitter();
}
diff --git a/src/app/core/form-dialog/form-dialog.service.ts b/src/app/core/form-dialog/form-dialog.service.ts
index 82cfef8e0e..ed8a666254 100644
--- a/src/app/core/form-dialog/form-dialog.service.ts
+++ b/src/app/core/form-dialog/form-dialog.service.ts
@@ -6,12 +6,12 @@ import {
} from "@angular/material/dialog";
import { ComponentType } from "@angular/cdk/overlay";
import { Entity } from "../entity/model/entity";
-import { RowDetailsComponent } from "../common-components/entity-subrecord/row-details/row-details.component";
-import { FormFieldConfig } from "../common-components/entity-form/entity-form/FormConfig";
+import { RowDetailsComponent } from "./row-details/row-details.component";
import {
ColumnConfig,
+ FormFieldConfig,
toFormFieldConfig,
-} from "../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
+} from "../common-components/entity-form/FormConfig";
import { EntitySchemaService } from "../entity/schema/entity-schema.service";
@Injectable({ providedIn: "root" })
diff --git a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.html b/src/app/core/form-dialog/row-details/row-details.component.html
similarity index 100%
rename from src/app/core/common-components/entity-subrecord/row-details/row-details.component.html
rename to src/app/core/form-dialog/row-details/row-details.component.html
diff --git a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.spec.ts b/src/app/core/form-dialog/row-details/row-details.component.spec.ts
similarity index 85%
rename from src/app/core/common-components/entity-subrecord/row-details/row-details.component.spec.ts
rename to src/app/core/form-dialog/row-details/row-details.component.spec.ts
index 8c8d8f2589..391500ce30 100644
--- a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.spec.ts
+++ b/src/app/core/form-dialog/row-details/row-details.component.spec.ts
@@ -5,9 +5,9 @@ import {
RowDetailsComponent,
} from "./row-details.component";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
-import { Entity } from "../../../entity/model/entity";
-import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
-import { EntityAbility } from "../../../permissions/ability/entity-ability";
+import { Entity } from "../../entity/model/entity";
+import { MockedTestingModule } from "../../../utils/mocked-testing.module";
+import { EntityAbility } from "../../permissions/ability/entity-ability";
import { NEVER } from "rxjs";
describe("RowDetailsComponent", () => {
diff --git a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts b/src/app/core/form-dialog/row-details/row-details.component.ts
similarity index 67%
rename from src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts
rename to src/app/core/form-dialog/row-details/row-details.component.ts
index 06c0f52aac..dcebe47d03 100644
--- a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts
+++ b/src/app/core/form-dialog/row-details/row-details.component.ts
@@ -1,23 +1,23 @@
import { Component, Inject } from "@angular/core";
-import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
-import { Entity } from "../../../entity/model/entity";
+import { Entity } from "../../entity/model/entity";
import {
EntityForm,
EntityFormService,
-} from "../../entity-form/entity-form.service";
-import { DialogCloseComponent } from "../../dialog-close/dialog-close.component";
-import { EntityFormComponent } from "../../entity-form/entity-form/entity-form.component";
+} from "../../common-components/entity-form/entity-form.service";
+import { DialogCloseComponent } from "../../common-components/dialog-close/dialog-close.component";
+import { EntityFormComponent } from "../../common-components/entity-form/entity-form/entity-form.component";
import { NgForOf, NgIf } from "@angular/common";
-import { PillComponent } from "../../pill/pill.component";
-import { DynamicComponentDirective } from "../../../config/dynamic-components/dynamic-component.directive";
+import { PillComponent } from "../../common-components/pill/pill.component";
+import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
import { MatTooltipModule } from "@angular/material/tooltip";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { DialogButtonsComponent } from "../../../form-dialog/dialog-buttons/dialog-buttons.component";
-import { EntityArchivedInfoComponent } from "../../../entity-details/entity-archived-info/entity-archived-info.component";
-import { FieldGroup } from "../../../entity-details/form/field-group";
-import { EntityFieldEditComponent } from "../../entity-field-edit/entity-field-edit.component";
-import { EntityFieldViewComponent } from "../../entity-field-view/entity-field-view.component";
+import { DialogButtonsComponent } from "../dialog-buttons/dialog-buttons.component";
+import { EntityArchivedInfoComponent } from "../../entity-details/entity-archived-info/entity-archived-info.component";
+import { FieldGroup } from "../../entity-details/form/field-group";
+import { EntityFieldEditComponent } from "../../common-components/entity-field-edit/entity-field-edit.component";
+import { EntityFieldViewComponent } from "../../common-components/entity-field-view/entity-field-view.component";
/**
* Data interface that must be given when opening the dialog
diff --git a/src/app/core/import/import-review-data/import-review-data.component.html b/src/app/core/import/import-review-data/import-review-data.component.html
index bb991d881c..7d4abe387a 100644
--- a/src/app/core/import/import-review-data/import-review-data.component.html
+++ b/src/app/core/import/import-review-data/import-review-data.component.html
@@ -14,9 +14,10 @@
-
0"
- [columns]="displayColumns"
+ [entityType]="entityConstructor"
+ [customColumns]="displayColumns"
[columnsToDisplay]="displayColumns"
[records]="mappedEntities"
clickMode="none"
diff --git a/src/app/core/import/import-review-data/import-review-data.component.spec.ts b/src/app/core/import/import-review-data/import-review-data.component.spec.ts
index 3d884af2d8..d2f0e16def 100644
--- a/src/app/core/import/import-review-data/import-review-data.component.spec.ts
+++ b/src/app/core/import/import-review-data/import-review-data.component.spec.ts
@@ -10,7 +10,7 @@ import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { MatDialog } from "@angular/material/dialog";
import { of } from "rxjs";
import { ImportService } from "../import.service";
-import { Entity } from "../../entity/model/entity";
+import { School } from "../../../child-dev-project/schools/model/school";
describe("ImportReviewDataComponent", () => {
let component: ImportReviewDataComponent;
@@ -35,11 +35,14 @@ describe("ImportReviewDataComponent", () => {
fixture = TestBed.createComponent(ImportReviewDataComponent);
component = fixture.componentInstance;
+
+ component.entityType = School.ENTITY_TYPE;
+
fixture.detectChanges();
});
it("should parse data whenever it changes", fakeAsync(() => {
- const testEntities = [new Entity("1")];
+ const testEntities = [new School("1")];
mockImportService.transformRawDataToEntities.and.resolveTo(testEntities);
component.columnMapping = [
{ column: "x", propertyName: "name" },
diff --git a/src/app/core/import/import-review-data/import-review-data.component.ts b/src/app/core/import/import-review-data/import-review-data.component.ts
index 0787b96957..1d39f5f9a8 100644
--- a/src/app/core/import/import-review-data/import-review-data.component.ts
+++ b/src/app/core/import/import-review-data/import-review-data.component.ts
@@ -7,7 +7,7 @@ import {
SimpleChanges,
} from "@angular/core";
import { ColumnMapping } from "../column-mapping";
-import { Entity } from "../../entity/model/entity";
+import { Entity, EntityConstructor } from "../../entity/model/entity";
import { ImportService } from "../import.service";
import { MatDialog } from "@angular/material/dialog";
import {
@@ -19,24 +19,21 @@ import { ImportMetadata } from "../import-metadata";
import { AdditionalImportAction } from "../import-additional-actions/additional-import-action";
import { MatButtonModule } from "@angular/material/button";
import { HelpButtonComponent } from "../../common-components/help-button/help-button.component";
-import { EntitySubrecordComponent } from "../../common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
import { NgIf } from "@angular/common";
+import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
+import { EntityRegistry } from "../../entity/database-entity.decorator";
@Component({
selector: "app-import-review-data",
templateUrl: "./import-review-data.component.html",
styleUrls: ["./import-review-data.component.scss"],
standalone: true,
- imports: [
- MatButtonModule,
- HelpButtonComponent,
- EntitySubrecordComponent,
- NgIf,
- ],
+ imports: [MatButtonModule, HelpButtonComponent, EntitiesTableComponent, NgIf],
})
export class ImportReviewDataComponent implements OnChanges {
@Input() rawData: any[];
@Input() entityType: string;
+ entityConstructor: EntityConstructor;
@Input() columnMapping: ColumnMapping[];
@Input() additionalActions: AdditionalImportAction[];
@@ -48,9 +45,12 @@ export class ImportReviewDataComponent implements OnChanges {
constructor(
private importService: ImportService,
private matDialog: MatDialog,
+ private entityRegistry: EntityRegistry,
) {}
ngOnChanges(changes: SimpleChanges) {
+ this.entityConstructor = this.entityRegistry.get(this.entityType);
+
// Every change requires a complete re-calculation
this.parseRawData();
}
diff --git a/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts b/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts
index 1e6e887121..960a4e0c2a 100644
--- a/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts
+++ b/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts
@@ -57,7 +57,7 @@ describe("ChangelogComponent", () => {
{
provide: UpdateManagerService,
useValue: jasmine.createSpyObj([
- "notifyUserWhenUpdateAvailable",
+ "listenToAppUpdates",
"regularlyCheckForUpdates",
"detectUnrecoverableState",
]),
diff --git a/src/app/core/ui/latest-changes/latest-changes.module.ts b/src/app/core/ui/latest-changes/latest-changes.module.ts
index e377ffb797..7056e58f3c 100644
--- a/src/app/core/ui/latest-changes/latest-changes.module.ts
+++ b/src/app/core/ui/latest-changes/latest-changes.module.ts
@@ -58,7 +58,7 @@ import { MatButtonModule } from "@angular/material/button";
})
export class LatestChangesModule {
constructor(private updateManagerService: UpdateManagerService) {
- this.updateManagerService.notifyUserWhenUpdateAvailable();
+ this.updateManagerService.listenToAppUpdates();
this.updateManagerService.regularlyCheckForUpdates();
this.updateManagerService.detectUnrecoverableState();
}
diff --git a/src/app/core/ui/latest-changes/update-manager.service.spec.ts b/src/app/core/ui/latest-changes/update-manager.service.spec.ts
index 04b10f74f3..31344b4e7d 100644
--- a/src/app/core/ui/latest-changes/update-manager.service.spec.ts
+++ b/src/app/core/ui/latest-changes/update-manager.service.spec.ts
@@ -10,6 +10,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { LatestChangesDialogService } from "./latest-changes-dialog.service";
import { Subject } from "rxjs";
import { LoggingService } from "../../logging/logging.service";
+import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";
describe("UpdateManagerService", () => {
let service: UpdateManagerService;
@@ -23,6 +24,7 @@ describe("UpdateManagerService", () => {
let stableSubject: Subject;
let latestChangesDialog: jasmine.SpyObj;
let mockLogger: jasmine.SpyObj;
+ let unsavedChanges: UnsavedChangesService;
beforeEach(() => {
mockLocation = jasmine.createSpyObj(["reload"]);
@@ -43,32 +45,51 @@ describe("UpdateManagerService", () => {
appRef = jasmine.createSpyObj([], { isStable: stableSubject });
latestChangesDialog = jasmine.createSpyObj(["showLatestChangesIfUpdated"]);
mockLogger = jasmine.createSpyObj(["error"]);
+ unsavedChanges = new UnsavedChangesService(undefined);
+ unsavedChanges.pending = true;
service = createService();
});
+ afterEach(() => localStorage.clear());
+
it("should create", () => {
expect(service).toBeTruthy();
});
- it("should show a snackBar that allows to reload the page when an update is available", fakeAsync(() => {
- service.notifyUserWhenUpdateAvailable();
+ it("should show a snackBar that allows to reload the page when an update is available", () => {
+ service.listenToAppUpdates();
// notify about new update
updateSubject.next({ type: "VERSION_READY" });
- tick();
expect(snackBar.open).toHaveBeenCalled();
// user activates update
snackBarAction.next(undefined);
- tick();
expect(mockLocation.reload).toHaveBeenCalled();
- }));
+ });
+
+ it("should reload app if no unsaved changes are detected", () => {
+ service.listenToAppUpdates();
+ unsavedChanges.pending = true;
+
+ updateSubject.next({ type: "VERSION_READY" });
+
+ expect(mockLocation.reload).not.toHaveBeenCalled();
+ expect(snackBar.open).toHaveBeenCalled();
+
+ createService();
+ unsavedChanges.pending = false;
+
+ updateSubject.next({ type: "VERSION_READY" });
+
+ expect(mockLocation.reload).toHaveBeenCalled();
+ });
it("should reload the page during construction if noted in the local storage", () => {
const version = "1.1.1";
- window.localStorage.setItem(
+ localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
"update-" + version,
);
@@ -76,31 +97,28 @@ describe("UpdateManagerService", () => {
createService();
expect(mockLocation.reload).toHaveBeenCalled();
- expect(
- window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
- ).toBe(version);
+ expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
+ version,
+ );
});
- it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", fakeAsync(() => {
+ it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", () => {
const version = "1.1.1";
- window.localStorage.setItem(
- LatestChangesDialogService.VERSION_KEY,
- version,
- );
- service.notifyUserWhenUpdateAvailable();
+ localStorage.setItem(LatestChangesDialogService.VERSION_KEY, version);
+ service.listenToAppUpdates();
updateSubject.next({ type: "VERSION_READY" });
- expect(
- window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
- ).toBe("update-" + version);
+ expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
+ "update-" + version,
+ );
// reload is triggered by clicking button on the snackbar
snackBarAction.next();
- expect(
- window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY),
- ).toBe(version);
- }));
+ expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe(
+ version,
+ );
+ });
it("should check for updates once on startup and then every hour", fakeAsync(() => {
service.regularlyCheckForUpdates();
@@ -138,7 +156,7 @@ describe("UpdateManagerService", () => {
it("should trigger the latest changes dialog on startup only if update note is set", () => {
latestChangesDialog.showLatestChangesIfUpdated.calls.reset();
- window.localStorage.setItem(
+ localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
"update-1.0.0",
);
@@ -148,10 +166,7 @@ describe("UpdateManagerService", () => {
latestChangesDialog.showLatestChangesIfUpdated,
).not.toHaveBeenCalled();
- window.localStorage.setItem(
- LatestChangesDialogService.VERSION_KEY,
- "1.0.0",
- );
+ localStorage.setItem(LatestChangesDialogService.VERSION_KEY, "1.0.0");
createService();
expect(latestChangesDialog.showLatestChangesIfUpdated).toHaveBeenCalled();
@@ -176,6 +191,7 @@ describe("UpdateManagerService", () => {
snackBar,
mockLogger,
latestChangesDialog,
+ unsavedChanges,
mockLocation,
);
}
diff --git a/src/app/core/ui/latest-changes/update-manager.service.ts b/src/app/core/ui/latest-changes/update-manager.service.ts
index 883561d318..e3e7df9262 100644
--- a/src/app/core/ui/latest-changes/update-manager.service.ts
+++ b/src/app/core/ui/latest-changes/update-manager.service.ts
@@ -23,6 +23,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { LoggingService } from "../../logging/logging.service";
import { LatestChangesDialogService } from "./latest-changes-dialog.service";
import { LOCATION_TOKEN } from "../../../utils/di-tokens";
+import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";
/**
* Check with the server whether a new version of the app is available in order to notify the user.
@@ -41,17 +42,18 @@ export class UpdateManagerService {
private snackBar: MatSnackBar,
private logger: LoggingService,
private latestChangesDialogService: LatestChangesDialogService,
+ private unsavedChanges: UnsavedChangesService,
@Inject(LOCATION_TOKEN) private location: Location,
) {
this.updates.unrecoverable.subscribe((err) => {
this.logger.error("App is in unrecoverable state: " + err.reason);
this.location.reload();
});
- const currentVersion = window.localStorage.getItem(
+ const currentVersion = localStorage.getItem(
LatestChangesDialogService.VERSION_KEY,
);
if (currentVersion && currentVersion.startsWith(this.UPDATE_PREFIX)) {
- window.localStorage.setItem(
+ localStorage.setItem(
LatestChangesDialogService.VERSION_KEY,
currentVersion.replace(this.UPDATE_PREFIX, ""),
);
@@ -64,13 +66,13 @@ export class UpdateManagerService {
/**
* Display a notification to the user in case a new app version is detected by the ServiceWorker.
*/
- public notifyUserWhenUpdateAvailable() {
+ public listenToAppUpdates() {
if (!this.updates.isEnabled) {
return;
}
this.updates.versionUpdates
.pipe(filter((e) => e.type === "VERSION_READY"))
- .subscribe(() => this.showUpdateNotification());
+ .subscribe(() => this.updateIfPossible());
}
/**
@@ -93,33 +95,37 @@ export class UpdateManagerService {
);
}
- private showUpdateNotification() {
+ private updateIfPossible() {
const currentVersion =
- window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || "";
+ localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || "";
if (currentVersion.startsWith(this.UPDATE_PREFIX)) {
// Sometimes this is triggered multiple times for one update
return;
}
- window.localStorage.setItem(
- LatestChangesDialogService.VERSION_KEY,
- this.UPDATE_PREFIX + currentVersion,
- );
-
- this.snackBar
- .open(
- $localize`A new version of the app is available!`,
- $localize`:Action that a user can update the app with:Update`,
- )
- .onAction()
- .subscribe(() => {
- window.localStorage.setItem(
- LatestChangesDialogService.VERSION_KEY,
- currentVersion,
- );
+ if (this.unsavedChanges.pending) {
+ // app cannot be safely reloaded
+ localStorage.setItem(
+ LatestChangesDialogService.VERSION_KEY,
+ this.UPDATE_PREFIX + currentVersion,
+ );
+ this.snackBar
+ .open(
+ $localize`A new version of the app is available!`,
+ $localize`:Action that a user can update the app with:Update`,
+ )
+ .onAction()
+ .subscribe(() => {
+ localStorage.setItem(
+ LatestChangesDialogService.VERSION_KEY,
+ currentVersion,
+ );
- this.location.reload();
- });
+ this.location.reload();
+ });
+ } else {
+ this.location.reload();
+ }
}
/**
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 a5b2f88344..2904ed908f 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,9 @@ 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/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
+import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component";
/**
* A general component that can be included on a entity details page through the config.
@@ -14,18 +14,21 @@ import { EntitySubrecordComponent } from "../../../core/common-components/entity
@DynamicComponent("HistoricalDataComponent")
@Component({
selector: "app-historical-data",
- template: ` `,
- imports: [EntitySubrecordComponent],
+ >`,
+ imports: [EntitiesTableComponent],
standalone: true,
})
export class HistoricalDataComponent implements OnInit {
@Input() entity: Entity;
@Input() config: FormFieldConfig[] = [];
- entries: HistoricalEntityData[] = [];
+ entries: HistoricalEntityData[];
+
+ entityConstructor = HistoricalEntityData;
constructor(private historicalDataService: HistoricalDataService) {}
diff --git a/src/app/features/historical-data/historical-data/historical-data.stories.ts b/src/app/features/historical-data/historical-data/historical-data.stories.ts
index cfc02bccfc..1ae6520166 100644
--- a/src/app/features/historical-data/historical-data/historical-data.stories.ts
+++ b/src/app/features/historical-data/historical-data/historical-data.stories.ts
@@ -5,7 +5,7 @@ import { HistoricalDataService } from "../historical-data.service";
import { ratingAnswers } from "../model/rating-answers";
import { StorybookBaseModule } from "../../../utils/storybook-base.module";
import { importProvidersFrom } from "@angular/core";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
export default {
title: "Features/HistoricalDataComponent",
diff --git a/src/app/features/matching-entities/matching-entities/matching-entities-config.ts b/src/app/features/matching-entities/matching-entities/matching-entities-config.ts
index b3569f83ee..df34be677b 100644
--- a/src/app/features/matching-entities/matching-entities/matching-entities-config.ts
+++ b/src/app/features/matching-entities/matching-entities/matching-entities-config.ts
@@ -1,9 +1,7 @@
import { FilterConfig } from "../../../core/entity-list/EntityListConfig";
-import {
- ColumnConfig,
- DataFilter,
-} from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { Entity, EntityConstructor } from "../../../core/entity/model/entity";
+import { ColumnConfig } from "../../../core/common-components/entity-form/FormConfig";
+import { DataFilter } from "../../../core/filter/filters/filters";
/**
* Config to be defined to set up a MatchingEntitiesComponent.
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 bb5c78b04d..b8a2cad3cc 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
@@ -100,14 +100,15 @@
(filterObjChange)="applySelectedFilters(side, $event)"
>
-
+ >
diff --git a/src/app/features/matching-entities/matching-entities/matching-entities.component.spec.ts b/src/app/features/matching-entities/matching-entities/matching-entities.component.spec.ts
index 1ade62f967..3be050af4e 100644
--- a/src/app/features/matching-entities/matching-entities/matching-entities.component.spec.ts
+++ b/src/app/features/matching-entities/matching-entities/matching-entities.component.spec.ts
@@ -17,7 +17,7 @@ import { ActivatedRoute } from "@angular/router";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { ConfigService } from "../../../core/config/config.service";
import { BehaviorSubject, NEVER, Subject } from "rxjs";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
import { Coordinates } from "../../location/coordinates";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { School } from "../../../child-dev-project/schools/model/school";
diff --git a/src/app/features/matching-entities/matching-entities/matching-entities.component.ts b/src/app/features/matching-entities/matching-entities/matching-entities.component.ts
index 247c614730..81d030b54d 100644
--- a/src/app/features/matching-entities/matching-entities/matching-entities.component.ts
+++ b/src/app/features/matching-entities/matching-entities/matching-entities.component.ts
@@ -15,10 +15,6 @@ import {
MatchingSideConfig,
NewMatchAction,
} from "./matching-entities-config";
-import {
- ColumnConfig,
- DataFilter,
-} from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface";
import { ActivatedRoute } from "@angular/router";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
@@ -31,7 +27,6 @@ import { MatTooltipModule } from "@angular/material/tooltip";
import { NgForOf, NgIf } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { EntityFieldViewComponent } from "../../../core/common-components/entity-field-view/entity-field-view.component";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
import { MapComponent } from "../../location/map/map.component";
import { FilterComponent } from "../../../core/filter/filter/filter.component";
import { Coordinates } from "../../location/coordinates";
@@ -40,8 +35,13 @@ import { LocationProperties } from "../../location/map/map-properties-popup/map-
import { getLocationProperties } from "../../location/map-utils";
import { FlattenArrayPipe } from "../../../utils/flatten-array/flatten-array.pipe";
import { isArrayDataType } from "../../../core/basic-datatypes/datatype-utils";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+import {
+ ColumnConfig,
+ FormFieldConfig,
+} from "../../../core/common-components/entity-form/FormConfig";
import { RouteTarget } from "../../../route-target";
+import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component";
+import { DataFilter } from "../../../core/filter/filters/filters";
export interface MatchingSide extends MatchingSideConfig {
/** pass along filters from app-filter to subrecord component */
@@ -78,7 +78,7 @@ export interface MatchingSide extends MatchingSideConfig {
NgIf,
MatButtonModule,
NgForOf,
- EntitySubrecordComponent,
+ EntitiesTableComponent,
EntityFieldViewComponent,
MapComponent,
FilterComponent,
diff --git a/src/app/features/todos/todo-details/todo-details.component.ts b/src/app/features/todos/todo-details/todo-details.component.ts
index c5d930712a..3886b6bde5 100644
--- a/src/app/features/todos/todo-details/todo-details.component.ts
+++ b/src/app/features/todos/todo-details/todo-details.component.ts
@@ -12,7 +12,7 @@ import {
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
-import { DetailsComponentData } from "../../../core/common-components/entity-subrecord/row-details/row-details.component";
+import { DetailsComponentData } from "../../../core/form-dialog/row-details/row-details.component";
import { TodoService } from "../todo.service";
import {
EntityForm,
diff --git a/src/app/features/todos/todo-details/todo-details.stories.ts b/src/app/features/todos/todo-details/todo-details.stories.ts
index be14167891..036b1b3822 100644
--- a/src/app/features/todos/todo-details/todo-details.stories.ts
+++ b/src/app/features/todos/todo-details/todo-details.stories.ts
@@ -2,7 +2,7 @@ import { applicationConfig, Meta, StoryFn } from "@storybook/angular";
import { StorybookBaseModule } from "../../../utils/storybook-base.module";
import { TodoDetailsComponent } from "./todo-details.component";
import { Todo } from "../model/todo";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
import { importProvidersFrom } from "@angular/core";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { NEVER } from "rxjs";
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 09bcdaef42..81b7b2c3db 100644
--- a/src/app/features/todos/todo-list/todo-list.component.ts
+++ b/src/app/features/todos/todo-list/todo-list.component.ts
@@ -1,72 +1,126 @@
import { Component, OnInit } from "@angular/core";
-import { ActivatedRoute } from "@angular/router";
import { Todo } from "../model/todo";
-import {
- EntityListConfig,
- PrebuiltFilterConfig,
-} from "../../../core/entity-list/EntityListConfig";
-import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface";
-import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
+import { PrebuiltFilterConfig } from "../../../core/entity-list/EntityListConfig";
import { TodoDetailsComponent } from "../todo-details/todo-details.component";
-import { LoggingService } from "../../../core/logging/logging.service";
import moment from "moment";
import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component";
-import { FilterSelectionOption } from "../../../core/filter/filters/filters";
+import {
+ DataFilter,
+ FilterSelectionOption,
+} from "../../../core/filter/filters/filters";
import { RouteTarget } from "../../../route-target";
-import { CurrentUserSubject } from "../../../core/session/current-user-subject";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { Sort } from "@angular/material/sort";
+import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service";
+import { ActivatedRoute, Router, RouterLink } from "@angular/router";
+import { AnalyticsService } from "../../../core/analytics/analytics.service";
+import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
+import { EntityRegistry } from "../../../core/entity/database-entity.decorator";
+import { MatDialog } from "@angular/material/dialog";
+import { DuplicateRecordService } from "../../../core/entity-list/duplicate-records/duplicate-records.service";
+import { CurrentUserSubject } from "../../../core/session/current-user-subject";
+import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
+import { LoggingService } from "../../../core/logging/logging.service";
+import { NgForOf, NgIf, NgStyle, NgTemplateOutlet } from "@angular/common";
+import { MatButtonModule } from "@angular/material/button";
+import { Angulartics2OnModule } from "angulartics2";
+import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
+import { MatMenuModule } from "@angular/material/menu";
+import { MatTabsModule } from "@angular/material/tabs";
+import { MatFormFieldModule } from "@angular/material/form-field";
+import { MatInputModule } from "@angular/material/input";
+import { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component";
+import { FormsModule } from "@angular/forms";
+import { FilterComponent } from "../../../core/filter/filter/filter.component";
+import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
+import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component";
+import { ExportDataDirective } from "../../../core/export/export-data-directive/export-data.directive";
+import { DisableEntityOperationDirective } from "../../../core/permissions/permission-directive/disable-entity-operation.directive";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { EntityCreateButtonComponent } from "../../../core/common-components/entity-create-button/entity-create-button.component";
@UntilDestroy()
@RouteTarget("TodoList")
@Component({
selector: "app-todo-list",
- template: `
-
- `,
+ templateUrl:
+ "../../../core/entity-list/entity-list/entity-list.component.html",
standalone: true,
- imports: [EntityListComponent],
+
+ imports: [
+ NgIf,
+ NgStyle,
+ MatButtonModule,
+ Angulartics2OnModule,
+ FontAwesomeModule,
+ MatMenuModule,
+ NgTemplateOutlet,
+ MatTabsModule,
+ NgForOf,
+ MatFormFieldModule,
+ MatInputModule,
+ EntitiesTableComponent,
+ FormsModule,
+ FilterComponent,
+ TabStateModule,
+ ViewTitleComponent,
+ ExportDataDirective,
+ DisableEntityOperationDirective,
+ RouterLink,
+ MatTooltipModule,
+ EntityCreateButtonComponent,
+ ],
})
-export class TodoListComponent implements OnInit {
+export class TodoListComponent
+ extends EntityListComponent
+ implements OnInit
+{
// TODO: make this component obsolete by generalizing Entity and EntityList so that we can define a viewDetailsComponent on the entity that gets opened as popup?
- listConfig: EntityListConfig;
entityConstructor = Todo;
+ override clickMode: "navigate" | "popup" | "none" = "none";
+
+ override defaultSort: Sort = {
+ active: "deadline",
+ direction: "asc",
+ };
+
+ override showInactive = true;
+
constructor(
- private route: ActivatedRoute,
+ screenWidthObserver: ScreenWidthObserver,
+ router: Router,
+ activatedRoute: ActivatedRoute,
+ analyticsService: AnalyticsService,
+ entityMapperService: EntityMapperService,
+ entities: EntityRegistry,
+ dialog: MatDialog,
+ duplicateRecord: DuplicateRecordService,
private currentUser: CurrentUserSubject,
private formDialog: FormDialogService,
private logger: LoggingService,
- ) {}
-
- ngOnInit() {
- this.route.data.subscribe(
- (data: DynamicComponentConfig) =>
- // TODO replace this use of route and rely on the RoutedViewComponent instead
- this.init(data.config),
+ ) {
+ super(
+ screenWidthObserver,
+ router,
+ activatedRoute,
+ analyticsService,
+ entityMapperService,
+ entities,
+ dialog,
+ duplicateRecord,
);
}
- private init(config: EntityListConfig) {
- this.listConfig = config;
- this.listConfig.defaultSort = this.listConfig.defaultSort ?? {
- active: "deadline",
- direction: "asc",
- };
+ ngOnInit() {
this.addPrebuiltFilters();
}
private addPrebuiltFilters() {
this.setFilterDefaultToCurrentUser();
- for (const prebuiltFilter of this.listConfig.filters.filter(
+ for (const prebuiltFilter of this.filters.filter(
(filter) => filter.type === "prebuilt",
)) {
switch (prebuiltFilter.id) {
@@ -88,9 +142,7 @@ export class TodoListComponent implements OnInit {
}
private setFilterDefaultToCurrentUser() {
- const assignedToFilter = this.listConfig.filters.find(
- (c) => c.id === "assignedTo",
- );
+ const assignedToFilter = this.filters.find((c) => c.id === "assignedTo");
if (assignedToFilter && !assignedToFilter.default) {
// filter based on currently logged-in user
this.currentUser
@@ -123,10 +175,14 @@ export class TodoListComponent implements OnInit {
filter.default = filter.default ?? "current";
}
- createNew() {
+ override addNew() {
this.showDetails(new Todo());
}
+ override onRowClick(entity: Todo) {
+ this.showDetails(entity);
+ }
+
showDetails(entity: Todo) {
this.formDialog.openFormPopup(entity, undefined, TodoDetailsComponent);
}
@@ -160,5 +216,5 @@ const filterCurrentlyActive: FilterSelectionOption = {
],
},
],
- },
+ } as DataFilter,
};
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 7f11ce30ab..e504e1a8c0 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 b8f69d9846..db781e9dd9 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,15 +1,15 @@
import { Component, Input, OnInit } from "@angular/core";
-import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig";
+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";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { TodoDetailsComponent } from "../todo-details/todo-details.component";
-import { DataFilter } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config";
-import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component";
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";
@DynamicComponent("TodosRelatedToEntity")
@Component({
@@ -17,11 +17,11 @@ import { FormsModule } from "@angular/forms";
templateUrl: "./todos-related-to-entity.component.html",
styleUrls: ["./todos-related-to-entity.component.scss"],
standalone: true,
- imports: [EntitySubrecordComponent, MatSlideToggleModule, FormsModule],
+ imports: [EntitiesTableComponent, MatSlideToggleModule, FormsModule],
})
export class TodosRelatedToEntityComponent implements OnInit {
- entries: Todo[] = [];
- isLoading: boolean;
+ entries: Todo[];
+ entityCtr = Todo;
@Input() entity: Entity;
@Input() columns: FormFieldConfig[] = [
@@ -42,7 +42,6 @@ export class TodosRelatedToEntityComponent implements OnInit {
// TODO: filter by current user as default in UX? --> custom filter component or some kind of variable interpolation?
filter: DataFilter = { isActive: true };
- includeInactive: boolean;
backgroundColorFn = (r: Todo) => {
if (!r.isActive) {
return "#e0e0e0";
@@ -69,9 +68,7 @@ export class TodosRelatedToEntityComponent implements OnInit {
}
private async loadDataFor(entityId: string): Promise {
- this.isLoading = true;
-
- const data = await this.dbIndexingService.queryIndexDocs(
+ return this.dbIndexingService.queryIndexDocs(
Todo,
"todo_index/by_" + this.referenceProperty,
{
@@ -80,9 +77,6 @@ export class TodosRelatedToEntityComponent implements OnInit {
descending: true,
},
);
-
- this.isLoading = false;
- return data;
}
public getNewEntryFunction(): () => Todo {
diff --git a/src/app/utils/core-testing.module.ts b/src/app/utils/core-testing.module.ts
index 084f3b4d73..961be17f42 100644
--- a/src/app/utils/core-testing.module.ts
+++ b/src/app/utils/core-testing.module.ts
@@ -10,6 +10,8 @@ import { ConfigurableEnumService } from "../core/basic-datatypes/configurable-en
import { ComponentRegistry } from "../dynamic-components";
import { EntityActionsService } from "../core/entity/entity-actions/entity-actions.service";
import { ConfigurableEnumModule } from "../core/basic-datatypes/configurable-enum/configurable-enum.module";
+import { EntityAbility } from "../core/permissions/ability/entity-ability";
+import { EntitySchemaService } from "../core/entity/schema/entity-schema.service";
/**
* A basic module that can be imported in unit tests to provide default datatypes.
@@ -28,6 +30,8 @@ import { ConfigurableEnumModule } from "../core/basic-datatypes/configurable-enu
provide: EntityActionsService,
useValue: jasmine.createSpyObj(["anonymize"]),
},
+ EntitySchemaService,
+ EntityAbility,
ComponentRegistry,
],
})
diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts
index 6a41a2a1e0..5fda46858a 100644
--- a/src/app/utils/utils.ts
+++ b/src/app/utils/utils.ts
@@ -5,7 +5,7 @@
import { Router } from "@angular/router";
import { ConfigurableEnumValue } from "../core/basic-datatypes/configurable-enum/configurable-enum.interface";
import { FactoryProvider, Injector } from "@angular/core";
-import { isConfigurableEnum } from "../core/common-components/entity-subrecord/entity-subrecord/value-accessor";
+import { isConfigurableEnum } from "../core/common-components/entities-table/value-accessor/value-accessor";
export function isValidDate(date: any): boolean {
return (
diff --git a/src/assets/locale/messages.de.xlf b/src/assets/locale/messages.de.xlf
index 21bc49c125..2f16261a3f 100644
--- a/src/assets/locale/messages.de.xlf
+++ b/src/assets/locale/messages.de.xlf
@@ -10,7 +10,7 @@
Show unrelated tooltip
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 41
+ 42
@@ -21,7 +21,7 @@
slider
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 42
+ 43
@@ -31,7 +31,7 @@
load-all button
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 52
+ 53
@@ -40,7 +40,7 @@
The month something took place
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 47
+ 48
@@ -50,7 +50,7 @@
How many children are present at a meeting
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 52
+ 53
@@ -345,7 +345,7 @@
Event
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 42
+ 43
@@ -1154,11 +1154,11 @@
src/app/core/config/config-fix.ts
- 312
+ 304
src/app/core/config/config-fix.ts
- 747
+ 739
@@ -1215,23 +1215,10 @@
166
-
-
- Dringend
- Filter-option for notes
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 65
-
-
Nachverfolgung nötig
- Filter-option for notes
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 70
-
+ Label warning level
src/app/child-dev-project/warning-level.ts
35
@@ -1243,7 +1230,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 81
+ 66
@@ -1252,7 +1239,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 86
+ 71
@@ -2294,7 +2281,7 @@
src/app/core/config/config-fix.ts
- 575
+ 567
@@ -2439,11 +2426,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 178
+ 172
src/app/core/config/config-fix.ts
- 182
+ 176
@@ -2452,19 +2439,19 @@
Translated name of mobile column group
src/app/core/config/config-fix.ts
- 179
+ 173
src/app/core/config/config-fix.ts
- 192
+ 186
src/app/core/config/config-fix.ts
- 422
+ 414
src/app/core/config/config-fix.ts
- 476
+ 468
@@ -2507,7 +2494,7 @@
Panel title
src/app/core/config/config-fix.ts
- 268
+ 260
@@ -2516,7 +2503,7 @@
Panel title
src/app/core/config/config-fix.ts
- 283
+ 275
@@ -2525,7 +2512,7 @@
Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
src/app/core/config/config-fix.ts
- 297
+ 289
@@ -2534,15 +2521,15 @@
Panel title
src/app/core/config/config-fix.ts
- 330
+ 322
src/app/core/config/config-fix.ts
- 510
+ 502
src/app/core/config/config-fix.ts
- 703
+ 695
@@ -2551,7 +2538,7 @@
Panel title
src/app/core/config/config-fix.ts
- 347
+ 339
@@ -2560,7 +2547,7 @@
Panel title
src/app/core/config/config-fix.ts
- 356
+ 348
@@ -2569,7 +2556,7 @@
Column label for age of child
src/app/core/config/config-fix.ts
- 379
+ 371
@@ -2578,7 +2565,7 @@
Column label for school attendance of child
src/app/core/config/config-fix.ts
- 397
+ 389
@@ -2587,7 +2574,7 @@
Column label for coaching attendance of child
src/app/core/config/config-fix.ts
- 406
+ 398
@@ -2596,11 +2583,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 421
+ 413
src/app/core/config/config-fix.ts
- 425
+ 417
@@ -2609,7 +2596,7 @@
Column group name
src/app/core/config/config-fix.ts
- 438
+ 430
@@ -2618,11 +2605,11 @@
Column group name
src/app/core/config/config-fix.ts
- 461
+ 453
src/app/core/config/config-fix.ts
- 597
+ 589
@@ -2631,7 +2618,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 520
+ 512
@@ -2640,7 +2627,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 524
+ 516
@@ -2649,7 +2636,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 528
+ 520
@@ -2658,7 +2645,7 @@
Panel title
src/app/core/config/config-fix.ts
- 536
+ 528
@@ -2667,7 +2654,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 539
+ 531
@@ -2676,7 +2663,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 559
+ 551
@@ -2685,7 +2672,7 @@
Child details section title
src/app/core/config/config-fix.ts
- 563
+ 555
@@ -2694,7 +2681,7 @@
Panel title
src/app/core/config/config-fix.ts
- 584
+ 576
@@ -2703,7 +2690,7 @@
description section
src/app/core/config/config-fix.ts
- 607
+ 599
@@ -2712,7 +2699,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 615
+ 607
@@ -2721,7 +2708,7 @@
Panel title
src/app/core/config/config-fix.ts
- 621
+ 613
@@ -2730,7 +2717,7 @@
Panel title
src/app/core/config/config-fix.ts
- 646
+ 638
@@ -2748,7 +2735,7 @@
Panel title
src/app/core/config/config-fix.ts
- 731
+ 723
@@ -2957,7 +2944,7 @@
src/app/core/config/config-fix.ts
- 663
+ 655
@@ -2970,7 +2957,7 @@
src/app/core/config/config-fix.ts
- 797
+ 789
@@ -3213,11 +3200,11 @@
src/app/core/config/config-fix.ts
- 374
+ 366
src/app/core/config/config-fix.ts
- 781
+ 773
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -3238,27 +3225,7 @@
Alle
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 75
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 89
-
-
- src/app/core/filter/filters/filters.ts
- 125
-
-
- src/app/core/filter/filters/filters.ts
- 192
-
-
- src/app/core/filter/filters/filters.ts
- 224
-
-
- src/app/core/filter/filters/filters.ts
- 246
+ 74
@@ -3345,7 +3312,7 @@
src/app/core/config/config-fix.ts
- 805
+ 797
@@ -3870,7 +3837,7 @@
src/app/core/config/config-fix.ts
- 385
+ 377
@@ -4020,11 +3987,11 @@
src/app/core/config/config-fix.ts
- 390
+ 382
src/app/core/config/config-fix.ts
- 492
+ 484
@@ -4152,7 +4119,7 @@
src/app/core/config/config-fix.ts
- 718
+ 710
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -4258,11 +4225,7 @@
src/app/core/config/config-fix.ts
- 204
-
-
- src/app/core/config/config-fix.ts
- 451
+ 443
@@ -4311,7 +4274,7 @@
src/app/core/config/config-fix.ts
- 746
+ 738
@@ -4320,11 +4283,11 @@
Table header, Short for Body Mass Index
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 30
+ 32
src/app/core/config/config-fix.ts
- 415
+ 407
@@ -4333,7 +4296,7 @@
Tooltip for BMI info
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 32
+ 34
@@ -4342,7 +4305,7 @@
Events of an attendance
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 61
+ 62
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -4355,11 +4318,11 @@
Percentage of people that attended an event
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 67
+ 68
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 45
+ 46
@@ -4385,20 +4348,20 @@
Hausbesuch
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 10
+ 6
- Interaction type/Category of a Note
Gespräch mit den Vormündern
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 14
+ 10
- Interaction type/Category of a Note
@@ -4406,17 +4369,17 @@
Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 18
+ 14
Allgemeine Notiz
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 22
+ 18
- Interaction type/Category of a Note
@@ -4424,7 +4387,7 @@
Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 26
+ 22
@@ -4566,11 +4529,11 @@
Coaching-Klasse
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 32
+ 28
- Interaction type/Category of a Note
@@ -4578,11 +4541,11 @@
Label for the address of a child
src/app/core/config/config-fix.ts
- 752
+ 744
src/app/core/config/config-fix.ts
- 793
+ 785
@@ -4591,7 +4554,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 756
+ 748
@@ -4600,7 +4563,7 @@
Label for the religion of a child
src/app/core/config/config-fix.ts
- 760
+ 752
@@ -4609,7 +4572,7 @@
Label for the mother tongue of a child
src/app/core/config/config-fix.ts
- 764
+ 756
@@ -4618,7 +4581,7 @@
Tooltip description for the mother tongue of a child
src/app/core/config/config-fix.ts
- 765
+ 757
@@ -4627,7 +4590,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 769
+ 761
@@ -4636,7 +4599,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 773
+ 765
@@ -4644,11 +4607,11 @@
Privatschule
src/app/core/config/config-fix.ts
- 319
+ 311
src/app/core/config/config-fix.ts
- 785
+ 777
@@ -4657,7 +4620,7 @@
Label for the language of a school
src/app/core/config/config-fix.ts
- 789
+ 781
@@ -4666,7 +4629,7 @@
Label for the timing of a school
src/app/core/config/config-fix.ts
- 801
+ 793
@@ -4675,7 +4638,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 814
+ 806
@@ -4684,7 +4647,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 815
+ 807
@@ -4693,7 +4656,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 820
+ 812
@@ -4702,7 +4665,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 821
+ 813
@@ -4711,7 +4674,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 826
+ 818
@@ -4720,7 +4683,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 827
+ 819
@@ -4729,7 +4692,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 832
+ 824
@@ -4738,7 +4701,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 833
+ 825
@@ -4747,7 +4710,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 838
+ 830
@@ -4756,7 +4719,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 839
+ 831
@@ -4765,17 +4728,17 @@
Label of user phone
src/app/core/config/config-fix.ts
- 847
+ 839
Schulklasse
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 38
+ 34
- Interaction type/Category of a Note
@@ -5075,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
- 220
+ 216
@@ -5083,7 +5046,7 @@
Speichern von fehlgeschlagen:
src/app/core/common-components/entity-form/entity-form.service.ts
- 231
+ 227
@@ -5381,7 +5344,11 @@
Examples of things to filter
src/app/core/entity-list/entity-list/entity-list.component.html
- 106
+ 88
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 88
@@ -5390,7 +5357,11 @@
Add a new entity to a list of multiple entities
src/app/core/entity-list/entity-list/entity-list.component.html
- 158
+ 141
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 141
@@ -5399,26 +5370,38 @@
Show filter options popup for list
src/app/core/entity-list/entity-list/entity-list.component.html
- 170
+ 153
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 153
Download alle (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 189
+ 172
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 172
- Download list contents as CSV
Download angezeigte (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 207
+ 190
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 190
- Download list contents as CSV
@@ -5427,7 +5410,11 @@
Filter placeholder
src/app/core/entity-list/entity-list/entity-list.component.html
- 100
+ 82
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 82
@@ -5435,7 +5422,11 @@
Datei importieren
src/app/core/entity-list/entity-list/entity-list.component.html
- 223
+ 206
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 206
@@ -5443,7 +5434,11 @@
Wählen Sie mehrere Datensätze aus, um gemeinsame Aktionen für alle auszuführen (z.B. Duplizieren oder Löschen)
src/app/core/entity-list/entity-list/entity-list.component.html
- 229
+ 212
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 212
@@ -5451,7 +5446,11 @@
Massen-Bearbeitung
src/app/core/entity-list/entity-list/entity-list.component.html
- 237
+ 220
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 220
@@ -5459,24 +5458,36 @@
Wählen Sie Zeilen aus, um diese zu kopieren
src/app/core/entity-list/entity-list/entity-list.component.html
- 252
+ 235
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 235
Duplizieren
+ bulk action button
src/app/core/entity-list/entity-list/entity-list.component.html
- 256,258
+ 239
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 239
- bulk action button
Abbrechen
src/app/core/entity-list/entity-list/entity-list.component.html
- 260,262
+ 243
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 243
@@ -5591,13 +5602,13 @@
[icon]="includeEventNotes ? 'toggle-on' : 'toggle-off'"
class="standard-icon-with-text color-accent"
>"/> Events einbeziehen
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
- 23,28
-
events are related to a
child
Slider that allows a user to also include events
+
+ src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
+ 22
+
@@ -5637,8 +5648,8 @@
96
- src/app/core/filter/filters/filters.ts
- 198
+ src/app/core/filter/filters/booleanFilter.ts
+ 13
@@ -5654,8 +5665,8 @@
104
- src/app/core/filter/filters/filters.ts
- 204
+ src/app/core/filter/filters/booleanFilter.ts
+ 19
@@ -5761,8 +5772,24 @@
also show entries that are archived
slider
- src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
- 156
+ src/app/core/common-components/entities-table/entities-table.component.html
+ 106
+
+
+
+
+ Erstelle einen neuen Eintrag
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 15
+
+
+
+
+ Neu
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 24
@@ -5895,7 +5922,7 @@
Eine neuere Version der App ist verfügbar!
src/app/core/ui/latest-changes/update-manager.service.ts
- 111
+ 114
@@ -5904,7 +5931,7 @@
Action that a user can update the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 112
+ 115
@@ -5912,7 +5939,7 @@
Die Anwendung ist in einem nicht wiederherstellbaren Zustand, bitte neu laden.
src/app/core/ui/latest-changes/update-manager.service.ts
- 137
+ 143
@@ -5921,7 +5948,7 @@
Action that a user can reload the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 138
+ 144
@@ -6283,16 +6310,6 @@
form field validation error
-
-
- Hinzufügen
-
- src/app/core/entity-list/entity-list/entity-list.component.html
- 29
-
-
Email
@@ -7260,7 +7277,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 137
+ 193
@@ -7269,7 +7286,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 107
+ 159
@@ -7278,7 +7295,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 112
+ 164
@@ -7287,7 +7304,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 117
+ 169
@@ -7295,7 +7312,7 @@
Alle
src/app/features/todos/todo-list/todo-list.component.ts
- 120
+ 172
@@ -7303,7 +7320,7 @@
fällige Aufgaben
src/app/features/todos/todo-list/todo-list.component.ts
- 122
+ 174
diff --git a/src/assets/locale/messages.fr.xlf b/src/assets/locale/messages.fr.xlf
index 28849202b0..a132941a27 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
- 805
+ 797
@@ -191,7 +191,7 @@
Show unrelated tooltip
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 41
+ 42
@@ -202,7 +202,7 @@
slider
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 42
+ 43
@@ -212,7 +212,7 @@
load-all button
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 52
+ 53
@@ -221,7 +221,7 @@
The month something took place
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 47
+ 48
@@ -231,7 +231,7 @@
How many children are present at a meeting
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 52
+ 53
@@ -240,7 +240,7 @@
Events of an attendance
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 61
+ 62
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -253,11 +253,11 @@
Percentage of people that attended an event
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 67
+ 68
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 45
+ 46
@@ -638,7 +638,7 @@
Evènement
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 42
+ 43
@@ -770,11 +770,11 @@
src/app/core/config/config-fix.ts
- 390
+ 382
src/app/core/config/config-fix.ts
- 492
+ 484
@@ -902,7 +902,7 @@
src/app/core/config/config-fix.ts
- 718
+ 710
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -981,27 +981,7 @@
Tous
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 75
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 89
-
-
- src/app/core/filter/filters/filters.ts
- 125
-
-
- src/app/core/filter/filters/filters.ts
- 192
-
-
- src/app/core/filter/filters/filters.ts
- 224
-
-
- src/app/core/filter/filters/filters.ts
- 246
+ 74
@@ -1092,7 +1072,7 @@
src/app/core/config/config-fix.ts
- 663
+ 655
@@ -1211,11 +1191,11 @@
src/app/core/config/config-fix.ts
- 374
+ 366
src/app/core/config/config-fix.ts
- 781
+ 773
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -1299,11 +1279,7 @@
src/app/core/config/config-fix.ts
- 204
-
-
- src/app/core/config/config-fix.ts
- 451
+ 443
@@ -1352,7 +1328,7 @@
src/app/core/config/config-fix.ts
- 797
+ 789
@@ -1365,7 +1341,7 @@
src/app/core/config/config-fix.ts
- 746
+ 738
@@ -1378,7 +1354,7 @@
src/app/core/config/config-fix.ts
- 385
+ 377
@@ -1664,11 +1640,11 @@
Table header, Short for Body Mass Index
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 30
+ 32
src/app/core/config/config-fix.ts
- 415
+ 407
@@ -1677,7 +1653,7 @@
Tooltip for BMI info
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 32
+ 34
@@ -2215,11 +2191,11 @@
src/app/core/config/config-fix.ts
- 312
+ 304
src/app/core/config/config-fix.ts
- 747
+ 739
@@ -2304,23 +2280,10 @@
84
-
-
- Urgent
- Filter-option for notes
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 65
-
-
Besoin de suivi
- Filter-option for notes
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 70
-
+ Label warning level
src/app/child-dev-project/warning-level.ts
35
@@ -2332,7 +2295,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 81
+ 66
@@ -2341,7 +2304,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 86
+ 71
@@ -3698,11 +3661,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 178
+ 172
src/app/core/config/config-fix.ts
- 182
+ 176
@@ -3711,19 +3674,19 @@
Translated name of mobile column group
src/app/core/config/config-fix.ts
- 179
+ 173
src/app/core/config/config-fix.ts
- 192
+ 186
src/app/core/config/config-fix.ts
- 422
+ 414
src/app/core/config/config-fix.ts
- 476
+ 468
@@ -3766,7 +3729,7 @@
Panel title
src/app/core/config/config-fix.ts
- 268
+ 260
@@ -3775,7 +3738,7 @@
Panel title
src/app/core/config/config-fix.ts
- 283
+ 275
@@ -3784,7 +3747,7 @@
Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
src/app/core/config/config-fix.ts
- 297
+ 289
@@ -3792,11 +3755,11 @@
École privée
src/app/core/config/config-fix.ts
- 319
+ 311
src/app/core/config/config-fix.ts
- 785
+ 777
@@ -3805,15 +3768,15 @@
Panel title
src/app/core/config/config-fix.ts
- 330
+ 322
src/app/core/config/config-fix.ts
- 510
+ 502
src/app/core/config/config-fix.ts
- 703
+ 695
@@ -3822,7 +3785,7 @@
Panel title
src/app/core/config/config-fix.ts
- 347
+ 339
@@ -3831,7 +3794,7 @@
Panel title
src/app/core/config/config-fix.ts
- 356
+ 348
@@ -3840,7 +3803,7 @@
Column label for age of child
src/app/core/config/config-fix.ts
- 379
+ 371
@@ -3849,7 +3812,7 @@
Column label for school attendance of child
src/app/core/config/config-fix.ts
- 397
+ 389
@@ -3858,7 +3821,7 @@
Column label for coaching attendance of child
src/app/core/config/config-fix.ts
- 406
+ 398
@@ -3867,7 +3830,7 @@
Column group name
src/app/core/config/config-fix.ts
- 438
+ 430
@@ -3876,11 +3839,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 421
+ 413
src/app/core/config/config-fix.ts
- 425
+ 417
@@ -3889,11 +3852,11 @@
Column group name
src/app/core/config/config-fix.ts
- 461
+ 453
src/app/core/config/config-fix.ts
- 597
+ 589
@@ -3902,7 +3865,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 520
+ 512
@@ -3911,7 +3874,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 524
+ 516
@@ -3920,7 +3883,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 528
+ 520
@@ -3929,7 +3892,7 @@
Panel title
src/app/core/config/config-fix.ts
- 536
+ 528
@@ -3938,7 +3901,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 539
+ 531
@@ -3947,7 +3910,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 559
+ 551
@@ -3956,7 +3919,7 @@
Child details section title
src/app/core/config/config-fix.ts
- 563
+ 555
@@ -3965,7 +3928,7 @@
Panel title
src/app/core/config/config-fix.ts
- 584
+ 576
@@ -3974,7 +3937,7 @@
description section
src/app/core/config/config-fix.ts
- 607
+ 599
@@ -3987,7 +3950,7 @@
src/app/core/config/config-fix.ts
- 575
+ 567
@@ -4009,7 +3972,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 615
+ 607
@@ -4018,7 +3981,7 @@
Panel title
src/app/core/config/config-fix.ts
- 621
+ 613
@@ -4027,7 +3990,7 @@
Panel title
src/app/core/config/config-fix.ts
- 646
+ 638
@@ -4036,7 +3999,7 @@
Panel title
src/app/core/config/config-fix.ts
- 731
+ 723
@@ -4180,11 +4143,11 @@
Label for the address of a child
src/app/core/config/config-fix.ts
- 752
+ 744
src/app/core/config/config-fix.ts
- 793
+ 785
@@ -4193,7 +4156,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 756
+ 748
@@ -4202,7 +4165,7 @@
Label for the religion of a child
src/app/core/config/config-fix.ts
- 760
+ 752
@@ -4211,7 +4174,7 @@
Label for the mother tongue of a child
src/app/core/config/config-fix.ts
- 764
+ 756
@@ -4220,7 +4183,7 @@
Tooltip description for the mother tongue of a child
src/app/core/config/config-fix.ts
- 765
+ 757
@@ -4229,7 +4192,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 769
+ 761
@@ -4238,7 +4201,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 773
+ 765
@@ -4247,7 +4210,7 @@
Label for the language of a school
src/app/core/config/config-fix.ts
- 789
+ 781
@@ -4256,7 +4219,7 @@
Label for the timing of a school
src/app/core/config/config-fix.ts
- 801
+ 793
@@ -4265,7 +4228,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 814
+ 806
@@ -4274,7 +4237,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 815
+ 807
@@ -4283,7 +4246,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 820
+ 812
@@ -4292,7 +4255,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 821
+ 813
@@ -4301,7 +4264,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 826
+ 818
@@ -4310,7 +4273,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 827
+ 819
@@ -4319,7 +4282,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 832
+ 824
@@ -4328,7 +4291,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 833
+ 825
@@ -4337,7 +4300,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 838
+ 830
@@ -4346,7 +4309,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 839
+ 831
@@ -4355,7 +4318,7 @@
Label of user phone
src/app/core/config/config-fix.ts
- 847
+ 839
@@ -4401,20 +4364,20 @@
Visite à domicile
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 10
+ 6
- Interaction type/Category of a Note
Entretien avec les tuteurs
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 14
+ 10
- Interaction type/Category of a Note
@@ -4422,17 +4385,17 @@
Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 18
+ 14
General Note
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 22
+ 18
- Interaction type/Category of a Note
@@ -4440,26 +4403,26 @@
Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 26
+ 22
Session de formation
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 32
+ 28
- Interaction type/Category of a Note
Classe
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 38
+ 34
- Interaction type/Category of a Note
@@ -4550,8 +4513,8 @@
96
- src/app/core/filter/filters/filters.ts
- 198
+ src/app/core/filter/filters/booleanFilter.ts
+ 13
@@ -4567,8 +4530,8 @@
104
- src/app/core/filter/filters/filters.ts
- 204
+ src/app/core/filter/filters/booleanFilter.ts
+ 19
@@ -4801,7 +4764,7 @@
Current user is not permitted to save these changes
src/app/core/common-components/entity-form/entity-form.service.ts
- 220
+ 216
@@ -4809,7 +4772,7 @@
Echec pour sauvegarder :
src/app/core/common-components/entity-form/entity-form.service.ts
- 231
+ 227
@@ -4844,7 +4807,11 @@
Examples of things to filter
src/app/core/entity-list/entity-list/entity-list.component.html
- 106
+ 88
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 88
@@ -4853,7 +4820,11 @@
Add a new entity to a list of multiple entities
src/app/core/entity-list/entity-list/entity-list.component.html
- 158
+ 141
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 141
@@ -4862,26 +4833,38 @@
Show filter options popup for list
src/app/core/entity-list/entity-list/entity-list.component.html
- 170
+ 153
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 153
Download all data (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 189
+ 172
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 172
- Download list contents as CSV
Download current (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 207
+ 190
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 190
- Download list contents as CSV
@@ -4890,7 +4873,11 @@
Filter placeholder
src/app/core/entity-list/entity-list/entity-list.component.html
- 100
+ 82
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 82
@@ -4898,7 +4885,11 @@
Import from file
src/app/core/entity-list/entity-list/entity-list.component.html
- 223
+ 206
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 206
@@ -4906,7 +4897,11 @@
Select multiple records for bulk actions like duplicating or deleting
src/app/core/entity-list/entity-list/entity-list.component.html
- 229
+ 212
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 212
@@ -4914,7 +4909,11 @@
Bulk Actions
src/app/core/entity-list/entity-list/entity-list.component.html
- 237
+ 220
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 220
@@ -4922,24 +4921,36 @@
Select rows to clone
src/app/core/entity-list/entity-list/entity-list.component.html
- 252
+ 235
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 235
Duplicate
+ bulk action button
src/app/core/entity-list/entity-list/entity-list.component.html
- 256,258
+ 239
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 239
- bulk action button
Annuler
src/app/core/entity-list/entity-list/entity-list.component.html
- 260,262
+ 243
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 243
@@ -5045,13 +5056,13 @@
[icon]="includeEventNotes ? 'toggle-on' : 'toggle-off'"
class="standard-icon-with-text color-accent"
>"/> Include events
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
- 23,28
-
events are related to a
child
Slider that allows a user to also include events
+
+ src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
+ 22
+
@@ -5128,8 +5139,24 @@
also show entries that are archived
slider
- src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
- 156
+ src/app/core/common-components/entities-table/entities-table.component.html
+ 106
+
+
+
+
+ Create a new record
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 15
+
+
+
+
+ Ajouter
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 24
@@ -5488,7 +5515,7 @@
Une nouvelle version de l'application est disponible!
src/app/core/ui/latest-changes/update-manager.service.ts
- 111
+ 114
@@ -5497,7 +5524,7 @@
Action that a user can update the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 112
+ 115
@@ -5505,7 +5532,7 @@
The app is in a unrecoverable state, please reload.
src/app/core/ui/latest-changes/update-manager.service.ts
- 137
+ 143
@@ -5514,7 +5541,7 @@
Action that a user can reload the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 138
+ 144
@@ -5936,16 +5963,6 @@
form field validation error
-
-
- Add New
-
- src/app/core/entity-list/entity-list/entity-list.component.html
- 29
-
-
Email
@@ -7330,7 +7347,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 137
+ 193
@@ -7339,7 +7356,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 107
+ 159
@@ -7348,7 +7365,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 112
+ 164
@@ -7357,7 +7374,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 117
+ 169
@@ -7365,7 +7382,7 @@
Any
src/app/features/todos/todo-list/todo-list.component.ts
- 120
+ 172
@@ -7373,7 +7390,7 @@
Tasks due
src/app/features/todos/todo-list/todo-list.component.ts
- 122
+ 174
diff --git a/src/assets/locale/messages.it.xlf b/src/assets/locale/messages.it.xlf
index ea43864fda..6fff4cd245 100644
--- a/src/assets/locale/messages.it.xlf
+++ b/src/assets/locale/messages.it.xlf
@@ -10,7 +10,7 @@
Show unrelated tooltip
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 41
+ 42
@@ -21,7 +21,7 @@
slider
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 42
+ 43
@@ -31,7 +31,7 @@
load-all button
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 52
+ 53
@@ -40,7 +40,7 @@
The month something took place
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 47
+ 48
@@ -50,7 +50,7 @@
How many children are present at a meeting
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 52
+ 53
@@ -59,7 +59,7 @@
Events of an attendance
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 61
+ 62
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -72,11 +72,11 @@
Percentage of people that attended an event
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 67
+ 68
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 45
+ 46
@@ -456,7 +456,7 @@
Event
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 42
+ 43
@@ -695,7 +695,7 @@
src/app/core/config/config-fix.ts
- 718
+ 710
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -816,7 +816,7 @@
src/app/core/config/config-fix.ts
- 805
+ 797
@@ -1057,7 +1057,7 @@
src/app/core/config/config-fix.ts
- 663
+ 655
@@ -1343,11 +1343,11 @@
Table header, Short for Body Mass Index
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 30
+ 32
src/app/core/config/config-fix.ts
- 415
+ 407
@@ -1356,7 +1356,7 @@
Tooltip for BMI info
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 32
+ 34
@@ -1400,11 +1400,11 @@
src/app/core/config/config-fix.ts
- 374
+ 366
src/app/core/config/config-fix.ts
- 781
+ 773
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -1488,11 +1488,7 @@
src/app/core/config/config-fix.ts
- 204
-
-
- src/app/core/config/config-fix.ts
- 451
+ 443
@@ -1541,7 +1537,7 @@
src/app/core/config/config-fix.ts
- 797
+ 789
@@ -1554,7 +1550,7 @@
src/app/core/config/config-fix.ts
- 746
+ 738
@@ -1575,11 +1571,11 @@
src/app/core/config/config-fix.ts
- 390
+ 382
src/app/core/config/config-fix.ts
- 492
+ 484
@@ -1592,7 +1588,7 @@
src/app/core/config/config-fix.ts
- 385
+ 377
@@ -2208,11 +2204,11 @@
src/app/core/config/config-fix.ts
- 312
+ 304
src/app/core/config/config-fix.ts
- 747
+ 739
@@ -2334,31 +2330,18 @@
[icon]="includeEventNotes ? 'toggle-on' : 'toggle-off'"
class="standard-icon-with-text color-accent"
>"/> Include events
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
- 23,28
-
events are related to a
child
Slider that allows a user to also include events
-
-
-
- Urgente
- Filter-option for notes
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 65
+ src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
+ 22
Da seguire
- Filter-option for notes
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 70
-
+ Label warning level
src/app/child-dev-project/warning-level.ts
35
@@ -2369,27 +2352,7 @@
Tutti
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 75
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 89
-
-
- src/app/core/filter/filters/filters.ts
- 125
-
-
- src/app/core/filter/filters/filters.ts
- 192
-
-
- src/app/core/filter/filters/filters.ts
- 224
-
-
- src/app/core/filter/filters/filters.ts
- 246
+ 74
@@ -2398,7 +2361,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 81
+ 66
@@ -2407,7 +2370,7 @@
Filter-option for notes
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 86
+ 71
@@ -3044,7 +3007,7 @@
src/app/core/config/config-fix.ts
- 575
+ 567
@@ -3190,11 +3153,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 178
+ 172
src/app/core/config/config-fix.ts
- 182
+ 176
@@ -3203,19 +3166,19 @@
Translated name of mobile column group
src/app/core/config/config-fix.ts
- 179
+ 173
src/app/core/config/config-fix.ts
- 192
+ 186
src/app/core/config/config-fix.ts
- 422
+ 414
src/app/core/config/config-fix.ts
- 476
+ 468
@@ -3258,7 +3221,7 @@
Panel title
src/app/core/config/config-fix.ts
- 268
+ 260
@@ -3267,7 +3230,7 @@
Panel title
src/app/core/config/config-fix.ts
- 283
+ 275
@@ -3276,7 +3239,7 @@
Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
src/app/core/config/config-fix.ts
- 297
+ 289
@@ -3285,15 +3248,15 @@
Panel title
src/app/core/config/config-fix.ts
- 330
+ 322
src/app/core/config/config-fix.ts
- 510
+ 502
src/app/core/config/config-fix.ts
- 703
+ 695
@@ -3302,7 +3265,7 @@
Panel title
src/app/core/config/config-fix.ts
- 347
+ 339
@@ -3311,7 +3274,7 @@
Panel title
src/app/core/config/config-fix.ts
- 356
+ 348
@@ -3320,7 +3283,7 @@
Column label for age of child
src/app/core/config/config-fix.ts
- 379
+ 371
@@ -3329,7 +3292,7 @@
Column label for school attendance of child
src/app/core/config/config-fix.ts
- 397
+ 389
@@ -3338,7 +3301,7 @@
Column label for coaching attendance of child
src/app/core/config/config-fix.ts
- 406
+ 398
@@ -3347,11 +3310,11 @@
Translated name of default column group
src/app/core/config/config-fix.ts
- 421
+ 413
src/app/core/config/config-fix.ts
- 425
+ 417
@@ -3360,7 +3323,7 @@
Column group name
src/app/core/config/config-fix.ts
- 438
+ 430
@@ -3369,11 +3332,11 @@
Column group name
src/app/core/config/config-fix.ts
- 461
+ 453
src/app/core/config/config-fix.ts
- 597
+ 589
@@ -3382,7 +3345,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 520
+ 512
@@ -3391,7 +3354,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 524
+ 516
@@ -3400,7 +3363,7 @@
Header for form section
src/app/core/config/config-fix.ts
- 528
+ 520
@@ -3409,7 +3372,7 @@
Panel title
src/app/core/config/config-fix.ts
- 536
+ 528
@@ -3418,7 +3381,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 539
+ 531
@@ -3427,7 +3390,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 559
+ 551
@@ -3436,7 +3399,7 @@
Child details section title
src/app/core/config/config-fix.ts
- 563
+ 555
@@ -3445,7 +3408,7 @@
Panel title
src/app/core/config/config-fix.ts
- 584
+ 576
@@ -3454,7 +3417,7 @@
description section
src/app/core/config/config-fix.ts
- 607
+ 599
@@ -3463,7 +3426,7 @@
Title inside a panel
src/app/core/config/config-fix.ts
- 615
+ 607
@@ -3472,7 +3435,7 @@
Panel title
src/app/core/config/config-fix.ts
- 621
+ 613
@@ -3481,7 +3444,7 @@
Panel title
src/app/core/config/config-fix.ts
- 646
+ 638
@@ -3499,7 +3462,7 @@
Panel title
src/app/core/config/config-fix.ts
- 731
+ 723
@@ -3595,11 +3558,11 @@
School Class
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 38
+ 34
- Interaction type/Category of a Note
@@ -3700,11 +3663,11 @@
Coaching Class
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 32
+ 28
- Interaction type/Category of a Note
@@ -3712,11 +3675,11 @@
Label for the address of a child
src/app/core/config/config-fix.ts
- 752
+ 744
src/app/core/config/config-fix.ts
- 793
+ 785
@@ -3725,7 +3688,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 756
+ 748
@@ -3734,7 +3697,7 @@
Label for the religion of a child
src/app/core/config/config-fix.ts
- 760
+ 752
@@ -3743,7 +3706,7 @@
Label for the mother tongue of a child
src/app/core/config/config-fix.ts
- 764
+ 756
@@ -3752,7 +3715,7 @@
Tooltip description for the mother tongue of a child
src/app/core/config/config-fix.ts
- 765
+ 757
@@ -3761,7 +3724,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 769
+ 761
@@ -3770,7 +3733,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 773
+ 765
@@ -3778,11 +3741,11 @@
Private School
src/app/core/config/config-fix.ts
- 319
+ 311
src/app/core/config/config-fix.ts
- 785
+ 777
@@ -3791,7 +3754,7 @@
Label for the language of a school
src/app/core/config/config-fix.ts
- 789
+ 781
@@ -3800,7 +3763,7 @@
Label for the timing of a school
src/app/core/config/config-fix.ts
- 801
+ 793
@@ -3809,7 +3772,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 814
+ 806
@@ -3818,7 +3781,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 815
+ 807
@@ -3827,7 +3790,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 820
+ 812
@@ -3836,7 +3799,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 821
+ 813
@@ -3845,7 +3808,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 826
+ 818
@@ -3854,7 +3817,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 827
+ 819
@@ -3863,7 +3826,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 832
+ 824
@@ -3872,7 +3835,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 833
+ 825
@@ -3881,7 +3844,7 @@
Label for a child attribute
src/app/core/config/config-fix.ts
- 838
+ 830
@@ -3890,7 +3853,7 @@
Description for a child attribute
src/app/core/config/config-fix.ts
- 839
+ 831
@@ -3899,7 +3862,7 @@
Label of user phone
src/app/core/config/config-fix.ts
- 847
+ 839
@@ -3945,38 +3908,38 @@
Home Visit
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 10
+ 6
- Interaction type/Category of a Note
Talk with Guardians
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 14
+ 10
- Interaction type/Category of a Note
Incident
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 18
+ 14
- Interaction type/Category of a Note
General Note
+ Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 22
+ 18
- Interaction type/Category of a Note
@@ -3984,7 +3947,7 @@
Interaction type/Category of a Note
src/app/core/config/default-config/default-interaction-types.ts
- 26
+ 22
@@ -4025,8 +3988,8 @@
96
- src/app/core/filter/filters/filters.ts
- 198
+ src/app/core/filter/filters/booleanFilter.ts
+ 13
@@ -4042,8 +4005,8 @@
104
- src/app/core/filter/filters/filters.ts
- 204
+ src/app/core/filter/filters/booleanFilter.ts
+ 19
@@ -4272,7 +4235,7 @@
Current user is not permitted to save these changes
src/app/core/common-components/entity-form/entity-form.service.ts
- 220
+ 216
@@ -4280,7 +4243,7 @@
Could not save :
src/app/core/common-components/entity-form/entity-form.service.ts
- 231
+ 227
@@ -4336,7 +4299,11 @@
Examples of things to filter
src/app/core/entity-list/entity-list/entity-list.component.html
- 106
+ 88
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 88
@@ -4345,7 +4312,11 @@
Add a new entity to a list of multiple entities
src/app/core/entity-list/entity-list/entity-list.component.html
- 158
+ 141
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 141
@@ -4354,26 +4325,38 @@
Show filter options popup for list
src/app/core/entity-list/entity-list/entity-list.component.html
- 170
+ 153
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 153
Download all data (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 189
+ 172
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 172
- Download list contents as CSV
Download current (.csv)
+ Download list contents as CSV
src/app/core/entity-list/entity-list/entity-list.component.html
- 207
+ 190
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 190
- Download list contents as CSV
@@ -4382,7 +4365,11 @@
Filter placeholder
src/app/core/entity-list/entity-list/entity-list.component.html
- 100
+ 82
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 82
@@ -4390,7 +4377,11 @@
Import from file
src/app/core/entity-list/entity-list/entity-list.component.html
- 223
+ 206
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 206
@@ -4398,7 +4389,11 @@
Select multiple records for bulk actions like duplicating or deleting
src/app/core/entity-list/entity-list/entity-list.component.html
- 229
+ 212
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 212
@@ -4406,7 +4401,11 @@
Bulk Actions
src/app/core/entity-list/entity-list/entity-list.component.html
- 237
+ 220
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 220
@@ -4414,24 +4413,36 @@
Select rows to clone
src/app/core/entity-list/entity-list/entity-list.component.html
- 252
+ 235
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 235
Duplicate
+ bulk action button
src/app/core/entity-list/entity-list/entity-list.component.html
- 256,258
+ 239
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 239
- bulk action button
Cancella
src/app/core/entity-list/entity-list/entity-list.component.html
- 260,262
+ 243
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 243
@@ -4584,8 +4595,24 @@
also show entries that are archived
slider
- src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
- 156
+ src/app/core/common-components/entities-table/entities-table.component.html
+ 106
+
+
+
+
+ Create a new record
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 15
+
+
+
+
+ Nuovo
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 24
@@ -5046,7 +5073,7 @@
È disponibile una nuova versione dell'app!
src/app/core/ui/latest-changes/update-manager.service.ts
- 111
+ 114
@@ -5055,7 +5082,7 @@
Action that a user can update the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 112
+ 115
@@ -5063,7 +5090,7 @@
The app is in a unrecoverable state, please reload.
src/app/core/ui/latest-changes/update-manager.service.ts
- 137
+ 143
@@ -5072,7 +5099,7 @@
Action that a user can reload the app with
src/app/core/ui/latest-changes/update-manager.service.ts
- 138
+ 144
@@ -5614,16 +5641,6 @@
form field validation error
-
-
- Add New
-
- src/app/core/entity-list/entity-list/entity-list.component.html
- 29
-
-
Email
@@ -7518,7 +7535,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 137
+ 193
@@ -7527,7 +7544,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 107
+ 159
@@ -7536,7 +7553,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 112
+ 164
@@ -7545,7 +7562,7 @@
Filter-option for todos
src/app/features/todos/todo-list/todo-list.component.ts
- 117
+ 169
@@ -7553,7 +7570,7 @@
Any
src/app/features/todos/todo-list/todo-list.component.ts
- 120
+ 172
@@ -7561,7 +7578,7 @@
Tasks due
src/app/features/todos/todo-list/todo-list.component.ts
- 122
+ 174
diff --git a/src/assets/locale/messages.xlf b/src/assets/locale/messages.xlf
index 9a2c3bb87e..2f64527268 100644
--- a/src/assets/locale/messages.xlf
+++ b/src/assets/locale/messages.xlf
@@ -6,7 +6,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 41
+ 42
Tooltip that will appear when hovered over the
show-unrelated button
@@ -16,7 +16,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 42,44
+ 43,45
show unrelated attendance-entries for an activity that are not
linked to the child of interest
@@ -26,7 +26,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.html
- 52,54
+ 53,55
load all records, not only the ones from the last 6 months
load-all button
@@ -35,7 +35,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 47
+ 48
The month something took place
@@ -43,7 +43,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 52
+ 53
Title of table column
How many children are present at a meeting
@@ -52,7 +52,7 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 61
+ 62
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -64,11 +64,11 @@
src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
- 67
+ 68
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 45
+ 46
Percentage of people that attended an event
@@ -417,7 +417,7 @@
src/app/child-dev-project/attendance/attendance-details/attendance-details.component.ts
- 42
+ 43
@@ -648,7 +648,7 @@
src/app/core/config/config-fix.ts
- 718
+ 710
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -760,7 +760,7 @@
src/app/core/config/config-fix.ts
- 805
+ 797
Label for the remarks of a ASER result
@@ -861,7 +861,7 @@
src/app/core/config/config-fix.ts
- 663
+ 655
Child status
@@ -1170,11 +1170,11 @@
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 30
+ 32
src/app/core/config/config-fix.ts
- 415
+ 407
Table header, Short for Body Mass Index
@@ -1182,7 +1182,7 @@
src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts
- 32
+ 34
Tooltip for BMI info
@@ -1222,11 +1222,11 @@
src/app/core/config/config-fix.ts
- 374
+ 366
src/app/core/config/config-fix.ts
- 781
+ 773
src/app/features/reporting/demo-report-config-generator.service.ts
@@ -1302,11 +1302,7 @@
src/app/core/config/config-fix.ts
- 204
-
-
- src/app/core/config/config-fix.ts
- 451
+ 443
Label for the status of a child
@@ -1350,7 +1346,7 @@
src/app/core/config/config-fix.ts
- 797
+ 789
Label for the phone number of a child
@@ -1362,7 +1358,7 @@
src/app/core/config/config-fix.ts
- 746
+ 738
Label for the child of a relation
@@ -1382,11 +1378,11 @@
src/app/core/config/config-fix.ts
- 390
+ 382
src/app/core/config/config-fix.ts
- 492
+ 484
Label for the school of a relation
@@ -1398,7 +1394,7 @@
src/app/core/config/config-fix.ts
- 385
+ 377
Label for the class of a relation
@@ -1918,11 +1914,11 @@
src/app/core/config/config-fix.ts
- 312
+ 304
src/app/core/config/config-fix.ts
- 747
+ 739
Label for the children of a note
@@ -1988,29 +1984,25 @@
>"/> Include events
src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
- 23,28
+ 22,27
events are related to a
child
Slider that allows a user to also include events
-
-
+
+
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 65
+ 66
Filter-option for notes
-
-
+
+
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 70
-
-
- src/app/child-dev-project/warning-level.ts
- 35
+ 71
Filter-option for notes
@@ -2018,44 +2010,8 @@
src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 75
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 89
-
-
- src/app/core/filter/filters/filters.ts
- 125
-
-
- src/app/core/filter/filters/filters.ts
- 192
-
-
- src/app/core/filter/filters/filters.ts
- 224
-
-
- src/app/core/filter/filters/filters.ts
- 246
-
-
-
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 81
+ 74
- Filter-option for notes
-
-
-
-
- src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
- 86
-
- Filter-option for notes
@@ -2127,6 +2083,14 @@
Label warning level
+
+
+
+ src/app/child-dev-project/warning-level.ts
+ 35
+
+ Label warning level
+
@@ -2940,8 +2904,8 @@
96
- src/app/core/filter/filters/filters.ts
- 198
+ src/app/core/filter/filters/booleanFilter.ts
+ 13
Confirmation dialog Yes
@@ -2956,8 +2920,8 @@
104
- src/app/core/filter/filters/filters.ts
- 204
+ src/app/core/filter/filters/booleanFilter.ts
+ 19
Confirmation dialog No
@@ -3014,6 +2978,29 @@
154
+
+
+
+ src/app/core/common-components/entities-table/entities-table.component.html
+ 106,108
+
+ also show entries that are archived
+ slider
+
+
+
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 15
+
+
+
+
+
+ src/app/core/common-components/entity-create-button/entity-create-button.component.html
+ 24
+
+
@@ -3074,14 +3061,14 @@
src/app/core/common-components/entity-form/entity-form.service.ts
- 220
+ 216
src/app/core/common-components/entity-form/entity-form.service.ts
- 231
+ 227
@@ -3122,15 +3109,6 @@
A placeholder for the input element when select options are not loaded yet
-
-
-
- src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html
- 156,158
-
- also show entries that are archived
- slider
-
@@ -3167,7 +3145,7 @@
src/app/core/config/config-fix.ts
- 575
+ 567
Menu item
@@ -3290,11 +3268,11 @@
src/app/core/config/config-fix.ts
- 178
+ 172
src/app/core/config/config-fix.ts
- 182
+ 176
Translated name of default column group
@@ -3302,19 +3280,19 @@
src/app/core/config/config-fix.ts
- 179
+ 173
src/app/core/config/config-fix.ts
- 192
+ 186
src/app/core/config/config-fix.ts
- 422
+ 414
src/app/core/config/config-fix.ts
- 476
+ 468
Translated name of mobile column group
@@ -3322,7 +3300,7 @@
src/app/core/config/config-fix.ts
- 268
+ 260
Panel title
@@ -3330,7 +3308,7 @@
src/app/core/config/config-fix.ts
- 283
+ 275
Panel title
@@ -3338,7 +3316,7 @@
src/app/core/config/config-fix.ts
- 297
+ 289
Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
@@ -3346,26 +3324,26 @@
src/app/core/config/config-fix.ts
- 319
+ 311
src/app/core/config/config-fix.ts
- 785
+ 777
src/app/core/config/config-fix.ts
- 330
+ 322
src/app/core/config/config-fix.ts
- 510
+ 502
src/app/core/config/config-fix.ts
- 703
+ 695
Panel title
@@ -3373,7 +3351,7 @@
src/app/core/config/config-fix.ts
- 347
+ 339
Panel title
@@ -3381,7 +3359,7 @@
src/app/core/config/config-fix.ts
- 356
+ 348
Panel title
@@ -3389,7 +3367,7 @@
src/app/core/config/config-fix.ts
- 379
+ 371
Column label for age of child
@@ -3397,7 +3375,7 @@
src/app/core/config/config-fix.ts
- 397
+ 389
Column label for school attendance of child
@@ -3405,7 +3383,7 @@
src/app/core/config/config-fix.ts
- 406
+ 398
Column label for coaching attendance of child
@@ -3413,11 +3391,11 @@
src/app/core/config/config-fix.ts
- 421
+ 413
src/app/core/config/config-fix.ts
- 425
+ 417
Translated name of default column group
@@ -3425,7 +3403,7 @@
src/app/core/config/config-fix.ts
- 438
+ 430
Column group name
@@ -3433,11 +3411,11 @@
src/app/core/config/config-fix.ts
- 461
+ 453
src/app/core/config/config-fix.ts
- 597
+ 589
Column group name
@@ -3445,7 +3423,7 @@
src/app/core/config/config-fix.ts
- 520
+ 512
Header for form section
@@ -3453,7 +3431,7 @@
src/app/core/config/config-fix.ts
- 524
+ 516
Header for form section
@@ -3461,7 +3439,7 @@
src/app/core/config/config-fix.ts
- 528
+ 520
Header for form section
@@ -3469,7 +3447,7 @@
src/app/core/config/config-fix.ts
- 536
+ 528
Panel title
@@ -3477,7 +3455,7 @@
src/app/core/config/config-fix.ts
- 539
+ 531
Title inside a panel
@@ -3485,7 +3463,7 @@
src/app/core/config/config-fix.ts
- 559
+ 551
Title inside a panel
@@ -3493,7 +3471,7 @@
src/app/core/config/config-fix.ts
- 563
+ 555
Child details section title
@@ -3501,7 +3479,7 @@
src/app/core/config/config-fix.ts
- 584
+ 576
Panel title
@@ -3509,7 +3487,7 @@
src/app/core/config/config-fix.ts
- 607
+ 599
description section
@@ -3517,7 +3495,7 @@
src/app/core/config/config-fix.ts
- 615
+ 607
Title inside a panel
@@ -3525,7 +3503,7 @@
src/app/core/config/config-fix.ts
- 621
+ 613
Panel title
@@ -3533,7 +3511,7 @@
src/app/core/config/config-fix.ts
- 646
+ 638
Panel title
@@ -3541,7 +3519,7 @@
src/app/core/config/config-fix.ts
- 731
+ 723
Panel title
@@ -3549,11 +3527,11 @@
src/app/core/config/config-fix.ts
- 752
+ 744
src/app/core/config/config-fix.ts
- 793
+ 785
Label for the address of a child
@@ -3561,7 +3539,7 @@
src/app/core/config/config-fix.ts
- 756
+ 748
Label for a child attribute
@@ -3569,7 +3547,7 @@
src/app/core/config/config-fix.ts
- 760
+ 752
Label for the religion of a child
@@ -3577,7 +3555,7 @@
src/app/core/config/config-fix.ts
- 764
+ 756
Label for the mother tongue of a child
@@ -3585,7 +3563,7 @@
src/app/core/config/config-fix.ts
- 765
+ 757
Tooltip description for the mother tongue of a child
@@ -3593,7 +3571,7 @@
src/app/core/config/config-fix.ts
- 769
+ 761
Label for a child attribute
@@ -3601,7 +3579,7 @@
src/app/core/config/config-fix.ts
- 773
+ 765
Label for a child attribute
@@ -3609,7 +3587,7 @@
src/app/core/config/config-fix.ts
- 789
+ 781
Label for the language of a school
@@ -3617,7 +3595,7 @@
src/app/core/config/config-fix.ts
- 801
+ 793
Label for the timing of a school
@@ -3625,7 +3603,7 @@
src/app/core/config/config-fix.ts
- 814
+ 806
Label for a child attribute
@@ -3633,7 +3611,7 @@
src/app/core/config/config-fix.ts
- 815
+ 807
Description for a child attribute
@@ -3641,7 +3619,7 @@
src/app/core/config/config-fix.ts
- 820
+ 812
Label for a child attribute
@@ -3649,7 +3627,7 @@
src/app/core/config/config-fix.ts
- 821
+ 813
Description for a child attribute
@@ -3657,7 +3635,7 @@
src/app/core/config/config-fix.ts
- 826
+ 818
Label for a child attribute
@@ -3665,7 +3643,7 @@
src/app/core/config/config-fix.ts
- 827
+ 819
Description for a child attribute
@@ -3673,7 +3651,7 @@
src/app/core/config/config-fix.ts
- 832
+ 824
Label for a child attribute
@@ -3681,7 +3659,7 @@
src/app/core/config/config-fix.ts
- 833
+ 825
Description for a child attribute
@@ -3689,7 +3667,7 @@
src/app/core/config/config-fix.ts
- 838
+ 830
Label for a child attribute
@@ -3697,7 +3675,7 @@
src/app/core/config/config-fix.ts
- 839
+ 831
Description for a child attribute
@@ -3705,7 +3683,7 @@
src/app/core/config/config-fix.ts
- 847
+ 839
Label of user phone
@@ -3749,7 +3727,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 10
+ 6
Interaction type/Category of a Note
@@ -3757,7 +3735,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 14
+ 10
Interaction type/Category of a Note
@@ -3765,7 +3743,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 18
+ 14
Interaction type/Category of a Note
@@ -3773,7 +3751,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 22
+ 18
Interaction type/Category of a Note
@@ -3781,7 +3759,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 26
+ 22
Interaction type/Category of a Note
@@ -3789,7 +3767,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 32
+ 28
Interaction type/Category of a Note
@@ -3797,7 +3775,7 @@
src/app/core/config/default-config/default-interaction-types.ts
- 38
+ 34
Interaction type/Category of a Note
@@ -4075,20 +4053,15 @@
Error assertValid failed
-
-
+
+
src/app/core/entity-list/entity-list/entity-list.component.html
- 29,35
+ 82,83
-
-
-
src/app/core/entity-list/entity-list/entity-list.component.html
- 100,101
+ 82,83
Allows the user to filter through entities
Filter placeholder
@@ -4097,7 +4070,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 106
+ 88
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 88
Examples of things to filter
@@ -4105,7 +4082,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 158,160
+ 141,143
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 141,143
Add a new entity to a list of multiple entities
@@ -4113,7 +4094,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 170
+ 153
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 153
Show filter options popup for list
@@ -4121,7 +4106,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 189
+ 172
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 172
Download list contents as CSV
@@ -4129,7 +4118,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 207
+ 190
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 190
Download list contents as CSV
@@ -4137,35 +4130,55 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 223
+ 206
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 206
src/app/core/entity-list/entity-list/entity-list.component.html
- 229
+ 212
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 212
src/app/core/entity-list/entity-list/entity-list.component.html
- 237
+ 220
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 220
src/app/core/entity-list/entity-list/entity-list.component.html
- 252
+ 235
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 235
src/app/core/entity-list/entity-list/entity-list.component.html
- 256,258
+ 239,241
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 239,241
bulk action button
@@ -4173,7 +4186,11 @@
src/app/core/entity-list/entity-list/entity-list.component.html
- 260,262
+ 243,245
+
+
+ src/app/core/entity-list/entity-list/entity-list.component.html
+ 243,245
@@ -5133,14 +5150,14 @@
src/app/core/ui/latest-changes/update-manager.service.ts
- 111
+ 114
src/app/core/ui/latest-changes/update-manager.service.ts
- 112
+ 115
Action that a user can update the app with
@@ -5148,14 +5165,14 @@
src/app/core/ui/latest-changes/update-manager.service.ts
- 137
+ 143
src/app/core/ui/latest-changes/update-manager.service.ts
- 138
+ 144
Action that a user can reload the app with
@@ -6624,7 +6641,7 @@
src/app/features/todos/todo-list/todo-list.component.ts
- 107
+ 159
Filter-option for todos
@@ -6632,7 +6649,7 @@
src/app/features/todos/todo-list/todo-list.component.ts
- 112
+ 164
Filter-option for todos
@@ -6640,7 +6657,7 @@
src/app/features/todos/todo-list/todo-list.component.ts
- 117
+ 169
Filter-option for todos
@@ -6648,21 +6665,21 @@
src/app/features/todos/todo-list/todo-list.component.ts
- 120
+ 172
src/app/features/todos/todo-list/todo-list.component.ts
- 122
+ 174
src/app/features/todos/todo-list/todo-list.component.ts
- 137
+ 193
Filter-option for todos