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 @@
- { 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 - - Urgent - Dringend - Filter-option for notes - - src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts - 65 - - Needs Follow-Up 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 @@ Home Visit 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 Talk with Guardians 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 Incident @@ -4406,17 +4369,17 @@ Interaction type/Category of a Note src/app/core/config/default-config/default-interaction-types.ts - 18 + 14 General Note 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 Guardians' Meeting @@ -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 Class 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 Address @@ -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 School Class 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 Add option @@ -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 all data (.csv) 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 current (.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 Filter @@ -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 Duplicate 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 Cancel 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 + Create new option @@ -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 + + + + Create a new record + Erstelle einen neuen Eintrag + + src/app/core/common-components/entity-create-button/entity-create-button.component.html + 15 + + + + Add New + 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 - - Add New - Hinzufügen - - src/app/core/entity-list/entity-list/entity-list.component.html - 29 - - Email 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 - Urgent - Filter-option for notes - - src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts - 65 - - Needs Follow-Up 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 @@ Home Visit 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 Talk with Guardians 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 Incident @@ -4422,17 +4385,17 @@ Interaction type/Category of a Note src/app/core/config/default-config/default-interaction-types.ts - 18 + 14 General 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 Guardians' Meeting @@ -4440,26 +4403,26 @@ Interaction type/Category of a Note src/app/core/config/default-config/default-interaction-types.ts - 26 + 22 Coaching Class 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 School Class 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 Add option @@ -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 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 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 Filter @@ -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 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 Cancel 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 + Creating new record. @@ -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 + Create a new record + + src/app/core/common-components/entity-create-button/entity-create-button.component.html + 15 + + + + Add New + 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 - Add New - - src/app/core/entity-list/entity-list/entity-list.component.html - 29 - - Email 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 - - - Urgent - 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 Needs Follow-Up 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 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 Add option @@ -3700,11 +3663,11 @@ Coaching Class 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 Address @@ -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 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 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 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 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 Guardians' Meeting @@ -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 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 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 Filter @@ -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 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 Cancel 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 + Create a new record + + src/app/core/common-components/entity-create-button/entity-create-button.component.html + 15 + + + + Add New + 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 - Add New - - src/app/core/entity-list/entity-list/entity-list.component.html - 29 - - Email 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 @@ Activate to also show entries for the activity that do not have any events with actual participation of this person 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 @@ Also show unrelated 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 @@ Load all records 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 @@ Month 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 @@ Present 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 @@ Events 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 @@ Attended 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 @@ Event 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 @@ BMI 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 @@ This is calculated using the height and the weight measure src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts - 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 - - Urgent + + This Week src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts - 65 + 66 Filter-option for notes - - Needs Follow-Up + + Since Last Week 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 @@ All 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 - - - - This Week - - src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts - 81 + 74 - Filter-option for notes - - - Since Last Week - - src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts - 86 - - Filter-option for notes High School @@ -2127,6 +2083,14 @@ Label warning level + + Needs Follow-Up + + src/app/child-dev-project/warning-level.ts + 35 + + Label warning level + Urgent Follow-Up @@ -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 + + Include archived records + + src/app/core/common-components/entities-table/entities-table.component.html + 106,108 + + also show entries that are archived + slider + + + Create a new record + + src/app/core/common-components/entity-create-button/entity-create-button.component.html + 15 + + + + Add New + + src/app/core/common-components/entity-create-button/entity-create-button.component.html + 24 + + Must be greater than @@ -3074,14 +3061,14 @@ Current user is not permitted to save these changes src/app/core/common-components/entity-form/entity-form.service.ts - 220 + 216 Could not save : 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 - - Include archived records - - src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html - 156,158 - - also show entries that are archived - slider - Select file @@ -3167,7 +3145,7 @@ src/app/core/config/config-fix.ts - 575 + 567 Menu item @@ -3290,11 +3268,11 @@ Standard 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 @@ Mobile 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 @@ User Information src/app/core/config/config-fix.ts - 268 + 260 Panel title @@ -3330,7 +3308,7 @@ Security src/app/core/config/config-fix.ts - 283 + 275 Panel title @@ -3338,7 +3316,7 @@ assets/help/help.en.md 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 @@ Private School src/app/core/config/config-fix.ts - 319 + 311 src/app/core/config/config-fix.ts - 785 + 777 Basic Information 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 @@ Students src/app/core/config/config-fix.ts - 347 + 339 Panel title @@ -3381,7 +3359,7 @@ Activities src/app/core/config/config-fix.ts - 356 + 348 Panel title @@ -3389,7 +3367,7 @@ Age src/app/core/config/config-fix.ts - 379 + 371 Column label for age of child @@ -3397,7 +3375,7 @@ Attendance (School) src/app/core/config/config-fix.ts - 397 + 389 Column label for school attendance of child @@ -3405,7 +3383,7 @@ Attendance (Coaching) src/app/core/config/config-fix.ts - 406 + 398 Column label for coaching attendance of child @@ -3413,11 +3391,11 @@ Basic Info 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 @@ School Info src/app/core/config/config-fix.ts - 438 + 430 Column group name @@ -3433,11 +3411,11 @@ Health 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 @@ Personal Information src/app/core/config/config-fix.ts - 520 + 512 Header for form section @@ -3453,7 +3431,7 @@ Additional src/app/core/config/config-fix.ts - 524 + 516 Header for form section @@ -3461,7 +3439,7 @@ Scholar activities src/app/core/config/config-fix.ts - 528 + 520 Header for form section @@ -3469,7 +3447,7 @@ Education src/app/core/config/config-fix.ts - 536 + 528 Panel title @@ -3477,7 +3455,7 @@ School History src/app/core/config/config-fix.ts - 539 + 531 Title inside a panel @@ -3485,7 +3463,7 @@ ASER Results src/app/core/config/config-fix.ts - 559 + 551 Title inside a panel @@ -3493,7 +3471,7 @@ Find a suitable new school src/app/core/config/config-fix.ts - 563 + 555 Child details section title @@ -3501,7 +3479,7 @@ Notes & Tasks src/app/core/config/config-fix.ts - 584 + 576 Panel title @@ -3509,7 +3487,7 @@ Health checkups are to be done regularly, at least every 6 months according to the program guidelines. src/app/core/config/config-fix.ts - 607 + 599 description section @@ -3517,7 +3495,7 @@ Height & Weight Tracking src/app/core/config/config-fix.ts - 615 + 607 Title inside a panel @@ -3525,7 +3503,7 @@ Educational Materials src/app/core/config/config-fix.ts - 621 + 613 Panel title @@ -3533,7 +3511,7 @@ Observations src/app/core/config/config-fix.ts - 646 + 638 Panel title @@ -3541,7 +3519,7 @@ Events & Attendance src/app/core/config/config-fix.ts - 731 + 723 Panel title @@ -3549,11 +3527,11 @@ Address 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 @@ Blood Group src/app/core/config/config-fix.ts - 756 + 748 Label for a child attribute @@ -3569,7 +3547,7 @@ Religion src/app/core/config/config-fix.ts - 760 + 752 Label for the religion of a child @@ -3577,7 +3555,7 @@ Mother Tongue src/app/core/config/config-fix.ts - 764 + 756 Label for the mother tongue of a child @@ -3585,7 +3563,7 @@ The primary language spoken at home src/app/core/config/config-fix.ts - 765 + 757 Tooltip description for the mother tongue of a child @@ -3593,7 +3571,7 @@ Last Dental Check-Up src/app/core/config/config-fix.ts - 769 + 761 Label for a child attribute @@ -3601,7 +3579,7 @@ Birth certificate src/app/core/config/config-fix.ts - 773 + 765 Label for a child attribute @@ -3609,7 +3587,7 @@ Language src/app/core/config/config-fix.ts - 789 + 781 Label for the language of a school @@ -3617,7 +3595,7 @@ School Timing src/app/core/config/config-fix.ts - 801 + 793 Label for the timing of a school @@ -3625,7 +3603,7 @@ Motivated src/app/core/config/config-fix.ts - 814 + 806 Label for a child attribute @@ -3633,7 +3611,7 @@ The child is motivated during the class. src/app/core/config/config-fix.ts - 815 + 807 Description for a child attribute @@ -3641,7 +3619,7 @@ Participating src/app/core/config/config-fix.ts - 820 + 812 Label for a child attribute @@ -3649,7 +3627,7 @@ The child is actively participating in the class. src/app/core/config/config-fix.ts - 821 + 813 Description for a child attribute @@ -3657,7 +3635,7 @@ Interacting src/app/core/config/config-fix.ts - 826 + 818 Label for a child attribute @@ -3665,7 +3643,7 @@ The child interacts with other students during the class. src/app/core/config/config-fix.ts - 827 + 819 Description for a child attribute @@ -3673,7 +3651,7 @@ Homework src/app/core/config/config-fix.ts - 832 + 824 Label for a child attribute @@ -3681,7 +3659,7 @@ The child does its homework. src/app/core/config/config-fix.ts - 833 + 825 Description for a child attribute @@ -3689,7 +3667,7 @@ Asking Questions src/app/core/config/config-fix.ts - 838 + 830 Label for a child attribute @@ -3697,7 +3675,7 @@ The child is asking questions during the class. src/app/core/config/config-fix.ts - 839 + 831 Description for a child attribute @@ -3705,7 +3683,7 @@ Contact src/app/core/config/config-fix.ts - 847 + 839 Label of user phone @@ -3749,7 +3727,7 @@ Home Visit src/app/core/config/default-config/default-interaction-types.ts - 10 + 6 Interaction type/Category of a Note @@ -3757,7 +3735,7 @@ Talk with Guardians src/app/core/config/default-config/default-interaction-types.ts - 14 + 10 Interaction type/Category of a Note @@ -3765,7 +3743,7 @@ Incident src/app/core/config/default-config/default-interaction-types.ts - 18 + 14 Interaction type/Category of a Note @@ -3773,7 +3751,7 @@ General Note src/app/core/config/default-config/default-interaction-types.ts - 22 + 18 Interaction type/Category of a Note @@ -3781,7 +3759,7 @@ Guardians' Meeting src/app/core/config/default-config/default-interaction-types.ts - 26 + 22 Interaction type/Category of a Note @@ -3789,7 +3767,7 @@ Coaching Class src/app/core/config/default-config/default-interaction-types.ts - 32 + 28 Interaction type/Category of a Note @@ -3797,7 +3775,7 @@ School Class 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 - - Add New + + Filter src/app/core/entity-list/entity-list/entity-list.component.html - 29,35 + 82,83 - - - Filter 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 @@ e.g. name, age 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 @@ Add New 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 @@ Filter options 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 @@ Download all data (.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 @@ -4129,7 +4118,11 @@ Download current (.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 @@ -4137,35 +4130,55 @@ 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 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 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 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 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 @@ Cancel 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 @@ A new version of the app is available! src/app/core/ui/latest-changes/update-manager.service.ts - 111 + 114 Update 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 @@ The app is in a unrecoverable state, please reload. src/app/core/ui/latest-changes/update-manager.service.ts - 137 + 143 Reload 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 @@ Overdue src/app/features/todos/todo-list/todo-list.component.ts - 107 + 159 Filter-option for todos @@ -6632,7 +6649,7 @@ Completed src/app/features/todos/todo-list/todo-list.component.ts - 112 + 164 Filter-option for todos @@ -6640,7 +6657,7 @@ All Open src/app/features/todos/todo-list/todo-list.component.ts - 117 + 169 Filter-option for todos @@ -6648,21 +6665,21 @@ Any src/app/features/todos/todo-list/todo-list.component.ts - 120 + 172 Tasks due src/app/features/todos/todo-list/todo-list.component.ts - 122 + 174 Currently Active src/app/features/todos/todo-list/todo-list.component.ts - 137 + 193 Filter-option for todos