From 024fa11681bdabce03c6e64bd0d7f24996f98a41 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 12 Jan 2023 12:51:07 +0100 Subject: [PATCH 01/83] refactor: extract enum dropdown into own component for easier re-use and testing --- .../edit-configurable-enum.component.html | 30 ++------- .../edit-configurable-enum.component.spec.ts | 22 ------- .../edit-configurable-enum.component.ts | 23 +------ .../enum-dropdown.component.html | 21 +++++++ .../enum-dropdown.component.scss | 0 .../enum-dropdown.component.spec.ts | 62 +++++++++++++++++++ .../enum-dropdown/enum-dropdown.component.ts | 47 ++++++++++++++ .../enum-dropdown/enum-dropdown.stories.ts | 37 +++++++++++ .../edit-number/edit-number.stories.ts | 22 ++++--- .../reporting/reporting/reporting.stories.ts | 6 +- 10 files changed, 194 insertions(+), 76 deletions(-) create mode 100644 src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html create mode 100644 src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss create mode 100644 src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts create mode 100644 src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts create mode 100644 src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html index a09e5d1b12..9bfabe9e72 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html @@ -1,24 +1,6 @@ - - - {{ label }} - - - - {{ o.label }} - - - - {{ o.label }} - - - - This field is required - - + diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts index bb791c4bef..fcffc7f834 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts @@ -63,28 +63,6 @@ describe("EditConfigurableEnumComponent", () => { expect(component).toBeTruthy(); }); - it("should add [invalid option] option from entity if given", () => { - const testEntity = new EditEnumTest(); - const invalidOption = { - id: "INVALID", - isInvalidOption: true, - label: "[invalid option] INVALID", - }; - const invalid2 = { - id: "X2", - isInvalidOption: true, - label: "[invalid option] X2", - }; - - testEntity.enum = invalidOption; - initForEntity(testEntity, "enum"); - expect(component.invalidOptions).toEqual([invalidOption]); - - testEntity.enumMulti = [invalidOption, invalid2]; - initForEntity(testEntity, "enumMulti"); - expect(component.invalidOptions).toEqual([invalidOption, invalid2]); - }); - function initForEntity(entity: EditEnumTest, field: "enum" | "enumMulti") { const formControl = testFormGroup.controls[field]; formControl.setValue(entity[field]); diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts index cfe6ed6795..fe86c25714 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.ts @@ -6,12 +6,12 @@ import { import { ConfigurableEnumValue } from "../configurable-enum.interface"; import { DynamicComponent } from "../../view/dynamic-components/dynamic-component.decorator"; import { arrayEntitySchemaDatatype } from "../../entity/schema-datatypes/datatype-array"; -import { compareEnums } from "../../../utils/utils"; import { MatFormFieldModule } from "@angular/material/form-field"; import { ReactiveFormsModule } from "@angular/forms"; import { MatSelectModule } from "@angular/material/select"; import { ConfigurableEnumDirective } from "../configurable-enum-directive/configurable-enum.directive"; -import { NgForOf, NgIf } from "@angular/common"; +import { NgIf } from "@angular/common"; +import { EnumDropdownComponent } from "../enum-dropdown/enum-dropdown.component"; @DynamicComponent("EditConfigurableEnum") @Component({ @@ -23,15 +23,13 @@ import { NgForOf, NgIf } from "@angular/common"; MatSelectModule, ConfigurableEnumDirective, NgIf, - NgForOf, + EnumDropdownComponent, ], standalone: true, }) export class EditConfigurableEnumComponent extends EditComponent { enumId: string; multi = false; - compareFun = compareEnums; - invalidOptions: ConfigurableEnumValue[] = []; onInitFromDynamicConfig(config: EditPropertyConfig) { super.onInitFromDynamicConfig(config); @@ -42,20 +40,5 @@ export class EditConfigurableEnumComponent extends EditComponent o.isInvalidOption - ); - } - return additionalOptions ?? []; } } diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html new file mode 100644 index 0000000000..ca82b4fad1 --- /dev/null +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -0,0 +1,21 @@ + + + {{ label }} + + + + {{ o.label }} + + + + {{ o.label }} + + + + This field is required + + diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts new file mode 100644 index 0000000000..84137f1bce --- /dev/null +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { EnumDropdownComponent } from "./enum-dropdown.component"; +import { ConfigService } from "../../config/config.service"; +import { FormControl } from "@angular/forms"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { SimpleChange } from "@angular/core"; + +describe("EnumDropdownComponent", () => { + let component: EnumDropdownComponent; + let fixture: ComponentFixture; + + let mockConfigService: jasmine.SpyObj; + + beforeEach(async () => { + mockConfigService = jasmine.createSpyObj(["getConfig"]); + mockConfigService.getConfig.and.returnValue([]); + + await TestBed.configureTestingModule({ + imports: [EnumDropdownComponent, NoopAnimationsModule], + providers: [{ provide: ConfigService, useValue: mockConfigService }], + }).compileComponents(); + + fixture = TestBed.createComponent(EnumDropdownComponent); + component = fixture.componentInstance; + + component.form = new FormControl(); + component.enumId = "test-enum"; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should add [invalid option] option from entity if given", () => { + const invalidOption = { + id: "INVALID", + isInvalidOption: true, + label: "[invalid option] INVALID", + }; + const invalid2 = { + id: "X2", + isInvalidOption: true, + label: "[invalid option] X2", + }; + + component.form = new FormControl(invalidOption); + component.ngOnChanges({ + form: new SimpleChange(null, component.form, false), + }); + expect(component.invalidOptions).toEqual([invalidOption]); + + component.form = new FormControl([invalidOption, invalid2]); + component.multi = true; + component.ngOnChanges({ + form: new SimpleChange(null, component.form, false), + }); + expect(component.invalidOptions).toEqual([invalidOption, invalid2]); + }); +}); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts new file mode 100644 index 0000000000..5387a440da --- /dev/null +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; +import { MatSelectModule } from "@angular/material/select"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { ConfigurableEnumDirective } from "../configurable-enum-directive/configurable-enum.directive"; +import { compareEnums } from "../../../utils/utils"; +import { NgForOf, NgIf } from "@angular/common"; +import { ConfigurableEnumValue } from "../configurable-enum.interface"; + +@Component({ + selector: "app-enum-dropdown", + templateUrl: "./enum-dropdown.component.html", + styleUrls: ["./enum-dropdown.component.scss"], + standalone: true, + imports: [ + MatSelectModule, + ReactiveFormsModule, + ConfigurableEnumDirective, + NgIf, + NgForOf, + ], +}) +export class EnumDropdownComponent implements OnChanges { + @Input() form: FormControl; // cannot be named "formControl" - otherwise the angular directive grabs this + @Input() label: string; + @Input() enumId: string; + @Input() multi?: boolean; + + compareFun = compareEnums; + invalidOptions: ConfigurableEnumValue[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.hasOwnProperty("enumId") || changes.hasOwnProperty("form")) { + this.invalidOptions = this.prepareInvalidOptions(); + } + } + + private prepareInvalidOptions(): ConfigurableEnumValue[] { + let additionalOptions; + if (!this.multi && this.form.value?.isInvalidOption) { + additionalOptions = [this.form.value]; + } + if (this.multi) { + additionalOptions = this.form.value?.filter((o) => o.isInvalidOption); + } + return additionalOptions ?? []; + } +} diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts new file mode 100644 index 0000000000..70e7d3acab --- /dev/null +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts @@ -0,0 +1,37 @@ +import { Meta, Story } from "@storybook/angular/types-6-0"; +import { moduleMetadata } from "@storybook/angular"; +import { StorybookBaseModule } from "../../../utils/storybook-base.module"; +import { EnumDropdownComponent } from "./enum-dropdown.component"; +import { FormControl } from "@angular/forms"; + +export default { + title: "Core/EntityComponents/Entity Property Fields/Enum Dropdown", + component: EnumDropdownComponent, + decorators: [ + moduleMetadata({ + imports: [EnumDropdownComponent, StorybookBaseModule], + providers: [], + }), + ], +} as Meta; + +const Template: Story = ( + args: EnumDropdownComponent +) => ({ + props: args, +}); + +export const Primary = Template.bind({}); +Primary.args = { + form: new FormControl(""), + label: "test field", + enumId: "center", +}; + +export const Multi = Template.bind({}); +Multi.args = { + form: new FormControl(""), + label: "test field", + enumId: "center", + multi: true, +}; diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts index b7c2369eef..e9648f23ba 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts @@ -1,7 +1,6 @@ -import { Story, Meta } from "@storybook/angular/types-6-0"; +import { Meta, Story } from "@storybook/angular/types-6-0"; import { moduleMetadata } from "@storybook/angular"; import { EntitySchemaService } from "../../../../entity/schema/entity-schema.service"; -import { EntityFormComponent } from "../../../entity-form/entity-form/entity-form.component"; import { FormFieldConfig } from "../../../entity-form/entity-form/FormConfig"; import { EntityMapperService } from "../../../../entity/entity-mapper.service"; import { Entity } from "../../../../entity/model/entity"; @@ -12,18 +11,26 @@ import { entityFormStorybookDefaulParameters, StorybookBaseModule, } from "../../../../../utils/storybook-base.module"; +import { AppModule } from "../../../../../app.module"; +import { mockEntityMapper } from "../../../../entity/mock-entity-mapper-service"; +import { FormComponent } from "../../../entity-details/form/form.component"; export default { title: "Core/EntityComponents/Entity Property Fields/Number", - component: EntityFormComponent, + component: FormComponent, decorators: [ moduleMetadata({ - imports: [EntityFormComponent, EditNumberComponent, StorybookBaseModule], + imports: [ + FormComponent, + EditNumberComponent, + AppModule, + StorybookBaseModule, + ], providers: [ EntitySchemaService, { provide: EntityMapperService, - useValue: { save: () => Promise.resolve() }, + useValue: mockEntityMapper(), }, ], }), @@ -31,8 +38,8 @@ export default { parameters: entityFormStorybookDefaulParameters, } as Meta; -const Template: Story = (args: EntityFormComponent) => ({ - component: EntityFormComponent, +const Template: Story> = (args: FormComponent) => ({ + component: FormComponent, props: args, }); @@ -53,6 +60,7 @@ const testEntity = new TestEntity(); testEntity.test = 5; export const Primary = Template.bind({}); +console.log("X"); Primary.args = { columns: [[fieldConfig]], entity: testEntity, diff --git a/src/app/features/reporting/reporting/reporting.stories.ts b/src/app/features/reporting/reporting/reporting.stories.ts index f585f4fd3b..88cb2126e5 100644 --- a/src/app/features/reporting/reporting/reporting.stories.ts +++ b/src/app/features/reporting/reporting/reporting.stories.ts @@ -5,8 +5,8 @@ import { ActivatedRoute } from "@angular/router"; import { of } from "rxjs"; import { DataAggregationService } from "../data-aggregation.service"; import { genders } from "../../../child-dev-project/children/model/genders"; -import { ExportService } from "../../../core/export/export-service/export.service"; import { StorybookBaseModule } from "../../../utils/storybook-base.module"; +import { DataTransformationService } from "../../../core/export/data-transformation-service/data-transformation.service"; const reportingService = { calculateReport: () => { @@ -194,8 +194,8 @@ export default { }, { provide: DataAggregationService, useValue: reportingService }, { - provide: ExportService, - useValue: { createJson: () => {}, createCsv: () => {} }, + provide: DataTransformationService, + useValue: { queryAndTransformData: () => [] }, }, ], }), From b9342e26b2ecb642ff4055ff28080208524a0505 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 12 Jan 2023 21:54:36 +0100 Subject: [PATCH 02/83] switch enum dropdown to autocomplete UI and generalize a reusable autocomplete form field component --- .../basic-autocomplete.component.html | 65 ++++++++++ .../basic-autocomplete.component.scss | 12 ++ .../basic-autocomplete.component.spec.ts | 114 ++++++++++++++++++ .../basic-autocomplete.component.ts | 102 ++++++++++++++++ .../edit-configurable-enum.component.html | 5 +- .../edit-configurable-enum.component.spec.ts | 7 +- .../enum-dropdown.component.html | 30 ++--- .../enum-dropdown.component.spec.ts | 11 +- .../enum-dropdown/enum-dropdown.component.ts | 23 +++- .../enum-dropdown/enum-dropdown.stories.ts | 15 +++ .../edit-single-entity.component.html | 77 ++---------- .../edit-single-entity.component.spec.ts | 65 +--------- .../edit-single-entity.component.ts | 49 +------- .../entity-select/entity-select.stories.ts | 16 ++- src/styles/variables/_colors.scss | 1 + 15 files changed, 386 insertions(+), 206 deletions(-) create mode 100644 src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html create mode 100644 src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss create mode 100644 src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts create mode 100644 src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html new file mode 100644 index 0000000000..e0240176fb --- /dev/null +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -0,0 +1,65 @@ + + + + + {{ label }} + + + + + + + + + + + + + + + + + This field is required + + diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss new file mode 100644 index 0000000000..768bff856c --- /dev/null +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss @@ -0,0 +1,12 @@ +@use "src/styles/variables/sizes"; +@use "src/styles/variables/colors"; + +.caret-suffix { + cursor: pointer; + padding: 0 sizes.$small; +} + +.disabled { + color: colors.$disabled; + cursor: default; +} diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts new file mode 100644 index 0000000000..994b15e45e --- /dev/null +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -0,0 +1,114 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; + +import { BasicAutocompleteComponent } from "./basic-autocomplete.component"; +import { School } from "../../../child-dev-project/schools/model/school"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { By } from "@angular/platform-browser"; +import { SimpleChange } from "@angular/core"; +import { Entity } from "../../entity/model/entity"; +import { FormControl } from "@angular/forms"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; + +describe("BasicAutocompleteComponent", () => { + let component: BasicAutocompleteComponent; + let fixture: ComponentFixture>; + + const entityToId = (e: Entity) => e?.getId(); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BasicAutocompleteComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BasicAutocompleteComponent); + component = fixture.componentInstance; + component.form = new FormControl(); + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should correctly show the autocomplete values", () => { + const school1 = School.create({ name: "Aaa" }); + const school2 = School.create({ name: "aab" }); + const school3 = School.create({ name: "cde" }); + component.options = [school1, school2, school3]; + + component.updateAutocomplete(""); + expect(component.autocompleteSuggestedOptions.value).toEqual([ + school1, + school2, + school3, + ]); + component.updateAutocomplete("Aa"); + expect(component.autocompleteSuggestedOptions.value).toEqual([ + school1, + school2, + ]); + component.updateAutocomplete("Aab"); + expect(component.autocompleteSuggestedOptions.value).toEqual([school2]); + }); + + it("should show name of the selected entity", fakeAsync(() => { + const child1 = Child.create("First Child"); + const child2 = Child.create("Second Child"); + component.form.setValue(child1.getId()); + component.options = [child1, child2]; + component.valueMapper = entityToId; + + component.ngOnChanges({ + form: new SimpleChange(null, component.form, false), + options: new SimpleChange(null, component.options, false), + }); + tick(); + fixture.detectChanges(); + + expect(component.selectedOption).toBe(child1); + expect( + fixture.debugElement.query(By.css("#inputElement")).nativeElement.value + ).toEqual("First Child"); + })); + + it("Should have the correct entity selected when it's name is entered", () => { + const child1 = Child.create("First Child"); + const child2 = Child.create("Second Child"); + component.options = [child1, child2]; + component.valueMapper = entityToId; + + component.select("First Child"); + + expect(component.selectedOption).toBe(child1); + expect(component.form.value).toBe(child1.getId()); + }); + + it("Should unselect if no entity can be matched", () => { + const first = Child.create("First"); + const second = Child.create("Second"); + component.options = [first, second]; + component.valueMapper = entityToId; + + component.select(first); + expect(component.selectedOption).toBe(first); + expect(component.form.value).toBe(first.getId()); + + component.select("second"); + expect(component.selectedOption).toBe(second); + expect(component.form.value).toBe(second.getId()); + + component.select("NonExistent"); + expect(component.selectedOption).toBe(undefined); + expect(component.form.value).toBe(undefined); + }); +}); diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts new file mode 100644 index 0000000000..03509305b4 --- /dev/null +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -0,0 +1,102 @@ +import { + Component, + ContentChild, + Input, + OnChanges, + SimpleChanges, + TemplateRef, +} from "@angular/core"; +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, + NgTemplateOutlet, +} from "@angular/common"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { MatInputModule } from "@angular/material/input"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { BehaviorSubject } from "rxjs"; + +@Component({ + selector: "app-basic-autocomplete", + templateUrl: "./basic-autocomplete.component.html", + styleUrls: ["./basic-autocomplete.component.scss"], + standalone: true, + imports: [ + NgForOf, + NgTemplateOutlet, + MatFormFieldModule, + ReactiveFormsModule, + MatInputModule, + NgIf, + FontAwesomeModule, + MatAutocompleteModule, + AsyncPipe, + NgClass, + ], + //changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BasicAutocompleteComponent implements OnChanges { + @Input() form: FormControl; // cannot be named "formControl" - otherwise the angular directive grabs this + @Input() label: string; + @Input() options: O[] = []; + @Input() multi?: boolean; + + @Input() valueMapper: (option: O) => V = (option) => option as any; + @Input() optionToString: (option: O) => string = (option) => + option?.toString(); + + @ContentChild(TemplateRef) templateRef: TemplateRef; + + autocompleteSuggestedOptions = new BehaviorSubject([]); + selectedOption: O; + + ngOnChanges(changes: SimpleChanges) { + if (changes.form || changes.options) { + this.selectInitialOption(); + } + } + + private selectInitialOption() { + const selectedOption = this.options.find( + (o) => this.valueMapper(o) === this.form.value + ); + if (selectedOption) { + this.selectedOption = selectedOption; + } + } + + updateAutocomplete(inputText: string) { + let filteredEntities = this.options; + if (inputText) { + filteredEntities = this.options.filter((option) => + this.optionToString(option) + .toLowerCase() + .includes(inputText.toLowerCase()) + ); + } + this.autocompleteSuggestedOptions.next(filteredEntities); + } + + select(selected: string | O) { + let option: O; + if (typeof selected === "string") { + option = this.options.find( + (e) => this.optionToString(e).toLowerCase() === selected.toLowerCase() + ); + } else { + option = selected; + } + + if (option) { + this.selectedOption = option; + this.form.setValue(this.valueMapper(option)); + } else { + this.selectedOption = undefined; + this.form.setValue(undefined); + } + } +} diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html index 9bfabe9e72..be688f4c0a 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html @@ -1,6 +1,7 @@ + [multi]="multi" +> + diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts index fcffc7f834..1e4dfecbc0 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts @@ -1,13 +1,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { EditConfigurableEnumComponent } from "./edit-configurable-enum.component"; -import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms"; +import { FormBuilder, FormControl } from "@angular/forms"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { ConfigService } from "../../config/config.service"; import { DatabaseEntity } from "../../entity/database-entity.decorator"; import { DatabaseField } from "../../entity/database-field.decorator"; import { Entity } from "../../entity/model/entity"; import { ConfigurableEnumValue } from "../configurable-enum.interface"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; describe("EditConfigurableEnumComponent", () => { let component: EditConfigurableEnumComponent; @@ -44,9 +45,9 @@ describe("EditConfigurableEnumComponent", () => { mockConfigService.getConfig.and.returnValue(testEnum); await TestBed.configureTestingModule({ imports: [ - NoopAnimationsModule, - ReactiveFormsModule, EditConfigurableEnumComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, ], providers: [{ provide: ConfigService, useValue: mockConfigService }], }).compileComponents(); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index ca82b4fad1..787b7e246d 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -1,21 +1,9 @@ - - - {{ label }} - - - - {{ o.label }} - - - - {{ o.label }} - - - - This field is required - - + + {{ item.label }} + diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts index 84137f1bce..4c3f64f5b5 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts @@ -5,6 +5,7 @@ import { ConfigService } from "../../config/config.service"; import { FormControl } from "@angular/forms"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { SimpleChange } from "@angular/core"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; describe("EnumDropdownComponent", () => { let component: EnumDropdownComponent; @@ -17,7 +18,11 @@ describe("EnumDropdownComponent", () => { mockConfigService.getConfig.and.returnValue([]); await TestBed.configureTestingModule({ - imports: [EnumDropdownComponent, NoopAnimationsModule], + imports: [ + EnumDropdownComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], providers: [{ provide: ConfigService, useValue: mockConfigService }], }).compileComponents(); @@ -26,6 +31,10 @@ describe("EnumDropdownComponent", () => { component.form = new FormControl(); component.enumId = "test-enum"; + component.ngOnChanges({ + form: new SimpleChange(null, component.form, true), + enumId: new SimpleChange(null, component.enumId, true), + }); fixture.detectChanges(); }); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts index 5387a440da..dbaedd428d 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -2,9 +2,14 @@ import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; import { MatSelectModule } from "@angular/material/select"; import { FormControl, ReactiveFormsModule } from "@angular/forms"; import { ConfigurableEnumDirective } from "../configurable-enum-directive/configurable-enum.directive"; -import { compareEnums } from "../../../utils/utils"; import { NgForOf, NgIf } from "@angular/common"; -import { ConfigurableEnumValue } from "../configurable-enum.interface"; +import { + CONFIGURABLE_ENUM_CONFIG_PREFIX, + ConfigurableEnumConfig, + ConfigurableEnumValue, +} from "../configurable-enum.interface"; +import { BasicAutocompleteComponent } from "../basic-autocomplete/basic-autocomplete.component"; +import { ConfigService } from "../../config/config.service"; @Component({ selector: "app-enum-dropdown", @@ -17,6 +22,7 @@ import { ConfigurableEnumValue } from "../configurable-enum.interface"; ConfigurableEnumDirective, NgIf, NgForOf, + BasicAutocompleteComponent, ], }) export class EnumDropdownComponent implements OnChanges { @@ -25,13 +31,24 @@ export class EnumDropdownComponent implements OnChanges { @Input() enumId: string; @Input() multi?: boolean; - compareFun = compareEnums; + enumOptions: ConfigurableEnumValue[] = []; invalidOptions: ConfigurableEnumValue[] = []; + options: ConfigurableEnumValue[]; + enumValueToString = (v: ConfigurableEnumValue) => v?.label; + + constructor(private configService: ConfigService) {} ngOnChanges(changes: SimpleChanges): void { + if (changes.hasOwnProperty("enumId")) { + // TODO: automatic checking for prefix would be handled automatically if enumConfigs become entities + this.enumOptions = this.configService.getConfig( + CONFIGURABLE_ENUM_CONFIG_PREFIX + this.enumId + ); + } if (changes.hasOwnProperty("enumId") || changes.hasOwnProperty("form")) { this.invalidOptions = this.prepareInvalidOptions(); } + this.options = [...this.enumOptions, ...this.invalidOptions]; } private prepareInvalidOptions(): ConfigurableEnumValue[] { diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts index 70e7d3acab..3f451992b3 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts @@ -3,6 +3,7 @@ import { moduleMetadata } from "@storybook/angular"; import { StorybookBaseModule } from "../../../utils/storybook-base.module"; import { EnumDropdownComponent } from "./enum-dropdown.component"; import { FormControl } from "@angular/forms"; +import { centersUnique } from "../../../child-dev-project/children/demo-data-generators/fixtures/centers"; export default { title: "Core/EntityComponents/Entity Property Fields/Enum Dropdown", @@ -13,6 +14,11 @@ export default { providers: [], }), ], + parameters: { + controls: { + //exclude: ["form"], + }, + }, } as Meta; const Template: Story = ( @@ -28,6 +34,15 @@ Primary.args = { enumId: "center", }; +const disabledControl = new FormControl(centersUnique[0]); +disabledControl.disable(); +export const Disabled = Template.bind({}); +Disabled.args = { + form: disabledControl, + label: "test field", + enumId: "center", +}; + export const Multi = Template.bind({}); Multi.args = { form: new FormControl(""), diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html index 9baf974579..933224c33b 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html @@ -1,64 +1,13 @@ - - - - {{ label }} - - - - - - - - - - - - - - - - This field is required - - + + + + + diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts index 99e42eec90..324b89c652 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts @@ -46,7 +46,7 @@ describe("EditSingleEntityComponent", () => { expect(component).toBeTruthy(); }); - it("should show all entities of the given type", fakeAsync(() => { + it("should load all entities of the given type as options", fakeAsync(() => { const school1 = School.create({ name: "First School" }); const school2 = School.create({ name: "Second School " }); loadTypeSpy.and.resolveTo([school1, school2]); @@ -56,71 +56,8 @@ describe("EditSingleEntityComponent", () => { expect(loadTypeSpy).toHaveBeenCalled(); expect(component.entities).toEqual([school1, school2]); - component.updateAutocomplete(""); - expect(component.autocompleteEntities.value).toEqual([school1, school2]); })); - it("should correctly show the autocomplete values", () => { - const school1 = School.create({ name: "Aaa" }); - const school2 = School.create({ name: "aab" }); - const school3 = School.create({ name: "cde" }); - component.entities = [school1, school2, school3]; - - component.updateAutocomplete(""); - expect(component.autocompleteEntities.value).toEqual([ - school1, - school2, - school3, - ]); - component.updateAutocomplete("Aa"); - expect(component.autocompleteEntities.value).toEqual([school1, school2]); - component.updateAutocomplete("Aab"); - expect(component.autocompleteEntities.value).toEqual([school2]); - }); - - it("should show name of the selected entity", fakeAsync(() => { - const child1 = Child.create("First Child"); - const child2 = Child.create("Second Child"); - component.formControl.setValue(child1.getId()); - loadTypeSpy.and.resolveTo([child1, child2]); - - initComponent(); - tick(); - fixture.detectChanges(); - - expect(component.selectedEntity).toBe(child1); - expect(component.input.nativeElement.value).toEqual("First Child"); - })); - - it("Should have the correct entity selected when it's name is entered", () => { - const child1 = Child.create("First Child"); - const child2 = Child.create("Second Child"); - component.entities = [child1, child2]; - - component.select("First Child"); - - expect(component.selectedEntity).toBe(child1); - expect(component.formControl).toHaveValue(child1.getId()); - }); - - it("Should unselect if no entity can be matched", () => { - const first = Child.create("First"); - const second = Child.create("Second"); - component.entities = [first, second]; - - component.select(first); - expect(component.selectedEntity).toBe(first); - expect(component.formControl.value).toBe(first.getId()); - - component.select("second"); - expect(component.selectedEntity).toBe(second); - expect(component.formControl.value).toBe(second.getId()); - - component.select("NonExistent"); - expect(component.selectedEntity).toBe(undefined); - expect(component.formControl.value).toBe(undefined); - }); - function initComponent(): Promise { return component.onInitFromDynamicConfig({ formFieldConfig: { id: "childId" }, diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts index 999d4137ef..8824c187bc 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts @@ -1,10 +1,9 @@ -import { Component, ElementRef, ViewChild } from "@angular/core"; +import { Component } from "@angular/core"; import { EditComponent, EditPropertyConfig, } from "../../entity-utils/dynamic-form-components/edit-component"; import { Entity } from "../../../entity/model/entity"; -import { BehaviorSubject } from "rxjs"; import { DynamicComponent } from "../../../view/dynamic-components/dynamic-component.decorator"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { MatFormFieldModule } from "@angular/material/form-field"; @@ -15,6 +14,7 @@ import { AsyncPipe, NgForOf, NgIf } from "@angular/common"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatButtonModule } from "@angular/material/button"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { BasicAutocompleteComponent } from "../../../configurable-enum/basic-autocomplete/basic-autocomplete.component"; @DynamicComponent("EditSingleEntity") @Component({ @@ -32,64 +32,23 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; FontAwesomeModule, AsyncPipe, NgForOf, + BasicAutocompleteComponent, ], standalone: true, }) export class EditSingleEntityComponent extends EditComponent { entities: Entity[] = []; - placeholder: string; - autocompleteEntities = new BehaviorSubject([]); - selectedEntity?: Entity; - - @ViewChild("inputElement") input: ElementRef; + entityToId = (e: Entity) => e?.getId(); constructor(private entityMapperService: EntityMapperService) { super(); } - updateAutocomplete(inputText: string) { - let filteredEntities = this.entities; - if (inputText) { - filteredEntities = this.entities.filter((entity) => - entity.toString().toLowerCase().includes(inputText.toLowerCase()) - ); - } - this.autocompleteEntities.next(filteredEntities); - } - async onInitFromDynamicConfig(config: EditPropertyConfig) { super.onInitFromDynamicConfig(config); - this.placeholder = $localize`:Placeholder for input to set an entity|context Select User:Select ${ - config.formFieldConfig.label || config.propertySchema?.label - }`; const entityType: string = config.formFieldConfig.additional || config.propertySchema.additional; this.entities = await this.entityMapperService.loadType(entityType); this.entities.sort((e1, e2) => e1.toString().localeCompare(e2.toString())); - const selectedEntity = this.entities.find( - (entity) => entity.getId() === this.formControl.value - ); - if (selectedEntity) { - this.selectedEntity = selectedEntity; - } - } - - select(selected: string | Entity) { - let entity: Entity; - if (typeof selected === "string") { - entity = this.entities.find( - (e) => e.toString().toLowerCase() === selected.toLowerCase() - ); - } else { - entity = selected; - } - - if (entity) { - this.selectedEntity = entity; - this.formControl.setValue(entity.getId()); - } else { - this.selectedEntity = undefined; - this.formControl.setValue(undefined); - } } } diff --git a/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts b/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts index 2520e739ca..811268a77b 100644 --- a/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts +++ b/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts @@ -1,14 +1,20 @@ -import { Story, Meta } from "@storybook/angular/types-6-0"; +import { Meta, Story } from "@storybook/angular/types-6-0"; import { moduleMetadata } from "@storybook/angular"; import { Child } from "../../../../child-dev-project/children/model/child"; -import { Database } from "../../../database/database"; import { BackupService } from "../../../admin/services/backup.service"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; -import { ChildrenService } from "../../../../child-dev-project/children/children.service"; import { BehaviorSubject } from "rxjs"; import { EntitySelectComponent } from "./entity-select.component"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { School } from "../../../../child-dev-project/schools/model/school"; +import { + componentRegistry, + ComponentRegistry, +} from "../../../../dynamic-components"; +import { ChildBlockComponent } from "../../../../child-dev-project/children/child-block/child-block.component"; +import { SchoolBlockComponent } from "../../../../child-dev-project/schools/school-block/school-block.component"; +import { Database } from "../../../database/database"; +import { ChildrenService } from "../../../../child-dev-project/children/children.service"; const child1 = new Child(); child1.name = "First Child"; @@ -53,6 +59,7 @@ export default { ]), }, }, + { provide: ComponentRegistry, useValue: componentRegistry }, { provide: Database, useValue: {} }, { provide: ChildrenService, useValue: {} }, ], @@ -74,6 +81,9 @@ export default { }, } as Meta; +componentRegistry.add("ChildBlock", async () => ChildBlockComponent); +componentRegistry.add("SchoolBlock", async () => SchoolBlockComponent); + const Template: Story> = ( args: EntitySelectComponent ) => ({ diff --git a/src/styles/variables/_colors.scss b/src/styles/variables/_colors.scss index 0a5f02ef47..75e0cddc38 100644 --- a/src/styles/variables/_colors.scss +++ b/src/styles/variables/_colors.scss @@ -26,6 +26,7 @@ $warn : mat.get-color-from-palette($warn-palette); $error : mat.get-color-from-palette($err-palette); $muted : rgba(0, 0, 0, 0.54); +$disabled : rgba(0, 0, 0, 0.38); /* * Quantized warning-levels. Each level represents a color from green (all is OK) From 660c6becd5421a80f42aadc06e5ed0ff81dc44d0 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Jan 2023 13:56:07 +0100 Subject: [PATCH 03/83] fixed story and component issues --- .../demo-data-generators/fixtures/centers.ts | 9 +++------ src/app/core/config/config-fix.ts | 16 ++-------------- .../basic-autocomplete.component.html | 15 ++------------- .../enum-dropdown/enum-dropdown.component.ts | 2 +- .../enum-dropdown/enum-dropdown.stories.ts | 2 +- .../edit-number/edit-number.stories.ts | 1 - 6 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts b/src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts index 6eeaa035d4..70d214c042 100644 --- a/src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts +++ b/src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts @@ -1,13 +1,10 @@ import { Center } from "../../model/child"; -export const centersWithProbability: Array
= [ - // multiple entries for the same value increase its probability - { id: "alipore", label: $localize`:center:Alipore` }, +export const centersUnique: Center[] = [ { id: "alipore", label: $localize`:center:Alipore` }, { id: "tollygunge", label: $localize`:center:Tollygunge` }, { id: "barabazar", label: $localize`:center:Barabazar` }, ]; -export const centersUnique = centersWithProbability.filter( - (value, index, self) => self.indexOf(value) === index -); +// multiple entries for the same value increase its probability +export const centersWithProbability = [0, 0, 1, 2].map((i) => centersUnique[i]); diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 8a115eef03..1bec32c0da 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -12,6 +12,7 @@ import { } from "../../child-dev-project/children/aser/model/skill-levels"; import { warningLevels } from "../../child-dev-project/warning-levels"; import { ratingAnswers } from "../../features/historical-data/model/rating-answers"; +import { centersUnique } from "../../child-dev-project/children/demo-data-generators/fixtures/centers"; // prettier-ignore export const defaultJsonConfig = { @@ -135,20 +136,7 @@ export const defaultJsonConfig = { "_ordinal": 6, } ], - "enum:center": [ - { - "id": "alipore", - "label": $localize`:center:Alipore` - }, - { - "id": "tollygunge", - "label": $localize`:center:Tollygunge` - }, - { - "id": "barabazar", - "label": $localize`:center:Barabazar` - } - ], + "enum:center": centersUnique, "enum:rating-answer": ratingAnswers, "view:": { diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index e0240176fb..5cf7434b20 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -1,22 +1,12 @@ - + - + {{ label }} - - - - - {{ label }} - + + + + @@ -48,8 +39,7 @@ *ngFor="let item of autocompleteSuggestedOptions | async" [value]="item" > - - + - + This field is required diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 994b15e45e..d63dfe9ccd 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -75,7 +75,7 @@ describe("BasicAutocompleteComponent", () => { tick(); fixture.detectChanges(); - expect(component.selectedOption).toBe(child1); + expect(component.inputValue).toBe(child1); expect( fixture.debugElement.query(By.css("#inputElement")).nativeElement.value ).toEqual("First Child"); @@ -89,7 +89,7 @@ describe("BasicAutocompleteComponent", () => { component.select("First Child"); - expect(component.selectedOption).toBe(child1); + expect(component.inputValue).toBe(child1); expect(component.form.value).toBe(child1.getId()); }); @@ -100,15 +100,15 @@ describe("BasicAutocompleteComponent", () => { component.valueMapper = entityToId; component.select(first); - expect(component.selectedOption).toBe(first); + expect(component.inputValue).toBe(first); expect(component.form.value).toBe(first.getId()); component.select("second"); - expect(component.selectedOption).toBe(second); + expect(component.inputValue).toBe(second); expect(component.form.value).toBe(second.getId()); component.select("NonExistent"); - expect(component.selectedOption).toBe(undefined); + expect(component.inputValue).toBe(undefined); expect(component.form.value).toBe(undefined); }); }); diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 54343f657b..ecc41a6a68 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -20,9 +20,10 @@ import { FormControl, ReactiveFormsModule } from "@angular/forms"; import { MatInputModule } from "@angular/material/input"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { BehaviorSubject, Subscription } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { MatCheckboxModule } from "@angular/material/checkbox"; +import { filter } from "rxjs/operators"; interface SelectableOption { initial: O; @@ -53,7 +54,27 @@ interface SelectableOption { //changeDetection: ChangeDetectionStrategy.OnPush }) export class BasicAutocompleteComponent implements OnChanges { - @Input() form: FormControl; // cannot be named "formControl" - otherwise the angular directive grabs this + // cannot be named "formControl" - otherwise the angular directive grabs this + @Input() set form(form: FormControl) { + this._form = form; + this.setInputValue(); + if (form.disabled) { + this.autocompleteForm.disable(); + } + form.statusChanges.subscribe((status) => { + if (status === "DISABLED") { + this.autocompleteForm.disable(); + } else { + this.autocompleteForm.enable(); + } + }); + form.valueChanges + .pipe(untilDestroyed(this)) + .subscribe(() => this.setInputValue()); + } + + _form: FormControl; + @Input() label: string; @Input() set options(options: O[]) { @@ -61,7 +82,6 @@ export class BasicAutocompleteComponent implements OnChanges { } _options: SelectableOption[] = []; - // TODO implement multi @Input() multi?: boolean; @Input() set valueMapper(value: (option: O) => V) { @@ -85,33 +105,49 @@ export class BasicAutocompleteComponent implements OnChanges { @ContentChild(TemplateRef) templateRef: TemplateRef; + autocompleteForm = new FormControl(""); + autocompleteSuggestedOptions = new BehaviorSubject[]>( [] ); showAddOption = false; addOptionTimeout: any; - selectedOption: SelectableOption; - private formSubscription: Subscription; + inputValue = ""; + + constructor() { + this.autocompleteForm.valueChanges + .pipe(filter((val) => typeof val === "string")) + .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); + } ngOnChanges(changes: SimpleChanges) { if (changes.form || changes.options) { - this.selectCurrentOption(); - } - if (!this.formSubscription && changes.form) { - this.formSubscription = this.form.valueChanges - .pipe(untilDestroyed(this)) - .subscribe(() => this.selectCurrentOption()); + if (this.multi) { + this._options + .filter(({ asValue }) => (this._form.value as V[])?.includes(asValue)) + .forEach((o) => (o.selected = true)); + } + this.setInputValue(); } } - private selectCurrentOption() { - this.selectedOption = this._options.find( - (o) => o.asValue === this.form.value - ); + private setInputValue() { + if (this.multi) { + this.autocompleteForm.setValue( + this._options + .filter((o) => o.selected) + .map((o) => o.asString) + .join(", ") + ); + } else { + const selected = this._options.find( + ({ asValue }) => asValue === this._form.value + ); + this.autocompleteForm.setValue(selected?.asString ?? ""); + } } updateAutocomplete(inputText: string) { - // TODO this behaves problematic when navigating with the up and down buttons let filteredEntities = this._options; this.showAddOption = false; clearTimeout(this.addOptionTimeout); @@ -143,8 +179,7 @@ export class BasicAutocompleteComponent implements OnChanges { } if (option) { - option.selected = true; - this.form.setValue(option.asValue); + this.selectOption(option); } else { if (selected) { const newOption = this.toSelectableOption( @@ -153,11 +188,23 @@ export class BasicAutocompleteComponent implements OnChanges { this._options.push(newOption); this.select(newOption); } else { - this.form.setValue(undefined); + this._form.setValue(undefined); } } } + private selectOption(option: SelectableOption) { + if (this.multi) { + option.selected = !option.selected; + const selected = this._options + .filter((o) => o.selected) + .map((o) => o.asValue); + this._form.setValue(selected); + } else { + this._form.setValue(option.asValue); + } + } + private toSelectableOption(opt: O): SelectableOption { return { initial: opt, diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts index 157fb7c21c..788715cfbc 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts @@ -7,6 +7,7 @@ import { ConfigurableEnumValue } from "../configurable-enum.interface"; /** * This component displays a {@link ConfigurableEnumValue} as text. * If the value has a `color` property, it is used as the background color. + * TODO should also work for multi-select */ @DynamicComponent("DisplayConfigurableEnum") @Component({ From fc8904e2d86bc6382a05df83f2c87dadab9f6884 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 24 Jan 2023 14:46:37 +0100 Subject: [PATCH 23/83] DisplayConfigurableEnum works with arrays --- .../display-configurable-enum.component.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts index 788715cfbc..3424c81c38 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts @@ -7,25 +7,32 @@ import { ConfigurableEnumValue } from "../configurable-enum.interface"; /** * This component displays a {@link ConfigurableEnumValue} as text. * If the value has a `color` property, it is used as the background color. - * TODO should also work for multi-select */ @DynamicComponent("DisplayConfigurableEnum") @Component({ selector: "app-display-configurable-enum", - template: `{{ value?.label }}`, + template: `{{ templateString }}`, standalone: true, }) -export class DisplayConfigurableEnumComponent extends ViewDirective { +export class DisplayConfigurableEnumComponent extends ViewDirective< + ConfigurableEnumValue | ConfigurableEnumValue[] +> { @HostBinding("style.background-color") private style; @HostBinding("style.padding") private padding; @HostBinding("style.border-radius") private radius; + templateString = ""; onInitFromDynamicConfig(config: ViewPropertyConfig) { super.onInitFromDynamicConfig(config); - if (this.value?.color) { - this.style = this.value.color; - this.padding = "5px"; - this.radius = "4px"; + if (Array.isArray(this.value)) { + this.templateString = this.value.map((v) => v.label).join(", "); + } else if (this.value) { + if (this.value.color) { + this.style = this.value.color; + this.padding = "5px"; + this.radius = "4px"; + } + this.templateString = this.value.label; } } } From 5f9d9ed27516b8e32deaa8205cf64b2f3e297ba8 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 26 Jan 2023 20:44:06 +0100 Subject: [PATCH 24/83] removed safeguard for configurable enum prefix --- src/app/core/config-setup/config-import-parser.service.ts | 1 + src/app/core/configurable-enum/configurable-enum.service.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/core/config-setup/config-import-parser.service.ts b/src/app/core/config-setup/config-import-parser.service.ts index 701c96acf2..724a45d822 100644 --- a/src/app/core/config-setup/config-import-parser.service.ts +++ b/src/app/core/config-setup/config-import-parser.service.ts @@ -25,6 +25,7 @@ export class ConfigImportParserService { "appConfig:usage-analytics", "navigationMenu", "view:", + // TODO what do we do with these? "enum:interaction-type", "enum:warning-levels", "view:note", diff --git a/src/app/core/configurable-enum/configurable-enum.service.ts b/src/app/core/configurable-enum/configurable-enum.service.ts index a75d162f4d..882b35a971 100644 --- a/src/app/core/configurable-enum/configurable-enum.service.ts +++ b/src/app/core/configurable-enum/configurable-enum.service.ts @@ -38,10 +38,7 @@ export class ConfigurableEnumService { } getEnum(id: string): ConfigurableEnum { - const entityId = Entity.createPrefixedId( - ConfigurableEnum.ENTITY_TYPE, - id.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, "") - ); + const entityId = Entity.createPrefixedId(ConfigurableEnum.ENTITY_TYPE, id); return this.enums.get(entityId); } } From acc12805dfafcebf8fd7eb7a7afed1cdd712c06f Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 26 Jan 2023 20:46:32 +0100 Subject: [PATCH 25/83] renamed popup component --- .../configure-enum-popup.component.html} | 0 .../configure-enum-popup.component.scss} | 0 .../configure-enum-popup.component.spec.ts | 22 ++++++++++++++++++ .../configure-enum-popup.component.ts} | 10 ++++---- .../edit-enum-popup.component.spec.ts | 23 ------------------- .../enum-dropdown/enum-dropdown.component.ts | 4 ++-- 6 files changed, 29 insertions(+), 30 deletions(-) rename src/app/core/configurable-enum/{edit-enum-popup/edit-enum-popup.component.html => configure-enum-popup/configure-enum-popup.component.html} (100%) rename src/app/core/configurable-enum/{edit-enum-popup/edit-enum-popup.component.scss => configure-enum-popup/configure-enum-popup.component.scss} (100%) create mode 100644 src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts rename src/app/core/configurable-enum/{edit-enum-popup/edit-enum-popup.component.ts => configure-enum-popup/configure-enum-popup.component.ts} (89%) delete mode 100644 src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.spec.ts diff --git a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.html b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html similarity index 100% rename from src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.html rename to src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html diff --git a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.scss b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.scss similarity index 100% rename from src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.scss rename to src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.scss diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts new file mode 100644 index 0000000000..30183d5385 --- /dev/null +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ConfigureEnumPopupComponent } from "./configure-enum-popup.component"; + +describe("ConfigureEnumPopupComponent", () => { + let component: ConfigureEnumPopupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ConfigureEnumPopupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ConfigureEnumPopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts similarity index 89% rename from src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.ts rename to src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 92698db997..07e3b937aa 100644 --- a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -23,9 +23,9 @@ import { MatButtonModule } from "@angular/material/button"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; @Component({ - selector: "app-edit-enum-popup", - templateUrl: "./edit-enum-popup.component.html", - styleUrls: ["./edit-enum-popup.component.scss"], + selector: "app-configure-enum-popup", + templateUrl: "./configure-enum-popup.component.html", + styleUrls: ["./configure-enum-popup.component.scss"], imports: [ MatDialogModule, NgForOf, @@ -40,10 +40,10 @@ import { ConfirmationDialogService } from "../../confirmation-dialog/confirmatio ], standalone: true, }) -export class EditEnumPopupComponent { +export class ConfigureEnumPopupComponent { constructor( @Inject(MAT_DIALOG_DATA) public enumEntity: ConfigurableEnum, - private dialog: MatDialogRef, + private dialog: MatDialogRef, private entityMapper: EntityMapperService, private confirmationService: ConfirmationDialogService ) { diff --git a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.spec.ts b/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.spec.ts deleted file mode 100644 index c0fc55ed4f..0000000000 --- a/src/app/core/configurable-enum/edit-enum-popup/edit-enum-popup.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { EditEnumPopupComponent } from './edit-enum-popup.component'; - -describe('EditEnumPopupComponent', () => { - let component: EditEnumPopupComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ EditEnumPopupComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(EditEnumPopupComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts index 1a09f56cbc..87b671d4b5 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -10,7 +10,7 @@ import { EntityMapperService } from "../../entity/entity-mapper.service"; import { ConfigurableEnum } from "../configurable-enum"; import { EntityAbility } from "../../permissions/ability/entity-ability"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; -import { EditEnumPopupComponent } from "../edit-enum-popup/edit-enum-popup.component"; +import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-enum-popup.component"; @Component({ selector: "app-enum-dropdown", @@ -80,7 +80,7 @@ export class EnumDropdownComponent implements OnChanges { } openSettings() { - const dialogRef = this.dialog.open(EditEnumPopupComponent, { + const dialogRef = this.dialog.open(ConfigureEnumPopupComponent, { data: this.enumEntity, }); dialogRef From ac5cdab3f95e8e782bd56adc0b744e70c2248e4d Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 27 Jan 2023 14:34:45 +0100 Subject: [PATCH 26/83] fixed enum saving logic for config service --- src/app/core/config/config.service.spec.ts | 69 ++++++++++++++++++++-- src/app/core/config/config.service.ts | 17 +++++- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 7db2a4381b..c0cee8b099 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -5,6 +5,7 @@ import { Config } from "./config"; import { firstValueFrom, Subject } from "rxjs"; import { UpdatedEntity } from "../entity/model/entity-update"; import { LoggingService } from "../logging/logging.service"; +import { ConfigurableEnum } from "../configurable-enum/configurable-enum"; describe("ConfigService", () => { let service: ConfigService; @@ -12,9 +13,18 @@ describe("ConfigService", () => { const updateSubject = new Subject>(); beforeEach(() => { - entityMapper = jasmine.createSpyObj(["load", "save", "receiveUpdates"]); + entityMapper = jasmine.createSpyObj([ + "load", + "save", + "receiveUpdates", + "saveAll", + "loadType", + ]); entityMapper.receiveUpdates.and.returnValue(updateSubject); entityMapper.load.and.rejectWith(); + entityMapper.loadType.and.resolveTo([]); + entityMapper.saveAll.and.resolveTo([]); + entityMapper.save.and.resolveTo([]); TestBed.configureTestingModule({ providers: [ { provide: EntityMapperService, useValue: entityMapper }, @@ -51,6 +61,7 @@ describe("ConfigService", () => { const testConfig = new Config(); testConfig.data = { testKey: "testValue" }; updateSubject.next({ type: "new", entity: testConfig }); + tick(); expect(service.getConfig("testKey")).toBe("testValue"); return expectAsync(configLoaded).toBeResolvedTo(testConfig); @@ -63,7 +74,7 @@ describe("ConfigService", () => { "other:1": { name: "wrong" }, "test:2": { name: "second" }, }; - entityMapper.load.and.returnValue(Promise.resolve(testConfig)); + entityMapper.load.and.resolveTo(testConfig); service.loadConfig(); tick(); const result = service.getAllConfigs("test:"); @@ -76,7 +87,7 @@ describe("ConfigService", () => { it("should return single field", fakeAsync(() => { const testConfig = new Config(); testConfig.data = { first: "correct", second: "wrong" }; - entityMapper.load.and.returnValue(Promise.resolve(testConfig)); + entityMapper.load.and.resolveTo(testConfig); service.loadConfig(); tick(); const result = service.getConfig("first"); @@ -92,12 +103,62 @@ describe("ConfigService", () => { expect(lastCall.data).toEqual({ test: "data" }); }); - it("should create export config string", () => { + it("should create export config string", fakeAsync(() => { const config = new Config(); config.data = { first: "foo", second: "bar" }; const expected = JSON.stringify(config.data); updateSubject.next({ entity: config, type: "update" }); + tick(); const result = service.exportConfig(); expect(result).toEqual(expected); + })); + + it("should save enum configs to db it they dont exist yet", async () => { + entityMapper.saveAll.and.resolveTo(); + const data = { + "enum:1": [{ id: "some_id", label: "Some Label" }], + "enum:two": [], + "some:other": {}, + }; + const enum1 = new ConfigurableEnum("1"); + enum1.values = data["enum:1"]; + const enumTwo = new ConfigurableEnum("two"); + enumTwo.values = []; + + await initConfig(data); + + expect(entityMapper.saveAll).toHaveBeenCalledWith([enum1, enumTwo]); + const config = entityMapper.save.calls.mostRecent().args[0] as Config; + expect(config.data).toEqual({ "some:other": {} }); }); + + it("should not fail config initialization if changed config cannot be saved", async () => { + entityMapper.saveAll.and.rejectWith(); + let configUpdate: Config; + service.configUpdates.subscribe((config) => (configUpdate = config)); + + await expectAsync(initConfig({ some: "config" })).toBeResolved(); + + expect(service.getConfig("some")).toBe("config"); + expect(configUpdate.data).toEqual({ some: "config" }); + }); + + it("should not save enums if they already exist in db", async () => { + entityMapper.loadType.and.resolveTo([new ConfigurableEnum()]); + entityMapper.save.and.resolveTo(); + + await initConfig({ "enum:1": [], some: "config" }); + + expect(entityMapper.saveAll).not.toHaveBeenCalled(); + expect(entityMapper.save).toHaveBeenCalledWith(jasmine.any(Config)); + expect(service.getConfig("enum:1")).toBeUndefined(); + expect(service.getConfig("some")).toBe("config"); + }); + + function initConfig(data) { + const config = new Config(); + config.data = data; + entityMapper.load.and.resolveTo(config); + return service.loadConfig(); + } }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 100830635b..5bea3d6fa4 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -35,7 +35,7 @@ export class ConfigService { } async loadConfig(): Promise { - this.entityMapper + return this.entityMapper .load(Config, Config.CONFIG_KEY) .then((config) => this.detectLegacyConfig(config)) .then((config) => this.updateConfigIfChanged(config)) @@ -50,16 +50,27 @@ export class ConfigService { } } - private saveAllEnumsToDB(config: Config) { + private async saveAllEnumsToDB(config: Config) { + const existingEnums = await this.entityMapper.loadType(ConfigurableEnum); + if (existingEnums.length > 0) { + Object.keys(config.data) + .filter((key) => key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) + .forEach((key) => delete config.data[key]); + return this.entityMapper.save(config).catch(() => {}); + } const enumEntities = Object.entries(config.data) .filter(([key]) => key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) .map(([key, value]) => { const id = key.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, ""); const newEnum = new ConfigurableEnum(id); newEnum.values = value as any; + delete config.data[key]; return newEnum; }); - return this.entityMapper.saveAll(enumEntities); + return this.entityMapper + .saveAll(enumEntities) + .then(() => this.entityMapper.save(config)) + .catch(() => {}); } public saveConfig(config: any): Promise { From a3408efd6829a65019dd9e4a9d88fdcde6f007b2 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 27 Jan 2023 17:10:08 +0100 Subject: [PATCH 27/83] added logic for showing usage of to-be-deleted enum option --- .../configure-enum-popup.component.spec.ts | 64 ++++++++++++++++++- .../configure-enum-popup.component.ts | 47 +++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts index 30183d5385..935d5c8cbf 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts @@ -1,14 +1,38 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ConfigureEnumPopupComponent } from "./configure-enum-popup.component"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { ConfigurableEnum } from "../configurable-enum"; +import { EntityMapperService } from "../../entity/entity-mapper.service"; +import { EMPTY } from "rxjs"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { + mockEntityMapper, + MockEntityMapperService, +} from "../../entity/mock-entity-mapper-service"; +import { genders } from "../../../child-dev-project/children/model/genders"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; +import { + entityRegistry, + EntityRegistry, +} from "../../entity/database-entity.decorator"; describe("ConfigureEnumPopupComponent", () => { let component: ConfigureEnumPopupComponent; let fixture: ComponentFixture; + let entityMapper: MockEntityMapperService; beforeEach(async () => { + entityMapper = mockEntityMapper(); await TestBed.configureTestingModule({ - declarations: [ConfigureEnumPopupComponent], + imports: [ConfigureEnumPopupComponent, FontAwesomeTestingModule], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: new ConfigurableEnum() }, + { provide: MatDialogRef, useValue: { afterClosed: () => EMPTY } }, + { provide: EntityMapperService, useValue: entityMapper }, + { provide: EntityRegistry, useValue: entityRegistry }, + ], }).compileComponents(); fixture = TestBed.createComponent(ConfigureEnumPopupComponent); @@ -19,4 +43,42 @@ describe("ConfigureEnumPopupComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + it("should show a popup if user tries to delete an enum that is still in use", async () => { + component.enumEntity = new ConfigurableEnum("genders"); + component.enumEntity.values = genders; + const male = genders.find((g) => g.id === "M"); + const female = genders.find((g) => g.id === "F"); + const m1 = new Child(); + m1.gender = male; + const m2 = new Child(); + m2.gender = male; + const f1 = new Child(); + f1.gender = female; + const other = new Child(); + entityMapper.addAll([m1, m2, f1, other]); + const confirmationSpy = spyOn( + TestBed.inject(ConfirmationDialogService), + "getConfirmation" + ); + + await component.delete(male, genders.indexOf(male)); + + expect(confirmationSpy).toHaveBeenCalledWith( + "Delete option", + jasmine.stringContaining( + `The option is still used in 2 ${Child.label} records.` + ) + ); + + entityMapper.delete(m1); + entityMapper.delete(m2); + + await component.delete(male, genders.indexOf(male)); + + expect(confirmationSpy).toHaveBeenCalledWith( + "Delete option", + `Are you sure that you want to delete the option ${male.label}?` + ); + }); }); diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 07e3b937aa..3f6deac6a0 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -21,6 +21,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { ConfigurableEnumValue } from "../configurable-enum.interface"; import { MatButtonModule } from "@angular/material/button"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; +import { EntityRegistry } from "../../entity/database-entity.decorator"; @Component({ selector: "app-configure-enum-popup", @@ -45,7 +46,8 @@ export class ConfigureEnumPopupComponent { @Inject(MAT_DIALOG_DATA) public enumEntity: ConfigurableEnum, private dialog: MatDialogRef, private entityMapper: EntityMapperService, - private confirmationService: ConfirmationDialogService + private confirmationService: ConfirmationDialogService, + private entities: EntityRegistry ) { this.dialog.afterClosed().subscribe((closeAndSave: boolean) => { if (closeAndSave) { @@ -63,13 +65,54 @@ export class ConfigureEnumPopupComponent { } async delete(value: ConfigurableEnumValue, index: number) { + const existingUsages = await this.getUsages(value); + let deletionText = $localize`Are you sure that you want to delete the option ${value.label}?`; + if (existingUsages.length > 0) { + deletionText += $localize` The option is still used in ${existingUsages.join( + ", " + )} records. If deleted, the records will not be lost but specially marked`; + } const confirmed = await this.confirmationService.getConfirmation( $localize`Delete option`, - $localize`Are you sure that you want to delete the option ${value.label}?` + deletionText ); if (confirmed) { this.enumEntity.values.splice(index, 1); await this.entityMapper.save(this.enumEntity); } } + + private async getUsages(value: ConfigurableEnumValue) { + const enumMap: { [key in string]: string[] } = {}; + for (const entity of this.entities.values()) { + const schemaFields = [...entity.schema.entries()] + .filter( + ([_, schema]) => + schema.innerDataType === this.enumEntity.getId() || + schema.additional === this.enumEntity.getId() + ) + .map(([name]) => name); + if (schemaFields.length > 0) { + enumMap[entity.ENTITY_TYPE] = schemaFields; + } + } + const entityPromises = Object.entries(enumMap).map(([entityType, props]) => + this.entityMapper + .loadType(entityType) + .then((res) => + res.filter((entity) => + props.some( + (prop) => + entity[prop] === value || entity[prop]?.includes?.(value) + ) + ) + ) + ); + const possibleEntities = await Promise.all(entityPromises); + return possibleEntities + .filter((entities) => entities.length > 0) + .map( + (entities) => `${entities.length} ${entities[0].getConstructor().label}` + ); + } } From 0e192748bc6b2b873246b127e8b869fb95c3553f Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 27 Jan 2023 17:25:00 +0100 Subject: [PATCH 28/83] removed unnecessary enum prefix --- .../basic-autocomplete/basic-autocomplete.component.html | 7 +++---- .../configurable-enum-datatype.ts | 9 +-------- .../configurable-enum.directive.ts | 5 ----- .../core/configurable-enum/configurable-enum.service.ts | 5 +---- 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index c1b9e7892d..810725a7a6 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -14,18 +14,17 @@ /> diff --git a/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts b/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts index c865e02e76..a2b6477db2 100644 --- a/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts +++ b/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts @@ -1,8 +1,5 @@ import { EntitySchemaDatatype } from "../../entity/schema/entity-schema-datatype"; -import { - CONFIGURABLE_ENUM_CONFIG_PREFIX, - ConfigurableEnumValue, -} from "../configurable-enum.interface"; +import { ConfigurableEnumValue } from "../configurable-enum.interface"; import { EntitySchemaField } from "../../entity/schema/entity-schema-field"; import { ConfigurableEnumService } from "../configurable-enum.service"; @@ -33,10 +30,6 @@ export class ConfigurableEnumDatatype schemaField: EntitySchemaField ): ConfigurableEnumValue { let enumId = schemaField.additional || schemaField.innerDataType; - if (!enumId.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) { - enumId = CONFIGURABLE_ENUM_CONFIG_PREFIX + enumId; - } - let enumOption = this.enumService .getEnumValues(enumId) ?.find((option) => option.id === value); diff --git a/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts b/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts index a28a826366..be4db3854d 100644 --- a/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts +++ b/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts @@ -1,5 +1,4 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core"; -import { CONFIGURABLE_ENUM_CONFIG_PREFIX } from "../configurable-enum.interface"; import { ConfigurableEnumService } from "../configurable-enum.service"; /** @@ -19,10 +18,6 @@ export class ConfigurableEnumDirective { * @param enumConfigId */ @Input() set appConfigurableEnumOf(enumConfigId: string) { - if (!enumConfigId.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) { - enumConfigId = CONFIGURABLE_ENUM_CONFIG_PREFIX + enumConfigId; - } - const options = this.enumService.getEnumValues(enumConfigId); for (const item of options) { this.viewContainerRef.createEmbeddedView(this.templateRef, { diff --git a/src/app/core/configurable-enum/configurable-enum.service.ts b/src/app/core/configurable-enum/configurable-enum.service.ts index 882b35a971..93c4e23e9a 100644 --- a/src/app/core/configurable-enum/configurable-enum.service.ts +++ b/src/app/core/configurable-enum/configurable-enum.service.ts @@ -2,10 +2,7 @@ import { Injectable } from "@angular/core"; import { ConfigService } from "../config/config.service"; import { ConfigurableEnum } from "./configurable-enum"; import { EntityMapperService } from "../entity/entity-mapper.service"; -import { - CONFIGURABLE_ENUM_CONFIG_PREFIX, - ConfigurableEnumValue, -} from "./configurable-enum.interface"; +import { ConfigurableEnumValue } from "./configurable-enum.interface"; import { Entity } from "../entity/model/entity"; @Injectable({ providedIn: "root" }) From f67703bfb66b9f234ee5d1a846c71cd887a6252a Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 27 Jan 2023 17:34:45 +0100 Subject: [PATCH 29/83] replaced save with close button --- .../configure-enum-popup.component.html | 17 +++++------------ .../configure-enum-popup.component.ts | 8 +++----- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html index a79d2c6801..9a15479ab0 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html @@ -1,7 +1,7 @@

Edit dropdown options

- + matIconPrefix class="grab-icon margin-right-small" > - + @@ -28,16 +28,9 @@

- diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 3f6deac6a0..4c91e94c27 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -49,11 +49,9 @@ export class ConfigureEnumPopupComponent { private confirmationService: ConfirmationDialogService, private entities: EntityRegistry ) { - this.dialog.afterClosed().subscribe((closeAndSave: boolean) => { - if (closeAndSave) { - this.entityMapper.save(this.enumEntity); - } - }); + this.dialog + .afterClosed() + .subscribe(() => this.entityMapper.save(this.enumEntity)); } drop(event: CdkDragDrop) { From 61222de0cfc455100ddbc07df14e4320e1176c2a Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 09:07:02 +0100 Subject: [PATCH 30/83] tests are runnning --- .../notes/model/note.spec.ts | 20 ++++++++-------- .../basic-autocomplete.component.spec.ts | 18 ++++++++------- .../configurable-enum.directive.spec.ts | 23 ++++++++----------- src/app/core/entity/model/entity.spec.ts | 8 +++---- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/app/child-dev-project/notes/model/note.spec.ts b/src/app/child-dev-project/notes/model/note.spec.ts index 2209d07770..6da654b968 100644 --- a/src/app/child-dev-project/notes/model/note.spec.ts +++ b/src/app/child-dev-project/notes/model/note.spec.ts @@ -78,16 +78,16 @@ describe("Note", () => { ]); beforeEach(waitForAsync(() => { - const testConfigs = {}; - testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + INTERACTION_TYPE_CONFIG_ID] = - testInteractionTypes; - testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + ATTENDANCE_STATUS_CONFIG_ID] = - testStatusTypes; - - entitySchemaService = new EntitySchemaService(); - entitySchemaService.registerSchemaDatatype( - new ConfigurableEnumDatatype(createTestingConfigService(testConfigs)) - ); + // const testConfigs = {}; + // testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + INTERACTION_TYPE_CONFIG_ID] = + // testInteractionTypes; + // testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + ATTENDANCE_STATUS_CONFIG_ID] = + // testStatusTypes; + // + // entitySchemaService = new EntitySchemaService(); + // entitySchemaService.registerSchemaDatatype( + // new ConfigurableEnumDatatype(createTestingConfigService(testConfigs)) + // ); })); testEntitySubclass("Note", Note, { diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index d63dfe9ccd..0979cb703c 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -51,14 +51,16 @@ describe("BasicAutocompleteComponent", () => { school1, school2, school3, - ]); + ] as any); component.updateAutocomplete("Aa"); expect(component.autocompleteSuggestedOptions.value).toEqual([ school1, school2, - ]); + ] as any); component.updateAutocomplete("Aab"); - expect(component.autocompleteSuggestedOptions.value).toEqual([school2]); + expect(component.autocompleteSuggestedOptions.value).toEqual([ + school2, + ] as any); }); it("should show name of the selected entity", fakeAsync(() => { @@ -75,7 +77,7 @@ describe("BasicAutocompleteComponent", () => { tick(); fixture.detectChanges(); - expect(component.inputValue).toBe(child1); + expect(component.inputValue).toBe(child1 as any); expect( fixture.debugElement.query(By.css("#inputElement")).nativeElement.value ).toEqual("First Child"); @@ -89,7 +91,7 @@ describe("BasicAutocompleteComponent", () => { component.select("First Child"); - expect(component.inputValue).toBe(child1); + expect(component.inputValue).toBe(child1 as any); expect(component.form.value).toBe(child1.getId()); }); @@ -99,12 +101,12 @@ describe("BasicAutocompleteComponent", () => { component.options = [first, second]; component.valueMapper = entityToId; - component.select(first); - expect(component.inputValue).toBe(first); + component.select(first as any); + expect(component.inputValue).toBe(first as any); expect(component.form.value).toBe(first.getId()); component.select("second"); - expect(component.inputValue).toBe(second); + expect(component.inputValue).toBe(second as any); expect(component.form.value).toBe(second.getId()); component.select("NonExistent"); diff --git a/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.spec.ts b/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.spec.ts index 7880a8f281..c40c42ff40 100644 --- a/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.spec.ts +++ b/src/app/core/configurable-enum/configurable-enum-directive/configurable-enum.directive.spec.ts @@ -1,23 +1,20 @@ import { ConfigurableEnumDirective } from "./configurable-enum.directive"; -import { ConfigService } from "../../config/config.service"; import { ViewContainerRef } from "@angular/core"; -import { - CONFIGURABLE_ENUM_CONFIG_PREFIX, - ConfigurableEnumConfig, -} from "../configurable-enum.interface"; +import { ConfigurableEnumConfig } from "../configurable-enum.interface"; +import { ConfigurableEnumService } from "../configurable-enum.service"; describe("ConfigurableEnumDirective", () => { let testTemplateRef; let mockViewContainerRef: jasmine.SpyObj; - let mockConfigService: jasmine.SpyObj; + let mockEnumService: jasmine.SpyObj; beforeEach(() => { testTemplateRef = {}; mockViewContainerRef = jasmine.createSpyObj("mockViewContainerRef", [ "createEmbeddedView", ]); - mockConfigService = jasmine.createSpyObj("mockConfigService", [ - "getConfig", + mockEnumService = jasmine.createSpyObj("mockConfigService", [ + "getEnumValues", ]); }); @@ -25,7 +22,7 @@ describe("ConfigurableEnumDirective", () => { const directive = new ConfigurableEnumDirective( testTemplateRef, mockViewContainerRef, - mockConfigService + mockEnumService ); expect(directive).toBeTruthy(); }); @@ -36,18 +33,18 @@ describe("ConfigurableEnumDirective", () => { { id: "1", label: "A" }, { id: "2", label: "B" }, ]; - mockConfigService.getConfig.and.returnValue(testEnumValues); + mockEnumService.getEnumValues.and.returnValue(testEnumValues); const directive = new ConfigurableEnumDirective( testTemplateRef, mockViewContainerRef, - mockConfigService + mockEnumService ); directive.appConfigurableEnumOf = testEnumConfigId; - expect(mockConfigService.getConfig).toHaveBeenCalledWith( - CONFIGURABLE_ENUM_CONFIG_PREFIX + testEnumConfigId + expect(mockEnumService.getEnumValues).toHaveBeenCalledWith( + testEnumConfigId ); expect(mockViewContainerRef.createEmbeddedView).toHaveBeenCalledTimes( testEnumValues.length diff --git a/src/app/core/entity/model/entity.spec.ts b/src/app/core/entity/model/entity.spec.ts index 487f2e0e8d..faf0f292e8 100644 --- a/src/app/core/entity/model/entity.spec.ts +++ b/src/app/core/entity/model/entity.spec.ts @@ -164,10 +164,10 @@ export function testEntitySubclass( it("should only load and store properties defined in the schema", () => { const schemaService = new EntitySchemaService(); - const configService = createTestingConfigService(); - schemaService.registerSchemaDatatype( - new ConfigurableEnumDatatype(configService) - ); + // const configService = createTestingConfigService(); + // schemaService.registerSchemaDatatype( + // new ConfigurableEnumDatatype(configService) + // ); const entity = new entityClass(); schemaService.loadDataIntoEntity( From 38363b61fa8fdd994433d8763f65769aa6dcd3f0 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 09:07:23 +0100 Subject: [PATCH 31/83] added todo for no selected option --- .../basic-autocomplete.component.html | 35 ++++++++++--------- .../basic-autocomplete.component.ts | 2 ++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 810725a7a6..f79a26800a 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -5,30 +5,15 @@ - - - - + + + + This field is required diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index ecc41a6a68..1e9be232eb 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -181,6 +181,7 @@ export class BasicAutocompleteComponent implements OnChanges { if (option) { this.selectOption(option); } else { + // TODO not automatically create option but only if clicked on purpose if (selected) { const newOption = this.toSelectableOption( this.createOption(selected as string) @@ -188,6 +189,7 @@ export class BasicAutocompleteComponent implements OnChanges { this._options.push(newOption); this.select(newOption); } else { + this.autocompleteForm.setValue(""); this._form.setValue(undefined); } } From 3e2a543ecb3a8daec3245b080b2d11f7a628f7a5 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 13:21:43 +0100 Subject: [PATCH 32/83] reset input if nothing is selected --- .../basic-autocomplete.component.html | 2 +- .../basic-autocomplete.component.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index f79a26800a..6ccb2df933 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -11,7 +11,7 @@ matInput style="text-overflow: ellipsis" [matAutocomplete]="autoSuggestions" - (focusout)="select(inputElement.value)" + (focusout)="resetIfInvalidOption(inputElement.value)" /> implements OnChanges { if (option) { this.selectOption(option); } else { - // TODO not automatically create option but only if clicked on purpose if (selected) { const newOption = this.toSelectableOption( this.createOption(selected as string) @@ -215,4 +214,14 @@ export class BasicAutocompleteComponent implements OnChanges { selected: false, }; } + + resetIfInvalidOption(input: string) { + // waiting for other tasks to finish and then reset input if nothing was selected + setTimeout(() => { + const activeOption = this._optionToString(this._form.value); + if (input !== activeOption) { + this.autocompleteForm.setValue(activeOption); + } + }); + } } From 132ef840b427e04b544413414725c2004a999697 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 13:32:28 +0100 Subject: [PATCH 33/83] working with ids instead of object equality --- .../configure-enum-popup/configure-enum-popup.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 4c91e94c27..149293ff28 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -101,7 +101,8 @@ export class ConfigureEnumPopupComponent { res.filter((entity) => props.some( (prop) => - entity[prop] === value || entity[prop]?.includes?.(value) + entity[prop]?.id === value?.id || + entity[prop]?.map?.((v) => v.id).includes(value.id) ) ) ) From a2b4e84bfc8c37a24ca87b557494454379e3ac53 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 13:47:45 +0100 Subject: [PATCH 34/83] cleaned up some code --- .../basic-autocomplete.component.ts | 18 ++++++------- .../configure-enum-popup.component.ts | 25 ++++++++++++------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index eadf0ad811..6c7394fd20 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -180,17 +180,15 @@ export class BasicAutocompleteComponent implements OnChanges { if (option) { this.selectOption(option); + } else if (selected) { + const newOption = this.toSelectableOption( + this.createOption(selected as string) + ); + this._options.push(newOption); + this.select(newOption); } else { - if (selected) { - const newOption = this.toSelectableOption( - this.createOption(selected as string) - ); - this._options.push(newOption); - this.select(newOption); - } else { - this.autocompleteForm.setValue(""); - this._form.setValue(undefined); - } + this.autocompleteForm.setValue(""); + this._form.setValue(undefined); } } diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 149293ff28..2c0d210cb3 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -22,6 +22,7 @@ import { ConfigurableEnumValue } from "../configurable-enum.interface"; import { MatButtonModule } from "@angular/material/button"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; import { EntityRegistry } from "../../entity/database-entity.decorator"; +import { Entity } from "../../entity/model/entity"; @Component({ selector: "app-configure-enum-popup", @@ -97,15 +98,7 @@ export class ConfigureEnumPopupComponent { const entityPromises = Object.entries(enumMap).map(([entityType, props]) => this.entityMapper .loadType(entityType) - .then((res) => - res.filter((entity) => - props.some( - (prop) => - entity[prop]?.id === value?.id || - entity[prop]?.map?.((v) => v.id).includes(value.id) - ) - ) - ) + .then((entities) => this.getEntitiesWithValue(entities, props, value)) ); const possibleEntities = await Promise.all(entityPromises); return possibleEntities @@ -114,4 +107,18 @@ export class ConfigureEnumPopupComponent { (entities) => `${entities.length} ${entities[0].getConstructor().label}` ); } + + private getEntitiesWithValue( + res: Entity[], + props: string[], + value: ConfigurableEnumValue + ) { + return res.filter((entity) => + props.some( + (prop) => + entity[prop]?.id === value?.id || + entity[prop]?.map?.((v) => v.id).includes(value.id) + ) + ); + } } From aa2e1817d684f99a23bae4923653bc67537e4b53 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 14:01:31 +0100 Subject: [PATCH 35/83] added confirmation dialog for creating a new option --- .../basic-autocomplete.component.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 6c7394fd20..3c20a70b55 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -24,6 +24,7 @@ import { BehaviorSubject } from "rxjs"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter } from "rxjs/operators"; +import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; interface SelectableOption { initial: O; @@ -114,7 +115,7 @@ export class BasicAutocompleteComponent implements OnChanges { addOptionTimeout: any; inputValue = ""; - constructor() { + constructor(private confirmation: ConfirmationDialogService) { this.autocompleteForm.valueChanges .pipe(filter((val) => typeof val === "string")) .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); @@ -169,29 +170,31 @@ export class BasicAutocompleteComponent implements OnChanges { } select(selected: string | SelectableOption) { - let option: SelectableOption; if (typeof selected === "string") { - option = this._options.find( - (o) => o.asString.toLowerCase() === selected.toLowerCase() - ); - } else { - option = selected; + this.createNewOption(selected); + return; } - if (option) { - this.selectOption(option); - } else if (selected) { - const newOption = this.toSelectableOption( - this.createOption(selected as string) - ); - this._options.push(newOption); - this.select(newOption); + if (selected) { + this.selectOption(selected); } else { this.autocompleteForm.setValue(""); this._form.setValue(undefined); } } + async createNewOption(option: string) { + const userConfirmed = await this.confirmation.getConfirmation( + $localize`Create new option`, + `Do you want to create the new option ${option}` + ); + if (userConfirmed) { + const newOption = this.toSelectableOption(this.createOption(option)); + this._options.push(newOption); + this.select(newOption); + } + } + private selectOption(option: SelectableOption) { if (this.multi) { option.selected = !option.selected; From 94199419b542b7f903fd363b0e74de60c09e434c Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 14:08:53 +0100 Subject: [PATCH 36/83] updated translations --- src/assets/locale/messages.de.xlf | 469 +++++++++++++++--------------- src/assets/locale/messages.fr.xlf | 467 ++++++++++++++--------------- src/assets/locale/messages.it.xlf | 469 +++++++++++++++--------------- src/assets/locale/messages.xlf | 460 ++++++++++++++--------------- 4 files changed, 955 insertions(+), 910 deletions(-) diff --git a/src/assets/locale/messages.de.xlf b/src/assets/locale/messages.de.xlf index c10d81bf91..4a086d39fc 100644 --- a/src/assets/locale/messages.de.xlf +++ b/src/assets/locale/messages.de.xlf @@ -1149,15 +1149,15 @@ src/app/core/config/config-fix.ts - 36 + 26 src/app/core/config/config-fix.ts - 593 + 526 src/app/core/config/config-fix.ts - 940 + 873 @@ -1411,6 +1411,22 @@ 75 + + Example form + Beispiel Formular + + src/app/features/public-form/demo-public-form-generator.service.ts + 18 + + + + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + Dieses Formular kann als Link geteilt oder auf einer Webseite eingebunden werden. Nutzer:innen können es ohne einen Account ausfüllen. Zum Beispiel können sich damit interessierte Teilnehmer:innen selbst registrieren und Sie können die Daten in Aam Digital überprüfen. + + src/app/features/public-form/demo-public-form-generator.service.ts + 19 + + Submit Form Abschicken @@ -1434,7 +1450,7 @@ Formular erfolgreich übermittelt src/app/features/public-form/public-form.component.ts - 58 + 62 @@ -1585,7 +1601,7 @@ Neue Daten importieren? src/app/features/data-import/data-import.service.ts - 75 + 76 @@ -1595,7 +1611,7 @@ Dadurch werden Einträge aus der Datei importiert. src/app/features/data-import/data-import.service.ts - 76 + 77 @@ -1603,7 +1619,7 @@ Alle existierende Einträge mit der TransactionID: '' werden gelöscht! src/app/features/data-import/data-import.service.ts - 79 + 80 @@ -1900,14 +1916,34 @@ 113 + + Select displayed locations + + Wähle angezeigte Orte aus + + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 1,3 + + Title of popup to select locations that are displayed in the map + + + Apply + Anwenden + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 18 + + Button for closing popup and applying changes + - km + km km e.g. 5 km distance with unit src/app/features/location/view-distance/view-distance.component.ts - 73 + 65 @@ -1922,11 +1958,11 @@ Select auswählen + header of section with entities available for selection src/app/features/matching-entities/matching-entities/matching-entities.component.html - 55,56 + 54 - header of section with entities available for selection create matching @@ -1934,7 +1970,7 @@ Matching button label src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 85 + 91 @@ -1943,7 +1979,7 @@ Matching View column name src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 273 + 312 @@ -2004,20 +2040,20 @@ Aam Digital - DEMO (automatically generated data) Aam Digital - DEMO (automatisch generierte Daten) + Page title src/app/core/config/config-fix.ts - 22 + 12 - Page title Dashboard Dashboard + Menu item src/app/core/config/config-fix.ts - 31 + 21 - Menu item Schools @@ -2029,11 +2065,11 @@ src/app/core/config/config-fix.ts - 41 + 31 src/app/core/config/config-fix.ts - 409 + 342 @@ -2042,25 +2078,25 @@ Menu item src/app/core/config/config-fix.ts - 46 + 36 src/app/core/config/config-fix.ts - 691 + 624 Tasks Aufgaben + Menu item src/app/core/config/config-fix.ts - 56 + 46 src/app/features/todos/model/todo.ts 32 - Menu item Admin @@ -2068,7 +2104,7 @@ Menu item src/app/core/config/config-fix.ts - 61 + 51 @@ -2077,7 +2113,7 @@ Menu item src/app/core/config/config-fix.ts - 66 + 56 @@ -2086,7 +2122,7 @@ Menu item src/app/core/config/config-fix.ts - 71 + 61 src/app/core/user/user.ts @@ -2099,7 +2135,7 @@ Menu item src/app/core/config/config-fix.ts - 76 + 66 @@ -2108,7 +2144,7 @@ Menu item src/app/core/config/config-fix.ts - 81 + 71 @@ -2117,61 +2153,7 @@ Menu item src/app/core/config/config-fix.ts - 86 - - - - OK (copy with us) - OK (Kopie eingereicht) - Document status - - src/app/core/config/config-fix.ts - 109 - - - - OK (copy needed for us) - OK (Kopie fehlt noch) - Document status - - src/app/core/config/config-fix.ts - 114 - - - - needs correction - benötigt Korrektur - Document status - - src/app/core/config/config-fix.ts - 119 - - - - applied - beantragt - Document status - - src/app/core/config/config-fix.ts - 124 - - - - doesn't have - nicht vorhanden - Document status - - src/app/core/config/config-fix.ts - 129 - - - - not eligible - nicht berechtigt - Document status - - src/app/core/config/config-fix.ts - 134 + 76 @@ -2181,7 +2163,7 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 163 + 91 @@ -2191,16 +2173,26 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 168 + 96 + + Public Registration Form + Öffentliches Regstrierungsformular + + src/app/core/config/config-fix.ts + 101 + + open public form + Dashboard shortcut widget + last week letzte Woche Attendance week dashboard widget label src/app/core/config/config-fix.ts - 215 + 148 @@ -2209,7 +2201,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 222 + 155 @@ -2218,7 +2210,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 208 + 141 @@ -2227,7 +2219,7 @@ Title for notes overview src/app/core/config/config-fix.ts - 244 + 177 @@ -2236,11 +2228,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 254 + 187 src/app/core/config/config-fix.ts - 258 + 191 @@ -2249,19 +2241,19 @@ Translated name of mobile column group src/app/core/config/config-fix.ts - 255 + 188 src/app/core/config/config-fix.ts - 268 + 201 src/app/core/config/config-fix.ts - 525 + 458 src/app/core/config/config-fix.ts - 579 + 512 @@ -2270,7 +2262,7 @@ Panel title src/app/core/config/config-fix.ts - 355 + 288 @@ -2279,7 +2271,7 @@ Panel title src/app/core/config/config-fix.ts - 374 + 307 @@ -2288,7 +2280,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 - 388 + 321 @@ -2297,7 +2289,7 @@ Label for private schools filter - true case src/app/core/config/config-fix.ts - 410 + 343 @@ -2306,7 +2298,7 @@ Label for private schools filter - false case src/app/core/config/config-fix.ts - 411 + 344 @@ -2315,15 +2307,15 @@ Panel title src/app/core/config/config-fix.ts - 423 + 356 src/app/core/config/config-fix.ts - 615 + 548 src/app/core/config/config-fix.ts - 798 + 731 @@ -2332,7 +2324,7 @@ Panel title src/app/core/config/config-fix.ts - 451 + 384 @@ -2341,7 +2333,7 @@ Panel title src/app/core/config/config-fix.ts - 460 + 393 @@ -2350,7 +2342,7 @@ Column label for age of child src/app/core/config/config-fix.ts - 482 + 415 @@ -2359,7 +2351,7 @@ Column label for school attendance of child src/app/core/config/config-fix.ts - 500 + 433 @@ -2368,7 +2360,7 @@ Column label for coaching attendance of child src/app/core/config/config-fix.ts - 509 + 442 @@ -2377,11 +2369,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 524 + 457 src/app/core/config/config-fix.ts - 528 + 461 @@ -2390,7 +2382,7 @@ Column group name src/app/core/config/config-fix.ts - 541 + 474 @@ -2399,11 +2391,11 @@ Column group name src/app/core/config/config-fix.ts - 564 + 497 src/app/core/config/config-fix.ts - 713 + 646 @@ -2412,7 +2404,7 @@ Active children filter label - true case src/app/core/config/config-fix.ts - 594 + 527 @@ -2421,7 +2413,7 @@ Active children filter label - false case src/app/core/config/config-fix.ts - 595 + 528 @@ -2430,7 +2422,7 @@ Header for form section src/app/core/config/config-fix.ts - 643 + 576 @@ -2439,7 +2431,7 @@ Header for form section src/app/core/config/config-fix.ts - 644 + 577 @@ -2448,7 +2440,7 @@ Header for form section src/app/core/config/config-fix.ts - 645 + 578 @@ -2457,7 +2449,7 @@ Panel title src/app/core/config/config-fix.ts - 652 + 585 @@ -2466,7 +2458,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 655 + 588 @@ -2475,7 +2467,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 675 + 608 @@ -2484,17 +2476,17 @@ Child details section title src/app/core/config/config-fix.ts - 679 + 612 Notes & Tasks Notizen & Aufgaben + Panel title src/app/core/config/config-fix.ts - 700 + 633 - Panel title Height & Weight Tracking @@ -2502,7 +2494,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 726 + 659 @@ -2511,7 +2503,7 @@ Panel title src/app/core/config/config-fix.ts - 732 + 665 @@ -2520,7 +2512,7 @@ Panel title src/app/core/config/config-fix.ts - 741 + 674 @@ -2538,7 +2530,7 @@ Panel title src/app/core/config/config-fix.ts - 828 + 761 @@ -2547,7 +2539,7 @@ Name of a report src/app/core/config/config-fix.ts - 843 + 776 @@ -2556,7 +2548,7 @@ Label of report query src/app/core/config/config-fix.ts - 847 + 780 @@ -2565,7 +2557,7 @@ Label for report query src/app/core/config/config-fix.ts - 852 + 785 @@ -2574,7 +2566,7 @@ Label for report query src/app/core/config/config-fix.ts - 855 + 788 @@ -2583,7 +2575,7 @@ Label for report query src/app/core/config/config-fix.ts - 859 + 792 @@ -2592,7 +2584,7 @@ Label for report query src/app/core/config/config-fix.ts - 864 + 797 @@ -2601,7 +2593,7 @@ Label for report query src/app/core/config/config-fix.ts - 868 + 801 @@ -2610,7 +2602,7 @@ Label for report query src/app/core/config/config-fix.ts - 873 + 806 @@ -2619,7 +2611,7 @@ Name of a report src/app/core/config/config-fix.ts - 881 + 814 @@ -2628,7 +2620,7 @@ Name of a report src/app/core/config/config-fix.ts - 898 + 831 @@ -2637,15 +2629,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 5 - - - src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 6 - - - src/app/core/config/config-fix.ts - 141 + 4 @@ -2654,11 +2638,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 7 - - - src/app/core/config/config-fix.ts - 145 + 5 @@ -2667,11 +2647,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 8 - - - src/app/core/config/config-fix.ts - 149 + 6 @@ -2763,7 +2739,7 @@ src/app/core/config/config-fix.ts - 758 + 691 @@ -2776,7 +2752,7 @@ src/app/core/config/config-fix.ts - 1021 + 954 @@ -2898,7 +2874,7 @@ src/app/core/config/config-fix.ts - 51 + 41 @@ -3019,15 +2995,15 @@ src/app/core/config/config-fix.ts - 477 + 410 src/app/core/config/config-fix.ts - 909 + 842 src/app/core/config/config-fix.ts - 993 + 926 @@ -3052,11 +3028,11 @@ src/app/core/config/config-fix.ts - 412 + 345 src/app/core/config/config-fix.ts - 596 + 529 src/app/core/entity-components/entity-list/filter-generator.service.ts @@ -3137,7 +3113,7 @@ src/app/core/config/config-fix.ts - 1035 + 968 @@ -3401,7 +3377,7 @@ src/app/core/config/config-fix.ts - 488 + 421 @@ -3554,11 +3530,11 @@ src/app/core/config/config-fix.ts - 493 + 426 src/app/core/config/config-fix.ts - 604 + 537 @@ -3682,11 +3658,11 @@ src/app/core/config/config-fix.ts - 813 + 746 src/app/core/config/config-fix.ts - 891 + 824 @@ -3788,11 +3764,11 @@ src/app/core/config/config-fix.ts - 280 + 213 src/app/core/config/config-fix.ts - 554 + 487 @@ -3841,7 +3817,7 @@ src/app/core/config/config-fix.ts - 939 + 872 @@ -3854,7 +3830,7 @@ src/app/core/config/config-fix.ts - 518 + 451 @@ -3876,7 +3852,7 @@ src/app/core/config/config-fix.ts - 886 + 819 @@ -4089,11 +4065,11 @@ Label for the address of a child src/app/core/config/config-fix.ts - 946 + 879 src/app/core/config/config-fix.ts - 1014 + 947 @@ -4102,7 +4078,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 953 + 886 @@ -4111,7 +4087,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 960 + 893 @@ -4120,7 +4096,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 967 + 900 @@ -4129,7 +4105,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 968 + 901 @@ -4138,7 +4114,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 975 + 908 @@ -4147,7 +4123,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 982 + 915 @@ -4156,7 +4132,7 @@ Label for if a school is a private school src/app/core/config/config-fix.ts - 1000 + 933 @@ -4165,7 +4141,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 1007 + 940 @@ -4174,7 +4150,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 1028 + 961 @@ -4183,7 +4159,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1047 + 980 @@ -4192,7 +4168,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1048 + 981 @@ -4201,7 +4177,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1056 + 989 @@ -4210,7 +4186,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1057 + 990 @@ -4219,7 +4195,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1065 + 998 @@ -4228,7 +4204,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1066 + 999 @@ -4237,7 +4213,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1074 + 1007 @@ -4246,7 +4222,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1075 + 1008 @@ -4255,7 +4231,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1083 + 1016 @@ -4264,7 +4240,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1084 + 1017 @@ -4273,7 +4249,7 @@ Label of user phone src/app/core/config/config-fix.ts - 1095 + 1028 @@ -4288,11 +4264,22 @@ [invalid option] [ungültige Option] + enum option label prefix for invalid id dummy src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts - 61 + 53 - enum option label prefix for invalid id dummy + + + Edit dropdown options + + Bearbeite Auswahl-Optionen + + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 2,3 + + title of dropdown options popup dialog Total @@ -4300,7 +4287,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 913 + 846 @@ -4309,7 +4296,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 917 + 850 @@ -4318,7 +4305,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 921 + 854 @@ -4327,7 +4314,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 925 + 858 @@ -4525,7 +4512,7 @@ src/app/features/data-import/data-import.service.ts - 60 + 61 @@ -4715,12 +4702,8 @@ Dieses Feld ist erforderlich Error message for any input - src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html - 21 - - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html - 62 + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html + 57 src/app/core/entity-components/entity-select/edit-text-with-autocomplete/edit-text-with-autocomplete.component.html @@ -4731,6 +4714,14 @@ 23 + + Create new option + Erstelle eine neue Option + + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts + 188 + + OK OK @@ -4802,16 +4793,6 @@ 16 - - Select - auswählen - context Select User - Placeholder for input to set an entity - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts - 62 - - Creating new record. Erstelle neuen Eintrag. @@ -4897,7 +4878,7 @@ Daten vorbereiten (indizieren) src/app/core/entity/database-indexing/database-indexing.service.ts - 60 + 59 @@ -4929,18 +4910,50 @@ Close Schließen - Generic close button + Close popup + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 34 + src/app/core/latest-changes/changelog/changelog.component.html 54 + + Are you sure that you want to delete the option ? + Sind Sie sicher, dass Sie die Option löschen wollen? + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 68 + + + + The option is still used in records. If deleted, the records will not be lost but specially marked + Die Option wird noch in Einträgen genutzt. Falls Sie die Option dennoch löschen, werden die Einträge nicht gelöscht aber gesondert markiert. + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 70,72 + + + + Delete option + Option Löschen + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 75 + + No Changelog Available Kein Changelog verfügbar src/app/core/latest-changes/changelog/changelog.component.ts - 87 + 89 @@ -4948,7 +4961,7 @@ Neuste Änderungen konnten nicht geladen werden: src/app/core/latest-changes/latest-changes.service.ts - 131 + 121 @@ -5181,7 +5194,7 @@ Navigate to user profile page src/app/core/ui/ui/ui.component.html - 84 + 85 @@ -5190,7 +5203,7 @@ Sign out of the app src/app/core/ui/ui/ui.component.html - 89 + 90 @@ -5461,7 +5474,7 @@ Import abgeschlossen src/app/features/data-import/data-import.service.ts - 59 + 60 diff --git a/src/assets/locale/messages.fr.xlf b/src/assets/locale/messages.fr.xlf index bb97a4b3cb..a9be18cbac 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 - 1035 + 968 @@ -244,7 +244,7 @@ src/app/core/config/config-fix.ts - 886 + 819 @@ -741,11 +741,11 @@ src/app/core/config/config-fix.ts - 493 + 426 src/app/core/config/config-fix.ts - 604 + 537 @@ -869,11 +869,11 @@ src/app/core/config/config-fix.ts - 813 + 746 src/app/core/config/config-fix.ts - 891 + 824 @@ -956,11 +956,11 @@ src/app/core/config/config-fix.ts - 412 + 345 src/app/core/config/config-fix.ts - 596 + 529 src/app/core/entity-components/entity-list/filter-generator.service.ts @@ -1045,7 +1045,7 @@ src/app/core/config/config-fix.ts - 758 + 691 @@ -1054,15 +1054,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 5 - - - src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 6 - - - src/app/core/config/config-fix.ts - 141 + 4 @@ -1071,11 +1063,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 7 - - - src/app/core/config/config-fix.ts - 145 + 5 @@ -1084,11 +1072,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 8 - - - src/app/core/config/config-fix.ts - 149 + 6 @@ -1180,15 +1164,15 @@ src/app/core/config/config-fix.ts - 477 + 410 src/app/core/config/config-fix.ts - 909 + 842 src/app/core/config/config-fix.ts - 993 + 926 @@ -1268,11 +1252,11 @@ src/app/core/config/config-fix.ts - 280 + 213 src/app/core/config/config-fix.ts - 554 + 487 @@ -1321,7 +1305,7 @@ src/app/core/config/config-fix.ts - 1021 + 954 @@ -1334,7 +1318,7 @@ src/app/core/config/config-fix.ts - 939 + 872 @@ -1347,7 +1331,7 @@ src/app/core/config/config-fix.ts - 488 + 421 @@ -1641,7 +1625,7 @@ src/app/core/config/config-fix.ts - 518 + 451 @@ -2201,15 +2185,15 @@ src/app/core/config/config-fix.ts - 36 + 26 src/app/core/config/config-fix.ts - 593 + 526 src/app/core/config/config-fix.ts - 940 + 873 @@ -2239,7 +2223,7 @@ src/app/core/config/config-fix.ts - 51 + 41 @@ -2647,6 +2631,22 @@ 75 + + Example form + Example form + + src/app/features/public-form/demo-public-form-generator.service.ts + 18 + + + + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + + src/app/features/public-form/demo-public-form-generator.service.ts + 19 + + Submit Form Submit Form @@ -2670,7 +2670,7 @@ Successfully submitted form src/app/features/public-form/public-form.component.ts - 58 + 62 @@ -3011,11 +3011,11 @@ Aam Digital - DEMO (automatically generated data) Aam Digital - DEMO (automatically generated data) + Page title src/app/core/config/config-fix.ts - 22 + 12 - Page title Dashboard @@ -3023,7 +3023,7 @@ Menu item src/app/core/config/config-fix.ts - 31 + 21 @@ -3036,11 +3036,11 @@ src/app/core/config/config-fix.ts - 41 + 31 src/app/core/config/config-fix.ts - 409 + 342 @@ -3058,7 +3058,7 @@ Menu item src/app/core/config/config-fix.ts - 61 + 51 @@ -3067,7 +3067,7 @@ Menu item src/app/core/config/config-fix.ts - 66 + 56 @@ -3076,7 +3076,7 @@ Menu item src/app/core/config/config-fix.ts - 71 + 61 src/app/core/user/user.ts @@ -3089,7 +3089,7 @@ Menu item src/app/core/config/config-fix.ts - 76 + 66 @@ -3098,7 +3098,7 @@ Menu item src/app/core/config/config-fix.ts - 81 + 71 @@ -3107,61 +3107,7 @@ Menu item src/app/core/config/config-fix.ts - 86 - - - - OK (copy with us) - OK (copie avec nous) - Document status - - src/app/core/config/config-fix.ts - 109 - - - - OK (copy needed for us) - OK (nous avons besoin de la copie) - Document status - - src/app/core/config/config-fix.ts - 114 - - - - needs correction - besoin de correction - Document status - - src/app/core/config/config-fix.ts - 119 - - - - applied - Remis - Document status - - src/app/core/config/config-fix.ts - 124 - - - - doesn't have - n'a pas - Document status - - src/app/core/config/config-fix.ts - 129 - - - - not eligible - non éligible - Document status - - src/app/core/config/config-fix.ts - 134 + 76 @@ -3171,7 +3117,7 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 163 + 91 @@ -3181,8 +3127,18 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 168 + 96 + + + + Public Registration Form + Public Registration Form + + src/app/core/config/config-fix.ts + 101 + open public form + Dashboard shortcut widget last week @@ -3190,7 +3146,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 215 + 148 @@ -3199,7 +3155,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 222 + 155 @@ -3208,7 +3164,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 208 + 141 @@ -3217,7 +3173,7 @@ Title for notes overview src/app/core/config/config-fix.ts - 244 + 177 @@ -3226,11 +3182,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 254 + 187 src/app/core/config/config-fix.ts - 258 + 191 @@ -3239,19 +3195,19 @@ Translated name of mobile column group src/app/core/config/config-fix.ts - 255 + 188 src/app/core/config/config-fix.ts - 268 + 201 src/app/core/config/config-fix.ts - 525 + 458 src/app/core/config/config-fix.ts - 579 + 512 @@ -3260,7 +3216,7 @@ Panel title src/app/core/config/config-fix.ts - 355 + 288 @@ -3269,7 +3225,7 @@ Panel title src/app/core/config/config-fix.ts - 374 + 307 @@ -3278,7 +3234,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 - 388 + 321 @@ -3287,7 +3243,7 @@ Label for private schools filter - true case src/app/core/config/config-fix.ts - 410 + 343 @@ -3296,7 +3252,7 @@ Label for private schools filter - false case src/app/core/config/config-fix.ts - 411 + 344 @@ -3305,7 +3261,7 @@ Label for if a school is a private school src/app/core/config/config-fix.ts - 1000 + 933 @@ -3314,15 +3270,15 @@ Panel title src/app/core/config/config-fix.ts - 423 + 356 src/app/core/config/config-fix.ts - 615 + 548 src/app/core/config/config-fix.ts - 798 + 731 @@ -3331,7 +3287,7 @@ Panel title src/app/core/config/config-fix.ts - 451 + 384 @@ -3340,7 +3296,7 @@ Panel title src/app/core/config/config-fix.ts - 460 + 393 @@ -3349,7 +3305,7 @@ Column label for age of child src/app/core/config/config-fix.ts - 482 + 415 @@ -3358,7 +3314,7 @@ Column label for school attendance of child src/app/core/config/config-fix.ts - 500 + 433 @@ -3367,7 +3323,7 @@ Column label for coaching attendance of child src/app/core/config/config-fix.ts - 509 + 442 @@ -3376,7 +3332,7 @@ Column group name src/app/core/config/config-fix.ts - 541 + 474 @@ -3385,11 +3341,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 524 + 457 src/app/core/config/config-fix.ts - 528 + 461 @@ -3398,11 +3354,11 @@ Column group name src/app/core/config/config-fix.ts - 564 + 497 src/app/core/config/config-fix.ts - 713 + 646 @@ -3411,7 +3367,7 @@ Active children filter label - true case src/app/core/config/config-fix.ts - 594 + 527 @@ -3420,7 +3376,7 @@ Active children filter label - false case src/app/core/config/config-fix.ts - 595 + 528 @@ -3429,7 +3385,7 @@ Header for form section src/app/core/config/config-fix.ts - 643 + 576 @@ -3438,7 +3394,7 @@ Header for form section src/app/core/config/config-fix.ts - 644 + 577 @@ -3447,7 +3403,7 @@ Header for form section src/app/core/config/config-fix.ts - 645 + 578 @@ -3456,7 +3412,7 @@ Panel title src/app/core/config/config-fix.ts - 652 + 585 @@ -3465,7 +3421,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 655 + 588 @@ -3474,7 +3430,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 675 + 608 @@ -3483,17 +3439,17 @@ Child details section title src/app/core/config/config-fix.ts - 679 + 612 Notes & Tasks Notes & Tasks + Panel title src/app/core/config/config-fix.ts - 700 + 633 - Panel title Attendance @@ -3501,25 +3457,25 @@ Menu item src/app/core/config/config-fix.ts - 46 + 36 src/app/core/config/config-fix.ts - 691 + 624 Tasks Tasks + Menu item src/app/core/config/config-fix.ts - 56 + 46 src/app/features/todos/model/todo.ts 32 - Menu item Height & Weight Tracking @@ -3527,7 +3483,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 726 + 659 @@ -3536,7 +3492,7 @@ Panel title src/app/core/config/config-fix.ts - 732 + 665 @@ -3545,7 +3501,7 @@ Panel title src/app/core/config/config-fix.ts - 741 + 674 @@ -3554,7 +3510,7 @@ Panel title src/app/core/config/config-fix.ts - 828 + 761 @@ -3563,7 +3519,7 @@ Name of a report src/app/core/config/config-fix.ts - 843 + 776 @@ -3572,7 +3528,7 @@ Label of report query src/app/core/config/config-fix.ts - 847 + 780 @@ -3581,7 +3537,7 @@ Label for report query src/app/core/config/config-fix.ts - 852 + 785 @@ -3590,7 +3546,7 @@ Label for report query src/app/core/config/config-fix.ts - 855 + 788 @@ -3599,7 +3555,7 @@ Label for report query src/app/core/config/config-fix.ts - 859 + 792 @@ -3608,7 +3564,7 @@ Label for report query src/app/core/config/config-fix.ts - 864 + 797 @@ -3617,7 +3573,7 @@ Label for report query src/app/core/config/config-fix.ts - 868 + 801 @@ -3626,7 +3582,7 @@ Label for report query src/app/core/config/config-fix.ts - 873 + 806 @@ -3635,7 +3591,7 @@ Name of a report src/app/core/config/config-fix.ts - 881 + 814 @@ -3644,7 +3600,7 @@ Name of a report src/app/core/config/config-fix.ts - 898 + 831 @@ -3653,7 +3609,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 913 + 846 @@ -3662,7 +3618,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 917 + 850 @@ -3671,7 +3627,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 921 + 854 @@ -3680,7 +3636,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 925 + 858 @@ -3689,11 +3645,11 @@ Label for the address of a child src/app/core/config/config-fix.ts - 946 + 879 src/app/core/config/config-fix.ts - 1014 + 947 @@ -3702,7 +3658,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 953 + 886 @@ -3711,7 +3667,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 960 + 893 @@ -3720,7 +3676,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 967 + 900 @@ -3729,7 +3685,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 968 + 901 @@ -3738,7 +3694,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 975 + 908 @@ -3747,7 +3703,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 982 + 915 @@ -3756,7 +3712,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 1007 + 940 @@ -3765,7 +3721,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 1028 + 961 @@ -3774,7 +3730,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1047 + 980 @@ -3783,7 +3739,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1048 + 981 @@ -3792,7 +3748,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1056 + 989 @@ -3801,7 +3757,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1057 + 990 @@ -3810,7 +3766,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1065 + 998 @@ -3819,7 +3775,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1066 + 999 @@ -3828,7 +3784,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1074 + 1007 @@ -3837,7 +3793,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1075 + 1008 @@ -3846,7 +3802,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1083 + 1016 @@ -3855,7 +3811,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1084 + 1017 @@ -3864,7 +3820,7 @@ Label of user phone src/app/core/config/config-fix.ts - 1095 + 1028 @@ -3973,23 +3929,30 @@ [invalid option] [invalid option] + enum option label prefix for invalid id dummy src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts - 61 + 53 - enum option label prefix for invalid id dummy + + + Edit dropdown options + + Edit dropdown options + + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 2,3 + + title of dropdown options popup dialog This field is required Ce champ doit être rempli Error message for any input - src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html - 21 - - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html - 62 + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html + 57 src/app/core/entity-components/entity-select/edit-text-with-autocomplete/edit-text-with-autocomplete.component.html @@ -4000,6 +3963,14 @@ 23 + + Create new option + Create new option + + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts + 188 + + OK OK @@ -4372,16 +4343,6 @@ 16 - - Select - Sélectionner - context Select User - Placeholder for input to set an entity - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts - 62 - - Creating new record. Creating new record. @@ -4468,7 +4429,7 @@ Préparation des données (Indexation) src/app/core/entity/database-indexing/database-indexing.service.ts - 60 + 59 @@ -4508,7 +4469,7 @@ src/app/features/data-import/data-import.service.ts - 60 + 61 @@ -4549,18 +4510,50 @@ Close Fermer - Generic close button + Close popup + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 34 + src/app/core/latest-changes/changelog/changelog.component.html 54 + + Are you sure that you want to delete the option ? + Are you sure that you want to delete the option ? + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 68 + + + + The option is still used in records. If deleted, the records will not be lost but specially marked + The option is still used in records. If deleted, the records will not be lost but specially marked + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 70,72 + + + + Delete option + Delete option + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 75 + + No Changelog Available Aucun historique des modifications disponible src/app/core/latest-changes/changelog/changelog.component.ts - 87 + 89 @@ -4568,7 +4561,7 @@ Les dernières modifications n'ont pas pu être chargées: src/app/core/latest-changes/latest-changes.service.ts - 131 + 121 @@ -4858,7 +4851,7 @@ Navigate to user profile page src/app/core/ui/ui/ui.component.html - 84 + 85 @@ -4867,7 +4860,7 @@ Sign out of the app src/app/core/ui/ui/ui.component.html - 89 + 90 @@ -5129,7 +5122,7 @@ Import completed src/app/features/data-import/data-import.service.ts - 59 + 60 @@ -5167,7 +5160,7 @@ Importer de nouvelles données ? src/app/features/data-import/data-import.service.ts - 75 + 76 @@ -5177,7 +5170,7 @@ This will add or update records from the loaded file. src/app/features/data-import/data-import.service.ts - 76 + 77 @@ -5185,7 +5178,7 @@ All existing records imported with the transaction id '' will be deleted! src/app/features/data-import/data-import.service.ts - 79 + 80 @@ -5482,14 +5475,34 @@ 113 + + Select displayed locations + + Select displayed locations + + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 1,3 + + Title of popup to select locations that are displayed in the map + + + Apply + Apply + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 18 + + Button for closing popup and applying changes + - km + km km e.g. 5 km distance with unit src/app/features/location/view-distance/view-distance.component.ts - 73 + 65 @@ -5504,11 +5517,11 @@ Select Select + header of section with entities available for selection src/app/features/matching-entities/matching-entities/matching-entities.component.html - 55,56 + 54 - header of section with entities available for selection create matching @@ -5516,7 +5529,7 @@ Matching button label src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 85 + 91 @@ -5525,7 +5538,7 @@ Matching View column name src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 273 + 312 diff --git a/src/assets/locale/messages.it.xlf b/src/assets/locale/messages.it.xlf index a4ff7a3d8f..eaf962f429 100644 --- a/src/assets/locale/messages.it.xlf +++ b/src/assets/locale/messages.it.xlf @@ -63,7 +63,7 @@ src/app/core/config/config-fix.ts - 886 + 819 @@ -662,11 +662,11 @@ src/app/core/config/config-fix.ts - 813 + 746 src/app/core/config/config-fix.ts - 891 + 824 @@ -783,7 +783,7 @@ src/app/core/config/config-fix.ts - 1035 + 968 @@ -1024,7 +1024,7 @@ src/app/core/config/config-fix.ts - 758 + 691 @@ -1033,15 +1033,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 5 - - - src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 6 - - - src/app/core/config/config-fix.ts - 141 + 4 @@ -1050,11 +1042,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 7 - - - src/app/core/config/config-fix.ts - 145 + 5 @@ -1063,11 +1051,7 @@ center src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 8 - - - src/app/core/config/config-fix.ts - 149 + 6 @@ -1342,7 +1326,7 @@ src/app/core/config/config-fix.ts - 518 + 451 @@ -1395,15 +1379,15 @@ src/app/core/config/config-fix.ts - 477 + 410 src/app/core/config/config-fix.ts - 909 + 842 src/app/core/config/config-fix.ts - 993 + 926 @@ -1483,11 +1467,11 @@ src/app/core/config/config-fix.ts - 280 + 213 src/app/core/config/config-fix.ts - 554 + 487 @@ -1536,7 +1520,7 @@ src/app/core/config/config-fix.ts - 1021 + 954 @@ -1549,7 +1533,7 @@ src/app/core/config/config-fix.ts - 939 + 872 @@ -1570,11 +1554,11 @@ src/app/core/config/config-fix.ts - 493 + 426 src/app/core/config/config-fix.ts - 604 + 537 @@ -1587,7 +1571,7 @@ src/app/core/config/config-fix.ts - 488 + 421 @@ -2193,15 +2177,15 @@ src/app/core/config/config-fix.ts - 36 + 26 src/app/core/config/config-fix.ts - 593 + 526 src/app/core/config/config-fix.ts - 940 + 873 @@ -2231,7 +2215,7 @@ src/app/core/config/config-fix.ts - 51 + 41 @@ -2356,11 +2340,11 @@ src/app/core/config/config-fix.ts - 412 + 345 src/app/core/config/config-fix.ts - 596 + 529 src/app/core/entity-components/entity-list/filter-generator.service.ts @@ -2725,20 +2709,20 @@ Aam Digital - DEMO (automatically generated data) Aam Digital - DEMO (automatically generated data) + Page title src/app/core/config/config-fix.ts - 22 + 12 - Page title Dashboard Dashboard + Menu item src/app/core/config/config-fix.ts - 31 + 21 - Menu item Schools @@ -2750,11 +2734,11 @@ src/app/core/config/config-fix.ts - 41 + 31 src/app/core/config/config-fix.ts - 409 + 342 @@ -2763,25 +2747,25 @@ Menu item src/app/core/config/config-fix.ts - 46 + 36 src/app/core/config/config-fix.ts - 691 + 624 Tasks Tasks + Menu item src/app/core/config/config-fix.ts - 56 + 46 src/app/features/todos/model/todo.ts 32 - Menu item Admin @@ -2789,7 +2773,7 @@ Menu item src/app/core/config/config-fix.ts - 61 + 51 @@ -2798,7 +2782,7 @@ Menu item src/app/core/config/config-fix.ts - 66 + 56 @@ -2807,7 +2791,7 @@ Menu item src/app/core/config/config-fix.ts - 71 + 61 src/app/core/user/user.ts @@ -2821,7 +2805,7 @@ Menu item src/app/core/config/config-fix.ts - 76 + 66 @@ -2830,7 +2814,7 @@ Menu item src/app/core/config/config-fix.ts - 81 + 71 @@ -2839,61 +2823,7 @@ Menu item src/app/core/config/config-fix.ts - 86 - - - - OK (copy with us) - OK (copy with us) - Document status - - src/app/core/config/config-fix.ts - 109 - - - - OK (copy needed for us) - OK (copy needed for us) - Document status - - src/app/core/config/config-fix.ts - 114 - - - - needs correction - needs correction - Document status - - src/app/core/config/config-fix.ts - 119 - - - - applied - applied - Document status - - src/app/core/config/config-fix.ts - 124 - - - - doesn't have - doesn't have - Document status - - src/app/core/config/config-fix.ts - 129 - - - - not eligible - not eligible - Document status - - src/app/core/config/config-fix.ts - 134 + 76 @@ -2903,7 +2833,7 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 163 + 91 @@ -2913,16 +2843,26 @@ Dashboard shortcut widget src/app/core/config/config-fix.ts - 168 + 96 + + Public Registration Form + Public Registration Form + + src/app/core/config/config-fix.ts + 101 + + open public form + Dashboard shortcut widget + last week last week Attendance week dashboard widget label src/app/core/config/config-fix.ts - 215 + 148 @@ -2931,7 +2871,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 222 + 155 @@ -2940,7 +2880,7 @@ Attendance week dashboard widget label src/app/core/config/config-fix.ts - 208 + 141 @@ -2949,7 +2889,7 @@ Title for notes overview src/app/core/config/config-fix.ts - 244 + 177 @@ -2958,11 +2898,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 254 + 187 src/app/core/config/config-fix.ts - 258 + 191 @@ -2971,19 +2911,19 @@ Translated name of mobile column group src/app/core/config/config-fix.ts - 255 + 188 src/app/core/config/config-fix.ts - 268 + 201 src/app/core/config/config-fix.ts - 525 + 458 src/app/core/config/config-fix.ts - 579 + 512 @@ -2992,7 +2932,7 @@ Panel title src/app/core/config/config-fix.ts - 355 + 288 @@ -3001,7 +2941,7 @@ Panel title src/app/core/config/config-fix.ts - 374 + 307 @@ -3010,7 +2950,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 - 388 + 321 @@ -3019,7 +2959,7 @@ Label for private schools filter - true case src/app/core/config/config-fix.ts - 410 + 343 @@ -3028,7 +2968,7 @@ Label for private schools filter - false case src/app/core/config/config-fix.ts - 411 + 344 @@ -3037,15 +2977,15 @@ Panel title src/app/core/config/config-fix.ts - 423 + 356 src/app/core/config/config-fix.ts - 615 + 548 src/app/core/config/config-fix.ts - 798 + 731 @@ -3054,7 +2994,7 @@ Panel title src/app/core/config/config-fix.ts - 451 + 384 @@ -3063,7 +3003,7 @@ Panel title src/app/core/config/config-fix.ts - 460 + 393 @@ -3072,7 +3012,7 @@ Column label for age of child src/app/core/config/config-fix.ts - 482 + 415 @@ -3081,7 +3021,7 @@ Column label for school attendance of child src/app/core/config/config-fix.ts - 500 + 433 @@ -3090,7 +3030,7 @@ Column label for coaching attendance of child src/app/core/config/config-fix.ts - 509 + 442 @@ -3099,11 +3039,11 @@ Translated name of default column group src/app/core/config/config-fix.ts - 524 + 457 src/app/core/config/config-fix.ts - 528 + 461 @@ -3112,7 +3052,7 @@ Column group name src/app/core/config/config-fix.ts - 541 + 474 @@ -3121,11 +3061,11 @@ Column group name src/app/core/config/config-fix.ts - 564 + 497 src/app/core/config/config-fix.ts - 713 + 646 @@ -3134,7 +3074,7 @@ Active children filter label - true case src/app/core/config/config-fix.ts - 594 + 527 @@ -3143,7 +3083,7 @@ Active children filter label - false case src/app/core/config/config-fix.ts - 595 + 528 @@ -3152,7 +3092,7 @@ Header for form section src/app/core/config/config-fix.ts - 643 + 576 @@ -3161,7 +3101,7 @@ Header for form section src/app/core/config/config-fix.ts - 644 + 577 @@ -3170,7 +3110,7 @@ Header for form section src/app/core/config/config-fix.ts - 645 + 578 @@ -3179,7 +3119,7 @@ Panel title src/app/core/config/config-fix.ts - 652 + 585 @@ -3188,7 +3128,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 655 + 588 @@ -3197,7 +3137,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 675 + 608 @@ -3206,17 +3146,17 @@ Child details section title src/app/core/config/config-fix.ts - 679 + 612 Notes & Tasks Notes & Tasks + Panel title src/app/core/config/config-fix.ts - 700 + 633 - Panel title Height & Weight Tracking @@ -3224,7 +3164,7 @@ Title inside a panel src/app/core/config/config-fix.ts - 726 + 659 @@ -3233,7 +3173,7 @@ Panel title src/app/core/config/config-fix.ts - 732 + 665 @@ -3242,7 +3182,7 @@ Panel title src/app/core/config/config-fix.ts - 741 + 674 @@ -3260,7 +3200,7 @@ Panel title src/app/core/config/config-fix.ts - 828 + 761 @@ -3269,7 +3209,7 @@ Name of a report src/app/core/config/config-fix.ts - 843 + 776 @@ -3278,7 +3218,7 @@ Label of report query src/app/core/config/config-fix.ts - 847 + 780 @@ -3287,7 +3227,7 @@ Label for report query src/app/core/config/config-fix.ts - 852 + 785 @@ -3296,7 +3236,7 @@ Label for report query src/app/core/config/config-fix.ts - 855 + 788 @@ -3305,7 +3245,7 @@ Label for report query src/app/core/config/config-fix.ts - 859 + 792 @@ -3314,7 +3254,7 @@ Label for report query src/app/core/config/config-fix.ts - 864 + 797 @@ -3323,7 +3263,7 @@ Label for report query src/app/core/config/config-fix.ts - 868 + 801 @@ -3332,7 +3272,7 @@ Label for report query src/app/core/config/config-fix.ts - 873 + 806 @@ -3341,7 +3281,7 @@ Name of a report src/app/core/config/config-fix.ts - 881 + 814 @@ -3350,7 +3290,7 @@ Name of a report src/app/core/config/config-fix.ts - 898 + 831 @@ -3365,11 +3305,22 @@ [invalid option] [invalid option] + enum option label prefix for invalid id dummy src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts - 61 + 53 - enum option label prefix for invalid id dummy + + + Edit dropdown options + + Edit dropdown options + + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 2,3 + + title of dropdown options popup dialog Total @@ -3377,7 +3328,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 913 + 846 @@ -3386,7 +3337,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 917 + 850 @@ -3395,7 +3346,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 921 + 854 @@ -3404,7 +3355,7 @@ Name of a column of a report src/app/core/config/config-fix.ts - 925 + 858 @@ -3422,11 +3373,11 @@ Label for the address of a child src/app/core/config/config-fix.ts - 946 + 879 src/app/core/config/config-fix.ts - 1014 + 947 @@ -3435,7 +3386,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 953 + 886 @@ -3444,7 +3395,7 @@ Label for the religion of a child src/app/core/config/config-fix.ts - 960 + 893 @@ -3453,7 +3404,7 @@ Label for the mother tongue of a child src/app/core/config/config-fix.ts - 967 + 900 @@ -3462,7 +3413,7 @@ Tooltip description for the mother tongue of a child src/app/core/config/config-fix.ts - 968 + 901 @@ -3471,7 +3422,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 975 + 908 @@ -3480,7 +3431,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 982 + 915 @@ -3489,7 +3440,7 @@ Label for if a school is a private school src/app/core/config/config-fix.ts - 1000 + 933 @@ -3498,7 +3449,7 @@ Label for the language of a school src/app/core/config/config-fix.ts - 1007 + 940 @@ -3507,7 +3458,7 @@ Label for the timing of a school src/app/core/config/config-fix.ts - 1028 + 961 @@ -3516,7 +3467,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1047 + 980 @@ -3525,7 +3476,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1048 + 981 @@ -3534,7 +3485,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1056 + 989 @@ -3543,7 +3494,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1057 + 990 @@ -3552,7 +3503,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1065 + 998 @@ -3561,7 +3512,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1066 + 999 @@ -3570,7 +3521,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1074 + 1007 @@ -3579,7 +3530,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1075 + 1008 @@ -3588,7 +3539,7 @@ Label for a child attribute src/app/core/config/config-fix.ts - 1083 + 1016 @@ -3597,7 +3548,7 @@ Description for a child attribute src/app/core/config/config-fix.ts - 1084 + 1017 @@ -3606,7 +3557,7 @@ Label of user phone src/app/core/config/config-fix.ts - 1095 + 1028 @@ -3699,12 +3650,8 @@ This field is required Error message for any input - src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html - 21 - - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html - 62 + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html + 57 src/app/core/entity-components/entity-select/edit-text-with-autocomplete/edit-text-with-autocomplete.component.html @@ -3715,6 +3662,14 @@ 23 + + Create new option + Create new option + + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts + 188 + + OK OK @@ -4078,16 +4033,6 @@ 16 - - Select - Selezionane - context Select User - Placeholder for input to set an entity - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts - 62 - - Creating new record. Creating new record. @@ -4186,7 +4131,7 @@ Preparazione dei dati src/app/core/entity/database-indexing/database-indexing.service.ts - 60 + 59 @@ -4226,7 +4171,7 @@ src/app/features/data-import/data-import.service.ts - 60 + 61 @@ -4353,18 +4298,50 @@ Close Chiudere - Generic close button + Close popup + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 34 + src/app/core/latest-changes/changelog/changelog.component.html 54 + + Are you sure that you want to delete the option ? + Are you sure that you want to delete the option ? + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 68 + + + + The option is still used in records. If deleted, the records will not be lost but specially marked + The option is still used in records. If deleted, the records will not be lost but specially marked + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 70,72 + + + + Delete option + Delete option + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 75 + + No Changelog Available Nessun registro delle modifiche disponibile src/app/core/latest-changes/changelog/changelog.component.ts - 87 + 89 @@ -4372,7 +4349,7 @@ Impossibile caricare le ultime modifiche: src/app/core/latest-changes/latest-changes.service.ts - 131 + 121 @@ -4842,7 +4819,7 @@ Please try again. If the problem persists contact Aam Digital support. Navigate to user profile page src/app/core/ui/ui/ui.component.html - 84 + 85 @@ -4851,7 +4828,7 @@ Please try again. If the problem persists contact Aam Digital support. Sign out of the app src/app/core/ui/ui/ui.component.html - 89 + 90 @@ -5113,7 +5090,7 @@ Please try again. If the problem persists contact Aam Digital support. Import completed src/app/features/data-import/data-import.service.ts - 59 + 60 @@ -5121,7 +5098,7 @@ Please try again. If the problem persists contact Aam Digital support. Import new data? src/app/features/data-import/data-import.service.ts - 75 + 76 @@ -5131,7 +5108,7 @@ Please try again. If the problem persists contact Aam Digital support. This will add or update records from the loaded file. src/app/features/data-import/data-import.service.ts - 76 + 77 @@ -5139,7 +5116,7 @@ Please try again. If the problem persists contact Aam Digital support. All existing records imported with the transaction id '' will be deleted! src/app/features/data-import/data-import.service.ts - 79 + 80 @@ -5439,14 +5416,34 @@ Please try again. If the problem persists contact Aam Digital support. 113 + + Select displayed locations + + Select displayed locations + + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 1,3 + + Title of popup to select locations that are displayed in the map + + + Apply + Apply + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 18 + + Button for closing popup and applying changes + - km + km km e.g. 5 km distance with unit src/app/features/location/view-distance/view-distance.component.ts - 73 + 65 @@ -5461,11 +5458,11 @@ Please try again. If the problem persists contact Aam Digital support. Select Select + header of section with entities available for selection src/app/features/matching-entities/matching-entities/matching-entities.component.html - 55,56 + 54 - header of section with entities available for selection create matching @@ -5473,7 +5470,7 @@ Please try again. If the problem persists contact Aam Digital support. Matching button label src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 85 + 91 @@ -5482,7 +5479,7 @@ Please try again. If the problem persists contact Aam Digital support. Matching View column name src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 273 + 312 @@ -5739,6 +5736,22 @@ Please try again. If the problem persists contact Aam Digital support. 75 + + Example form + Example form + + src/app/features/public-form/demo-public-form-generator.service.ts + 18 + + + + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + + src/app/features/public-form/demo-public-form-generator.service.ts + 19 + + Submit Form Submit Form @@ -5762,7 +5775,7 @@ Please try again. If the problem persists contact Aam Digital support. Successfully submitted form src/app/features/public-form/public-form.component.ts - 58 + 62 diff --git a/src/assets/locale/messages.xlf b/src/assets/locale/messages.xlf index 7bd1128dc8..cb03256f81 100644 --- a/src/assets/locale/messages.xlf +++ b/src/assets/locale/messages.xlf @@ -56,7 +56,7 @@ src/app/core/config/config-fix.ts - 886 + 819 Events of an attendance @@ -607,11 +607,11 @@ src/app/core/config/config-fix.ts - 813 + 746 src/app/core/config/config-fix.ts - 891 + 824 Label for the participants of a recurring activity @@ -719,7 +719,7 @@ src/app/core/config/config-fix.ts - 1035 + 968 Label for the remarks of a ASER result @@ -930,7 +930,7 @@ src/app/core/config/config-fix.ts - 758 + 691 Child status @@ -938,15 +938,7 @@ Alipore src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 5 - - - src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 6 - - - src/app/core/config/config-fix.ts - 141 + 4 center @@ -954,11 +946,7 @@ Tollygunge src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 7 - - - src/app/core/config/config-fix.ts - 145 + 5 center @@ -966,11 +954,7 @@ Barabazar src/app/child-dev-project/children/demo-data-generators/fixtures/centers.ts - 8 - - - src/app/core/config/config-fix.ts - 149 + 6 center @@ -1216,7 +1200,7 @@ src/app/core/config/config-fix.ts - 518 + 451 Table header, Short for Body Mass Index @@ -1264,15 +1248,15 @@ src/app/core/config/config-fix.ts - 477 + 410 src/app/core/config/config-fix.ts - 909 + 842 src/app/core/config/config-fix.ts - 993 + 926 Label for the name of a child @@ -1344,11 +1328,11 @@ src/app/core/config/config-fix.ts - 280 + 213 src/app/core/config/config-fix.ts - 554 + 487 Label for the status of a child @@ -1392,7 +1376,7 @@ src/app/core/config/config-fix.ts - 1021 + 954 Label for the phone number of a child @@ -1404,7 +1388,7 @@ src/app/core/config/config-fix.ts - 939 + 872 Label for the child of a relation @@ -1424,11 +1408,11 @@ src/app/core/config/config-fix.ts - 493 + 426 src/app/core/config/config-fix.ts - 604 + 537 Label for the school of a relation @@ -1440,7 +1424,7 @@ src/app/core/config/config-fix.ts - 488 + 421 Label for the class of a relation @@ -1954,7 +1938,7 @@ src/app/core/config/config-fix.ts - 51 + 41 label (plural) for entity @@ -1966,15 +1950,15 @@ src/app/core/config/config-fix.ts - 36 + 26 src/app/core/config/config-fix.ts - 593 + 526 src/app/core/config/config-fix.ts - 940 + 873 Label for the children of a note @@ -2101,11 +2085,11 @@ src/app/core/config/config-fix.ts - 412 + 345 src/app/core/config/config-fix.ts - 596 + 529 src/app/core/entity-components/entity-list/filter-generator.service.ts @@ -2245,11 +2229,11 @@ src/app/core/config/config-fix.ts - 41 + 31 src/app/core/config/config-fix.ts - 409 + 342 label (plural) for entity @@ -2439,7 +2423,7 @@ Aam Digital - DEMO (automatically generated data) src/app/core/config/config-fix.ts - 22 + 12 Page title @@ -2447,7 +2431,7 @@ Dashboard src/app/core/config/config-fix.ts - 31 + 21 Menu item @@ -2455,11 +2439,11 @@ Attendance src/app/core/config/config-fix.ts - 46 + 36 src/app/core/config/config-fix.ts - 691 + 624 Menu item @@ -2467,7 +2451,7 @@ Tasks src/app/core/config/config-fix.ts - 56 + 46 src/app/features/todos/model/todo.ts @@ -2479,7 +2463,7 @@ Admin src/app/core/config/config-fix.ts - 61 + 51 Menu item @@ -2487,7 +2471,7 @@ Import src/app/core/config/config-fix.ts - 66 + 56 Menu item @@ -2495,7 +2479,7 @@ Users src/app/core/config/config-fix.ts - 71 + 61 src/app/core/user/user.ts @@ -2507,7 +2491,7 @@ Reports src/app/core/config/config-fix.ts - 76 + 66 Menu item @@ -2515,7 +2499,7 @@ Database Conflicts src/app/core/config/config-fix.ts - 81 + 71 Menu item @@ -2523,63 +2507,15 @@ Help src/app/core/config/config-fix.ts - 86 + 76 Menu item - - OK (copy with us) - - src/app/core/config/config-fix.ts - 109 - - Document status - - - OK (copy needed for us) - - src/app/core/config/config-fix.ts - 114 - - Document status - - - needs correction - - src/app/core/config/config-fix.ts - 119 - - Document status - - - applied - - src/app/core/config/config-fix.ts - 124 - - Document status - - - doesn't have - - src/app/core/config/config-fix.ts - 129 - - Document status - - - not eligible - - src/app/core/config/config-fix.ts - 134 - - Document status - Record Attendance src/app/core/config/config-fix.ts - 163 + 91 record attendance shortcut Dashboard shortcut widget @@ -2588,16 +2524,25 @@ Add Child src/app/core/config/config-fix.ts - 168 + 96 record attendance shortcut Dashboard shortcut widget + + Public Registration Form + + src/app/core/config/config-fix.ts + 101 + + open public form + Dashboard shortcut widget + this week src/app/core/config/config-fix.ts - 208 + 141 Attendance week dashboard widget label @@ -2605,7 +2550,7 @@ last week src/app/core/config/config-fix.ts - 215 + 148 Attendance week dashboard widget label @@ -2613,7 +2558,7 @@ Late last week src/app/core/config/config-fix.ts - 222 + 155 Attendance week dashboard widget label @@ -2621,7 +2566,7 @@ Notes & Reports src/app/core/config/config-fix.ts - 244 + 177 Title for notes overview @@ -2629,11 +2574,11 @@ Standard src/app/core/config/config-fix.ts - 254 + 187 src/app/core/config/config-fix.ts - 258 + 191 Translated name of default column group @@ -2641,19 +2586,19 @@ Mobile src/app/core/config/config-fix.ts - 255 + 188 src/app/core/config/config-fix.ts - 268 + 201 src/app/core/config/config-fix.ts - 525 + 458 src/app/core/config/config-fix.ts - 579 + 512 Translated name of mobile column group @@ -2661,7 +2606,7 @@ User Information src/app/core/config/config-fix.ts - 355 + 288 Panel title @@ -2669,7 +2614,7 @@ Security src/app/core/config/config-fix.ts - 374 + 307 Panel title @@ -2677,7 +2622,7 @@ assets/help/help.en.md src/app/core/config/config-fix.ts - 388 + 321 Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!) @@ -2685,7 +2630,7 @@ Private src/app/core/config/config-fix.ts - 410 + 343 Label for private schools filter - true case @@ -2693,7 +2638,7 @@ Government src/app/core/config/config-fix.ts - 411 + 344 Label for private schools filter - false case @@ -2701,15 +2646,15 @@ Basic Information src/app/core/config/config-fix.ts - 423 + 356 src/app/core/config/config-fix.ts - 615 + 548 src/app/core/config/config-fix.ts - 798 + 731 Panel title @@ -2717,7 +2662,7 @@ Students src/app/core/config/config-fix.ts - 451 + 384 Panel title @@ -2725,7 +2670,7 @@ Activities src/app/core/config/config-fix.ts - 460 + 393 Panel title @@ -2733,7 +2678,7 @@ Age src/app/core/config/config-fix.ts - 482 + 415 Column label for age of child @@ -2741,7 +2686,7 @@ Attendance (School) src/app/core/config/config-fix.ts - 500 + 433 Column label for school attendance of child @@ -2749,7 +2694,7 @@ Attendance (Coaching) src/app/core/config/config-fix.ts - 509 + 442 Column label for coaching attendance of child @@ -2757,11 +2702,11 @@ Basic Info src/app/core/config/config-fix.ts - 524 + 457 src/app/core/config/config-fix.ts - 528 + 461 Translated name of default column group @@ -2769,7 +2714,7 @@ School Info src/app/core/config/config-fix.ts - 541 + 474 Column group name @@ -2777,11 +2722,11 @@ Health src/app/core/config/config-fix.ts - 564 + 497 src/app/core/config/config-fix.ts - 713 + 646 Column group name @@ -2789,7 +2734,7 @@ Active src/app/core/config/config-fix.ts - 594 + 527 Active children filter label - true case @@ -2797,7 +2742,7 @@ Inactive src/app/core/config/config-fix.ts - 595 + 528 Active children filter label - false case @@ -2805,7 +2750,7 @@ Personal Information src/app/core/config/config-fix.ts - 643 + 576 Header for form section @@ -2813,7 +2758,7 @@ Additional src/app/core/config/config-fix.ts - 644 + 577 Header for form section @@ -2821,7 +2766,7 @@ Scholar activities src/app/core/config/config-fix.ts - 645 + 578 Header for form section @@ -2829,7 +2774,7 @@ Education src/app/core/config/config-fix.ts - 652 + 585 Panel title @@ -2837,7 +2782,7 @@ School History src/app/core/config/config-fix.ts - 655 + 588 Title inside a panel @@ -2845,7 +2790,7 @@ ASER Results src/app/core/config/config-fix.ts - 675 + 608 Title inside a panel @@ -2853,7 +2798,7 @@ Find a suitable new school src/app/core/config/config-fix.ts - 679 + 612 Child details section title @@ -2861,7 +2806,7 @@ Notes & Tasks src/app/core/config/config-fix.ts - 700 + 633 Panel title @@ -2869,7 +2814,7 @@ Height & Weight Tracking src/app/core/config/config-fix.ts - 726 + 659 Title inside a panel @@ -2877,7 +2822,7 @@ Educational Materials src/app/core/config/config-fix.ts - 732 + 665 Panel title @@ -2885,7 +2830,7 @@ Observations src/app/core/config/config-fix.ts - 741 + 674 Panel title @@ -2893,7 +2838,7 @@ Events & Attendance src/app/core/config/config-fix.ts - 828 + 761 Panel title @@ -2901,7 +2846,7 @@ Basic Report src/app/core/config/config-fix.ts - 843 + 776 Name of a report @@ -2909,7 +2854,7 @@ All children src/app/core/config/config-fix.ts - 847 + 780 Label of report query @@ -2917,7 +2862,7 @@ All schools src/app/core/config/config-fix.ts - 852 + 785 Label for report query @@ -2925,7 +2870,7 @@ Children attending a school src/app/core/config/config-fix.ts - 855 + 788 Label for report query @@ -2933,7 +2878,7 @@ Governmental schools src/app/core/config/config-fix.ts - 859 + 792 Label for report query @@ -2941,7 +2886,7 @@ Children attending a governmental school src/app/core/config/config-fix.ts - 864 + 797 Label for report query @@ -2949,7 +2894,7 @@ Private schools src/app/core/config/config-fix.ts - 868 + 801 Label for report query @@ -2957,7 +2902,7 @@ Children attending a private school src/app/core/config/config-fix.ts - 873 + 806 Label for report query @@ -2965,7 +2910,7 @@ Event Report src/app/core/config/config-fix.ts - 881 + 814 Name of a report @@ -2973,7 +2918,7 @@ Attendance Report src/app/core/config/config-fix.ts - 898 + 831 Name of a report @@ -2981,7 +2926,7 @@ Total src/app/core/config/config-fix.ts - 913 + 846 Name of a column of a report @@ -2989,7 +2934,7 @@ Present src/app/core/config/config-fix.ts - 917 + 850 Name of a column of a report @@ -2997,7 +2942,7 @@ Rate src/app/core/config/config-fix.ts - 921 + 854 Name of a column of a report @@ -3005,7 +2950,7 @@ Late src/app/core/config/config-fix.ts - 925 + 858 Name of a column of a report @@ -3013,11 +2958,11 @@ Address src/app/core/config/config-fix.ts - 946 + 879 src/app/core/config/config-fix.ts - 1014 + 947 Label for the address of a child @@ -3025,7 +2970,7 @@ Blood Group src/app/core/config/config-fix.ts - 953 + 886 Label for a child attribute @@ -3033,7 +2978,7 @@ Religion src/app/core/config/config-fix.ts - 960 + 893 Label for the religion of a child @@ -3041,7 +2986,7 @@ Mother Tongue src/app/core/config/config-fix.ts - 967 + 900 Label for the mother tongue of a child @@ -3049,7 +2994,7 @@ The primary language spoken at home src/app/core/config/config-fix.ts - 968 + 901 Tooltip description for the mother tongue of a child @@ -3057,7 +3002,7 @@ Last Dental Check-Up src/app/core/config/config-fix.ts - 975 + 908 Label for a child attribute @@ -3065,7 +3010,7 @@ Birth certificate src/app/core/config/config-fix.ts - 982 + 915 Label for a child attribute @@ -3073,7 +3018,7 @@ Private School src/app/core/config/config-fix.ts - 1000 + 933 Label for if a school is a private school @@ -3081,7 +3026,7 @@ Language src/app/core/config/config-fix.ts - 1007 + 940 Label for the language of a school @@ -3089,7 +3034,7 @@ School Timing src/app/core/config/config-fix.ts - 1028 + 961 Label for the timing of a school @@ -3097,7 +3042,7 @@ Motivated src/app/core/config/config-fix.ts - 1047 + 980 Label for a child attribute @@ -3105,7 +3050,7 @@ The child is motivated during the class. src/app/core/config/config-fix.ts - 1048 + 981 Description for a child attribute @@ -3113,7 +3058,7 @@ Participating src/app/core/config/config-fix.ts - 1056 + 989 Label for a child attribute @@ -3121,7 +3066,7 @@ The child is actively participating in the class. src/app/core/config/config-fix.ts - 1057 + 990 Description for a child attribute @@ -3129,7 +3074,7 @@ Interacting src/app/core/config/config-fix.ts - 1065 + 998 Label for a child attribute @@ -3137,7 +3082,7 @@ The child interacts with other students during the class. src/app/core/config/config-fix.ts - 1066 + 999 Description for a child attribute @@ -3145,7 +3090,7 @@ Homework src/app/core/config/config-fix.ts - 1074 + 1007 Label for a child attribute @@ -3153,7 +3098,7 @@ The child does its homework. src/app/core/config/config-fix.ts - 1075 + 1008 Description for a child attribute @@ -3161,7 +3106,7 @@ Asking Questions src/app/core/config/config-fix.ts - 1083 + 1016 Label for a child attribute @@ -3169,7 +3114,7 @@ The child is asking questions during the class. src/app/core/config/config-fix.ts - 1084 + 1017 Description for a child attribute @@ -3177,7 +3122,7 @@ Contact src/app/core/config/config-fix.ts - 1095 + 1028 Label of user phone @@ -3273,33 +3218,80 @@ Interaction type/Category of a Note + + This field is required + + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html + 57,58 + + + src/app/core/entity-components/entity-select/edit-text-with-autocomplete/edit-text-with-autocomplete.component.html + 32,33 + + + src/app/features/todos/recurring-interval/edit-recurring-interval/edit-recurring-interval.component.html + 23,25 + + Error message for any input + + + Create new option + + src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts + 188 + + [invalid option] src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.ts - 61 + 53 enum option label prefix for invalid id dummy - - This field is required + + Edit dropdown options + - src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html - 21,23 + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 2,3 + title of dropdown options popup dialog + + + Close - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html - 62,63 + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.html + 34,35 - src/app/core/entity-components/entity-select/edit-text-with-autocomplete/edit-text-with-autocomplete.component.html - 32,33 + src/app/core/latest-changes/changelog/changelog.component.html + 54,56 + Close popup + + + Are you sure that you want to delete the option ? - src/app/features/todos/recurring-interval/edit-recurring-interval/edit-recurring-interval.component.html - 23,25 + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 68 + + + + The option is still used in records. If deleted, the records will not be lost but specially marked + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 70,72 + + + + Delete option + + src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts + 75 - Error message for any input OK @@ -3592,15 +3584,6 @@ context 10 in total Displaying the amount of involved entities - - Select - - src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts - 62,64 - - context Select User - Placeholder for input to set an entity - Creating new record. @@ -3723,7 +3706,7 @@ Preparing data (Indexing) src/app/core/entity/database-indexing/database-indexing.service.ts - 60 + 59 @@ -3758,7 +3741,7 @@ src/app/features/data-import/data-import.service.ts - 60 + 61 Undo deleting an entity @@ -3864,26 +3847,18 @@ 46,48 - - Close - - src/app/core/latest-changes/changelog/changelog.component.html - 54,56 - - Generic close button - No Changelog Available src/app/core/latest-changes/changelog/changelog.component.ts - 87 + 89 Could not load latest changes: src/app/core/latest-changes/latest-changes.service.ts - 131 + 121 @@ -4289,7 +4264,7 @@ Please try again. If the problem persists contact Aam Digital support. Profile src/app/core/ui/ui/ui.component.html - 84,86 + 85,87 Navigate to user profile page @@ -4297,7 +4272,7 @@ Please try again. If the problem persists contact Aam Digital support. Sign out src/app/core/ui/ui/ui.component.html - 89,91 + 90,92 Sign out of the app @@ -4523,14 +4498,14 @@ Please try again. If the problem persists contact Aam Digital support. Import completed src/app/features/data-import/data-import.service.ts - 59 + 60 Import new data? src/app/features/data-import/data-import.service.ts - 75 + 76 @@ -4538,14 +4513,14 @@ Please try again. If the problem persists contact Aam Digital support. This will add or update records from the loaded file. src/app/features/data-import/data-import.service.ts - 76,77 + 77,78 All existing records imported with the transaction id '' will be deleted! src/app/features/data-import/data-import.service.ts - 79 + 80 @@ -4809,11 +4784,28 @@ Please try again. If the problem persists contact Aam Digital support. help text in map popup + + Select displayed locations + + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 1,3 + + Title of popup to select locations that are displayed in the map + + + Apply + + src/app/features/location/map/map-properties-popup/map-properties-popup.component.html + 18 + + Button for closing popup and applying changes + - km + km src/app/features/location/view-distance/view-distance.component.ts - 73 + 65 e.g. 5 km distance with unit @@ -4830,7 +4822,7 @@ Please try again. If the problem persists contact Aam Digital support. Select src/app/features/matching-entities/matching-entities/matching-entities.component.html - 55,56 + 54,55 header of section with entities available for selection @@ -4838,7 +4830,7 @@ Please try again. If the problem persists contact Aam Digital support. create matching src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 85 + 91 Matching button label @@ -4846,7 +4838,7 @@ Please try again. If the problem persists contact Aam Digital support. Distance src/app/features/matching-entities/matching-entities/matching-entities.component.ts - 273 + 312 Matching View column name @@ -5077,6 +5069,20 @@ Please try again. If the problem persists contact Aam Digital support. The progress, e.g. of a certain activity + + Example form + + src/app/features/public-form/demo-public-form-generator.service.ts + 18 + + + + This is a form that can be shared as a link or embedded in a website. It can be filled by users without having an account. For example you can let participants self-register their details and just review the records within Aam Digital. + + src/app/features/public-form/demo-public-form-generator.service.ts + 19 + + Submit Form @@ -5097,7 +5103,7 @@ Please try again. If the problem persists contact Aam Digital support. Successfully submitted form src/app/features/public-form/public-form.component.ts - 58 + 62 From dea782b3851f418b00fa7d34399654edf412dcc5 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 16:22:46 +0100 Subject: [PATCH 37/83] fixed a lot of tests --- .../notes/model/note.spec.ts | 22 ++--- .../note-details.component.spec.ts | 4 +- .../basic-autocomplete.component.spec.ts | 2 + .../configurable-enum-datatype.spec.ts | 10 +-- .../configurable-enum-testing.ts | 42 ++++++++++ .../configurable-enum/configurable-enum.ts | 2 +- ...emo-configurable-enum-generator.service.ts | 30 +------ ...isplay-configurable-enum.component.spec.ts | 22 +++-- .../edit-configurable-enum.component.spec.ts | 81 ++++++++----------- .../enum-dropdown.component.spec.ts | 23 +++--- src/app/core/entity/model/entity.spec.ts | 15 ++-- src/app/core/filter/filter.service.spec.ts | 13 +-- src/app/utils/database-testing.module.ts | 6 ++ src/app/utils/mocked-testing.module.ts | 6 ++ 14 files changed, 151 insertions(+), 127 deletions(-) create mode 100644 src/app/core/configurable-enum/configurable-enum-testing.ts diff --git a/src/app/child-dev-project/notes/model/note.spec.ts b/src/app/child-dev-project/notes/model/note.spec.ts index 6da654b968..1e6db00e0f 100644 --- a/src/app/child-dev-project/notes/model/note.spec.ts +++ b/src/app/child-dev-project/notes/model/note.spec.ts @@ -24,7 +24,7 @@ import { import { testEntitySubclass } from "../../../core/entity/model/entity.spec"; import { defaultInteractionTypes } from "../../../core/config/default-config/default-interaction-types"; import { Ordering } from "../../../core/configurable-enum/configurable-enum-ordering"; -import { createTestingConfigService } from "../../../core/config/testing-config-service"; +import { createTestingConfigurableEnumService } from "../../../core/configurable-enum/configurable-enum-testing"; const testStatusTypes: ConfigurableEnumConfig = [ { @@ -78,16 +78,16 @@ describe("Note", () => { ]); beforeEach(waitForAsync(() => { - // const testConfigs = {}; - // testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + INTERACTION_TYPE_CONFIG_ID] = - // testInteractionTypes; - // testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + ATTENDANCE_STATUS_CONFIG_ID] = - // testStatusTypes; - // - // entitySchemaService = new EntitySchemaService(); - // entitySchemaService.registerSchemaDatatype( - // new ConfigurableEnumDatatype(createTestingConfigService(testConfigs)) - // ); + const testConfigs = {}; + testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + INTERACTION_TYPE_CONFIG_ID] = + testInteractionTypes; + testConfigs[CONFIGURABLE_ENUM_CONFIG_PREFIX + ATTENDANCE_STATUS_CONFIG_ID] = + testStatusTypes; + + entitySchemaService = new EntitySchemaService(); + entitySchemaService.registerSchemaDatatype( + new ConfigurableEnumDatatype(createTestingConfigurableEnumService()) + ); })); testEntitySubclass("Note", Note, { diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts b/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts index 69e5552e78..a9f5192c14 100644 --- a/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts +++ b/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts @@ -35,7 +35,7 @@ describe("NoteDetailsComponent", () => { let children: Child[]; let testNote: Note; - beforeEach(() => { + beforeEach(async () => { children = [new Child("1"), new Child("2"), new Child("3")]; testNote = generateTestNote(children); @@ -46,7 +46,7 @@ describe("NoteDetailsComponent", () => { const dialogRefMock = { beforeClosed: () => EMPTY, close: () => {} }; - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ imports: [ NoteDetailsComponent, MockedTestingModule.withState(LoginState.LOGGED_IN, children), diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 0979cb703c..0ec2b87de3 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -14,6 +14,7 @@ import { Entity } from "../../entity/model/entity"; import { FormControl } from "@angular/forms"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; describe("BasicAutocompleteComponent", () => { let component: BasicAutocompleteComponent; @@ -27,6 +28,7 @@ describe("BasicAutocompleteComponent", () => { BasicAutocompleteComponent, FontAwesomeTestingModule, NoopAnimationsModule, + MatDialogModule, ], }).compileComponents(); diff --git a/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.spec.ts b/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.spec.ts index 6edfe5cb1f..041efe6e0f 100644 --- a/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.spec.ts +++ b/src/app/core/configurable-enum/configurable-enum-datatype/configurable-enum-datatype.spec.ts @@ -22,9 +22,9 @@ import { Entity } from "../../entity/model/entity"; import { DatabaseField } from "../../entity/database-field.decorator"; import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; import { TestBed, waitForAsync } from "@angular/core/testing"; -import { ConfigService } from "../../config/config.service"; import { DatabaseEntity } from "../../entity/database-entity.decorator"; import { ConfigurableEnumModule } from "../configurable-enum.module"; +import { ConfigurableEnumService } from "../configurable-enum.service"; describe("ConfigurableEnumDatatype", () => { const TEST_CONFIG: ConfigurableEnumConfig = [ @@ -48,15 +48,15 @@ describe("ConfigurableEnumDatatype", () => { } let entitySchemaService: EntitySchemaService; - let configService: jasmine.SpyObj; + let enumService: jasmine.SpyObj; beforeEach(waitForAsync(() => { - configService = jasmine.createSpyObj("configService", ["getConfig"]); - configService.getConfig.and.returnValue(TEST_CONFIG); + enumService = jasmine.createSpyObj(["getEnumValues"]); + enumService.getEnumValues.and.returnValue(TEST_CONFIG); TestBed.configureTestingModule({ imports: [ConfigurableEnumModule], - providers: [{ provide: ConfigService, useValue: configService }], + providers: [{ provide: ConfigurableEnumService, useValue: enumService }], }); entitySchemaService = diff --git a/src/app/core/configurable-enum/configurable-enum-testing.ts b/src/app/core/configurable-enum/configurable-enum-testing.ts new file mode 100644 index 0000000000..7df737d413 --- /dev/null +++ b/src/app/core/configurable-enum/configurable-enum-testing.ts @@ -0,0 +1,42 @@ +import { genders } from "../../child-dev-project/children/model/genders"; +import { materials } from "../../child-dev-project/children/educational-material/model/materials"; +import { + mathLevels, + readingLevels, +} from "../../child-dev-project/children/aser/model/skill-levels"; +import { ConfigurableEnum } from "./configurable-enum"; +import { ConfigurableEnumService } from "./configurable-enum.service"; +import { NEVER, of } from "rxjs"; +import { defaultInteractionTypes } from "../config/default-config/default-interaction-types"; +import { warningLevels } from "../../child-dev-project/warning-levels"; +import { ratingAnswers } from "../../features/historical-data/model/rating-answers"; +import { centersUnique } from "../../child-dev-project/children/demo-data-generators/fixtures/centers"; +import { defaultAttendanceStatusTypes } from "../config/default-config/default-attendance-status-types"; + +export const demoEnums = Object.entries({ + genders: genders, + materials: materials, + "math-levels": mathLevels, + "reading-levels": readingLevels, + "warning-levels": warningLevels, + "rating-answer": ratingAnswers, + center: centersUnique, + "attendance-status": defaultAttendanceStatusTypes, + "interaction-type": defaultInteractionTypes, +}).map(([key, value]) => { + const e = new ConfigurableEnum(key); + e.values = value; + return e; +}); + +export function createTestingConfigurableEnumService() { + let service: ConfigurableEnumService; + service = new ConfigurableEnumService( + { + receiveUpdates: () => NEVER, + loadType: () => Promise.resolve(demoEnums), + } as any, + { configUpdates: of(undefined) } as any + ); + return service; +} diff --git a/src/app/core/configurable-enum/configurable-enum.ts b/src/app/core/configurable-enum/configurable-enum.ts index dea56231fa..a2f3cf1412 100644 --- a/src/app/core/configurable-enum/configurable-enum.ts +++ b/src/app/core/configurable-enum/configurable-enum.ts @@ -5,5 +5,5 @@ import { DatabaseField } from "../entity/database-field.decorator"; @DatabaseEntity("ConfigurableEnum") export class ConfigurableEnum extends Entity { - @DatabaseField() values: ConfigurableEnumValue[]; + @DatabaseField() values: ConfigurableEnumValue[] = []; } diff --git a/src/app/core/configurable-enum/demo-configurable-enum-generator.service.ts b/src/app/core/configurable-enum/demo-configurable-enum-generator.service.ts index e47f7160a3..268ecaba27 100644 --- a/src/app/core/configurable-enum/demo-configurable-enum-generator.service.ts +++ b/src/app/core/configurable-enum/demo-configurable-enum-generator.service.ts @@ -1,17 +1,7 @@ import { Injectable } from "@angular/core"; import { DemoDataGenerator } from "../demo-data/demo-data-generator"; import { ConfigurableEnum } from "./configurable-enum"; -import { genders } from "../../child-dev-project/children/model/genders"; -import { materials } from "../../child-dev-project/children/educational-material/model/materials"; -import { - mathLevels, - readingLevels, -} from "../../child-dev-project/children/aser/model/skill-levels"; -import { warningLevels } from "../../child-dev-project/warning-levels"; -import { ratingAnswers } from "../../features/historical-data/model/rating-answers"; -import { centersUnique } from "../../child-dev-project/children/demo-data-generators/fixtures/centers"; -import { defaultAttendanceStatusTypes } from "../config/default-config/default-attendance-status-types"; -import { defaultInteractionTypes } from "../config/default-config/default-interaction-types"; +import { demoEnums } from "./configurable-enum-testing"; @Injectable() export class DemoConfigurableEnumGeneratorService extends DemoDataGenerator { @@ -24,23 +14,7 @@ export class DemoConfigurableEnumGeneratorService extends DemoDataGenerator { - const e = new ConfigurableEnum(key); - e.values = value; - return e; - }); + return demoEnums; } } diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts index fea9a5b0ce..c3f4ff297b 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts @@ -16,7 +16,9 @@ describe("DisplayConfigurableEnumComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(DisplayConfigurableEnumComponent); component = fixture.componentInstance; - component.value = { id: "testCategory", label: "Test Category" }; + component.onInitFromDynamicConfig({ + value: { id: "testCategory", label: "Test Category" }, + } as any); fixture.detectChanges(); }); @@ -24,11 +26,11 @@ describe("DisplayConfigurableEnumComponent", () => { expect(component).toBeTruthy(); }); - it("displays value's label", () => { + it("should display label of value", () => { expect(fixture.debugElement.nativeElement.innerHTML).toBe("Test Category"); }); - it("should use the background color is available", () => { + it("should use the background color if available", () => { const elem = fixture.debugElement.nativeElement; expect(elem.style["background-color"]).toBe(""); @@ -38,13 +40,21 @@ describe("DisplayConfigurableEnumComponent", () => { color: "black", _ordinal: 1, }; - const entity = new Note(); - entity.warningLevel = value; - component.onInitFromDynamicConfig({ id: "warningLevel", entity, value }); + component.onInitFromDynamicConfig({ value } as any); fixture.detectChanges(); expect(elem.style["background-color"]).toBe("black"); expect(elem.style.padding).toBe("5px"); expect(elem.style["border-radius"]).toBe("4px"); }); + + it("should concatenate multiple values", () => { + const first = { id: "1", label: "First" }; + + const second = { id: "2", label: "Second" }; + component.onInitFromDynamicConfig({ value: [first, second] } as any); + fixture.detectChanges(); + + expect(fixture.debugElement.nativeElement.innerHTML).toBe("First, Second"); + }); }); diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts index 1e4dfecbc0..0a95a64516 100644 --- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.spec.ts @@ -1,62 +1,33 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { EditConfigurableEnumComponent } from "./edit-configurable-enum.component"; -import { FormBuilder, FormControl } from "@angular/forms"; -import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { ConfigService } from "../../config/config.service"; -import { DatabaseEntity } from "../../entity/database-entity.decorator"; -import { DatabaseField } from "../../entity/database-field.decorator"; +import { FormControl, FormGroup } from "@angular/forms"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import { EntitySchemaField } from "../../entity/schema/entity-schema-field"; import { Entity } from "../../entity/model/entity"; -import { ConfigurableEnumValue } from "../configurable-enum.interface"; -import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { ConfigurableEnumService } from "../configurable-enum.service"; +import { ConfigurableEnum } from "../configurable-enum"; describe("EditConfigurableEnumComponent", () => { let component: EditConfigurableEnumComponent; let fixture: ComponentFixture; - let mockConfigService: jasmine.SpyObj; - const testEnum: ConfigurableEnumValue[] = [ - { id: "1", label: "option-1" }, - { id: "2", label: "option-2" }, - ]; - - @DatabaseEntity("EditEnumTest") - class EditEnumTest extends Entity { - @DatabaseField({ dataType: "configurable-enum", additional: "test-enum" }) - enum: ConfigurableEnumValue; - - @DatabaseField({ - dataType: "array", - innerDataType: "configurable-enum", - additional: "test-enum", - }) - enumMulti: ConfigurableEnumValue[]; - } - - let testFormGroup; - beforeEach(async () => { - testFormGroup = new FormBuilder().group({ - enum: new FormControl(), - enumMulti: new FormControl(), - }); - - mockConfigService = jasmine.createSpyObj(["getConfig"]); - mockConfigService.getConfig.and.returnValue(testEnum); await TestBed.configureTestingModule({ - imports: [ - EditConfigurableEnumComponent, - FontAwesomeTestingModule, - NoopAnimationsModule, + imports: [EditConfigurableEnumComponent, MockedTestingModule.withState()], + providers: [ + { + provide: ConfigurableEnumService, + useValue: { getEnum: () => new ConfigurableEnum() }, + }, ], - providers: [{ provide: ConfigService, useValue: mockConfigService }], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(EditConfigurableEnumComponent); component = fixture.componentInstance; - initForEntity(new EditEnumTest(), "enum"); + initWithSchema({ innerDataType: "some-id" }); fixture.detectChanges(); }); @@ -64,15 +35,29 @@ describe("EditConfigurableEnumComponent", () => { expect(component).toBeTruthy(); }); - function initForEntity(entity: EditEnumTest, field: "enum" | "enumMulti") { - const formControl = testFormGroup.controls[field]; - formControl.setValue(entity[field]); + it("should extract the enum ID", () => { + initWithSchema({ innerDataType: "some-id" }); + expect(component.enumId).toBe("some-id"); + + initWithSchema({ dataType: "array", additional: "other-id" }); + expect(component.enumId).toBe("other-id"); + }); + + it("should detect multi selection mode", () => { + initWithSchema({ innerDataType: "some-id" }); + expect(component.multi).toBeFalse(); + + initWithSchema({ dataType: "array", additional: "some-id" }); + expect(component.multi).toBeTrue(); + }); + function initWithSchema(schema: EntitySchemaField) { + const fromGroup = new FormGroup({ test: new FormControl() }); component.onInitFromDynamicConfig({ - formControl: formControl, - formFieldConfig: { id: field }, - propertySchema: entity.getSchema().get(field), - entity: entity, + formControl: fromGroup.get("test"), + formFieldConfig: { id: "test" }, + propertySchema: schema, + entity: new Entity(), }); } }); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts index 4c3f64f5b5..74a86644ee 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts @@ -1,29 +1,24 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { EnumDropdownComponent } from "./enum-dropdown.component"; -import { ConfigService } from "../../config/config.service"; import { FormControl } from "@angular/forms"; -import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { SimpleChange } from "@angular/core"; -import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { ConfigurableEnumService } from "../configurable-enum.service"; +import { ConfigurableEnum } from "../configurable-enum"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; describe("EnumDropdownComponent", () => { let component: EnumDropdownComponent; let fixture: ComponentFixture; - - let mockConfigService: jasmine.SpyObj; - beforeEach(async () => { - mockConfigService = jasmine.createSpyObj(["getConfig"]); - mockConfigService.getConfig.and.returnValue([]); - await TestBed.configureTestingModule({ - imports: [ - EnumDropdownComponent, - FontAwesomeTestingModule, - NoopAnimationsModule, + imports: [EnumDropdownComponent, MockedTestingModule.withState()], + providers: [ + { + provide: ConfigurableEnumService, + useValue: { getEnum: () => new ConfigurableEnum() }, + }, ], - providers: [{ provide: ConfigService, useValue: mockConfigService }], }).compileComponents(); fixture = TestBed.createComponent(EnumDropdownComponent); diff --git a/src/app/core/entity/model/entity.spec.ts b/src/app/core/entity/model/entity.spec.ts index faf0f292e8..111e5bff2a 100644 --- a/src/app/core/entity/model/entity.spec.ts +++ b/src/app/core/entity/model/entity.spec.ts @@ -20,7 +20,8 @@ import { EntitySchemaService } from "../schema/entity-schema.service"; import { DatabaseField } from "../database-field.decorator"; import { ConfigurableEnumDatatype } from "../../configurable-enum/configurable-enum-datatype/configurable-enum-datatype"; import { DatabaseEntity } from "../database-entity.decorator"; -import { createTestingConfigService } from "../../config/testing-config-service"; +import { createTestingConfigurableEnumService } from "../../configurable-enum/configurable-enum-testing"; +import { fakeAsync, tick } from "@angular/core/testing"; describe("Entity", () => { let entitySchemaService: EntitySchemaService; @@ -162,12 +163,12 @@ export function testEntitySubclass( expect(Entity.extractTypeFromId(entity._id)).toBe(entityType); }); - it("should only load and store properties defined in the schema", () => { + it("should only load and store properties defined in the schema", fakeAsync(() => { const schemaService = new EntitySchemaService(); - // const configService = createTestingConfigService(); - // schemaService.registerSchemaDatatype( - // new ConfigurableEnumDatatype(configService) - // ); + schemaService.registerSchemaDatatype( + new ConfigurableEnumDatatype(createTestingConfigurableEnumService()) + ); + tick(); const entity = new entityClass(); schemaService.loadDataIntoEntity( @@ -179,5 +180,5 @@ export function testEntitySubclass( delete rawData.searchIndices; } expect(rawData).toEqual(expectedDatabaseFormat); - }); + })); } diff --git a/src/app/core/filter/filter.service.spec.ts b/src/app/core/filter/filter.service.spec.ts index 4ce3b4abd0..c1687dc72e 100644 --- a/src/app/core/filter/filter.service.spec.ts +++ b/src/app/core/filter/filter.service.spec.ts @@ -4,16 +4,19 @@ import { FilterService } from "./filter.service"; import { defaultInteractionTypes } from "../config/default-config/default-interaction-types"; import { DataFilter } from "../entity-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; import { Note } from "../../child-dev-project/notes/model/note"; -import { ConfigService } from "../config/config.service"; -import { createTestingConfigService } from "../config/testing-config-service"; +import { ConfigurableEnumService } from "../configurable-enum/configurable-enum.service"; +import { createTestingConfigurableEnumService } from "../configurable-enum/configurable-enum-testing"; describe("FilterService", () => { let service: FilterService; - beforeEach(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ providers: [ - { provide: ConfigService, useValue: createTestingConfigService() }, + { + provide: ConfigurableEnumService, + useValue: createTestingConfigurableEnumService(), + }, ], }); service = TestBed.inject(FilterService); diff --git a/src/app/utils/database-testing.module.ts b/src/app/utils/database-testing.module.ts index 4ad5d01c28..694d3af51e 100644 --- a/src/app/utils/database-testing.module.ts +++ b/src/app/utils/database-testing.module.ts @@ -8,6 +8,8 @@ import { environment } from "../../environments/environment"; import { createTestingConfigService } from "../core/config/testing-config-service"; import { AppModule } from "../app.module"; import { ComponentRegistry } from "../dynamic-components"; +import { ConfigurableEnumService } from "../core/configurable-enum/configurable-enum.service"; +import { createTestingConfigurableEnumService } from "../core/configurable-enum/configurable-enum-testing"; /** * Utility module that creates a simple environment where a correctly configured database and session is set up. @@ -24,6 +26,10 @@ import { ComponentRegistry } from "../dynamic-components"; providers: [ { provide: SessionService, useClass: LocalSession }, { provide: ConfigService, useValue: createTestingConfigService() }, + { + provide: ConfigurableEnumService, + useValue: createTestingConfigurableEnumService(), + }, ], }) export class DatabaseTestingModule { diff --git a/src/app/utils/mocked-testing.module.ts b/src/app/utils/mocked-testing.module.ts index 475f73b189..3509a9e944 100644 --- a/src/app/utils/mocked-testing.module.ts +++ b/src/app/utils/mocked-testing.module.ts @@ -20,6 +20,8 @@ import { HttpClientTestingModule } from "@angular/common/http/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { AppModule } from "../app.module"; import { ComponentRegistry } from "../dynamic-components"; +import { ConfigurableEnumService } from "../core/configurable-enum/configurable-enum.service"; +import { createTestingConfigurableEnumService } from "../core/configurable-enum/configurable-enum-testing"; export const TEST_USER = "test"; export const TEST_PASSWORD = "pass"; @@ -74,6 +76,10 @@ export class MockedTestingModule { { provide: SessionService, useValue: session }, { provide: EntityMapperService, useValue: mockedEntityMapper }, { provide: ConfigService, useValue: createTestingConfigService() }, + { + provide: ConfigurableEnumService, + useValue: createTestingConfigurableEnumService(), + }, { provide: Database, useValue: session.getDatabase() }, ], }; From ae8f446f909db6138d6c8b1244f90e34e860c6e5 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 30 Jan 2023 17:36:00 +0100 Subject: [PATCH 38/83] only saving config if something changed --- src/app/core/config/config.service.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 5bea3d6fa4..dc14fdbf8f 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -51,22 +51,25 @@ export class ConfigService { } private async saveAllEnumsToDB(config: Config) { + const enumValues = Object.entries(config.data).filter(([key]) => + key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX) + ); + if (enumValues.length === 0) { + return; + } const existingEnums = await this.entityMapper.loadType(ConfigurableEnum); if (existingEnums.length > 0) { - Object.keys(config.data) - .filter((key) => key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) - .forEach((key) => delete config.data[key]); + enumValues.forEach(([key]) => delete config.data[key]); return this.entityMapper.save(config).catch(() => {}); } - const enumEntities = Object.entries(config.data) - .filter(([key]) => key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX)) - .map(([key, value]) => { - const id = key.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, ""); - const newEnum = new ConfigurableEnum(id); - newEnum.values = value as any; - delete config.data[key]; - return newEnum; - }); + + const enumEntities = enumValues.map(([key, value]) => { + const id = key.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, ""); + const newEnum = new ConfigurableEnum(id); + newEnum.values = value as any; + delete config.data[key]; + return newEnum; + }); return this.entityMapper .saveAll(enumEntities) .then(() => this.entityMapper.save(config)) From 7600826f54c3c97be087d0a052a7fae276885c20 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 09:51:56 +0100 Subject: [PATCH 39/83] correctly cleaning up after test --- src/app/features/location/map/map.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/features/location/map/map.component.spec.ts b/src/app/features/location/map/map.component.spec.ts index a16b0d0dce..178a259486 100644 --- a/src/app/features/location/map/map.component.spec.ts +++ b/src/app/features/location/map/map.component.spec.ts @@ -92,6 +92,7 @@ describe("MapComponent", () => { }); marker.fireEvent("click"); + Child.schema.delete("address"); }); it("should open a popup with the same marker data", async () => { From d4168828f11f27e94fbc8a41cc6abffb9de1b7cc Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 09:52:14 +0100 Subject: [PATCH 40/83] fixed tests for autocomplete component --- .../basic-autocomplete.component.spec.ts | 72 ++++++++----------- .../basic-autocomplete.component.ts | 3 +- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 0ec2b87de3..da9d6fb8ff 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, fakeAsync, + flush, TestBed, tick, } from "@angular/core/testing"; @@ -8,17 +9,19 @@ import { import { BasicAutocompleteComponent } from "./basic-autocomplete.component"; import { School } from "../../../child-dev-project/schools/model/school"; import { Child } from "../../../child-dev-project/children/model/child"; -import { By } from "@angular/platform-browser"; -import { SimpleChange } from "@angular/core"; import { Entity } from "../../entity/model/entity"; import { FormControl } from "@angular/forms"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { MatDialogModule } from "@angular/material/dialog"; +import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; +import { HarnessLoader } from "@angular/cdk/testing"; +import { MatInputHarness } from "@angular/material/input/testing"; describe("BasicAutocompleteComponent", () => { let component: BasicAutocompleteComponent; let fixture: ComponentFixture>; + let loader: HarnessLoader; const entityToId = (e: Entity) => e?.getId(); @@ -33,6 +36,7 @@ describe("BasicAutocompleteComponent", () => { }).compileComponents(); fixture = TestBed.createComponent(BasicAutocompleteComponent); + loader = TestbedHarnessEnvironment.loader(fixture); component = fixture.componentInstance; component.form = new FormControl(); fixture.detectChanges(); @@ -49,41 +53,27 @@ describe("BasicAutocompleteComponent", () => { component.options = [school1, school2, school3]; component.updateAutocomplete(""); - expect(component.autocompleteSuggestedOptions.value).toEqual([ - school1, - school2, - school3, - ] as any); + expect(getAutocompleteOptions()).toEqual([school1, school2, school3]); component.updateAutocomplete("Aa"); - expect(component.autocompleteSuggestedOptions.value).toEqual([ - school1, - school2, - ] as any); + expect(getAutocompleteOptions()).toEqual([school1, school2]); component.updateAutocomplete("Aab"); - expect(component.autocompleteSuggestedOptions.value).toEqual([ - school2, - ] as any); + expect(getAutocompleteOptions()).toEqual([school2]); }); - it("should show name of the selected entity", fakeAsync(() => { + it("should show name of the selected entity", async () => { const child1 = Child.create("First Child"); const child2 = Child.create("Second Child"); - component.form.setValue(child1.getId()); + component._form.setValue(child1.getId()); component.options = [child1, child2]; component.valueMapper = entityToId; - component.ngOnChanges({ - form: new SimpleChange(null, component.form, false), - options: new SimpleChange(null, component.options, false), - }); - tick(); + component.ngOnChanges({ form: true, options: true } as any); fixture.detectChanges(); - expect(component.inputValue).toBe(child1 as any); - expect( - fixture.debugElement.query(By.css("#inputElement")).nativeElement.value - ).toEqual("First Child"); - })); + expect(component.autocompleteForm).toHaveValue("First Child"); + const inputElement = await loader.getHarness(MatInputHarness); + await expectAsync(inputElement.getValue()).toBeResolvedTo("First Child"); + }); it("Should have the correct entity selected when it's name is entered", () => { const child1 = Child.create("First Child"); @@ -91,28 +81,28 @@ describe("BasicAutocompleteComponent", () => { component.options = [child1, child2]; component.valueMapper = entityToId; - component.select("First Child"); + component.select({ asValue: child1.getId() } as any); - expect(component.inputValue).toBe(child1 as any); - expect(component.form.value).toBe(child1.getId()); + expect(component._form.value).toBe(child1.getId()); }); - it("Should unselect if no entity can be matched", () => { + it("should reset if nothing has been selected", fakeAsync(() => { const first = Child.create("First"); const second = Child.create("Second"); component.options = [first, second]; component.valueMapper = entityToId; - component.select(first as any); - expect(component.inputValue).toBe(first as any); - expect(component.form.value).toBe(first.getId()); + component.select({ asValue: first.getId() } as any); + expect(component._form.value).toBe(first.getId()); - component.select("second"); - expect(component.inputValue).toBe(second as any); - expect(component.form.value).toBe(second.getId()); + component.resetIfInvalidOption("Non existent"); + tick(); - component.select("NonExistent"); - expect(component.inputValue).toBe(undefined); - expect(component.form.value).toBe(undefined); - }); + expect(component._form.value).toBe(first.getId()); + flush(); + })); + + function getAutocompleteOptions() { + return component.autocompleteSuggestedOptions.value.map((o) => o.asValue); + } }); diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 3c20a70b55..a0d327a19c 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -83,6 +83,7 @@ export class BasicAutocompleteComponent implements OnChanges { } _options: SelectableOption[] = []; + @Input() multi?: boolean; @Input() set valueMapper(value: (option: O) => V) { @@ -107,13 +108,11 @@ export class BasicAutocompleteComponent implements OnChanges { @ContentChild(TemplateRef) templateRef: TemplateRef; autocompleteForm = new FormControl(""); - autocompleteSuggestedOptions = new BehaviorSubject[]>( [] ); showAddOption = false; addOptionTimeout: any; - inputValue = ""; constructor(private confirmation: ConfirmationDialogService) { this.autocompleteForm.valueChanges From cc4bf0c0d535ccdafc0684d30477d90e5e032dc6 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 09:55:28 +0100 Subject: [PATCH 41/83] added test for not saving unedited config --- src/app/core/config/config.service.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index c0cee8b099..a64bf2e063 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -155,6 +155,13 @@ describe("ConfigService", () => { expect(service.getConfig("some")).toBe("config"); }); + it("should not save config if nothing has been changed", async () => { + await initConfig({ some: "config", other: "config" }); + + expect(entityMapper.save).not.toHaveBeenCalled(); + expect(entityMapper.saveAll).not.toHaveBeenCalled(); + }); + function initConfig(data) { const config = new Config(); config.data = data; From 09eccdd0e8e637b48b2cf8224effb88c3624c716 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 11:20:27 +0100 Subject: [PATCH 42/83] got custom form control running --- .../basic-autocomplete.component.html | 116 +++++----- .../basic-autocomplete.component.ts | 206 ++++++++++++++---- .../enum-dropdown.component.html | 32 ++- 3 files changed, 249 insertions(+), 105 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 6ccb2df933..1b1488b300 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -1,59 +1,65 @@ - - {{ label }} + - - - - - - + - - - - - - Add option {{ inputElement.value }} - - - - + + + + + Add option {{ inputElement.value }} + + + + + + + + + Add option {{ inputElement.value }} + + - + + + + + + + - - This field is required - - diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index a0d327a19c..fbf0039508 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -1,12 +1,18 @@ import { Component, ContentChild, - EventEmitter, + ElementRef, + forwardRef, + HostBinding, + Inject, Input, OnChanges, - Output, + OnDestroy, + Optional, + Self, SimpleChanges, TemplateRef, + ViewChild, } from "@angular/core"; import { AsyncPipe, @@ -15,16 +21,28 @@ import { NgIf, NgTemplateOutlet, } from "@angular/common"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { + MAT_FORM_FIELD, + MatFormField, + MatFormFieldControl, + MatFormFieldModule, +} from "@angular/material/form-field"; +import { + ControlValueAccessor, + FormControl, + NG_VALUE_ACCESSOR, + NgControl, + ReactiveFormsModule, +} from "@angular/forms"; import { MatInputModule } from "@angular/material/input"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { BehaviorSubject } from "rxjs"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { BehaviorSubject, Subject } from "rxjs"; +import { UntilDestroy } from "@ngneat/until-destroy"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; interface SelectableOption { initial: O; @@ -42,7 +60,6 @@ interface SelectableOption { imports: [ NgForOf, NgTemplateOutlet, - MatFormFieldModule, ReactiveFormsModule, MatInputModule, NgIf, @@ -52,31 +69,133 @@ interface SelectableOption { NgClass, MatCheckboxModule, ], + providers: [ + { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent }, + ], //changeDetection: ChangeDetectionStrategy.OnPush }) -export class BasicAutocompleteComponent implements OnChanges { - // cannot be named "formControl" - otherwise the angular directive grabs this - @Input() set form(form: FormControl) { - this._form = form; - this.setInputValue(); - if (form.disabled) { - this.autocompleteForm.disable(); +export class BasicAutocompleteComponent + implements + MatFormFieldControl, + OnDestroy, + OnChanges, + ControlValueAccessor +{ + stateChanges = new Subject(); + + writeValue(obj: V | V[]): void { + this.value = obj; + } + + registerOnChange(fn: any): void {} + + registerOnTouched(fn: any): void {} + + setDisabledState(isDisabled: boolean): void {} + + @Input() get value(): V | V[] { + return this.selected; + } + + set value(value: V | V[]) { + this.selected = value; + this.stateChanges.next(); + } + + selected: V | V[]; + + static nextId = 0; + @HostBinding() + id = `basic-autocomplete-${BasicAutocompleteComponent.nextId++}`; + + @Input() + get placeholder() { + return this._placeholder; + } + + set placeholder(plh: string) { + this._placeholder = plh; + this.stateChanges.next(); + } + + private _placeholder: string; + + @ViewChild("inputElement") inputElement: ElementRef; + + focused = false; + touched = false; + + onFocusIn(event: FocusEvent) { + if (!this.focused) { + this.focused = true; + this.stateChanges.next(); } - form.statusChanges.subscribe((status) => { - if (status === "DISABLED") { - this.autocompleteForm.disable(); - } else { - this.autocompleteForm.enable(); - } - }); - form.valueChanges - .pipe(untilDestroyed(this)) - .subscribe(() => this.setInputValue()); } - _form: FormControl; + onFocusOut(event: FocusEvent) { + if ( + !this._elementRef.nativeElement.contains(event.relatedTarget as Element) + ) { + this.touched = true; + this.focused = false; + this.resetIfInvalidOption(this.inputElement.nativeElement.value); + this.stateChanges.next(); + } + } + + get empty() { + return !this.selected; + } + + @HostBinding("class.floating") + get shouldLabelFloat() { + return this.focused || !this.empty; + } + + @Input() + get required() { + return this._required; + } + + set required(req) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + + private _required = false; + + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + this.stateChanges.next(); + } + + private _disabled = false; + + get errorState(): boolean { + return this.touched; + } + + controlType = "basic-autocomplete"; - @Input() label: string; + @Input("aria-describedby") userAriaDescribedBy: string; + + setDescribedByIds(ids: string[]) { + const controlElement = this._elementRef.nativeElement.querySelector( + ".autocomplete-input" + )!; + controlElement.setAttribute("aria-describedby", ids.join(" ")); + } + + onContainerClick(event: MouseEvent) { + if ((event.target as Element).tagName.toLowerCase() != "input") { + this._elementRef.nativeElement.focus(); + } + } @Input() set options(options: O[]) { this._options = options.map((o) => this.toSelectableOption(o)); @@ -102,9 +221,6 @@ export class BasicAutocompleteComponent implements OnChanges { @Input() createOption: (input: string) => O; - @Input() showWrench = false; - @Output() wrenchClick = new EventEmitter(); - @ContentChild(TemplateRef) templateRef: TemplateRef; autocompleteForm = new FormControl(""); @@ -114,17 +230,32 @@ export class BasicAutocompleteComponent implements OnChanges { showAddOption = false; addOptionTimeout: any; - constructor(private confirmation: ConfirmationDialogService) { + constructor( + private confirmation: ConfirmationDialogService, + private _elementRef: ElementRef, + @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField, + @Optional() @Self() public ngControl: NgControl + ) { + // Replace the provider from above with this. + if (this.ngControl != null) { + // Setting the value accessor directly (instead of using + // the providers) to avoid running into a circular import. + this.ngControl.valueAccessor = this; + } this.autocompleteForm.valueChanges .pipe(filter((val) => typeof val === "string")) .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); } + ngOnDestroy() { + this.stateChanges.complete(); + } + ngOnChanges(changes: SimpleChanges) { - if (changes.form || changes.options) { + if (changes.value || changes.options) { if (this.multi) { this._options - .filter(({ asValue }) => (this._form.value as V[])?.includes(asValue)) + .filter(({ asValue }) => (this.value as V[])?.includes(asValue)) .forEach((o) => (o.selected = true)); } this.setInputValue(); @@ -141,7 +272,7 @@ export class BasicAutocompleteComponent implements OnChanges { ); } else { const selected = this._options.find( - ({ asValue }) => asValue === this._form.value + ({ asValue }) => asValue === this.value ); this.autocompleteForm.setValue(selected?.asString ?? ""); } @@ -178,7 +309,7 @@ export class BasicAutocompleteComponent implements OnChanges { this.selectOption(selected); } else { this.autocompleteForm.setValue(""); - this._form.setValue(undefined); + this.value = undefined; } } @@ -197,12 +328,11 @@ export class BasicAutocompleteComponent implements OnChanges { private selectOption(option: SelectableOption) { if (this.multi) { option.selected = !option.selected; - const selected = this._options + this.value = this._options .filter((o) => o.selected) .map((o) => o.asValue); - this._form.setValue(selected); } else { - this._form.setValue(option.asValue); + this.value = option.asValue; } } @@ -218,7 +348,7 @@ export class BasicAutocompleteComponent implements OnChanges { resetIfInvalidOption(input: string) { // waiting for other tasks to finish and then reset input if nothing was selected setTimeout(() => { - const activeOption = this._optionToString(this._form.value); + const activeOption = this._optionToString(this.value); if (input !== activeOption) { this.autocompleteForm.setValue(activeOption); } diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index 131af0f6c4..d5838b0653 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -1,12 +1,20 @@ - - {{ item.label }} - + + {{label}} + + + {{ item.label }} + + From eb62759c2100f7aa6570db1954906a36328850ec Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 11:32:12 +0100 Subject: [PATCH 43/83] basic autocomplete is working --- .../basic-autocomplete.component.html | 1 - .../basic-autocomplete.component.ts | 68 +++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 1b1488b300..107006e1a4 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -1,7 +1,6 @@ { stateChanges = new Subject(); - writeValue(obj: V | V[]): void { - this.value = obj; - } - - registerOnChange(fn: any): void {} - - registerOnTouched(fn: any): void {} - - setDisabledState(isDisabled: boolean): void {} + onChange = (_: any) => {}; + onTouched = () => {}; @Input() get value(): V | V[] { return this.selected; @@ -99,6 +89,7 @@ export class BasicAutocompleteComponent set value(value: V | V[]) { this.selected = value; + this.setInputValue(); this.stateChanges.next(); } @@ -138,7 +129,7 @@ export class BasicAutocompleteComponent ) { this.touched = true; this.focused = false; - this.resetIfInvalidOption(this.inputElement.nativeElement.value); + // this.resetIfInvalidOption(this.inputElement.nativeElement.value); this.stateChanges.next(); } } @@ -177,26 +168,13 @@ export class BasicAutocompleteComponent private _disabled = false; get errorState(): boolean { - return this.touched; + return false; } controlType = "basic-autocomplete"; @Input("aria-describedby") userAriaDescribedBy: string; - setDescribedByIds(ids: string[]) { - const controlElement = this._elementRef.nativeElement.querySelector( - ".autocomplete-input" - )!; - controlElement.setAttribute("aria-describedby", ids.join(" ")); - } - - onContainerClick(event: MouseEvent) { - if ((event.target as Element).tagName.toLowerCase() != "input") { - this._elementRef.nativeElement.focus(); - } - } - @Input() set options(options: O[]) { this._options = options.map((o) => this.toSelectableOption(o)); } @@ -243,7 +221,12 @@ export class BasicAutocompleteComponent this.ngControl.valueAccessor = this; } this.autocompleteForm.valueChanges - .pipe(filter((val) => typeof val === "string")) + .pipe( + filter((val) => { + console.log("value", val); + return typeof val === "string"; + }) + ) .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); } @@ -354,4 +337,33 @@ export class BasicAutocompleteComponent } }); } + + setDescribedByIds(ids: string[]) { + const controlElement = this._elementRef.nativeElement.querySelector( + ".autocomplete-input" + )!; + controlElement.setAttribute("aria-describedby", ids.join(" ")); + } + + onContainerClick(event: MouseEvent) { + if ((event.target as Element).tagName.toLowerCase() != "input") { + this._elementRef.nativeElement.focus(); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(obj: V | V[]): void { + this.value = obj; + } } From 9194354a618f355bb727c5eb3a82045a33325eb0 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 11:51:06 +0100 Subject: [PATCH 44/83] added caret to parent component --- .../basic-autocomplete.component.html | 9 --------- .../basic-autocomplete.component.scss | 12 ------------ .../basic-autocomplete.component.ts | 1 + .../enum-dropdown/enum-dropdown.component.html | 8 ++++++++ .../enum-dropdown/enum-dropdown.component.scss | 6 ++++++ .../enum-dropdown/enum-dropdown.component.ts | 2 ++ 6 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 107006e1a4..1d93270c0e 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -53,12 +53,3 @@ Add option {{ inputElement.value }} - - - - - - - - - diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss index 768bff856c..e69de29bb2 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss @@ -1,12 +0,0 @@ -@use "src/styles/variables/sizes"; -@use "src/styles/variables/colors"; - -.caret-suffix { - cursor: pointer; - padding: 0 sizes.$small; -} - -.disabled { - color: colors.$disabled; - cursor: default; -} diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 383cc81609..bd38ae32db 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -280,6 +280,7 @@ export class BasicAutocompleteComponent } } this.autocompleteSuggestedOptions.next(filteredEntities); + this.inputElement?.nativeElement.focus(); } select(selected: string | SelectableOption) { diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index d5838b0653..b853daddc0 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -1,6 +1,7 @@ {{label}} {{ item.label }} + diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss index e69de29bb2..58e875b11c 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.scss @@ -0,0 +1,6 @@ +@use "src/styles/variables/sizes"; + +.caret-suffix { + cursor: pointer; + padding: 0 sizes.$small; +} diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts index 87b671d4b5..55e45adf38 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -11,6 +11,7 @@ import { ConfigurableEnum } from "../configurable-enum"; import { EntityAbility } from "../../permissions/ability/entity-ability"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-enum-popup.component"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; @Component({ selector: "app-enum-dropdown", @@ -25,6 +26,7 @@ import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-e NgIf, NgForOf, BasicAutocompleteComponent, + FontAwesomeModule, ], }) export class EnumDropdownComponent implements OnChanges { From 70fe4d4e4eb015d3fd08b6f0bf7e0901130baed2 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 11:54:16 +0100 Subject: [PATCH 45/83] added additional options --- .../enum-dropdown/enum-dropdown.component.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index b853daddc0..293bcfa9a5 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -9,15 +9,15 @@ [createOption]="createNewOption" > - {{ item.label }} + Date: Tue, 31 Jan 2023 12:23:18 +0100 Subject: [PATCH 46/83] keeping focus when dropdown is clicked --- .../basic-autocomplete.component.html | 2 +- .../basic-autocomplete.component.ts | 60 +++++++------------ .../enum-dropdown.component.html | 2 +- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 1d93270c0e..5898bdcc39 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -5,7 +5,7 @@ matInput style="text-overflow: ellipsis" [matAutocomplete]="autoSuggestions" - (focusin)="onFocusIn($event)" + (focusin)="onFocusIn()" (focusout)="onFocusOut($event)" /> diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index bd38ae32db..11843f2540 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -13,13 +13,7 @@ import { TemplateRef, ViewChild, } from "@angular/core"; -import { - AsyncPipe, - NgClass, - NgForOf, - NgIf, - NgTemplateOutlet, -} from "@angular/common"; +import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from "@angular/common"; import { MAT_FORM_FIELD, MatFormField, @@ -32,8 +26,10 @@ import { ReactiveFormsModule, } from "@angular/forms"; import { MatInputModule } from "@angular/material/input"; -import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { + MatAutocomplete, + MatAutocompleteModule, +} from "@angular/material/autocomplete"; import { BehaviorSubject, Subject } from "rxjs"; import { UntilDestroy } from "@ngneat/until-destroy"; import { MatCheckboxModule } from "@angular/material/checkbox"; @@ -55,16 +51,14 @@ interface SelectableOption { styleUrls: ["./basic-autocomplete.component.scss"], standalone: true, imports: [ - NgForOf, - NgTemplateOutlet, ReactiveFormsModule, MatInputModule, - NgIf, - FontAwesomeModule, MatAutocompleteModule, AsyncPipe, - NgClass, + NgForOf, MatCheckboxModule, + NgIf, + NgTemplateOutlet, ], providers: [ { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent }, @@ -99,24 +93,14 @@ export class BasicAutocompleteComponent @HostBinding() id = `basic-autocomplete-${BasicAutocompleteComponent.nextId++}`; - @Input() - get placeholder() { - return this._placeholder; - } - - set placeholder(plh: string) { - this._placeholder = plh; - this.stateChanges.next(); - } - - private _placeholder: string; + @Input() placeholder: string; @ViewChild("inputElement") inputElement: ElementRef; - + @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete; focused = false; touched = false; - onFocusIn(event: FocusEvent) { + onFocusIn() { if (!this.focused) { this.focused = true; this.stateChanges.next(); @@ -125,17 +109,18 @@ export class BasicAutocompleteComponent onFocusOut(event: FocusEvent) { if ( + !this.autocomplete.isOpen && !this._elementRef.nativeElement.contains(event.relatedTarget as Element) ) { this.touched = true; this.focused = false; - // this.resetIfInvalidOption(this.inputElement.nativeElement.value); + this.resetIfInvalidOption(this.inputElement.nativeElement.value); this.stateChanges.next(); } } get empty() { - return !this.selected; + return !this.value; } @HostBinding("class.floating") @@ -167,9 +152,7 @@ export class BasicAutocompleteComponent private _disabled = false; - get errorState(): boolean { - return false; - } + errorState = false; controlType = "basic-autocomplete"; @@ -221,12 +204,7 @@ export class BasicAutocompleteComponent this.ngControl.valueAccessor = this; } this.autocompleteForm.valueChanges - .pipe( - filter((val) => { - console.log("value", val); - return typeof val === "string"; - }) - ) + .pipe(filter((val) => typeof val === "string")) .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); } @@ -261,6 +239,11 @@ export class BasicAutocompleteComponent } } + showAutocomplete(inputText?: string) { + this.updateAutocomplete(inputText); + this.inputElement?.nativeElement.focus(); + } + updateAutocomplete(inputText: string) { let filteredEntities = this._options; this.showAddOption = false; @@ -280,7 +263,6 @@ export class BasicAutocompleteComponent } } this.autocompleteSuggestedOptions.next(filteredEntities); - this.inputElement?.nativeElement.focus(); } select(selected: string | SelectableOption) { diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index 293bcfa9a5..3d60d50efa 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -22,7 +22,7 @@ *ngIf="form.enabled" icon="caret-down" class="caret-suffix" - (click)="autocomplete.updateAutocomplete('')" + (click)="autocomplete.showAutocomplete()" matSuffix > From e9f5ffab4836f25c2f523128aff63aac19232d7e Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 12:24:31 +0100 Subject: [PATCH 47/83] disabled prettier warning --- .../basic-autocomplete/basic-autocomplete.component.ts | 1 + .../configurable-enum/enum-dropdown/enum-dropdown.component.html | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 11843f2540..a1b452a9eb 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -156,6 +156,7 @@ export class BasicAutocompleteComponent controlType = "basic-autocomplete"; + // eslint-disable-next-line @angular-eslint/no-input-rename @Input("aria-describedby") userAriaDescribedBy: string; @Input() set options(options: O[]) { diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index 3d60d50efa..e025625ca6 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -7,7 +7,6 @@ [options]="options" [optionToString]="enumValueToString" [createOption]="createNewOption" - > {{ item.label }} From 98486e031a4216067b1cff7617e8d01c7885f80c Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 15:21:50 +0100 Subject: [PATCH 48/83] fixed all tests --- .../basic-autocomplete.component.spec.ts | 10 +++--- .../entity-list/entity-list.component.spec.ts | 1 - .../edit-single-entity.component.html | 36 ++++++++++++------- .../edit-single-entity.component.scss | 6 ++++ .../edit-single-entity.component.ts | 13 ++++++- .../session/login/login.component.spec.ts | 11 ++++-- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index da9d6fb8ff..5786ba2d89 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -10,7 +10,6 @@ import { BasicAutocompleteComponent } from "./basic-autocomplete.component"; import { School } from "../../../child-dev-project/schools/model/school"; import { Child } from "../../../child-dev-project/children/model/child"; import { Entity } from "../../entity/model/entity"; -import { FormControl } from "@angular/forms"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { MatDialogModule } from "@angular/material/dialog"; @@ -38,7 +37,6 @@ describe("BasicAutocompleteComponent", () => { fixture = TestBed.createComponent(BasicAutocompleteComponent); loader = TestbedHarnessEnvironment.loader(fixture); component = fixture.componentInstance; - component.form = new FormControl(); fixture.detectChanges(); }); @@ -63,7 +61,7 @@ describe("BasicAutocompleteComponent", () => { it("should show name of the selected entity", async () => { const child1 = Child.create("First Child"); const child2 = Child.create("Second Child"); - component._form.setValue(child1.getId()); + component.value = child1.getId(); component.options = [child1, child2]; component.valueMapper = entityToId; @@ -83,7 +81,7 @@ describe("BasicAutocompleteComponent", () => { component.select({ asValue: child1.getId() } as any); - expect(component._form.value).toBe(child1.getId()); + expect(component.value).toBe(child1.getId()); }); it("should reset if nothing has been selected", fakeAsync(() => { @@ -93,12 +91,12 @@ describe("BasicAutocompleteComponent", () => { component.valueMapper = entityToId; component.select({ asValue: first.getId() } as any); - expect(component._form.value).toBe(first.getId()); + expect(component.value).toBe(first.getId()); component.resetIfInvalidOption("Non existent"); tick(); - expect(component._form.value).toBe(first.getId()); + expect(component.value).toBe(first.getId()); flush(); })); diff --git a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts index 02409d4aeb..f28da23f9a 100644 --- a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts +++ b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts @@ -131,7 +131,6 @@ describe("EntityListComponent", () => { createComponent(); await initComponentInputs(); expect(component.selectedColumnGroupIndex).toBe(1); - console.log("component groups", component.columnGroups); const tabGroup = await loader.getHarness(MatTabGroupHarness); const groups = await tabGroup.getTabs(); diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html index 933224c33b..2036d46527 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.html @@ -1,13 +1,23 @@ - - - - - + + {{label}} + + + + + + + diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.scss b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.scss index b1387128db..d038e23e96 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.scss +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.scss @@ -1,5 +1,11 @@ @use "../../entity-utils/common-styles" as *; +@use "src/styles/variables/sizes"; .block-wrapper { @include entity-block-border; } + +.caret-suffix { + cursor: pointer; + padding: 0 sizes.$small; +} diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts index f3a8140f5c..e77391a831 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.ts @@ -8,13 +8,24 @@ import { DynamicComponent } from "../../../view/dynamic-components/dynamic-compo import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { DisplayEntityComponent } from "../display-entity/display-entity.component"; import { BasicAutocompleteComponent } from "../../../configurable-enum/basic-autocomplete/basic-autocomplete.component"; +import { ReactiveFormsModule } from "@angular/forms"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { NgIf } from "@angular/common"; @DynamicComponent("EditSingleEntity") @Component({ selector: "app-edit-single-entity", templateUrl: "./edit-single-entity.component.html", styleUrls: ["./edit-single-entity.component.scss"], - imports: [BasicAutocompleteComponent, DisplayEntityComponent], + imports: [ + BasicAutocompleteComponent, + DisplayEntityComponent, + ReactiveFormsModule, + MatFormFieldModule, + FontAwesomeModule, + NgIf, + ], standalone: true, }) export class EditSingleEntityComponent extends EditComponent { diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts index 898d2141db..2af9ccf772 100644 --- a/src/app/core/session/login/login.component.spec.ts +++ b/src/app/core/session/login/login.component.spec.ts @@ -31,12 +31,16 @@ import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { AuthService } from "../auth/auth.service"; import { Subject } from "rxjs"; import { ActivatedRoute, Router } from "@angular/router"; +import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; +import { HarnessLoader } from "@angular/cdk/testing"; +import { MatInputHarness } from "@angular/material/input/testing"; describe("LoginComponent", () => { let component: LoginComponent; let fixture: ComponentFixture; let mockSessionService: jasmine.SpyObj; let loginState = new Subject(); + let loader: HarnessLoader; beforeEach(waitForAsync(() => { mockSessionService = jasmine.createSpyObj(["login"], { loginState }); @@ -51,6 +55,7 @@ describe("LoginComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); + loader = TestbedHarnessEnvironment.loader(fixture); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -85,13 +90,13 @@ describe("LoginComponent", () => { expect(component.errorMessage).toBeTruthy(); })); - it("should focus the first input element on initialization", fakeAsync(() => { + it("should focus the first input element on initialization", fakeAsync(async () => { component.ngAfterViewInit(); tick(); fixture.detectChanges(); - const firstInputElement = document.getElementsByTagName("input")[0]; - expect(document.activeElement).toBe(firstInputElement); + const firstInputElement = await loader.getHarness(MatInputHarness); + await expectAsync(firstInputElement.isFocused()).toBeResolvedTo(true); })); it("should route to redirect uri once state changes to 'logged-in'", () => { From c12b58ccd0759ab1d915a5c720d4e36b14de7c65 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 31 Jan 2023 17:52:15 +0100 Subject: [PATCH 49/83] fixed basic autocomplete component --- .../basic-autocomplete.component.ts | 212 +++++++++--------- 1 file changed, 101 insertions(+), 111 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index a1b452a9eb..3390284487 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -26,115 +26,95 @@ import { ReactiveFormsModule, } from "@angular/forms"; import { MatInputModule } from "@angular/material/input"; -import { - MatAutocomplete, - MatAutocompleteModule, -} from "@angular/material/autocomplete"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { BehaviorSubject, Subject } from "rxjs"; -import { UntilDestroy } from "@ngneat/until-destroy"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; -import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; -interface SelectableOption { +export interface SelectableOption { initial: O; asString: string; asValue: V; selected: boolean; } -@UntilDestroy() +/** Custom `MatFormFieldControl` for telephone number input. */ @Component({ selector: "app-basic-autocomplete", - templateUrl: "./basic-autocomplete.component.html", - styleUrls: ["./basic-autocomplete.component.scss"], + templateUrl: "basic-autocomplete.component.html", + styleUrls: ["basic-autocomplete.component.scss"], + providers: [ + { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent }, + ], standalone: true, imports: [ ReactiveFormsModule, MatInputModule, MatAutocompleteModule, - AsyncPipe, NgForOf, MatCheckboxModule, NgIf, + AsyncPipe, NgTemplateOutlet, ], - providers: [ - { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent }, - ], - //changeDetection: ChangeDetectionStrategy.OnPush }) export class BasicAutocompleteComponent implements + ControlValueAccessor, MatFormFieldControl, OnDestroy, - OnChanges, - ControlValueAccessor + OnChanges { - stateChanges = new Subject(); - - onChange = (_: any) => {}; - onTouched = () => {}; - - @Input() get value(): V | V[] { - return this.selected; - } - - set value(value: V | V[]) { - this.selected = value; - this.setInputValue(); - this.stateChanges.next(); - } - - selected: V | V[]; - static nextId = 0; @HostBinding() id = `basic-autocomplete-${BasicAutocompleteComponent.nextId++}`; - - @Input() placeholder: string; - + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input("aria-describedby") userAriaDescribedBy: string; + @ContentChild(TemplateRef) templateRef: TemplateRef; @ViewChild("inputElement") inputElement: ElementRef; - @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete; + + stateChanges = new Subject(); focused = false; touched = false; - - onFocusIn() { - if (!this.focused) { - this.focused = true; - this.stateChanges.next(); - } - } - - onFocusOut(event: FocusEvent) { - if ( - !this.autocomplete.isOpen && - !this._elementRef.nativeElement.contains(event.relatedTarget as Element) - ) { - this.touched = true; - this.focused = false; - this.resetIfInvalidOption(this.inputElement.nativeElement.value); - this.stateChanges.next(); - } - } + controlType = "basic-autocomplete"; + autocompleteForm = new FormControl(""); + autocompleteSuggestedOptions = new BehaviorSubject[]>( + [] + ); + showAddOption = false; + addOptionTimeout: any; + onChange = (_: any) => {}; + onTouched = () => {}; get empty() { return !this.value; } - @HostBinding("class.floating") get shouldLabelFloat() { return this.focused || !this.empty; } @Input() - get required() { + get placeholder(): string { + return this._placeholder; + } + + set placeholder(value: string) { + this._placeholder = value; + this.stateChanges.next(); + } + + private _placeholder: string; + + @Input() + get required(): boolean { return this._required; } - set required(req) { - this._required = coerceBooleanProperty(req); + set required(value: BooleanInput) { + this._required = coerceBooleanProperty(value); this.stateChanges.next(); } @@ -145,19 +125,33 @@ export class BasicAutocompleteComponent return this._disabled; } - set disabled(value: boolean) { + set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); + if (this._disabled) { + this.autocompleteForm.disable(); + } else { + this.autocompleteForm.enable(); + } this.stateChanges.next(); } private _disabled = false; - errorState = false; + @Input() get value(): V | V[] { + return this._value; + } - controlType = "basic-autocomplete"; + set value(value: V | V[]) { + this._value = value; + this.setInputValue(); + this.stateChanges.next(); + } - // eslint-disable-next-line @angular-eslint/no-input-rename - @Input("aria-describedby") userAriaDescribedBy: string; + private _value: V | V[]; + + get errorState(): boolean { + return false; + } @Input() set options(options: O[]) { this._options = options.map((o) => this.toSelectableOption(o)); @@ -165,8 +159,6 @@ export class BasicAutocompleteComponent _options: SelectableOption[] = []; - @Input() multi?: boolean; - @Input() set valueMapper(value: (option: O) => V) { this._valueMapper = value; this._options.forEach((opt) => (opt.asValue = value(opt.initial))); @@ -183,25 +175,15 @@ export class BasicAutocompleteComponent @Input() createOption: (input: string) => O; - @ContentChild(TemplateRef) templateRef: TemplateRef; - - autocompleteForm = new FormControl(""); - autocompleteSuggestedOptions = new BehaviorSubject[]>( - [] - ); - showAddOption = false; - addOptionTimeout: any; + @Input() multi?: boolean; constructor( + private elementRef: ElementRef, private confirmation: ConfirmationDialogService, - private _elementRef: ElementRef, @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField, @Optional() @Self() public ngControl: NgControl ) { - // Replace the provider from above with this. if (this.ngControl != null) { - // Setting the value accessor directly (instead of using - // the providers) to avoid running into a circular import. this.ngControl.valueAccessor = this; } this.autocompleteForm.valueChanges @@ -224,28 +206,12 @@ export class BasicAutocompleteComponent } } - private setInputValue() { - if (this.multi) { - this.autocompleteForm.setValue( - this._options - .filter((o) => o.selected) - .map((o) => o.asString) - .join(", ") - ); - } else { - const selected = this._options.find( - ({ asValue }) => asValue === this.value - ); - this.autocompleteForm.setValue(selected?.asString ?? ""); - } - } - showAutocomplete(inputText?: string) { this.updateAutocomplete(inputText); this.inputElement?.nativeElement.focus(); } - updateAutocomplete(inputText: string) { + private updateAutocomplete(inputText: string) { let filteredEntities = this._options; this.showAddOption = false; clearTimeout(this.addOptionTimeout); @@ -266,6 +232,22 @@ export class BasicAutocompleteComponent this.autocompleteSuggestedOptions.next(filteredEntities); } + private setInputValue() { + if (this.multi) { + this.autocompleteForm.setValue( + this._options + .filter((o) => o.selected) + .map((o) => o.asString) + .join(", ") + ); + } else { + const selected = this._options.find( + ({ asValue }) => asValue === this.value + ); + this.autocompleteForm.setValue(selected?.asString ?? ""); + } + } + select(selected: string | SelectableOption) { if (typeof selected === "string") { this.createNewOption(selected); @@ -312,18 +294,26 @@ export class BasicAutocompleteComponent }; } - resetIfInvalidOption(input: string) { - // waiting for other tasks to finish and then reset input if nothing was selected - setTimeout(() => { - const activeOption = this._optionToString(this.value); - if (input !== activeOption) { - this.autocompleteForm.setValue(activeOption); - } - }); + onFocusIn() { + if (!this.focused) { + this.focused = true; + this.stateChanges.next(); + } + } + + onFocusOut(event: FocusEvent) { + if ( + !this.elementRef.nativeElement.contains(event.relatedTarget as Element) + ) { + this.touched = true; + this.focused = false; + this.onTouched(); + this.stateChanges.next(); + } } setDescribedByIds(ids: string[]) { - const controlElement = this._elementRef.nativeElement.querySelector( + const controlElement = this.elementRef.nativeElement.querySelector( ".autocomplete-input" )!; controlElement.setAttribute("aria-describedby", ids.join(" ")); @@ -331,10 +321,14 @@ export class BasicAutocompleteComponent onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName.toLowerCase() != "input") { - this._elementRef.nativeElement.focus(); + this.elementRef.nativeElement.focus(); } } + writeValue(val: V | V[]): void { + this.value = val; + } + registerOnChange(fn: any): void { this.onChange = fn; } @@ -346,8 +340,4 @@ export class BasicAutocompleteComponent setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } - - writeValue(obj: V | V[]): void { - this.value = obj; - } } From 9b4cb3df1ae3fc2692abaf3aa81c23db89049ee0 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 Feb 2023 10:44:26 +0100 Subject: [PATCH 50/83] properly triggering changes --- .../basic-autocomplete/basic-autocomplete.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 3390284487..b761f79cec 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -260,6 +260,7 @@ export class BasicAutocompleteComponent this.autocompleteForm.setValue(""); this.value = undefined; } + this.onChange(this.value); } async createNewOption(option: string) { From 02ed9c941bde305c1feb465c0f8e98964c1e63a4 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 Feb 2023 15:21:23 +0100 Subject: [PATCH 51/83] in multi mode, form is cleared on focus in --- .../basic-autocomplete.component.ts | 58 ++++++++++++++----- .../enum-dropdown/enum-dropdown.stories.ts | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index b761f79cec..a746dd8bd0 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -26,8 +26,11 @@ import { ReactiveFormsModule, } from "@angular/forms"; import { MatInputModule } from "@angular/material/input"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { BehaviorSubject, Subject } from "rxjs"; +import { + MatAutocompleteModule, + MatAutocompleteTrigger, +} from "@angular/material/autocomplete"; +import { BehaviorSubject, Subject, Subscription } from "rxjs"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; @@ -74,6 +77,7 @@ export class BasicAutocompleteComponent @Input("aria-describedby") userAriaDescribedBy: string; @ContentChild(TemplateRef) templateRef: TemplateRef; @ViewChild("inputElement") inputElement: ElementRef; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; stateChanges = new Subject(); focused = false; @@ -83,6 +87,7 @@ export class BasicAutocompleteComponent autocompleteSuggestedOptions = new BehaviorSubject[]>( [] ); + closeSubscription: Subscription; showAddOption = false; addOptionTimeout: any; onChange = (_: any) => {}; @@ -143,7 +148,6 @@ export class BasicAutocompleteComponent set value(value: V | V[]) { this._value = value; - this.setInputValue(); this.stateChanges.next(); } @@ -188,7 +192,7 @@ export class BasicAutocompleteComponent } this.autocompleteForm.valueChanges .pipe(filter((val) => typeof val === "string")) - .subscribe((val) => this.updateAutocomplete(val?.split(", ").pop())); + .subscribe((val) => this.updateAutocomplete(val)); } ngOnDestroy() { @@ -234,12 +238,7 @@ export class BasicAutocompleteComponent private setInputValue() { if (this.multi) { - this.autocompleteForm.setValue( - this._options - .filter((o) => o.selected) - .map((o) => o.asString) - .join(", ") - ); + this.displaySelectedOptions(); } else { const selected = this._options.find( ({ asValue }) => asValue === this.value @@ -248,6 +247,15 @@ export class BasicAutocompleteComponent } } + private displaySelectedOptions() { + this.autocompleteForm.setValue( + this._options + .filter((o) => o.selected) + .map((o) => o.asString) + .join(", ") + ); + } + select(selected: string | SelectableOption) { if (typeof selected === "string") { this.createNewOption(selected); @@ -281,7 +289,11 @@ export class BasicAutocompleteComponent this.value = this._options .filter((o) => o.selected) .map((o) => o.asValue); + // re-open autocomplete to select next option + this.autocompleteForm.setValue(""); + setTimeout(() => this.autocomplete.openPanel(), 100); } else { + this.autocompleteForm.setValue(option.asString); this.value = option.asValue; } } @@ -297,6 +309,10 @@ export class BasicAutocompleteComponent onFocusIn() { if (!this.focused) { + this.closeSubscription?.unsubscribe(); + if (this.multi) { + this.autocompleteForm.setValue(""); + } this.focused = true; this.stateChanges.next(); } @@ -306,11 +322,25 @@ export class BasicAutocompleteComponent if ( !this.elementRef.nativeElement.contains(event.relatedTarget as Element) ) { - this.touched = true; - this.focused = false; - this.onTouched(); - this.stateChanges.next(); + if (!this.autocomplete.panelOpen) { + this.notifyFocusOut(); + } else { + // trigger focus out once panel is closed + this.closeSubscription = this.autocomplete.panelClosingActions + .pipe(filter((res) => res === null)) + .subscribe(() => this.notifyFocusOut()); + } + } + } + + private notifyFocusOut() { + if (this.multi) { + this.displaySelectedOptions(); } + this.touched = true; + this.focused = false; + this.onTouched(); + this.stateChanges.next(); } setDescribedByIds(ids: string[]) { diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts index 697eba8331..c163aa02da 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.stories.ts @@ -57,7 +57,7 @@ Disabled.args = { export const Multi = Template.bind({}); Multi.args = { - form: new FormControl([]), + form: new FormControl([centersUnique[0], centersUnique[2]]), label: "test field", enumId: "center", multi: true, From 6ec0e284cc3ecf90eb53f7f23e552e6802b022f6 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 Feb 2023 15:39:38 +0100 Subject: [PATCH 52/83] improved de-focus and focus of input element --- .../basic-autocomplete.component.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index a746dd8bd0..69a8bf4290 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -30,7 +30,7 @@ import { MatAutocompleteModule, MatAutocompleteTrigger, } from "@angular/material/autocomplete"; -import { BehaviorSubject, Subject, Subscription } from "rxjs"; +import { BehaviorSubject, Subject } from "rxjs"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; @@ -87,9 +87,9 @@ export class BasicAutocompleteComponent autocompleteSuggestedOptions = new BehaviorSubject[]>( [] ); - closeSubscription: Subscription; + private delayedBlur: any; showAddOption = false; - addOptionTimeout: any; + private addOptionTimeout: any; onChange = (_: any) => {}; onTouched = () => {}; @@ -161,7 +161,7 @@ export class BasicAutocompleteComponent this._options = options.map((o) => this.toSelectableOption(o)); } - _options: SelectableOption[] = []; + private _options: SelectableOption[] = []; @Input() set valueMapper(value: (option: O) => V) { this._valueMapper = value; @@ -308,8 +308,8 @@ export class BasicAutocompleteComponent } onFocusIn() { + clearTimeout(this.delayedBlur); if (!this.focused) { - this.closeSubscription?.unsubscribe(); if (this.multi) { this.autocompleteForm.setValue(""); } @@ -326,9 +326,7 @@ export class BasicAutocompleteComponent this.notifyFocusOut(); } else { // trigger focus out once panel is closed - this.closeSubscription = this.autocomplete.panelClosingActions - .pipe(filter((res) => res === null)) - .subscribe(() => this.notifyFocusOut()); + this.delayedBlur = setTimeout(() => this.notifyFocusOut(), 100); } } } @@ -352,7 +350,7 @@ export class BasicAutocompleteComponent onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName.toLowerCase() != "input") { - this.elementRef.nativeElement.focus(); + this.inputElement.nativeElement.focus(); } } From db1bf5abec6381183a6d26f9ffbdf0d01beca851 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 Feb 2023 15:49:03 +0100 Subject: [PATCH 53/83] some refactoring --- .../basic-autocomplete/basic-autocomplete.component.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 69a8bf4290..41284aa8d9 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -82,6 +82,7 @@ export class BasicAutocompleteComponent stateChanges = new Subject(); focused = false; touched = false; + errorState = false; controlType = "basic-autocomplete"; autocompleteForm = new FormControl(""); autocompleteSuggestedOptions = new BehaviorSubject[]>( @@ -153,10 +154,6 @@ export class BasicAutocompleteComponent private _value: V | V[]; - get errorState(): boolean { - return false; - } - @Input() set options(options: O[]) { this._options = options.map((o) => this.toSelectableOption(o)); } From f11f26b4a7663853a3646226c3fe286f7657c3c5 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 13:54:49 +0100 Subject: [PATCH 54/83] error state handling works in autocomplete component --- .../basic-autocomplete.component.html | 23 --------- .../basic-autocomplete.component.ts | 51 +++++++++++++++++-- .../enum-dropdown.component.html | 5 +- .../enum-dropdown/enum-dropdown.component.ts | 11 ++-- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html index 5898bdcc39..710964a9d8 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html @@ -8,29 +8,6 @@ (focusin)="onFocusIn()" (focusout)="onFocusOut($event)" /> - - - - - - - - Add option {{ inputElement.value }} - - { +interface SelectableOption { initial: O; asString: string; asValue: V; @@ -68,7 +73,8 @@ export class BasicAutocompleteComponent ControlValueAccessor, MatFormFieldControl, OnDestroy, - OnChanges + OnChanges, + DoCheck { static nextId = 0; @HostBinding() @@ -88,9 +94,9 @@ export class BasicAutocompleteComponent autocompleteSuggestedOptions = new BehaviorSubject[]>( [] ); - private delayedBlur: any; showAddOption = false; private addOptionTimeout: any; + private delayedBlur: any; onChange = (_: any) => {}; onTouched = () => {}; @@ -181,8 +187,11 @@ export class BasicAutocompleteComponent constructor( private elementRef: ElementRef, private confirmation: ConfirmationDialogService, - @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField, - @Optional() @Self() public ngControl: NgControl + private errorStateMatcher: ErrorStateMatcher, + @Optional() @Inject(MAT_FORM_FIELD) private formField: MatFormField, + @Optional() @Self() public ngControl: NgControl, + @Optional() private parentForm: NgForm, + @Optional() private parentFormGroup: FormGroupDirective ) { if (this.ngControl != null) { this.ngControl.valueAccessor = this; @@ -224,6 +233,7 @@ export class BasicAutocompleteComponent (o) => o.asString.toLowerCase() === inputText.toLowerCase() ); if (!exists) { + // show 'add option' after short timeout if user doesn't enter anythign this.addOptionTimeout = setTimeout( () => (this.showAddOption = true), 1000 @@ -330,7 +340,15 @@ export class BasicAutocompleteComponent private notifyFocusOut() { if (this.multi) { + // show all selected option this.displaySelectedOptions(); + } else if ( + this._optionToString(this.value) !== this.autocompleteForm.value + ) { + // clear input if it doesn't match the last selected value + this.autocompleteForm.setValue(""); + this.value = undefined; + this.onChange(this.value); } this.touched = true; this.focused = false; @@ -366,4 +384,27 @@ export class BasicAutocompleteComponent setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } + + ngDoCheck() { + this.updateErrorState(); + } + + /** + * Updates the error state based on the form control + * Taken from {@link https://github.com/angular/components/blob/a1d5614f18066c0c2dc2580c7b5099e8f68a8e74/src/material/core/common-behaviors/error-state.ts#L59} + * @private + */ + private updateErrorState() { + const oldState = this.errorState; + const parent = this.parentFormGroup || this.parentForm; + const control = this.ngControl + ? (this.ngControl.control as AbstractControl) + : null; + const newState = this.errorStateMatcher.isErrorState(control, parent); + + if (newState !== oldState) { + this.errorState = newState; + this.stateChanges.next(); + } + } } diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html index e025625ca6..031d7ada0c 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.html @@ -15,7 +15,7 @@ icon="wrench" class="caret-suffix" matSuffix - (click)="openSettings()" + (click)="openSettings($event)" > + + + diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts index 55e45adf38..b93f6be33d 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -12,6 +12,7 @@ import { EntityAbility } from "../../permissions/ability/entity-ability"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-enum-popup.component"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { ErrorHintComponent } from "../../entity-components/entity-utils/error-hint/error-hint.component"; @Component({ selector: "app-enum-dropdown", @@ -27,6 +28,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; NgForOf, BasicAutocompleteComponent, FontAwesomeModule, + ErrorHintComponent, ], }) export class EnumDropdownComponent implements OnChanges { @@ -81,11 +83,10 @@ export class EnumDropdownComponent implements OnChanges { return option; } - openSettings() { - const dialogRef = this.dialog.open(ConfigureEnumPopupComponent, { - data: this.enumEntity, - }); - dialogRef + openSettings(event: Event) { + event.stopPropagation(); + this.dialog + .open(ConfigureEnumPopupComponent, { data: this.enumEntity }) .afterClosed() .subscribe( () => From a3f23c623ebc9aa7a122f301aed50b4b34debc63 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 13:55:02 +0100 Subject: [PATCH 55/83] fixed storybook component registry issues --- src/app/utils/storybook-base.module.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/utils/storybook-base.module.ts b/src/app/utils/storybook-base.module.ts index 07ecc67000..974c46793b 100644 --- a/src/app/utils/storybook-base.module.ts +++ b/src/app/utils/storybook-base.module.ts @@ -21,6 +21,9 @@ import { createTestingConfigService } from "../core/config/testing-config-servic import { componentRegistry } from "../dynamic-components"; import { AppModule } from "../app.module"; +componentRegistry.allowDuplicates(); +entityRegistry.allowDuplicates(); + export const entityFormStorybookDefaulParameters = { controls: { exclude: ["_columns"], @@ -65,7 +68,5 @@ export const mockAbilityService = { export class StorybookBaseModule { constructor(icons: FaIconLibrary) { icons.addIconPacks(fas, far); - entityRegistry.allowDuplicates(); - componentRegistry.allowDuplicates(); } } From b4621a0ae09aa3e3db1376121be2ca934ed5651b Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 14:16:32 +0100 Subject: [PATCH 56/83] fixed test --- .../basic-autocomplete.component.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 5786ba2d89..ba40b69cfa 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -50,11 +50,11 @@ describe("BasicAutocompleteComponent", () => { const school3 = School.create({ name: "cde" }); component.options = [school1, school2, school3]; - component.updateAutocomplete(""); + component.autocompleteForm.setValue(""); expect(getAutocompleteOptions()).toEqual([school1, school2, school3]); - component.updateAutocomplete("Aa"); + component.autocompleteForm.setValue("Aa"); expect(getAutocompleteOptions()).toEqual([school1, school2]); - component.updateAutocomplete("Aab"); + component.autocompleteForm.setValue("Aab"); expect(getAutocompleteOptions()).toEqual([school2]); }); @@ -93,10 +93,11 @@ describe("BasicAutocompleteComponent", () => { component.select({ asValue: first.getId() } as any); expect(component.value).toBe(first.getId()); - component.resetIfInvalidOption("Non existent"); + component.autocompleteForm.setValue("Non existent"); + component.onFocusOut({} as any); tick(); - expect(component.value).toBe(first.getId()); + expect(component.value).toBe(undefined); flush(); })); From 5531a586b275abcc5dd0ea9ff54cff7f5643a38f Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 17:20:17 +0100 Subject: [PATCH 57/83] improved test coverage for basic autocomplete component --- .../basic-autocomplete.component.spec.ts | 92 +++++++++++++++++-- .../basic-autocomplete.component.ts | 59 ++++-------- 2 files changed, 101 insertions(+), 50 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index ba40b69cfa..4befa463c2 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -16,15 +16,25 @@ import { MatDialogModule } from "@angular/material/dialog"; import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; import { HarnessLoader } from "@angular/cdk/testing"; import { MatInputHarness } from "@angular/material/input/testing"; +import { MatAutocompleteHarness } from "@angular/material/autocomplete/testing"; +import { + FormControl, + FormGroup, + NgControl, + NgForm, + Validators, +} from "@angular/forms"; describe("BasicAutocompleteComponent", () => { let component: BasicAutocompleteComponent; let fixture: ComponentFixture>; let loader: HarnessLoader; - + let testControl: FormControl; const entityToId = (e: Entity) => e?.getId(); beforeEach(async () => { + testControl = new FormControl(""); + const formGroup = new FormGroup({ testControl }); await TestBed.configureTestingModule({ imports: [ BasicAutocompleteComponent, @@ -32,7 +42,17 @@ describe("BasicAutocompleteComponent", () => { NoopAnimationsModule, MatDialogModule, ], - }).compileComponents(); + providers: [{ provide: NgForm, useValue: formGroup }], + }) + .overrideComponent(BasicAutocompleteComponent, { + // overwrite @Self dependency + add: { + providers: [ + { provide: NgControl, useValue: { control: testControl } }, + ], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(BasicAutocompleteComponent); loader = TestbedHarnessEnvironment.loader(fixture); @@ -44,18 +64,22 @@ describe("BasicAutocompleteComponent", () => { expect(component).toBeTruthy(); }); - it("should correctly show the autocomplete values", () => { + it("should correctly show the autocomplete values", async () => { const school1 = School.create({ name: "Aaa" }); const school2 = School.create({ name: "aab" }); const school3 = School.create({ name: "cde" }); component.options = [school1, school2, school3]; + let currentAutocompleteSuggestions: School[]; + component.autocompleteSuggestedOptions.subscribe( + (value) => (currentAutocompleteSuggestions = value.map((o) => o.asValue)) + ); component.autocompleteForm.setValue(""); - expect(getAutocompleteOptions()).toEqual([school1, school2, school3]); + expect(currentAutocompleteSuggestions).toEqual([school1, school2, school3]); component.autocompleteForm.setValue("Aa"); - expect(getAutocompleteOptions()).toEqual([school1, school2]); + expect(currentAutocompleteSuggestions).toEqual([school1, school2]); component.autocompleteForm.setValue("Aab"); - expect(getAutocompleteOptions()).toEqual([school2]); + expect(currentAutocompleteSuggestions).toEqual([school2]); }); it("should show name of the selected entity", async () => { @@ -73,7 +97,7 @@ describe("BasicAutocompleteComponent", () => { await expectAsync(inputElement.getValue()).toBeResolvedTo("First Child"); }); - it("Should have the correct entity selected when it's name is entered", () => { + it("should have the correct entity selected when it's name is entered", () => { const child1 = Child.create("First Child"); const child2 = Child.create("Second Child"); component.options = [child1, child2]; @@ -101,7 +125,55 @@ describe("BasicAutocompleteComponent", () => { flush(); })); - function getAutocompleteOptions() { - return component.autocompleteSuggestedOptions.value.map((o) => o.asValue); - } + it("should disable the form if the control is disabled", () => { + component.disabled = false; + expect(component.autocompleteForm.disabled).toBeFalse(); + component.disabled = true; + expect(component.autocompleteForm.disabled).toBeTrue(); + }); + + it("should initialize the options in multi select mode", async () => { + const autocomplete = await loader.getHarness(MatAutocompleteHarness); + component.options = [1, 2, 3]; + component.multi = true; + component.value = [1, 2]; + component.ngOnChanges({ options: true, value: true } as any); + + component.showAutocomplete(); + await autocomplete.focus(); + const options = await autocomplete.getOptions(); + expect(options).toHaveSize(3); + + await options[2].click(); + await options[1].click(); + + expect(component.value).toEqual([1, 3]); + }); + + it("should clear the input when focusing in multi select mode", () => { + component.multi = true; + component.options = ["some", "values", "and", "other", "options"]; + component.value = ["some", "values"]; + component.ngOnChanges({ value: true, options: true } as any); + expect(component.autocompleteForm).toHaveValue("some, values"); + + component.onFocusIn(); + expect(component.autocompleteForm).toHaveValue(""); + + component.onFocusOut({} as any); + expect(component.autocompleteForm).toHaveValue("some, values"); + }); + + it("should update the error state if the form is invalid", () => { + testControl.setValidators([Validators.required]); + testControl.setValue(null); + component.ngDoCheck(); + + expect(component.errorState).toBeFalse(); + + testControl.markAsTouched(); + component.ngDoCheck(); + + expect(component.errorState).toBeTrue(); + }); }); diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index e5fd41fe27..558a82333a 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -34,9 +34,9 @@ import { MatAutocompleteModule, MatAutocompleteTrigger, } from "@angular/material/autocomplete"; -import { BehaviorSubject, Subject } from "rxjs"; +import { concat, of, skip, Subject } from "rxjs"; import { MatCheckboxModule } from "@angular/material/checkbox"; -import { filter } from "rxjs/operators"; +import { filter, map, startWith } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; import { ErrorStateMatcher } from "@angular/material/core"; @@ -68,7 +68,7 @@ interface SelectableOption { NgTemplateOutlet, ], }) -export class BasicAutocompleteComponent +export class BasicAutocompleteComponent implements ControlValueAccessor, MatFormFieldControl, @@ -85,14 +85,18 @@ export class BasicAutocompleteComponent @ViewChild("inputElement") inputElement: ElementRef; @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @Input() placeholder: string; + @Input() required = false; stateChanges = new Subject(); focused = false; touched = false; errorState = false; controlType = "basic-autocomplete"; autocompleteForm = new FormControl(""); - autocompleteSuggestedOptions = new BehaviorSubject[]>( - [] + autocompleteSuggestedOptions = this.autocompleteForm.valueChanges.pipe( + filter((val) => typeof val === "string"), + map((val) => this.updateAutocomplete(val)), + startWith([]) ); showAddOption = false; private addOptionTimeout: any; @@ -108,30 +112,6 @@ export class BasicAutocompleteComponent return this.focused || !this.empty; } - @Input() - get placeholder(): string { - return this._placeholder; - } - - set placeholder(value: string) { - this._placeholder = value; - this.stateChanges.next(); - } - - private _placeholder: string; - - @Input() - get required(): boolean { - return this._required; - } - - set required(value: BooleanInput) { - this._required = coerceBooleanProperty(value); - this.stateChanges.next(); - } - - private _required = false; - @Input() get disabled(): boolean { return this._disabled; @@ -196,9 +176,6 @@ export class BasicAutocompleteComponent if (this.ngControl != null) { this.ngControl.valueAccessor = this; } - this.autocompleteForm.valueChanges - .pipe(filter((val) => typeof val === "string")) - .subscribe((val) => this.updateAutocomplete(val)); } ngOnDestroy() { @@ -216,31 +193,33 @@ export class BasicAutocompleteComponent } } - showAutocomplete(inputText?: string) { - this.updateAutocomplete(inputText); - this.inputElement?.nativeElement.focus(); + showAutocomplete() { + this.autocompleteSuggestedOptions = concat( + of(this._options), + this.autocompleteSuggestedOptions.pipe(skip(1)) + ); } - private updateAutocomplete(inputText: string) { - let filteredEntities = this._options; + private updateAutocomplete(inputText: string): SelectableOption[] { + let filteredOptions = this._options; this.showAddOption = false; clearTimeout(this.addOptionTimeout); if (inputText) { - filteredEntities = this._options.filter((option) => + filteredOptions = this._options.filter((option) => option.asString.toLowerCase().includes(inputText.toLowerCase()) ); const exists = this._options.find( (o) => o.asString.toLowerCase() === inputText.toLowerCase() ); if (!exists) { - // show 'add option' after short timeout if user doesn't enter anythign + // show 'add option' after short timeout if user doesn't enter anything this.addOptionTimeout = setTimeout( () => (this.showAddOption = true), 1000 ); } } - this.autocompleteSuggestedOptions.next(filteredEntities); + return filteredOptions; } private setInputValue() { From ab483368c44c7487991bbf051f52b4f9bf37b093 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 17:58:24 +0100 Subject: [PATCH 58/83] improved test coverage for enum dropdown component --- .../enum-dropdown.component.spec.ts | 51 +++++++++++++++++++ .../enum-dropdown/enum-dropdown.component.ts | 3 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts index 74a86644ee..a43a931483 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.spec.ts @@ -6,11 +6,16 @@ import { SimpleChange } from "@angular/core"; import { ConfigurableEnumService } from "../configurable-enum.service"; import { ConfigurableEnum } from "../configurable-enum"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import { EntityMapperService } from "../../entity/entity-mapper.service"; +import { MatDialog } from "@angular/material/dialog"; +import { of } from "rxjs"; describe("EnumDropdownComponent", () => { let component: EnumDropdownComponent; let fixture: ComponentFixture; + let mockDialog: jasmine.SpyObj; beforeEach(async () => { + mockDialog = jasmine.createSpyObj(["open"]); await TestBed.configureTestingModule({ imports: [EnumDropdownComponent, MockedTestingModule.withState()], providers: [ @@ -18,6 +23,7 @@ describe("EnumDropdownComponent", () => { provide: ConfigurableEnumService, useValue: { getEnum: () => new ConfigurableEnum() }, }, + { provide: MatDialog, useValue: mockDialog }, ], }).compileComponents(); @@ -63,4 +69,49 @@ describe("EnumDropdownComponent", () => { }); expect(component.invalidOptions).toEqual([invalidOption, invalid2]); }); + + it("should extend the existing enum with the new option", () => { + const saveSpy = spyOn(TestBed.inject(EntityMapperService), "save"); + const enumEntity = new ConfigurableEnum(); + enumEntity.values = [{ id: "1", label: "first" }]; + component.enumEntity = enumEntity; + + const res = component.createNewOption("second"); + + expect(res).toEqual({ id: "second", label: "second" }); + expect(enumEntity.values).toEqual([ + { id: "1", label: "first" }, + { id: "second", label: "second" }, + ]); + expect(saveSpy).toHaveBeenCalledWith(enumEntity); + }); + + it("should open the configure enum dialog and re-initialize the available options afterwards", () => { + component.enumEntity = new ConfigurableEnum(); + component.enumEntity.values = [{ id: "1", label: "1" }]; + component.form = new FormControl({ + id: "a", + label: "a", + isInvalidOption: true, + }); + + component.ngOnChanges({ form: true } as any); + + expect(component.options).toEqual([ + { id: "1", label: "1" }, + { id: "a", label: "a", isInvalidOption: true }, + ]); + + mockDialog.open.and.returnValue({ afterClosed: () => of({}) } as any); + component.enumEntity.values.push({ id: "2", label: "2" }); + + component.openSettings({ stopPropagation: () => {} } as any); + + expect(mockDialog.open).toHaveBeenCalled(); + expect(component.options).toEqual([ + { id: "1", label: "1" }, + { id: "2", label: "2" }, + { id: "a", label: "a", isInvalidOption: true }, + ]); + }); }); diff --git a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts index b93f6be33d..31adbc342a 100644 --- a/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts +++ b/src/app/core/configurable-enum/enum-dropdown/enum-dropdown.component.ts @@ -9,7 +9,7 @@ import { ConfigurableEnumService } from "../configurable-enum.service"; import { EntityMapperService } from "../../entity/entity-mapper.service"; import { ConfigurableEnum } from "../configurable-enum"; import { EntityAbility } from "../../permissions/ability/entity-ability"; -import { MatDialog, MatDialogModule } from "@angular/material/dialog"; +import { MatDialog } from "@angular/material/dialog"; import { ConfigureEnumPopupComponent } from "../configure-enum-popup/configure-enum-popup.component"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { ErrorHintComponent } from "../../entity-components/entity-utils/error-hint/error-hint.component"; @@ -21,7 +21,6 @@ import { ErrorHintComponent } from "../../entity-components/entity-utils/error-h standalone: true, imports: [ MatSelectModule, - MatDialogModule, ReactiveFormsModule, ConfigurableEnumDirective, NgIf, From 481460b503476ad0565250830179bd0a25eb818e Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 18:23:47 +0100 Subject: [PATCH 59/83] fix lint issues --- .../basic-autocomplete/basic-autocomplete.component.scss | 0 .../basic-autocomplete/basic-autocomplete.component.ts | 1 - .../display-configurable-enum.component.spec.ts | 1 - .../edit-single-entity/edit-single-entity.component.spec.ts | 1 - 4 files changed, 3 deletions(-) delete mode 100644 src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 558a82333a..4a1322c4b5 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -52,7 +52,6 @@ interface SelectableOption { @Component({ selector: "app-basic-autocomplete", templateUrl: "basic-autocomplete.component.html", - styleUrls: ["basic-autocomplete.component.scss"], providers: [ { provide: MatFormFieldControl, useExisting: BasicAutocompleteComponent }, ], diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts index c3f4ff297b..3fe1ca8381 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { DisplayConfigurableEnumComponent } from "./display-configurable-enum.component"; -import { Note } from "../../../child-dev-project/notes/model/note"; import { Ordering } from "../configurable-enum-ordering"; describe("DisplayConfigurableEnumComponent", () => { diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts index 324b89c652..c02fd0720c 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts +++ b/src/app/core/entity-components/entity-select/edit-single-entity/edit-single-entity.component.spec.ts @@ -10,7 +10,6 @@ import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { EntityFormService } from "../../entity-form/entity-form.service"; import { ChildSchoolRelation } from "../../../../child-dev-project/children/model/childSchoolRelation"; import { School } from "../../../../child-dev-project/schools/model/school"; -import { Child } from "../../../../child-dev-project/children/model/child"; import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; import { FormControl } from "@angular/forms"; From 22f003ca5c91b31687dee28d46cbe5b22bdf0968 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 20:58:24 +0100 Subject: [PATCH 60/83] new child school relations don't trigger a form conflict due to different date format --- .../child-school-overview/child-school-overview.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cb8890ba81..e56ef8e9ad 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 @@ -143,7 +143,7 @@ export class ChildSchoolOverviewComponent newRelation.start = this.allRecords.length && this.allRecords[0].end ? moment(this.allRecords[0].end).add(1, "day").toDate() - : new Date(); + : moment().startOf("day").toDate(); } else if (mode === "school") { newRelation.schoolId = entityId; } From 17f6c3ad1102e10b9e19ac2a9525bfd118481c98 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 21:26:23 +0100 Subject: [PATCH 61/83] new child school relations don't trigger a form conflict due to different date format --- .../basic-autocomplete.component.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 4a1322c4b5..e60d3da9eb 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -320,13 +320,18 @@ export class BasicAutocompleteComponent if (this.multi) { // show all selected option this.displaySelectedOptions(); - } else if ( - this._optionToString(this.value) !== this.autocompleteForm.value - ) { - // clear input if it doesn't match the last selected value - this.autocompleteForm.setValue(""); - this.value = undefined; - this.onChange(this.value); + } else { + const inputValue = this.autocompleteForm.value; + const selectedOption = this._options.find( + ({ asValue }) => asValue === this._value + ); + if (selectedOption?.asString !== inputValue) { + // try to select the option that matches the input string + const matchingOption = this._options.find( + ({ asString }) => asString.toLowerCase() === inputValue.toLowerCase() + ); + this.select(matchingOption); + } } this.touched = true; this.focused = false; From 8c35e1df75f7187dfe18a8676d8a9df4073318f0 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 21:53:50 +0100 Subject: [PATCH 62/83] fixed e2e test --- e2e/integration/LinkingChildToSchool.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/integration/LinkingChildToSchool.cy.ts b/e2e/integration/LinkingChildToSchool.cy.ts index 87e7b1c93a..e34cb8b00f 100644 --- a/e2e/integration/LinkingChildToSchool.cy.ts +++ b/e2e/integration/LinkingChildToSchool.cy.ts @@ -24,9 +24,9 @@ describe("Scenario: Linking a child to a school - E2E test", function () { .click(); // choose the school to add - cy.get('[ng-reflect-placeholder="Select School"]') - .type("E2E School", { force: true }) - .click(); + cy.contains("mat-form-field", "School") + .find("[matInput]") + .type("E2E School{enter}"); // save school in child profile cy.contains("button", "Save").click({ force: true }); From 27b66d5f7fdb9d4d750ba80c6ae81f6b9810cb78 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 Feb 2023 22:45:10 +0100 Subject: [PATCH 63/83] fixed autocomplete test --- .../basic-autocomplete/basic-autocomplete.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 4befa463c2..c02f5be7ad 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -140,7 +140,7 @@ describe("BasicAutocompleteComponent", () => { component.ngOnChanges({ options: true, value: true } as any); component.showAutocomplete(); - await autocomplete.focus(); + component.autocomplete.openPanel(); const options = await autocomplete.getOptions(); expect(options).toHaveSize(3); From ce4f475b8417da13bc97fc68ff2ff365b7bce69d Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 06:10:37 +0100 Subject: [PATCH 64/83] abstracted custom form control logic into abstract class --- .../basic-autocomplete.component.ts | 142 +++----------- .../custom-form-control.directive.ts | 175 ++++++++++++++++++ 2 files changed, 200 insertions(+), 117 deletions(-) create mode 100644 src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index e60d3da9eb..8f8cdca740 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -1,13 +1,10 @@ import { Component, ContentChild, - DoCheck, ElementRef, - HostBinding, Inject, Input, OnChanges, - OnDestroy, Optional, Self, SimpleChanges, @@ -21,8 +18,6 @@ import { MatFormFieldControl, } from "@angular/material/form-field"; import { - AbstractControl, - ControlValueAccessor, FormControl, FormGroupDirective, NgControl, @@ -34,12 +29,13 @@ import { MatAutocompleteModule, MatAutocompleteTrigger, } from "@angular/material/autocomplete"; -import { concat, of, skip, Subject } from "rxjs"; +import { concat, of, skip } from "rxjs"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { filter, map, startWith } from "rxjs/operators"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; -import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; import { ErrorStateMatcher } from "@angular/material/core"; +import { CustomFormControlDirective } from "./custom-form-control.directive"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; interface SelectableOption { initial: O; @@ -68,29 +64,13 @@ interface SelectableOption { ], }) export class BasicAutocompleteComponent - implements - ControlValueAccessor, - MatFormFieldControl, - OnDestroy, - OnChanges, - DoCheck + extends CustomFormControlDirective + implements OnChanges { - static nextId = 0; - @HostBinding() - id = `basic-autocomplete-${BasicAutocompleteComponent.nextId++}`; - // eslint-disable-next-line @angular-eslint/no-input-rename - @Input("aria-describedby") userAriaDescribedBy: string; @ContentChild(TemplateRef) templateRef: TemplateRef; @ViewChild("inputElement") inputElement: ElementRef; @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; - @Input() placeholder: string; - @Input() required = false; - stateChanges = new Subject(); - focused = false; - touched = false; - errorState = false; - controlType = "basic-autocomplete"; autocompleteForm = new FormControl(""); autocompleteSuggestedOptions = this.autocompleteForm.valueChanges.pipe( filter((val) => typeof val === "string"), @@ -100,45 +80,19 @@ export class BasicAutocompleteComponent showAddOption = false; private addOptionTimeout: any; private delayedBlur: any; - onChange = (_: any) => {}; - onTouched = () => {}; - get empty() { - return !this.value; - } - - get shouldLabelFloat() { - return this.focused || !this.empty; - } - - @Input() get disabled(): boolean { return this._disabled; } - set disabled(value: BooleanInput) { + set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); - if (this._disabled) { - this.autocompleteForm.disable(); - } else { - this.autocompleteForm.enable(); - } + this._disabled + ? this.autocompleteForm.disable() + : this.autocompleteForm.enable(); this.stateChanges.next(); } - private _disabled = false; - - @Input() get value(): V | V[] { - return this._value; - } - - set value(value: V | V[]) { - this._value = value; - this.stateChanges.next(); - } - - private _value: V | V[]; - @Input() set options(options: O[]) { this._options = options.map((o) => this.toSelectableOption(o)); } @@ -164,23 +118,27 @@ export class BasicAutocompleteComponent @Input() multi?: boolean; constructor( - private elementRef: ElementRef, + elementRef: ElementRef, private confirmation: ConfirmationDialogService, - private errorStateMatcher: ErrorStateMatcher, - @Optional() @Inject(MAT_FORM_FIELD) private formField: MatFormField, - @Optional() @Self() public ngControl: NgControl, - @Optional() private parentForm: NgForm, - @Optional() private parentFormGroup: FormGroupDirective + errorStateMatcher: ErrorStateMatcher, + @Optional() @Inject(MAT_FORM_FIELD) formField: MatFormField, + @Optional() @Self() ngControl: NgControl, + @Optional() parentForm: NgForm, + @Optional() parentFormGroup: FormGroupDirective ) { + super( + elementRef, + errorStateMatcher, + formField, + ngControl, + parentForm, + parentFormGroup + ); if (this.ngControl != null) { this.ngControl.valueAccessor = this; } } - ngOnDestroy() { - this.stateChanges.complete(); - } - ngOnChanges(changes: SimpleChanges) { if (changes.value || changes.options) { if (this.multi) { @@ -298,8 +256,7 @@ export class BasicAutocompleteComponent if (this.multi) { this.autocompleteForm.setValue(""); } - this.focused = true; - this.stateChanges.next(); + this.focus(); } } @@ -333,17 +290,7 @@ export class BasicAutocompleteComponent this.select(matchingOption); } } - this.touched = true; - this.focused = false; - this.onTouched(); - this.stateChanges.next(); - } - - setDescribedByIds(ids: string[]) { - const controlElement = this.elementRef.nativeElement.querySelector( - ".autocomplete-input" - )!; - controlElement.setAttribute("aria-describedby", ids.join(" ")); + this.blur(); } onContainerClick(event: MouseEvent) { @@ -351,43 +298,4 @@ export class BasicAutocompleteComponent this.inputElement.nativeElement.focus(); } } - - writeValue(val: V | V[]): void { - this.value = val; - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - } - - ngDoCheck() { - this.updateErrorState(); - } - - /** - * Updates the error state based on the form control - * Taken from {@link https://github.com/angular/components/blob/a1d5614f18066c0c2dc2580c7b5099e8f68a8e74/src/material/core/common-behaviors/error-state.ts#L59} - * @private - */ - private updateErrorState() { - const oldState = this.errorState; - const parent = this.parentFormGroup || this.parentForm; - const control = this.ngControl - ? (this.ngControl.control as AbstractControl) - : null; - const newState = this.errorStateMatcher.isErrorState(control, parent); - - if (newState !== oldState) { - this.errorState = newState; - this.stateChanges.next(); - } - } } diff --git a/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts b/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts new file mode 100644 index 0000000000..ba4881fc55 --- /dev/null +++ b/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts @@ -0,0 +1,175 @@ +import { + AbstractControl, + ControlValueAccessor, + FormGroupDirective, + NgControl, + NgForm, +} from "@angular/forms"; +import { + MatFormField, + MatFormFieldControl, +} from "@angular/material/form-field"; +import { + Directive, + DoCheck, + ElementRef, + HostBinding, + Input, + OnDestroy, +} from "@angular/core"; +import { Subject } from "rxjs"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { ErrorStateMatcher } from "@angular/material/core"; + +@Directive() +export abstract class CustomFormControlDirective + implements ControlValueAccessor, MatFormFieldControl, OnDestroy, DoCheck +{ + static nextId = 0; + @HostBinding() + id = `custom-form-control-${CustomFormControlDirective.nextId++}`; + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input("aria-describedby") userAriaDescribedBy: string; + + stateChanges = new Subject(); + focused = false; + touched = false; + errorState = false; + controlType = "custom-control"; + onChange = (_: any) => {}; + onTouched = () => {}; + + get empty() { + return !this.value; + } + + get shouldLabelFloat() { + return this.focused || !this.empty; + } + + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + this.stateChanges.next(); + } + + _disabled = false; + + @Input() get value(): T { + return this._value; + } + + set value(value: T) { + this._value = value; + this.stateChanges.next(); + } + + _value: T; + + @Input() get placeholder(): string { + return this._placeholder; + } + + set placeholder(value: string) { + this._placeholder = value; + this.stateChanges.next(); + } + + _placeholder: string; + + @Input() get required(): boolean { + return this._required; + } + + set required(value: boolean) { + this._required = value; + this.stateChanges.next(); + } + + _required = false; + + constructor( + public elementRef: ElementRef, + public errorStateMatcher: ErrorStateMatcher, + public formField: MatFormField, + public ngControl: NgControl, + public parentForm: NgForm, + public parentFormGroup: FormGroupDirective + ) { + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + } + + ngOnDestroy() { + this.stateChanges.complete(); + } + + focus() { + this.focused = true; + this.stateChanges.next(); + } + + blur() { + this.touched = true; + this.focused = false; + this.onTouched(); + this.stateChanges.next(); + } + + setDescribedByIds(ids: string[]) { + const controlElement = this.elementRef.nativeElement.querySelector( + ".autocomplete-input" + )!; + controlElement.setAttribute("aria-describedby", ids.join(" ")); + } + + onContainerClick(event: MouseEvent) { + if ((event.target as Element).tagName.toLowerCase() != "input") { + this.elementRef.nativeElement.focus(); + } + } + + writeValue(val: T): void { + this.value = val; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + ngDoCheck() { + this.updateErrorState(); + } + + /** + * Updates the error state based on the form control + * Taken from {@link https://github.com/angular/components/blob/a1d5614f18066c0c2dc2580c7b5099e8f68a8e74/src/material/core/common-behaviors/error-state.ts#L59} + * @private + */ + private updateErrorState() { + const oldState = this.errorState; + const parent = this.parentFormGroup || this.parentForm; + const control = this.ngControl + ? (this.ngControl.control as AbstractControl) + : null; + const newState = this.errorStateMatcher.isErrorState(control, parent); + + if (newState !== oldState) { + this.errorState = newState; + this.stateChanges.next(); + } + } +} From 6fbbb95b814db88384941d56bcd7d80c8f28d67b Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 06:15:14 +0100 Subject: [PATCH 65/83] merged all groupBy implementations --- .../attendance/attendance.service.ts | 7 ++-- .../attendance-week-dashboard.component.ts | 8 ++--- .../children-bmi-dashboard.component.ts | 3 +- .../entity-count-dashboard.component.ts | 36 ++++++------------- .../data-transformation.service.ts | 2 +- .../reporting/data-aggregation.service.ts | 31 ++++------------ src/app/utils/utils.ts | 27 +++++++------- 7 files changed, 41 insertions(+), 73 deletions(-) diff --git a/src/app/child-dev-project/attendance/attendance.service.ts b/src/app/child-dev-project/attendance/attendance.service.ts index e8b2fcf17e..a3bf6e8234 100644 --- a/src/app/child-dev-project/attendance/attendance.service.ts +++ b/src/app/child-dev-project/attendance/attendance.service.ts @@ -160,6 +160,7 @@ export class AttendanceService { sinceDate?: Date ): Promise { const periods = new Map(); + function getOrCreateAttendancePeriod(event) { const month = new Date(event.date.getFullYear(), event.date.getMonth()); let attMonth = periods.get(month.getTime()); @@ -192,11 +193,7 @@ export class AttendanceService { until: Date ): Promise { const matchingEvents = await this.getEventsOnDate(from, until); - - const groupedEvents: Map = groupBy( - matchingEvents, - "relatesTo" - ); + const groupedEvents = groupBy(matchingEvents, "relatesTo"); const records = []; for (const [activityId, activityEvents] of groupedEvents) { diff --git a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts index a93f28dd75..4ff2193532 100644 --- a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts +++ b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts @@ -144,10 +144,10 @@ export class AttendanceWeekDashboardComponent .forEach((r) => lowAttendanceCases.add(r.childId)); } - const groupedRecords = groupBy(records, "childId"); - this.tableDataSource.data = Array.from(lowAttendanceCases.values()).map( - (childId) => groupedRecords.get(childId) - ); + const groups = groupBy(records, "childId"); + this.tableDataSource.data = groups + .filter(([childId]) => lowAttendanceCases.has(childId)) + .map(([_, attendance]) => attendance); this.loadingDone = true; } diff --git a/src/app/child-dev-project/children/dashboard-widgets/children-bmi-dashboard/children-bmi-dashboard.component.ts b/src/app/child-dev-project/children/dashboard-widgets/children-bmi-dashboard/children-bmi-dashboard.component.ts index c4f490f5fd..5b0cd96a3b 100644 --- a/src/app/child-dev-project/children/dashboard-widgets/children-bmi-dashboard/children-bmi-dashboard.component.ts +++ b/src/app/child-dev-project/children/dashboard-widgets/children-bmi-dashboard/children-bmi-dashboard.component.ts @@ -55,9 +55,8 @@ export class ChildrenBmiDashboardComponent async loadBMIData() { // Maybe replace this by a smart index function const healthChecks = await this.entityMapper.loadType(HealthCheck); - const healthCheckMap = groupBy(healthChecks, "child"); const BMIs: BmiRow[] = []; - healthCheckMap.forEach((checks, childId) => { + groupBy(healthChecks, "child").forEach(([childId, checks]) => { const latest = checks.reduce((prev, cur) => cur.date > prev.date ? cur : prev ); diff --git a/src/app/child-dev-project/children/dashboard-widgets/entity-count-dashboard/entity-count-dashboard.component.ts b/src/app/child-dev-project/children/dashboard-widgets/entity-count-dashboard/entity-count-dashboard.component.ts index a52d3311f7..7ce332b2d9 100644 --- a/src/app/child-dev-project/children/dashboard-widgets/entity-count-dashboard/entity-count-dashboard.component.ts +++ b/src/app/child-dev-project/children/dashboard-widgets/entity-count-dashboard/entity-count-dashboard.component.ts @@ -13,6 +13,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { Angulartics2Module } from "angulartics2"; import { DashboardWidgetComponent } from "../../../../core/dashboard/dashboard-widget/dashboard-widget.component"; import { WidgetContentComponent } from "../../../../core/dashboard/dashboard-widget/widget-content/widget-content.component"; +import { groupBy } from "../../../../utils/utils"; @DynamicComponent("ChildrenCountDashboard") @DynamicComponent("EntityCountDashboard") @@ -63,7 +64,7 @@ export class EntityCountDashboardComponent async ngOnInit() { const entities = await this.entityMapper.loadType(this.entity); - this.updateCounts(entities); + this.updateCounts(entities.filter((e) => e.isActive)); } goToChildrenList(filterId: string) { @@ -74,31 +75,16 @@ export class EntityCountDashboardComponent } private updateCounts(entities: Entity[]) { - this.totalEntities = 0; - - const countMap = new Map(); - entities.forEach((entity) => { - if (entity.isActive) { - let count = countMap.get(entity[this.groupBy]); - if (count === undefined) { - count = 0; - } - - count++; - this.totalEntities++; - countMap.set(entity[this.groupBy], count); - } + this.totalEntities = entities.length; + const groups = groupBy(entities, this.groupBy as keyof Entity); + this.entityGroupCounts = groups.map(([group, entities]) => { + const label = extractHumanReadableLabel(group); + return { + label: label, + value: entities.length, + id: group?.["id"] || label, + }; }); - - this.entityGroupCounts = Array.from(countMap.entries()) // direct use of Map creates change detection problems - .map((entry) => { - const label = extractHumanReadableLabel(entry[0]); - return { - label: label, - value: entry[1], - id: entry[0]?.id || label, - }; - }); this.loading = false; } } 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 4b52082c18..a20f4fc45c 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 @@ -67,7 +67,7 @@ export class DataTransformationService { const result: ExportRow[] = []; if (groupByProperty) { const groups = groupBy(data, groupByProperty.property); - for (const [group, values] of groups.entries()) { + for (const [group, values] of groups) { const groupColumn: ExportColumnConfig = { label: groupByProperty.label, query: `:setString(${getReadableValue(group)})`, diff --git a/src/app/features/reporting/data-aggregation.service.ts b/src/app/features/reporting/data-aggregation.service.ts index 3a7cb62379..d745f73d57 100644 --- a/src/app/features/reporting/data-aggregation.service.ts +++ b/src/app/features/reporting/data-aggregation.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { QueryService } from "../../core/export/query.service"; import { GroupByDescription, ReportRow } from "./report-row"; +import { groupBy } from "../../utils/utils"; export interface Aggregation { query: string; @@ -90,24 +91,24 @@ export class DataAggregationService { for (let i = properties.length; i > 0; i--) { const currentProperty = properties[i - 1]; const remainingProperties = properties.slice(i); - const groupingResults = this.groupBy(data, currentProperty); - for (const grouping of groupingResults) { + const groupingResults = groupBy(data, currentProperty); + for (const [group, entries] of groupingResults) { const groupingValues = additionalValues.concat({ property: currentProperty, - value: grouping.value, + value: group, }); const newRow: ReportRow = { header: { label: label, groupedBy: groupingValues, - result: grouping.data.length, + result: entries.length, }, subRows: [], }; newRow.subRows.push( ...(await this.calculateAggregations( aggregations, - grouping.data, + entries, groupingValues )) ); @@ -116,7 +117,7 @@ export class DataAggregationService { remainingProperties, aggregations, label, - grouping.data, + entries, groupingValues )) ); @@ -125,22 +126,4 @@ export class DataAggregationService { } return resultRows; } - - private groupBy( - data: ENTITY[], - groupByProperty: PROPERTY - ): { value: ENTITY[PROPERTY]; data: ENTITY[] }[] { - return data.reduce((allGroups, currentElement) => { - const currentValue = currentElement[groupByProperty]; - let existingGroup = allGroups.find( - (group) => group.value === currentValue - ); - if (!existingGroup) { - existingGroup = { value: currentValue, data: [] }; - allGroups.push(existingGroup); - } - existingGroup.data.push(currentElement); - return allGroups; - }, new Array<{ value: ENTITY[PROPERTY]; data: ENTITY[] }>()); - } } diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 5d4d51439c..89a1def2d3 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -26,23 +26,26 @@ export function getParentUrl(router: Router): string { } /** - * Group an array by the given property into a map of parts of the array. + * Group an array by the given property. * * @param array A simple array to be grouped. * @param propertyToGroupBy The key of the property in the elements by whose value the result is grouped. + * @returns an array where the first entry is the value of this group and the second all entries that have this value. */ -export function groupBy( +export function groupBy( array: T[], - propertyToGroupBy: keyof T -): Map { - return array.reduce( - (entryMap, element) => - entryMap.set(element[propertyToGroupBy], [ - ...(entryMap.get(element[propertyToGroupBy]) || []), - element, - ]), - new Map() - ); + propertyToGroupBy: P +): [T[P], T[]][] { + return array.reduce((allGroups, currentElement) => { + const currentValue = currentElement[propertyToGroupBy]; + let existingGroup = allGroups.find(([group]) => group === currentValue); + if (!existingGroup) { + existingGroup = [currentValue, []]; + allGroups.push(existingGroup); + } + existingGroup[1].push(currentElement); + return allGroups; + }, new Array<[T[P], T[]]>()); } export function calculateAge(dateOfBirth: Date): number { From 23ffe2507e1c4cd9cb007bcca47629994491a34c Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 06:32:03 +0100 Subject: [PATCH 66/83] creating groups with more advanced equality check --- .../entity-subrecord/value-accessor.ts | 2 +- src/app/utils/utils.spec.ts | 17 ++++++++++++++++- src/app/utils/utils.ts | 18 +++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/app/core/entity-components/entity-subrecord/entity-subrecord/value-accessor.ts b/src/app/core/entity-components/entity-subrecord/entity-subrecord/value-accessor.ts index 791fc9c72a..6f2af736f5 100644 --- a/src/app/core/entity-components/entity-subrecord/entity-subrecord/value-accessor.ts +++ b/src/app/core/entity-components/entity-subrecord/entity-subrecord/value-accessor.ts @@ -41,6 +41,6 @@ export function getReadableValue(value: any): any { } } -function isConfigurableEnum(value: any): value is ConfigurableEnumValue { +export function isConfigurableEnum(value: any): value is ConfigurableEnumValue { return typeof value === "object" && value && "label" in value; } diff --git a/src/app/utils/utils.spec.ts b/src/app/utils/utils.spec.ts index bf53f1ed9f..676045bc43 100644 --- a/src/app/utils/utils.spec.ts +++ b/src/app/utils/utils.spec.ts @@ -1,4 +1,4 @@ -import { calculateAge, isValidDate, sortByAttribute } from "./utils"; +import { calculateAge, groupBy, isValidDate, sortByAttribute } from "./utils"; import moment from "moment"; describe("Utils", () => { @@ -62,4 +62,19 @@ describe("Utils", () => { expect(sortedDesc).toEqual([third, second, first]); }); + + it("should create groups with the same ID, not just object equality", () => { + const first = { a: { id: "a", label: "A" } }; + const second = { a: { id: "a", label: "A" } }; + const third = { a: { id: "b", label: "B" } }; + // we don't have object equality + expect(first.a).not.toBe(second.a); + + const groups = groupBy([first, second, third], "a"); + + expect(groups).toEqual([ + [{ id: "a", label: "A" }, [first, second]], + [{ id: "b", label: "B" }, [third]], + ]); + }); }); diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 89a1def2d3..10fb6f88db 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -5,6 +5,7 @@ import { Router } from "@angular/router"; import { ConfigurableEnumValue } from "../core/configurable-enum/configurable-enum.interface"; import { FactoryProvider, Injector } from "@angular/core"; +import { isConfigurableEnum } from "../core/entity-components/entity-subrecord/entity-subrecord/value-accessor"; export function isValidDate(date: any): boolean { return ( @@ -38,7 +39,9 @@ export function groupBy( ): [T[P], T[]][] { return array.reduce((allGroups, currentElement) => { const currentValue = currentElement[propertyToGroupBy]; - let existingGroup = allGroups.find(([group]) => group === currentValue); + let existingGroup = allGroups.find(([group]) => + equals(group, currentValue) + ); if (!existingGroup) { existingGroup = [currentValue, []]; allGroups.push(existingGroup); @@ -48,6 +51,19 @@ export function groupBy( }, new Array<[T[P], T[]]>()); } +/** + * Comparing two values for equality that might be different than just object equality + * @param a + * @param b + */ +function equals(a, b): boolean { + if (isConfigurableEnum(a) && isConfigurableEnum(b)) { + return a.id === b.id; + } else { + return a === b; + } +} + export function calculateAge(dateOfBirth: Date): number { const now = new Date(); let age = now.getFullYear() - dateOfBirth.getFullYear(); From ff557212aa1518e22b2141c338424bcd5b6203c2 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 06:52:44 +0100 Subject: [PATCH 67/83] automatically creating new enum if not available --- .../configurable-enum.service.spec.ts | 42 +++++++++++++++++++ .../configurable-enum.service.ts | 5 +++ 2 files changed, 47 insertions(+) create mode 100644 src/app/core/configurable-enum/configurable-enum.service.spec.ts diff --git a/src/app/core/configurable-enum/configurable-enum.service.spec.ts b/src/app/core/configurable-enum/configurable-enum.service.spec.ts new file mode 100644 index 0000000000..b5e26e5872 --- /dev/null +++ b/src/app/core/configurable-enum/configurable-enum.service.spec.ts @@ -0,0 +1,42 @@ +import { TestBed } from "@angular/core/testing"; +import { ConfigurableEnumService } from "./configurable-enum.service"; +import { EntityMapperService } from "../entity/entity-mapper.service"; +import { ConfigService } from "../config/config.service"; +import { NEVER, of } from "rxjs"; + +describe("ConfigurableEnumService", () => { + let service: ConfigurableEnumService; + let mockEntityMapper: jasmine.SpyObj; + let mockConfigService: jasmine.SpyObj; + beforeEach(async () => { + mockEntityMapper = jasmine.createSpyObj([ + "save", + "loadType", + "receiveUpdates", + ]); + mockEntityMapper.receiveUpdates.and.returnValue(NEVER); + mockEntityMapper.loadType.and.resolveTo([]); + mockConfigService = jasmine.createSpyObj([], { configUpdates: of({}) }); + await TestBed.configureTestingModule({ + providers: [ + { provide: EntityMapperService, useValue: mockEntityMapper }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }).compileComponents(); + service = TestBed.inject(ConfigurableEnumService); + }); + + it("should create", () => { + expect(service).toBeTruthy(); + }); + + it("should create a new enum if it cannot be found", () => { + const newEnum = service.getEnum("new-id"); + + expect(newEnum.getId()).toEqual("new-id"); + expect(newEnum.values).toEqual([]); + expect(mockEntityMapper.save).toHaveBeenCalledWith(newEnum); + // returns same enum in consecutive calls + expect(service.getEnum("new-id")).toBe(newEnum); + }); +}); diff --git a/src/app/core/configurable-enum/configurable-enum.service.ts b/src/app/core/configurable-enum/configurable-enum.service.ts index 93c4e23e9a..52a5359610 100644 --- a/src/app/core/configurable-enum/configurable-enum.service.ts +++ b/src/app/core/configurable-enum/configurable-enum.service.ts @@ -36,6 +36,11 @@ export class ConfigurableEnumService { getEnum(id: string): ConfigurableEnum { const entityId = Entity.createPrefixedId(ConfigurableEnum.ENTITY_TYPE, id); + if (!this.enums.has(entityId)) { + const newEnum = new ConfigurableEnum(id); + this.cacheEnum(newEnum); + this.entityMapper.save(newEnum); + } return this.enums.get(entityId); } } From 5660c9f2ed47c7d53b6aa19d8fae6ed24270e7c4 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 07:08:44 +0100 Subject: [PATCH 68/83] cleaned up custom form control and basic autocomplete --- .../basic-autocomplete.component.ts | 67 +++++++------------ .../custom-form-control.directive.ts | 43 ++---------- 2 files changed, 30 insertions(+), 80 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 8f8cdca740..d54bf8c652 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -2,21 +2,15 @@ import { Component, ContentChild, ElementRef, - Inject, Input, OnChanges, Optional, Self, - SimpleChanges, TemplateRef, ViewChild, } from "@angular/core"; import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from "@angular/common"; -import { - MAT_FORM_FIELD, - MatFormField, - MatFormFieldControl, -} from "@angular/material/form-field"; +import { MatFormFieldControl } from "@angular/material/form-field"; import { FormControl, FormGroupDirective, @@ -71,6 +65,11 @@ export class BasicAutocompleteComponent @ViewChild("inputElement") inputElement: ElementRef; @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @Input() valueMapper = (option: O) => option as unknown as V; + @Input() optionToString = (option) => option?.toString(); + @Input() createOption: (input: string) => O; + @Input() multi?: boolean; + autocompleteForm = new FormControl(""); autocompleteSuggestedOptions = this.autocompleteForm.valueChanges.pipe( filter((val) => typeof val === "string"), @@ -99,29 +98,10 @@ export class BasicAutocompleteComponent private _options: SelectableOption[] = []; - @Input() set valueMapper(value: (option: O) => V) { - this._valueMapper = value; - this._options.forEach((opt) => (opt.asValue = value(opt.initial))); - } - - private _valueMapper = (option: O) => option as unknown as V; - - @Input() set optionToString(value: (option: O) => string) { - this._optionToString = value; - this._options.forEach((opt) => (opt.asString = value(opt.initial))); - } - - private _optionToString = (option) => option?.toString(); - - @Input() createOption: (input: string) => O; - - @Input() multi?: boolean; - constructor( elementRef: ElementRef, private confirmation: ConfirmationDialogService, errorStateMatcher: ErrorStateMatcher, - @Optional() @Inject(MAT_FORM_FIELD) formField: MatFormField, @Optional() @Self() ngControl: NgControl, @Optional() parentForm: NgForm, @Optional() parentFormGroup: FormGroupDirective @@ -129,24 +109,25 @@ export class BasicAutocompleteComponent super( elementRef, errorStateMatcher, - formField, ngControl, parentForm, parentFormGroup ); - if (this.ngControl != null) { - this.ngControl.valueAccessor = this; - } } - ngOnChanges(changes: SimpleChanges) { + ngOnChanges(changes: { [key in keyof this]: any }) { + if (changes.valueMapper) { + this._options.forEach( + (opt) => (opt.asValue = this.valueMapper(opt.initial)) + ); + } + if (changes.optionToString) { + this._options.forEach( + (opt) => (opt.asString = this.optionToString(opt.initial)) + ); + } if (changes.value || changes.options) { - if (this.multi) { - this._options - .filter(({ asValue }) => (this.value as V[])?.includes(asValue)) - .forEach((o) => (o.selected = true)); - } - this.setInputValue(); + this.setInitialInputValue(); } } @@ -179,8 +160,11 @@ export class BasicAutocompleteComponent return filteredOptions; } - private setInputValue() { + private setInitialInputValue() { if (this.multi) { + this._options + .filter(({ asValue }) => (this.value as V[])?.includes(asValue)) + .forEach((o) => (o.selected = true)); this.displaySelectedOptions(); } else { const selected = this._options.find( @@ -222,7 +206,7 @@ export class BasicAutocompleteComponent if (userConfirmed) { const newOption = this.toSelectableOption(this.createOption(option)); this._options.push(newOption); - this.select(newOption); + this.selectOption(newOption); } } @@ -244,8 +228,8 @@ export class BasicAutocompleteComponent private toSelectableOption(opt: O): SelectableOption { return { initial: opt, - asValue: this._valueMapper(opt), - asString: this._optionToString(opt), + asValue: this.valueMapper(opt), + asString: this.optionToString(opt), selected: false, }; } @@ -275,7 +259,6 @@ export class BasicAutocompleteComponent private notifyFocusOut() { if (this.multi) { - // show all selected option this.displaySelectedOptions(); } else { const inputValue = this.autocompleteForm.value; diff --git a/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts b/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts index ba4881fc55..c986873d5d 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/custom-form-control.directive.ts @@ -5,10 +5,7 @@ import { NgControl, NgForm, } from "@angular/forms"; -import { - MatFormField, - MatFormFieldControl, -} from "@angular/material/form-field"; +import { MatFormFieldControl } from "@angular/material/form-field"; import { Directive, DoCheck, @@ -30,6 +27,8 @@ export abstract class CustomFormControlDirective id = `custom-form-control-${CustomFormControlDirective.nextId++}`; // eslint-disable-next-line @angular-eslint/no-input-rename @Input("aria-describedby") userAriaDescribedBy: string; + @Input() placeholder: string; + @Input() required = false; stateChanges = new Subject(); focused = false; @@ -70,32 +69,9 @@ export abstract class CustomFormControlDirective _value: T; - @Input() get placeholder(): string { - return this._placeholder; - } - - set placeholder(value: string) { - this._placeholder = value; - this.stateChanges.next(); - } - - _placeholder: string; - - @Input() get required(): boolean { - return this._required; - } - - set required(value: boolean) { - this._required = value; - this.stateChanges.next(); - } - - _required = false; - constructor( public elementRef: ElementRef, public errorStateMatcher: ErrorStateMatcher, - public formField: MatFormField, public ngControl: NgControl, public parentForm: NgForm, public parentFormGroup: FormGroupDirective @@ -128,11 +104,7 @@ export abstract class CustomFormControlDirective controlElement.setAttribute("aria-describedby", ids.join(" ")); } - onContainerClick(event: MouseEvent) { - if ((event.target as Element).tagName.toLowerCase() != "input") { - this.elementRef.nativeElement.focus(); - } - } + abstract onContainerClick(event: MouseEvent); writeValue(val: T): void { this.value = val; @@ -150,16 +122,11 @@ export abstract class CustomFormControlDirective this.disabled = isDisabled; } - ngDoCheck() { - this.updateErrorState(); - } - /** * Updates the error state based on the form control * Taken from {@link https://github.com/angular/components/blob/a1d5614f18066c0c2dc2580c7b5099e8f68a8e74/src/material/core/common-behaviors/error-state.ts#L59} - * @private */ - private updateErrorState() { + ngDoCheck() { const oldState = this.errorState; const parent = this.parentFormGroup || this.parentForm; const control = this.ngControl From 769e8f0af24fc07fe7094311ff6caeeab36a1ba4 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 07:56:02 +0100 Subject: [PATCH 69/83] fixed test --- .../basic-autocomplete/basic-autocomplete.component.spec.ts | 6 +++--- .../basic-autocomplete/basic-autocomplete.component.ts | 2 +- src/app/features/file/couchdb-file.service.spec.ts | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index c02f5be7ad..2df06db56e 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -89,7 +89,7 @@ describe("BasicAutocompleteComponent", () => { component.options = [child1, child2]; component.valueMapper = entityToId; - component.ngOnChanges({ form: true, options: true } as any); + component.ngOnChanges({ value: true, options: true, valueMapper: true }); fixture.detectChanges(); expect(component.autocompleteForm).toHaveValue("First Child"); @@ -137,7 +137,7 @@ describe("BasicAutocompleteComponent", () => { component.options = [1, 2, 3]; component.multi = true; component.value = [1, 2]; - component.ngOnChanges({ options: true, value: true } as any); + component.ngOnChanges({ options: true, value: true }); component.showAutocomplete(); component.autocomplete.openPanel(); @@ -154,7 +154,7 @@ describe("BasicAutocompleteComponent", () => { component.multi = true; component.options = ["some", "values", "and", "other", "options"]; component.value = ["some", "values"]; - component.ngOnChanges({ value: true, options: true } as any); + component.ngOnChanges({ value: true, options: true }); expect(component.autocompleteForm).toHaveValue("some, values"); component.onFocusIn(); diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index d54bf8c652..e473c5c9ea 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -115,7 +115,7 @@ export class BasicAutocompleteComponent ); } - ngOnChanges(changes: { [key in keyof this]: any }) { + ngOnChanges(changes: { [key in keyof this]?: any }) { if (changes.valueMapper) { this._options.forEach( (opt) => (opt.asValue = this.valueMapper(opt.initial)) diff --git a/src/app/features/file/couchdb-file.service.spec.ts b/src/app/features/file/couchdb-file.service.spec.ts index 9bd55eaed6..fadc1702cb 100644 --- a/src/app/features/file/couchdb-file.service.spec.ts +++ b/src/app/features/file/couchdb-file.service.spec.ts @@ -63,6 +63,10 @@ describe("CouchdbFileService", () => { service = TestBed.inject(CouchdbFileService); }); + afterEach(() => { + Entity.schema.delete("testProp"); + }); + it("should be created", () => { expect(service).toBeTruthy(); }); From 88c92056925b4d7066c350bd998af500b24b577b Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 08:43:57 +0100 Subject: [PATCH 70/83] increased timeout for e2e test --- e2e/integration/MarkingChildAsDropout.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/integration/MarkingChildAsDropout.cy.ts b/e2e/integration/MarkingChildAsDropout.cy.ts index 43bdeba91a..f48b0d5bc5 100644 --- a/e2e/integration/MarkingChildAsDropout.cy.ts +++ b/e2e/integration/MarkingChildAsDropout.cy.ts @@ -33,7 +33,9 @@ describe("Scenario: Marking a child as dropout - E2E test", function () { it("AND I should see the child when I activate the 'inactive' filter", function () { // click on the button with the content "Inactive" cy.get('[ng-reflect-placeholder="isActive"]').click(); - cy.contains("span", "Inactive").should("be.visible").click(); + cy.contains("span", "Inactive", { timeout: 10000 }) + .should("be.visible") + .click(); // find at this table the name of child and it should exist cy.get("table").contains(this.childName.trim()).should("exist"); }); From 1865881278ed73f4eb4b5c49488d8d374433c558 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 08:55:06 +0100 Subject: [PATCH 71/83] increased timeout for e2e test --- e2e/support/commands.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/support/commands.ts b/e2e/support/commands.ts index c9bab81cd0..ec4befa2d0 100644 --- a/e2e/support/commands.ts +++ b/e2e/support/commands.ts @@ -15,7 +15,9 @@ declare namespace Cypress { } function create(menuItem: string, name: string): void { - cy.get(`[ng-reflect-angulartics-label="${menuItem}"]`).click(); + cy.get(`[ng-reflect-angulartics-label="${menuItem}"]`, { + timeout: 10000, + }).click(); cy.contains("button", "Add New").click(); cy.contains("mat-label", "Name").type(name); cy.contains("button", "Save").click(); From 2da97b822518d676cd7921b80f28ad128a472b3c Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 Feb 2023 09:11:21 +0100 Subject: [PATCH 72/83] improve waiting for demo data generation --- e2e/integration/MarkingChildAsDropout.cy.ts | 6 ++---- e2e/support/commands.ts | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/e2e/integration/MarkingChildAsDropout.cy.ts b/e2e/integration/MarkingChildAsDropout.cy.ts index f48b0d5bc5..c8df26ad27 100644 --- a/e2e/integration/MarkingChildAsDropout.cy.ts +++ b/e2e/integration/MarkingChildAsDropout.cy.ts @@ -32,10 +32,8 @@ describe("Scenario: Marking a child as dropout - E2E test", function () { it("AND I should see the child when I activate the 'inactive' filter", function () { // click on the button with the content "Inactive" - cy.get('[ng-reflect-placeholder="isActive"]').click(); - cy.contains("span", "Inactive", { timeout: 10000 }) - .should("be.visible") - .click(); + cy.get('[ng-reflect-placeholder="isActive"]', { timeout: 10000 }).click(); + cy.contains("span", "Inactive").should("be.visible").click(); // find at this table the name of child and it should exist cy.get("table").contains(this.childName.trim()).should("exist"); }); diff --git a/e2e/support/commands.ts b/e2e/support/commands.ts index ec4befa2d0..40abd96732 100644 --- a/e2e/support/commands.ts +++ b/e2e/support/commands.ts @@ -15,9 +15,7 @@ declare namespace Cypress { } function create(menuItem: string, name: string): void { - cy.get(`[ng-reflect-angulartics-label="${menuItem}"]`, { - timeout: 10000, - }).click(); + cy.get(`[ng-reflect-angulartics-label="${menuItem}"]`).click(); cy.contains("button", "Add New").click(); cy.contains("mat-label", "Name").type(name); cy.contains("button", "Save").click(); @@ -32,6 +30,9 @@ Cypress.Commands.overwrite("visit", (originalFun, url, options) => { cy.get("app-search", { timeout: 10000 }).should("be.visible"); // wait for demo data generation cy.wait(4000); + cy.contains("div", "Generating sample data", { + timeout: 20000, + }).should("not.exist"); // wait for indexing cy.contains("button", "Continue in background", { timeout: 10000 }).should( "not.exist" From 0f3a9d6c589460fe10e16628504e90b2a6ce3ab2 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 07:56:34 +0100 Subject: [PATCH 73/83] Update src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts Co-authored-by: Sebastian --- .../basic-autocomplete/basic-autocomplete.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index e473c5c9ea..89863f455c 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -201,7 +201,7 @@ export class BasicAutocompleteComponent async createNewOption(option: string) { const userConfirmed = await this.confirmation.getConfirmation( $localize`Create new option`, - `Do you want to create the new option ${option}` + `Do you want to create the new option "${option}"?` ); if (userConfirmed) { const newOption = this.toSelectableOption(this.createOption(option)); From 2143048d4e400809f4a630ef95388c04ae926d62 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 07:56:52 +0100 Subject: [PATCH 74/83] Update src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts Co-authored-by: Sebastian --- .../configure-enum-popup/configure-enum-popup.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 2c0d210cb3..126e927107 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -65,7 +65,7 @@ export class ConfigureEnumPopupComponent { async delete(value: ConfigurableEnumValue, index: number) { const existingUsages = await this.getUsages(value); - let deletionText = $localize`Are you sure that you want to delete the option ${value.label}?`; + let deletionText = $localize`Are you sure that you want to delete the option "${value.label}"?`; if (existingUsages.length > 0) { deletionText += $localize` The option is still used in ${existingUsages.join( ", " From 2906752c5faf00c489f6ac7f8723f70dd05ac766 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 07:57:02 +0100 Subject: [PATCH 75/83] Update src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts Co-authored-by: Sebastian --- .../configure-enum-popup/configure-enum-popup.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 126e927107..479cbf662c 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -69,7 +69,7 @@ export class ConfigureEnumPopupComponent { if (existingUsages.length > 0) { deletionText += $localize` The option is still used in ${existingUsages.join( ", " - )} records. If deleted, the records will not be lost but specially marked`; + )} records. If deleted, the records will not be lost but specially marked.`; } const confirmed = await this.confirmationService.getConfirmation( $localize`Delete option`, From 84ae17950ec3b6134636ea411ea59932b3b5cf02 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 07:49:05 +0100 Subject: [PATCH 76/83] properly detecting which enums already exist and which not --- src/app/core/config/config.service.spec.ts | 10 ++-- src/app/core/config/config.service.ts | 58 +++++++++++----------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index a64bf2e063..27e5a08f26 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -143,13 +143,15 @@ describe("ConfigService", () => { expect(configUpdate.data).toEqual({ some: "config" }); }); - it("should not save enums if they already exist in db", async () => { - entityMapper.loadType.and.resolveTo([new ConfigurableEnum()]); + it("should not save enums that already exist in db", async () => { + entityMapper.loadType.and.resolveTo([new ConfigurableEnum("1")]); entityMapper.save.and.resolveTo(); - await initConfig({ "enum:1": [], some: "config" }); + await initConfig({ "enum:1": [], "enum:2": [], some: "config" }); - expect(entityMapper.saveAll).not.toHaveBeenCalled(); + expect(entityMapper.saveAll).toHaveBeenCalledWith([ + new ConfigurableEnum("2"), + ]); expect(entityMapper.save).toHaveBeenCalledWith(jasmine.any(Config)); expect(service.getConfig("enum:1")).toBeUndefined(); expect(service.getConfig("some")).toBe("config"); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index dc14fdbf8f..05b2d4d936 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -44,38 +44,11 @@ export class ConfigService { private async updateConfigIfChanged(config: Config) { if (!this.currentConfig || config._rev !== this.currentConfig?._rev) { - await this.saveAllEnumsToDB(config); this.currentConfig = config; this._configUpdates.next(config); } } - private async saveAllEnumsToDB(config: Config) { - const enumValues = Object.entries(config.data).filter(([key]) => - key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX) - ); - if (enumValues.length === 0) { - return; - } - const existingEnums = await this.entityMapper.loadType(ConfigurableEnum); - if (existingEnums.length > 0) { - enumValues.forEach(([key]) => delete config.data[key]); - return this.entityMapper.save(config).catch(() => {}); - } - - const enumEntities = enumValues.map(([key, value]) => { - const id = key.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, ""); - const newEnum = new ConfigurableEnum(id); - newEnum.values = value as any; - delete config.data[key]; - return newEnum; - }); - return this.entityMapper - .saveAll(enumEntities) - .then(() => this.entityMapper.save(config)) - .catch(() => {}); - } - public saveConfig(config: any): Promise { return this.entityMapper.save(new Config(Config.CONFIG_KEY, config), true); } @@ -99,7 +72,7 @@ export class ConfigService { return matchingConfigs; } - private detectLegacyConfig(config: Config): Config { + private async detectLegacyConfig(config: Config): Promise { // ugly but easy ... could use https://www.npmjs.com/package/jsonpath-plus in future const configString = JSON.stringify(config); if ( @@ -117,6 +90,35 @@ export class ConfigService { ); } + await this.migrateEnumsToEntities(config); + return config; } + + private async migrateEnumsToEntities(config: Config) { + const enumValues = Object.entries(config.data).filter(([key]) => + key.startsWith(CONFIGURABLE_ENUM_CONFIG_PREFIX) + ); + if (enumValues.length === 0) { + return; + } + const existingEnums = await this.entityMapper + .loadType(ConfigurableEnum) + .then((res) => res.map((e) => e.getId())); + + const newEnums: ConfigurableEnum[] = []; + enumValues.forEach(([key, value]) => { + const id = key.replace(CONFIGURABLE_ENUM_CONFIG_PREFIX, ""); + if (!existingEnums.includes(id)) { + const newEnum = new ConfigurableEnum(id); + newEnum.values = value as any; + newEnums.push(newEnum); + } + delete config.data[key]; + }); + return this.entityMapper + .saveAll(newEnums) + .then(() => this.entityMapper.save(config)) + .catch(() => {}); + } } From 2dc0a6af0b3e393e6c5cd4a7459bad2baf1828be Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 08:00:01 +0100 Subject: [PATCH 77/83] improved test readability --- .../basic-autocomplete/basic-autocomplete.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts index 2df06db56e..6b8d4986aa 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts @@ -134,9 +134,9 @@ describe("BasicAutocompleteComponent", () => { it("should initialize the options in multi select mode", async () => { const autocomplete = await loader.getHarness(MatAutocompleteHarness); - component.options = [1, 2, 3]; + component.options = [0, 1, 2]; component.multi = true; - component.value = [1, 2]; + component.value = [0, 1]; component.ngOnChanges({ options: true, value: true }); component.showAutocomplete(); @@ -147,7 +147,7 @@ describe("BasicAutocompleteComponent", () => { await options[2].click(); await options[1].click(); - expect(component.value).toEqual([1, 3]); + expect(component.value).toEqual([0, 2]); }); it("should clear the input when focusing in multi select mode", () => { From 5a526a1f3915a96a7a3ae6cdf924f707b2e4bfdc Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 08:21:53 +0100 Subject: [PATCH 78/83] fixed typo --- .../edit-entity-array/entity-reference-array.stories.ts | 9 +++------ .../edit-single-entity/entity-reference.stories.ts | 9 +++------ .../edit-boolean/edit-boolean.stories.ts | 4 ++-- .../edit-number/edit-number.stories.ts | 4 ++-- .../location/edit-location/edit-location.stories.ts | 4 ++-- src/app/utils/storybook-base.module.ts | 2 +- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/app/core/entity-components/entity-select/edit-entity-array/entity-reference-array.stories.ts b/src/app/core/entity-components/entity-select/edit-entity-array/entity-reference-array.stories.ts index 5a44e82f90..26ce68a0ea 100644 --- a/src/app/core/entity-components/entity-select/edit-entity-array/entity-reference-array.stories.ts +++ b/src/app/core/entity-components/entity-select/edit-entity-array/entity-reference-array.stories.ts @@ -5,7 +5,7 @@ import { EntityFormComponent } from "../../entity-form/entity-form/entity-form.c import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { - entityFormStorybookDefaulParameters, + entityFormStorybookDefaultParameters, StorybookBaseModule, } from "../../../../utils/storybook-base.module"; import { DatabaseEntity } from "../../../entity/database-entity.decorator"; @@ -24,10 +24,7 @@ export default { component: EntityFormComponent, decorators: [ moduleMetadata({ - imports: [ - EntityFormComponent, - StorybookBaseModule, - ], + imports: [EntityFormComponent, StorybookBaseModule], providers: [ EntitySchemaService, { @@ -37,7 +34,7 @@ export default { ], }), ], - parameters: entityFormStorybookDefaulParameters, + parameters: entityFormStorybookDefaultParameters, } as Meta; const Template: Story = (args: EntityFormComponent) => ({ diff --git a/src/app/core/entity-components/entity-select/edit-single-entity/entity-reference.stories.ts b/src/app/core/entity-components/entity-select/edit-single-entity/entity-reference.stories.ts index 6984eb6974..2aec8a3fcd 100644 --- a/src/app/core/entity-components/entity-select/edit-single-entity/entity-reference.stories.ts +++ b/src/app/core/entity-components/entity-select/edit-single-entity/entity-reference.stories.ts @@ -5,7 +5,7 @@ import { EntityFormComponent } from "../../entity-form/entity-form/entity-form.c import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { - entityFormStorybookDefaulParameters, + entityFormStorybookDefaultParameters, StorybookBaseModule, } from "../../../../utils/storybook-base.module"; import { DatabaseEntity } from "../../../entity/database-entity.decorator"; @@ -27,10 +27,7 @@ export default { component: EntityFormComponent, decorators: [ moduleMetadata({ - imports: [ - EntityFormComponent, - StorybookBaseModule, - ], + imports: [EntityFormComponent, StorybookBaseModule], providers: [ EntitySchemaService, { @@ -40,7 +37,7 @@ export default { ], }), ], - parameters: entityFormStorybookDefaulParameters, + parameters: entityFormStorybookDefaultParameters, } as Meta; const Template: Story = (args: EntityFormComponent) => ({ diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-boolean/edit-boolean.stories.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-boolean/edit-boolean.stories.ts index 1adeff1627..bdc906a38d 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-boolean/edit-boolean.stories.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-boolean/edit-boolean.stories.ts @@ -8,7 +8,7 @@ import { Entity } from "../../../../entity/model/entity"; import { DatabaseField } from "../../../../entity/database-field.decorator"; import { DatabaseEntity } from "../../../../entity/database-entity.decorator"; import { - entityFormStorybookDefaulParameters, + entityFormStorybookDefaultParameters, StorybookBaseModule, } from "../../../../../utils/storybook-base.module"; @@ -27,7 +27,7 @@ export default { ], }), ], - parameters: entityFormStorybookDefaulParameters, + parameters: entityFormStorybookDefaultParameters, } as Meta; const Template: Story = (args: EntityFormComponent) => ({ diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts index 191aa695b5..54a16e5e31 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.stories.ts @@ -8,7 +8,7 @@ import { DatabaseField } from "../../../../entity/database-field.decorator"; import { DatabaseEntity } from "../../../../entity/database-entity.decorator"; import { EditNumberComponent } from "./edit-number.component"; import { - entityFormStorybookDefaulParameters, + entityFormStorybookDefaultParameters, StorybookBaseModule, } from "../../../../../utils/storybook-base.module"; import { AppModule } from "../../../../../app.module"; @@ -35,7 +35,7 @@ export default { ], }), ], - parameters: entityFormStorybookDefaulParameters, + parameters: entityFormStorybookDefaultParameters, } as Meta; const Template: Story> = (args: FormComponent) => ({ diff --git a/src/app/features/location/edit-location/edit-location.stories.ts b/src/app/features/location/edit-location/edit-location.stories.ts index 8bab690134..7bcd8e3b1f 100644 --- a/src/app/features/location/edit-location/edit-location.stories.ts +++ b/src/app/features/location/edit-location/edit-location.stories.ts @@ -1,6 +1,6 @@ import { moduleMetadata } from "@storybook/angular"; import { - entityFormStorybookDefaulParameters, + entityFormStorybookDefaultParameters, StorybookBaseModule, } from "../../../utils/storybook-base.module"; import { Meta, Story } from "@storybook/angular/types-6-0"; @@ -33,7 +33,7 @@ export default { ], }), ], - parameters: entityFormStorybookDefaulParameters, + parameters: entityFormStorybookDefaultParameters, } as Meta; @DatabaseEntity("LocationTest") diff --git a/src/app/utils/storybook-base.module.ts b/src/app/utils/storybook-base.module.ts index 974c46793b..99f9481e4a 100644 --- a/src/app/utils/storybook-base.module.ts +++ b/src/app/utils/storybook-base.module.ts @@ -24,7 +24,7 @@ import { AppModule } from "../app.module"; componentRegistry.allowDuplicates(); entityRegistry.allowDuplicates(); -export const entityFormStorybookDefaulParameters = { +export const entityFormStorybookDefaultParameters = { controls: { exclude: ["_columns"], }, From 6abdca32a16bcc7cefeda4fa9d47ca40b1bc65e7 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 08:48:12 +0100 Subject: [PATCH 79/83] in multi mode configurable enums are also shown with their color --- .../display-configurable-enum.component.html | 8 +++++ .../display-configurable-enum.component.scss | 4 +++ ...isplay-configurable-enum.component.spec.ts | 32 ------------------- .../display-configurable-enum.component.ts | 21 +++++------- 4 files changed, 20 insertions(+), 45 deletions(-) create mode 100644 src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.html create mode 100644 src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.scss diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.html b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.html new file mode 100644 index 0000000000..ba203de7e2 --- /dev/null +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.html @@ -0,0 +1,8 @@ + + {{ val.label }} + + diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.scss b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.scss new file mode 100644 index 0000000000..6ca1460f59 --- /dev/null +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.scss @@ -0,0 +1,4 @@ +.colored { + padding: 5px; + border-radius: 4px; +} diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts index 3fe1ca8381..abf9e83cc6 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts @@ -24,36 +24,4 @@ describe("DisplayConfigurableEnumComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); - - it("should display label of value", () => { - expect(fixture.debugElement.nativeElement.innerHTML).toBe("Test Category"); - }); - - it("should use the background color if available", () => { - const elem = fixture.debugElement.nativeElement; - expect(elem.style["background-color"]).toBe(""); - - const value: Ordering.EnumValue = { - label: "withColor", - id: "WITH_COLOR", - color: "black", - _ordinal: 1, - }; - component.onInitFromDynamicConfig({ value } as any); - fixture.detectChanges(); - - expect(elem.style["background-color"]).toBe("black"); - expect(elem.style.padding).toBe("5px"); - expect(elem.style["border-radius"]).toBe("4px"); - }); - - it("should concatenate multiple values", () => { - const first = { id: "1", label: "First" }; - - const second = { id: "2", label: "Second" }; - component.onInitFromDynamicConfig({ value: [first, second] } as any); - fixture.detectChanges(); - - expect(fixture.debugElement.nativeElement.innerHTML).toBe("First, Second"); - }); }); diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts index 3424c81c38..f2970718bc 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.ts @@ -1,8 +1,9 @@ -import { Component, HostBinding } from "@angular/core"; +import { Component } from "@angular/core"; import { ViewPropertyConfig } from "app/core/entity-components/entity-list/EntityListConfig"; import { ViewDirective } from "../../entity-components/entity-utils/view-components/view.directive"; import { DynamicComponent } from "../../view/dynamic-components/dynamic-component.decorator"; import { ConfigurableEnumValue } from "../configurable-enum.interface"; +import { NgClass, NgForOf, NgIf } from "@angular/common"; /** * This component displays a {@link ConfigurableEnumValue} as text. @@ -11,28 +12,22 @@ import { ConfigurableEnumValue } from "../configurable-enum.interface"; @DynamicComponent("DisplayConfigurableEnum") @Component({ selector: "app-display-configurable-enum", - template: `{{ templateString }}`, + templateUrl: "./display-configurable-enum.component.html", + styleUrls: ["./display-configurable-enum.component.scss"], standalone: true, + imports: [NgForOf, NgIf, NgClass], }) export class DisplayConfigurableEnumComponent extends ViewDirective< ConfigurableEnumValue | ConfigurableEnumValue[] > { - @HostBinding("style.background-color") private style; - @HostBinding("style.padding") private padding; - @HostBinding("style.border-radius") private radius; - templateString = ""; + iterableValue: ConfigurableEnumValue[] = []; onInitFromDynamicConfig(config: ViewPropertyConfig) { super.onInitFromDynamicConfig(config); if (Array.isArray(this.value)) { - this.templateString = this.value.map((v) => v.label).join(", "); + this.iterableValue = this.value; } else if (this.value) { - if (this.value.color) { - this.style = this.value.color; - this.padding = "5px"; - this.radius = "4px"; - } - this.templateString = this.value.label; + this.iterableValue = [this.value]; } } } From ceacb16a5242e442ddd5f0739eef837e2a7fd73c Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 09:17:13 +0100 Subject: [PATCH 80/83] config services checks permissions before updating saving config --- src/app/core/config/config-fix.ts | 66 ++++++++++--------- src/app/core/config/config.service.spec.ts | 16 +++++ src/app/core/config/config.service.ts | 21 ++++-- src/app/core/config/testing-config-service.ts | 3 +- ...isplay-configurable-enum.component.spec.ts | 1 - src/app/core/demo-data/demo-data.module.ts | 2 +- 6 files changed, 69 insertions(+), 40 deletions(-) diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 0a5a297658..e320e2ceef 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -222,21 +222,21 @@ export const defaultJsonConfig = { } ], "exportConfig": [ - {"label": "event_id", "query": "_id"}, - {"label": "date", "query": "date"}, - {"label": "event title", "query": "subject"}, - {"label": "event type", "query": "category"}, - {"label": "event description", "query": "text"}, + { "label": "event_id", "query": "_id" }, + { "label": "date", "query": "date" }, + { "label": "event title", "query": "subject" }, + { "label": "event type", "query": "category" }, + { "label": "event description", "query": "text" }, { "query": ":getAttendanceArray(true)", "subQueries": [ { "query": ".participant:toEntities(Child)", "subQueries": [ - {"label": "participant_id", "query": "_id"}, - {"label": "participant", "query": "name"}, - {"label": "gender", "query": "gender"}, - {"label": "religion", "query": "religion"}, + { "label": "participant_id", "query": "_id" }, + { "label": "participant", "query": "name" }, + { "label": "gender", "query": "gender" }, + { "label": "religion", "query": "religion" }, ] }, { @@ -246,8 +246,8 @@ export const defaultJsonConfig = { { "query": ".school:toEntities(School)", "subQueries": [ - {"label": "school_name", "query": "name"}, - {"label": "school_id", "query": "entityId"} + { "label": "school_name", "query": "name" }, + { "label": "school_id", "query": "entityId" } ] } ], @@ -460,11 +460,7 @@ export const defaultJsonConfig = { "projectNumber", "name", "age", - "gender", - "schoolClass", - "schoolId", - "center", - "status" + "test" ] }, { @@ -554,6 +550,7 @@ export const defaultJsonConfig = { "name", "projectNumber", "admissionDate", + "test" ], [ "dateOfBirth", @@ -611,7 +608,7 @@ export const defaultJsonConfig = { "config": { "rightSide": { "entityType": School.ENTITY_TYPE, - "availableFilters": [{"id": "language"}], + "availableFilters": [{ "id": "language" }], }, } } @@ -675,11 +672,11 @@ export const defaultJsonConfig = { component: "HistoricalDataComponent", config: [ "date", - {id: "isMotivatedDuringClass", visibleFrom: "lg"}, - {id: "isParticipatingInClass", visibleFrom: "lg"}, - {id: "isInteractingWithOthers", visibleFrom: "lg"}, - {id: "doesHomework", visibleFrom: "lg"}, - {id: "asksQuestions", visibleFrom: "lg"}, + { id: "isMotivatedDuringClass", visibleFrom: "lg" }, + { id: "isParticipatingInClass", visibleFrom: "lg" }, + { id: "isInteractingWithOthers", visibleFrom: "lg" }, + { id: "doesHomework", visibleFrom: "lg" }, + { id: "asksQuestions", visibleFrom: "lg" }, ] } ] @@ -713,9 +710,9 @@ export const defaultJsonConfig = { "assignedTo" ], "exportConfig": [ - {label: "Title", query: "title"}, - {label: "Type", query: "type"}, - {label: "Assigned users", query: "assignedTo"} + { label: "Title", query: "title" }, + { label: "Type", query: "type" }, + { label: "Assigned users", query: "assignedTo" } ] } }, @@ -830,7 +827,7 @@ export const defaultJsonConfig = { "aggregationDefinitions": [ { "query": `${EventNote.ENTITY_TYPE}:toArray[* date >= ? & date <= ?]`, - groupBy: {label: "Type", property: "category"}, + groupBy: { label: "Type", property: "category" }, "subQueries": [ { query: ":getAttendanceArray:getAttendanceReport", @@ -869,6 +866,15 @@ export const defaultJsonConfig = { "label": $localize`:Label for child:Child`, "labelPlural": $localize`:Plural label for child:Children`, "attributes": [ + { + "name": "test", + "schema": { + dataType: "array", + innerDataType: "configurable-enum", + additional: "interaction-type", + label: "Test" + } + }, { "name": "address", "schema": { @@ -1032,10 +1038,10 @@ export const defaultJsonConfig = { config: { rightSide: { entityType: School.ENTITY_TYPE, - prefilter: {"privateSchool": true}, - availableFilters: [{"id": "language"}], + prefilter: { "privateSchool": true }, + availableFilters: [{ "id": "language" }], }, - leftSide: {entityType: Child.ENTITY_TYPE}, + leftSide: { entityType: Child.ENTITY_TYPE }, } }, "appConfig:matching-entities": { @@ -1062,7 +1068,7 @@ export const defaultJsonConfig = { "entity": "Todo", "columns": ["deadline", "subject", "assignedTo", "startDate", "relatedEntities"], "filters": [ - {"id": "assignedTo"}, + { "id": "assignedTo" }, { "id": "due-status", diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 27e5a08f26..ef003d22eb 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -6,6 +6,7 @@ import { firstValueFrom, Subject } from "rxjs"; import { UpdatedEntity } from "../entity/model/entity-update"; import { LoggingService } from "../logging/logging.service"; import { ConfigurableEnum } from "../configurable-enum/configurable-enum"; +import { EntityAbility } from "../permissions/ability/entity-ability"; describe("ConfigService", () => { let service: ConfigService; @@ -30,9 +31,13 @@ describe("ConfigService", () => { { provide: EntityMapperService, useValue: entityMapper }, ConfigService, LoggingService, + EntityAbility, ], }); service = TestBed.inject(ConfigService); + TestBed.inject(EntityAbility).update([ + { subject: "all", action: "manage" }, + ]); }); it("should be created", () => { @@ -164,6 +169,17 @@ describe("ConfigService", () => { expect(entityMapper.saveAll).not.toHaveBeenCalled(); }); + it("should not save config if permissions prevent it", async () => { + // user can only read config + TestBed.inject(EntityAbility).update([ + { subject: "Config", action: "read" }, + ]); + + await initConfig({ "enum:1": [], other: "config" }); + + expect(entityMapper.save).not.toHaveBeenCalled(); + }); + function initConfig(data) { const config = new Config(); config.data = data; diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 05b2d4d936..3cee91e7fc 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -6,6 +6,7 @@ import { CONFIGURABLE_ENUM_CONFIG_PREFIX } from "../configurable-enum/configurab import { filter } from "rxjs/operators"; import { LoggingService } from "../logging/logging.service"; import { ConfigurableEnum } from "../configurable-enum/configurable-enum"; +import { EntityAbility } from "../permissions/ability/entity-ability"; /** * Access dynamic app configuration retrieved from the database @@ -25,7 +26,8 @@ export class ConfigService { constructor( private entityMapper: EntityMapperService, - private logger: LoggingService + private logger: LoggingService, + private ability: EntityAbility ) { this.loadConfig(); this.entityMapper @@ -37,13 +39,13 @@ export class ConfigService { async loadConfig(): Promise { return this.entityMapper .load(Config, Config.CONFIG_KEY) - .then((config) => this.detectLegacyConfig(config)) .then((config) => this.updateConfigIfChanged(config)) .catch(() => {}); } private async updateConfigIfChanged(config: Config) { if (!this.currentConfig || config._rev !== this.currentConfig?._rev) { + await this.detectLegacyConfig(config); this.currentConfig = config; this._configUpdates.next(config); } @@ -90,7 +92,9 @@ export class ConfigService { ); } - await this.migrateEnumsToEntities(config); + await this.migrateEnumsToEntities(config).catch((err) => + this.logger.error(`ConfigurableEnum migration error: ${err}`) + ); return config; } @@ -116,9 +120,12 @@ export class ConfigService { } delete config.data[key]; }); - return this.entityMapper - .saveAll(newEnums) - .then(() => this.entityMapper.save(config)) - .catch(() => {}); + + if (this.ability.can("create", ConfigurableEnum)) { + await this.entityMapper.saveAll(newEnums); + } + if (this.ability.can("update", config)) { + await this.entityMapper.save(config); + } } } diff --git a/src/app/core/config/testing-config-service.ts b/src/app/core/config/testing-config-service.ts index 81c8134754..f173a722e9 100644 --- a/src/app/core/config/testing-config-service.ts +++ b/src/app/core/config/testing-config-service.ts @@ -9,7 +9,8 @@ export function createTestingConfigService( ): ConfigService { const configService = new ConfigService( mockEntityMapper(), - new LoggingService() + new LoggingService(), + { can: () => true } as any ); configService["currentConfig"] = new Config(Config.CONFIG_KEY, configsObject); return configService; diff --git a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts index abf9e83cc6..7ec79b0531 100644 --- a/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts +++ b/src/app/core/configurable-enum/display-configurable-enum/display-configurable-enum.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { DisplayConfigurableEnumComponent } from "./display-configurable-enum.component"; -import { Ordering } from "../configurable-enum-ordering"; describe("DisplayConfigurableEnumComponent", () => { let component: DisplayConfigurableEnumComponent; diff --git a/src/app/core/demo-data/demo-data.module.ts b/src/app/core/demo-data/demo-data.module.ts index bf039e5233..3abd466245 100644 --- a/src/app/core/demo-data/demo-data.module.ts +++ b/src/app/core/demo-data/demo-data.module.ts @@ -40,6 +40,7 @@ import { DemoConfigurableEnumGeneratorService } from "../configurable-enum/demo- import { DemoPublicFormGeneratorService } from "../../features/public-form/demo-public-form-generator.service"; const demoDataGeneratorProviders = [ + ...DemoPermissionGeneratorService.provider(), ...DemoPublicFormGeneratorService.provider(), ...DemoConfigGeneratorService.provider(), ...DemoUserGeneratorService.provider(), @@ -65,7 +66,6 @@ const demoDataGeneratorProviders = [ minCountAttributes: 2, maxCountAttributes: 5, }), - ...DemoPermissionGeneratorService.provider(), ...DemoTodoGeneratorService.provider(), ]; From 34e760d562e0b63b0682e4ae8cc692e7216d5f72 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Feb 2023 10:07:27 +0100 Subject: [PATCH 81/83] fixed test --- .../configure-enum-popup/configure-enum-popup.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts index 935d5c8cbf..85605b90a1 100644 --- a/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts +++ b/src/app/core/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts @@ -78,7 +78,7 @@ describe("ConfigureEnumPopupComponent", () => { expect(confirmationSpy).toHaveBeenCalledWith( "Delete option", - `Are you sure that you want to delete the option ${male.label}?` + `Are you sure that you want to delete the option "${male.label}"?` ); }); }); From 0cfdba893145bbb82f76016d79ce286895ad9f78 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 7 Feb 2023 09:58:41 +0100 Subject: [PATCH 82/83] onChange is executed when after creating a new option --- .../basic-autocomplete/basic-autocomplete.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts index 89863f455c..7433e9064c 100644 --- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts +++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts @@ -206,7 +206,7 @@ export class BasicAutocompleteComponent if (userConfirmed) { const newOption = this.toSelectableOption(this.createOption(option)); this._options.push(newOption); - this.selectOption(newOption); + this.select(newOption); } } From 0df462a241cacb6fe1f31c2824e98aab788466a5 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 7 Feb 2023 10:01:15 +0100 Subject: [PATCH 83/83] undone temporary config changes --- src/app/core/config/config-fix.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index e320e2ceef..7b996e01b2 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -460,7 +460,11 @@ export const defaultJsonConfig = { "projectNumber", "name", "age", - "test" + "gender", + "schoolClass", + "schoolId", + "center", + "status" ] }, { @@ -550,7 +554,6 @@ export const defaultJsonConfig = { "name", "projectNumber", "admissionDate", - "test" ], [ "dateOfBirth", @@ -866,15 +869,6 @@ export const defaultJsonConfig = { "label": $localize`:Label for child:Child`, "labelPlural": $localize`:Plural label for child:Children`, "attributes": [ - { - "name": "test", - "schema": { - dataType: "array", - innerDataType: "configurable-enum", - additional: "interaction-type", - label: "Test" - } - }, { "name": "address", "schema": {