From 48fb139d70cacf0fd10def2edf4aa5d3da7c04f6 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:53:22 +0200 Subject: [PATCH 01/19] Initial commit --- .../display-entity/display-entity.stories.ts | 2 +- .../display-dynamic-value.component.spec.ts | 25 +++++++++ .../display-dynamic-value.component.ts | 43 +++++++++++++++ .../display-dynamic-value.stories.ts | 52 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts create mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts create mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts diff --git a/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts b/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts index a163f16ba8..8d072833ed 100644 --- a/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts +++ b/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts @@ -31,7 +31,7 @@ const Template: Story = ( }); const testChild = new Child(); -testChild.name = "Test Name"; +testChild.name = "Test NameXXX"; testChild.projectNumber = "10"; export const ChildComponent = Template.bind({}); ChildComponent.args = { diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts new file mode 100644 index 0000000000..66f455035b --- /dev/null +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DisplayPercentageComponent } from "./display-dynamic-value.component"; + +describe("DisplayDynamicValueComponent", () => { + let component: DisplayDynamicValueComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DisplayDynamicValueComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DisplayDynamicValueComponent); + component = fixture.componentInstance; + component.value = 10; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts new file mode 100644 index 0000000000..b9c6fbd2c1 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts @@ -0,0 +1,43 @@ +import { Component, HostBinding, OnInit } from "@angular/core"; +import { ViewDirective } from "../view.directive"; +import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; + +@DynamicComponent("DisplayDynamicValue") +@Component({ + selector: "app-display-dynamic-value", + template: "{{ testinput1 }}", + standalone: true, +}) +export class DisplayDynamicValueComponent + extends ViewDirective + implements OnInit +{ + @HostBinding("style") style = {}; + private testinput1: string; + + /** + * returns a css-compatible color value from green to red using the given + * input value + * @param percent The percentage from 0-100 (both inclusive). 0 will be completely red, 100 will be completely green + * Everything between will have suitable colors (orange, yellow,...) + * If the color is NaN, the color will be a light grey + */ + private static fromPercent(percent: number): string { + if (Number.isNaN(percent)) { + return "rgba(130,130,130,0.4)"; + } + // the hsv color-value is to be between 0 (red) and 120 (green) + // percent is between 0-100, so we have to normalize it first + const color = (percent / 100) * 120; + return "hsl(" + color + ", 100%, 85%)"; + } + + ngOnInit() { + this.style = { + "background-color": DisplayDynamicValueComponent.fromPercent(this.value), + "border-radius": "5%", + padding: "5px", + width: "min-content", + }; + } +} diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts new file mode 100644 index 0000000000..0b186512c0 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts @@ -0,0 +1,52 @@ +import { Meta, Story } from "@storybook/angular/types-6-0"; +import { moduleMetadata } from "@storybook/angular"; +import { StorybookBaseModule } from "../../../../../utils/storybook-base.module"; +import { DisplayDynamicValueComponent } from "./display-dynamic-value.component"; +import { DateWithAge } from "../../../../../child-dev-project/children/model/dateWithAge"; + +export default { + title: "Core/Entities/Display Properties/DisplayDynamicValue", + component: DisplayDynamicValueComponent, + decorators: [ + moduleMetadata({ + imports: [StorybookBaseModule, DisplayDynamicValueComponent], + providers: [], + }), + ], +} as Meta; + +const Template: Story = ( + args: DisplayDynamicValueComponent +) => ({ + props: args, +}); + +const date = new DateWithAge("2001-12-25"); +// currently Storybook can't handle classes extending Date - so this doesn't work: https://github.com/storybookjs/storybook/issues/14618 + +export const Summarize = Template.bind({}); +Summarize.args = { + entity: {totalDays: 10, activeDays: 5}, + config: { + properties: ["totalDays", "activeDays"], + calculation: "summarize" + } +}; + +export const Percentage = Template.bind({}); +Percentage.args = { + entity: {totalDays: 10, activeDays: 5}, + config: { + properties: ["totalDays", "activeDays"], + calculation: "percentage" + } +}; + + + + +export const WithoutValue = Template.bind({}); +WithoutValue.args = { + config: "dateOfBirth", + entity: {}, +}; From 835d3be57e8d01550c61426b2ce1e7bf633762a0 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 13 Jul 2023 21:40:40 +0200 Subject: [PATCH 02/19] Playing around with storybook and displayDynamicValueComponent --- .../display-dynamic-value.component.ts | 30 ++++++++++++------- .../display-dynamic-value.stories.ts | 28 +++++------------ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts index b9c6fbd2c1..b2e2c69dc6 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts @@ -1,19 +1,22 @@ -import { Component, HostBinding, OnInit } from "@angular/core"; +import { Component, HostBinding, Input, OnInit } from "@angular/core"; import { ViewDirective } from "../view.directive"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; @DynamicComponent("DisplayDynamicValue") @Component({ selector: "app-display-dynamic-value", - template: "{{ testinput1 }}", + template: "{{ result }}", standalone: true, }) export class DisplayDynamicValueComponent extends ViewDirective implements OnInit { - @HostBinding("style") style = {}; - private testinput1: string; + @Input() data: any; + @Input() config: { + calculation: "percentage" | "summarize"; + }; + public result: string; /** * returns a css-compatible color value from green to red using the given @@ -33,11 +36,18 @@ export class DisplayDynamicValueComponent } ngOnInit() { - this.style = { - "background-color": DisplayDynamicValueComponent.fromPercent(this.value), - "border-radius": "5%", - padding: "5px", - width: "min-content", - }; + if (this.config.calculation === "summarize") { + let calc = 0; + (this.data as Array).forEach((e) => { + calc += e; + }); + this.result = calc.toString(); + } else if (this.config.calculation === "percentage") { + let calc = + Math.round( + ((this.data.part / this.data.total) * 100 + Number.EPSILON) * 100 + ) / 100; + this.result = calc.toFixed(2) + " %"; + } } } diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts index 0b186512c0..096f4ba026 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts @@ -2,7 +2,6 @@ import { Meta, Story } from "@storybook/angular/types-6-0"; import { moduleMetadata } from "@storybook/angular"; import { StorybookBaseModule } from "../../../../../utils/storybook-base.module"; import { DisplayDynamicValueComponent } from "./display-dynamic-value.component"; -import { DateWithAge } from "../../../../../child-dev-project/children/model/dateWithAge"; export default { title: "Core/Entities/Display Properties/DisplayDynamicValue", @@ -21,32 +20,19 @@ const Template: Story = ( props: args, }); -const date = new DateWithAge("2001-12-25"); -// currently Storybook can't handle classes extending Date - so this doesn't work: https://github.com/storybookjs/storybook/issues/14618 - export const Summarize = Template.bind({}); Summarize.args = { - entity: {totalDays: 10, activeDays: 5}, + data: [10, 5], config: { - properties: ["totalDays", "activeDays"], - calculation: "summarize" - } + properties: [], + calculation: "summarize", + }, }; export const Percentage = Template.bind({}); Percentage.args = { - entity: {totalDays: 10, activeDays: 5}, + data: { total: 110, part: 5 }, config: { - properties: ["totalDays", "activeDays"], - calculation: "percentage" - } -}; - - - - -export const WithoutValue = Template.bind({}); -WithoutValue.args = { - config: "dateOfBirth", - entity: {}, + calculation: "percentage", + }, }; From 59abe4679706d0ce5c44e63694765894079a8c41 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:40:29 +0200 Subject: [PATCH 03/19] Limited display-dynamic-value.component to percentage --- .../display-dynamic-value.component.ts | 46 ++++--------------- .../display-dynamic-value.stories.ts | 19 +++----- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts index b2e2c69dc6..28b3879a9b 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, Input, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ViewDirective } from "../view.directive"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; @@ -9,45 +9,19 @@ import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-co standalone: true, }) export class DisplayDynamicValueComponent - extends ViewDirective + extends ViewDirective< + number, + { total: string; actual: string; numberOfDigits?: number } + > implements OnInit { - @Input() data: any; - @Input() config: { - calculation: "percentage" | "summarize"; - }; public result: string; - /** - * returns a css-compatible color value from green to red using the given - * input value - * @param percent The percentage from 0-100 (both inclusive). 0 will be completely red, 100 will be completely green - * Everything between will have suitable colors (orange, yellow,...) - * If the color is NaN, the color will be a light grey - */ - private static fromPercent(percent: number): string { - if (Number.isNaN(percent)) { - return "rgba(130,130,130,0.4)"; - } - // the hsv color-value is to be between 0 (red) and 120 (green) - // percent is between 0-100, so we have to normalize it first - const color = (percent / 100) * 120; - return "hsl(" + color + ", 100%, 85%)"; - } - ngOnInit() { - if (this.config.calculation === "summarize") { - let calc = 0; - (this.data as Array).forEach((e) => { - calc += e; - }); - this.result = calc.toString(); - } else if (this.config.calculation === "percentage") { - let calc = - Math.round( - ((this.data.part / this.data.total) * 100 + Number.EPSILON) * 100 - ) / 100; - this.result = calc.toFixed(2) + " %"; - } + this.result = + ( + (this.entity[this.config.actual] / this.entity[this.config.total]) * + 100 + ).toFixed(this.config.numberOfDigits ?? 2) + " %"; } } diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts index 096f4ba026..e0fa58be70 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts @@ -20,19 +20,12 @@ const Template: Story = ( props: args, }); -export const Summarize = Template.bind({}); -Summarize.args = { - data: [10, 5], +export const Primary = Template.bind({}); +Primary.args = { + entity: { allDays: 110, presentDays: 5 }, config: { - properties: [], - calculation: "summarize", - }, -}; - -export const Percentage = Template.bind({}); -Percentage.args = { - data: { total: 110, part: 5 }, - config: { - calculation: "percentage", + actual: "presentDays", + total: "allDays", + numberOfDigits: 1, }, }; From aee81f01272a7cfb6d8045a75b932eb2dec5490d Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:35:47 +0200 Subject: [PATCH 04/19] Added decimalPlaces in display-percentage.component --- .../display-dyanamic-value/display-dynamic-value.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts index e0fa58be70..407e31dc5e 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts @@ -26,6 +26,6 @@ Primary.args = { config: { actual: "presentDays", total: "allDays", - numberOfDigits: 1, + decimalPlaces: 2, }, }; From 456357ce8c229063de6ed90eccedcd8ff2800b39 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:39:20 +0200 Subject: [PATCH 05/19] Added decimalPlaces to display-percentage.component --- .../display-dynamic-value.component.ts | 12 ++++++------ .../display-percentage.component.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts index 28b3879a9b..5b87bfcfdd 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts @@ -1,12 +1,15 @@ import { Component, OnInit } from "@angular/core"; import { ViewDirective } from "../view.directive"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; +import { DisplayPercentageComponent } from "../display-percentage/display-percentage.component"; @DynamicComponent("DisplayDynamicValue") @Component({ selector: "app-display-dynamic-value", - template: "{{ result }}", + template: + "", standalone: true, + imports: [DisplayPercentageComponent], }) export class DisplayDynamicValueComponent extends ViewDirective< @@ -15,13 +18,10 @@ export class DisplayDynamicValueComponent > implements OnInit { - public result: string; + public result: number; ngOnInit() { this.result = - ( - (this.entity[this.config.actual] / this.entity[this.config.total]) * - 100 - ).toFixed(this.config.numberOfDigits ?? 2) + " %"; + (this.entity[this.config.actual] / this.entity[this.config.total]) * 100; } } diff --git a/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts index 8222052de1..9e4e26dce9 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts @@ -1,18 +1,21 @@ import { Component, HostBinding, OnInit } from "@angular/core"; import { ViewDirective } from "../view.directive"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; +import { CommonModule } from "@angular/common"; @DynamicComponent("DisplayPercentage") @Component({ selector: "app-display-percentage", - template: "{{ value ? value + '%' : '-' }}", + template: "{{ value ? (value | number : pipe) + '%' : '-' }}", standalone: true, + imports: [CommonModule], }) export class DisplayPercentageComponent extends ViewDirective implements OnInit { @HostBinding("style") style = {}; + pipe: string; /** * returns a css-compatible color value from green to red using the given @@ -32,6 +35,11 @@ export class DisplayPercentageComponent } ngOnInit() { + this.pipe = + "1." + + (this.config.decimalPlaces + ? this.config.decimalPlaces + "-" + this.config.decimalPlaces + : "0-0"); this.style = { "background-color": DisplayPercentageComponent.fromPercent(this.value), "border-radius": "5%", From b00bd6bfd0e3ad598bcaa4a9b74831fb16a650ee Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:59:23 +0200 Subject: [PATCH 06/19] Renamed display-dynamic-value to display-dynamic-percentage --- ...splay-dynamic-percentage.component.spec.ts | 25 +++++++++++++++ .../display-dynamic-percentage.component.ts} | 6 ++-- .../display-dynamic-percentage.stories.ts | 31 +++++++++++++++++++ .../display-dynamic-value.component.spec.ts | 25 --------------- .../display-dynamic-value.stories.ts | 31 ------------------- .../display-percentage.component.ts | 6 ++-- 6 files changed, 62 insertions(+), 62 deletions(-) create mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts rename src/app/core/entity-components/entity-utils/view-components/{display-dyanamic-value/display-dynamic-value.component.ts => display-dyanamic-percentage/display-dynamic-percentage.component.ts} (84%) create mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.stories.ts delete mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts delete mode 100644 src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts new file mode 100644 index 0000000000..e028f312f5 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DisplayPercentageComponent } from "./display-dynamic-percentage.component"; + +describe("DisplayDynamicPercentageComponent", () => { + let component: DisplayDynamicPercentageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DisplayDynamicPercentageComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DisplayDynamicPercentageComponent); + component = fixture.componentInstance; + component.value = 10; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.ts similarity index 84% rename from src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts rename to src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.ts index 5b87bfcfdd..608b0a6a83 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.component.ts @@ -3,15 +3,15 @@ import { ViewDirective } from "../view.directive"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; import { DisplayPercentageComponent } from "../display-percentage/display-percentage.component"; -@DynamicComponent("DisplayDynamicValue") +@DynamicComponent("DisplayDynamicPercentage") @Component({ - selector: "app-display-dynamic-value", + selector: "app-display-dynamic-percentage", template: "", standalone: true, imports: [DisplayPercentageComponent], }) -export class DisplayDynamicValueComponent +export class DisplayDynamicPercentageComponent extends ViewDirective< number, { total: string; actual: string; numberOfDigits?: number } diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.stories.ts new file mode 100644 index 0000000000..c143118383 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-percentage/display-dynamic-percentage.stories.ts @@ -0,0 +1,31 @@ +import { Meta, Story } from "@storybook/angular/types-6-0"; +import { moduleMetadata } from "@storybook/angular"; +import { StorybookBaseModule } from "../../../../../utils/storybook-base.module"; +import { DisplayDynamicPercentageComponent } from "./display-dynamic-percentage.component"; + +export default { + title: "Core/Entities/Display Properties/DisplayDynamicPercentage", + component: DisplayDynamicPercentageComponent, + decorators: [ + moduleMetadata({ + imports: [StorybookBaseModule, DisplayDynamicPercentageComponent], + providers: [], + }), + ], +} as Meta; + +const Template: Story = ( + args: DisplayDynamicPercentageComponent +) => ({ + props: args, +}); + +export const Primary = Template.bind({}); +Primary.args = { + entity: { allDays: 110, presentDays: 17 }, + config: { + actual: "presentDays", + total: "allDays", + decimalPlaces: 3, + }, +}; diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts deleted file mode 100644 index 66f455035b..0000000000 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { DisplayPercentageComponent } from "./display-dynamic-value.component"; - -describe("DisplayDynamicValueComponent", () => { - let component: DisplayDynamicValueComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DisplayDynamicValueComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DisplayDynamicValueComponent); - component = fixture.componentInstance; - component.value = 10; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts b/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts deleted file mode 100644 index 407e31dc5e..0000000000 --- a/src/app/core/entity-components/entity-utils/view-components/display-dyanamic-value/display-dynamic-value.stories.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Meta, Story } from "@storybook/angular/types-6-0"; -import { moduleMetadata } from "@storybook/angular"; -import { StorybookBaseModule } from "../../../../../utils/storybook-base.module"; -import { DisplayDynamicValueComponent } from "./display-dynamic-value.component"; - -export default { - title: "Core/Entities/Display Properties/DisplayDynamicValue", - component: DisplayDynamicValueComponent, - decorators: [ - moduleMetadata({ - imports: [StorybookBaseModule, DisplayDynamicValueComponent], - providers: [], - }), - ], -} as Meta; - -const Template: Story = ( - args: DisplayDynamicValueComponent -) => ({ - props: args, -}); - -export const Primary = Template.bind({}); -Primary.args = { - entity: { allDays: 110, presentDays: 5 }, - config: { - actual: "presentDays", - total: "allDays", - decimalPlaces: 2, - }, -}; diff --git a/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts b/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts index 9e4e26dce9..fea6faa98c 100644 --- a/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts +++ b/src/app/core/entity-components/entity-utils/view-components/display-percentage/display-percentage.component.ts @@ -6,7 +6,7 @@ import { CommonModule } from "@angular/common"; @DynamicComponent("DisplayPercentage") @Component({ selector: "app-display-percentage", - template: "{{ value ? (value | number : pipe) + '%' : '-' }}", + template: "{{ value ? (value | number : decimalPipe) + '%' : '-' }}", standalone: true, imports: [CommonModule], }) @@ -15,7 +15,7 @@ export class DisplayPercentageComponent implements OnInit { @HostBinding("style") style = {}; - pipe: string; + decimalPipe: string; /** * returns a css-compatible color value from green to red using the given @@ -35,7 +35,7 @@ export class DisplayPercentageComponent } ngOnInit() { - this.pipe = + this.decimalPipe = "1." + (this.config.decimalPlaces ? this.config.decimalPlaces + "-" + this.config.decimalPlaces From a9e78edacd6d290bc664949d9e5866a1791097ae Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 31 Aug 2023 20:33:07 +0200 Subject: [PATCH 07/19] Adoptions to new architecture --- .../display-dynamic-percentage.component.spec.ts | 2 +- .../display-dynamic-percentage.component.ts | 4 ++-- .../display-dynamic-percentage.stories.ts | 15 +++++++-------- .../display-percentage.component.ts | 4 ++-- .../display-percentage.stories.ts | 12 ++++++++++-- src/app/core/core-components.ts | 7 +++++++ src/app/core/core.module.ts | 2 ++ 7 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts index e028f312f5..5622834f4e 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { DisplayPercentageComponent } from "./display-dynamic-percentage.component"; +import { DisplayDynamicPercentageComponent } from "./display-dynamic-percentage.component"; describe("DisplayDynamicPercentageComponent", () => { let component: DisplayDynamicPercentageComponent; diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts index 608b0a6a83..cbe6cbd070 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from "@angular/core"; -import { ViewDirective } from "../view.directive"; -import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; +import { ViewDirective } from "app/core/entity/default-datatype/view.directive"; +import { DynamicComponent } from "app/core/config/dynamic-components/dynamic-component.decorator"; import { DisplayPercentageComponent } from "../display-percentage/display-percentage.component"; @DynamicComponent("DisplayDynamicPercentage") diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.stories.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.stories.ts index c143118383..8189aa7f97 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.stories.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.stories.ts @@ -1,21 +1,20 @@ -import { Meta, Story } from "@storybook/angular/types-6-0"; -import { moduleMetadata } from "@storybook/angular"; -import { StorybookBaseModule } from "../../../../../utils/storybook-base.module"; +import { Meta, StoryFn, applicationConfig } from "@storybook/angular"; +import { StorybookBaseModule } from "app/utils/storybook-base.module"; import { DisplayDynamicPercentageComponent } from "./display-dynamic-percentage.component"; +import { importProvidersFrom } from "@angular/core"; export default { title: "Core/Entities/Display Properties/DisplayDynamicPercentage", component: DisplayDynamicPercentageComponent, decorators: [ - moduleMetadata({ - imports: [StorybookBaseModule, DisplayDynamicPercentageComponent], - providers: [], + applicationConfig({ + providers: [importProvidersFrom(StorybookBaseModule)], }), ], } as Meta; -const Template: Story = ( - args: DisplayDynamicPercentageComponent +const Template: StoryFn = ( + args: DisplayDynamicPercentageComponent, ) => ({ props: args, }); diff --git a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts index b0d33c0d6b..4cbaac1199 100644 --- a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts @@ -1,7 +1,7 @@ import { Component, HostBinding, OnInit } from "@angular/core"; -import { CommonModule } from "@angular/common"; import { ViewDirective } from "../../../entity/default-datatype/view.directive"; import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator"; +import { CommonModule } from "@angular/common"; @DynamicComponent("DisplayPercentage") @Component({ @@ -37,7 +37,7 @@ export class DisplayPercentageComponent ngOnInit() { this.decimalPipe = "1." + - (this.config.decimalPlaces + (this.config && this.config.decimalPlaces ? this.config.decimalPlaces + "-" + this.config.decimalPlaces : "0-0"); this.style = { diff --git a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.stories.ts b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.stories.ts index a1b04a1ced..7e2678e79e 100644 --- a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.stories.ts +++ b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.stories.ts @@ -1,6 +1,11 @@ -import { applicationConfig, Meta, StoryFn } from "@storybook/angular"; -import { DisplayPercentageComponent } from "./display-percentage.component"; +import { + applicationConfig, + Meta, + moduleMetadata, + StoryFn, +} from "@storybook/angular"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; +import { DisplayPercentageComponent } from "./display-percentage.component"; import { importProvidersFrom } from "@angular/core"; export default { @@ -10,6 +15,9 @@ export default { applicationConfig({ providers: [importProvidersFrom(StorybookBaseModule)], }), + moduleMetadata({ + imports: [DisplayPercentageComponent], + }), ], } as Meta; diff --git a/src/app/core/core-components.ts b/src/app/core/core-components.ts index 9ef4c7a6be..16a0541db6 100644 --- a/src/app/core/core-components.ts +++ b/src/app/core/core-components.ts @@ -155,6 +155,13 @@ export const coreComponents: ComponentTuple[] = [ "./basic-datatypes/number/display-percentage/display-percentage.component" ).then((c) => c.DisplayPercentageComponent), ], + [ + "DisplayDynamicPercentage", + () => + import( + "./basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component" + ).then((c) => c.DisplayDynamicPercentageComponent), + ], [ "DisplayUnit", () => diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index a62edf1357..a87e0fc30c 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -18,6 +18,7 @@ import { EntityArrayDatatype } from "./basic-datatypes/entity-array/entity-array import { NumberDatatype } from "./basic-datatypes/number/number.datatype"; import { Entity } from "./entity/model/entity"; import { TimePeriod } from "./entity-details/related-time-period-entities/time-period"; +import { CommonModule } from "@angular/common"; /** * Core module registering basic parts like datatypes and components. @@ -38,6 +39,7 @@ import { TimePeriod } from "./entity-details/related-time-period-entities/time-p { provide: DefaultDatatype, useClass: EntityDatatype, multi: true }, { provide: DefaultDatatype, useClass: EntityArrayDatatype, multi: true }, ], + imports: [CommonModule], }) export class CoreModule { static databaseEntities = [Entity, User, Config, TimePeriod]; From 5a39da4627d4bf1392dc72acd2db3c104e065572 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:06:00 +0200 Subject: [PATCH 08/19] Trying to actually use the display-dynamic-percentage component --- .../activity-attendance-section.component.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts index 6f14756a16..5fc90f0f74 100644 --- a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts +++ b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts @@ -75,6 +75,14 @@ export class ActivityAttendanceSectionComponent implements OnInit { "1.0-0", ), }, + { + id: "display-percentage", + label: "display-percentage", + view: "DisplayPercentage", + additional: { + value: 5, + }, + }, ]; constructor( From c5bf2e459eb9fd9e167906f64e21070b516cc8b9 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:30:25 +0200 Subject: [PATCH 09/19] Showcasing in health-checkup and added tests --- .../activity-attendance-section.component.ts | 8 ----- .../health-checkup.component.ts | 10 ++++++ ...splay-dynamic-percentage.component.spec.ts | 35 +++++++++++++++++-- .../display-dynamic-percentage.component.ts | 11 ++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts index 5fc90f0f74..6f14756a16 100644 --- a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts +++ b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts @@ -75,14 +75,6 @@ export class ActivityAttendanceSectionComponent implements OnInit { "1.0-0", ), }, - { - id: "display-percentage", - label: "display-percentage", - view: "DisplayPercentage", - additional: { - value: 5, - }, - }, ]; constructor( diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts index 1b7f284d14..4495c53066 100644 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts @@ -32,6 +32,16 @@ export class HealthCheckupComponent implements OnInit { tooltip: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, additional: (entity: HealthCheck) => this.getBMI(entity), }, + { + id: "display-percentage", + label: "display-percentage", + view: "DisplayDynamicPercentage", + additional: { + actual: "weight", + total: "height", + decimalPlaces: 0, + }, + }, ], }; @Input() entity: Child; diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts index 5622834f4e..9ec1fa7cbc 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts @@ -1,9 +1,11 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { DisplayDynamicPercentageComponent } from "./display-dynamic-percentage.component"; +import { Entity } from "app/core/entity/model/entity"; -describe("DisplayDynamicPercentageComponent", () => { +fdescribe("DisplayDynamicPercentageComponent", () => { let component: DisplayDynamicPercentageComponent; + let fixture: ComponentFixture; beforeEach(async () => { @@ -12,14 +14,43 @@ describe("DisplayDynamicPercentageComponent", () => { }).compileComponents(); }); - beforeEach(() => { + beforeEach(async () => { fixture = TestBed.createComponent(DisplayDynamicPercentageComponent); + // await TestBed.configureTestingModule({ + // imports: [DisplayDynamicPercentageComponent], + // }).compileComponents(); + component = fixture.componentInstance; component.value = 10; + component.config = { + total: "totalValue", + actual: "actualValue", + }; + component.entity = new Entity(); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); + + it("should display the correct percentage value", async () => { + component.entity["totalValue"] = 200; + component.entity["actualValue"] = 50; + await component.ngOnInit(); + expect(component.result).toEqual(25); + }); + + it("should not display a value if one of the two values is not a number", async () => { + component.entity["totalValue"] = 15; + await component.ngOnInit(); + expect(component.result).toBeUndefined; + }); + + it("should not display a value if totalValue is 0", async () => { + component.entity["totalValue"] = 0; + component.entity["actualValue"] = 15; + await component.ngOnInit(); + expect(component.result).toBeUndefined; + }); }); diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts index cbe6cbd070..485eec5fde 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts @@ -21,7 +21,14 @@ export class DisplayDynamicPercentageComponent public result: number; ngOnInit() { - this.result = - (this.entity[this.config.actual] / this.entity[this.config.total]) * 100; + if ( + Number.isFinite(this.entity[this.config.actual]) && + Number.isFinite(this.entity[this.config.total]) && + this.entity[this.config.total] != 0 + ) { + this.result = + (this.entity[this.config.actual] / this.entity[this.config.total]) * + 100; + } } } From 96d28e1ecb083b14ee48df3744a9765deadd4fb0 Mon Sep 17 00:00:00 2001 From: Christoph Scheuing <47225324+christophscheuing@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:44:11 +0200 Subject: [PATCH 10/19] Cleanup --- ...splay-dynamic-percentage.component.spec.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts index 9ec1fa7cbc..28f10f2375 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { DisplayDynamicPercentageComponent } from "./display-dynamic-percentage.component"; import { Entity } from "app/core/entity/model/entity"; -fdescribe("DisplayDynamicPercentageComponent", () => { +describe("DisplayDynamicPercentageComponent", () => { let component: DisplayDynamicPercentageComponent; let fixture: ComponentFixture; @@ -16,12 +16,7 @@ fdescribe("DisplayDynamicPercentageComponent", () => { beforeEach(async () => { fixture = TestBed.createComponent(DisplayDynamicPercentageComponent); - // await TestBed.configureTestingModule({ - // imports: [DisplayDynamicPercentageComponent], - // }).compileComponents(); - component = fixture.componentInstance; - component.value = 10; component.config = { total: "totalValue", actual: "actualValue", @@ -34,23 +29,23 @@ fdescribe("DisplayDynamicPercentageComponent", () => { expect(component).toBeTruthy(); }); - it("should display the correct percentage value", async () => { + it("should display the correct percentage value", () => { component.entity["totalValue"] = 200; component.entity["actualValue"] = 50; - await component.ngOnInit(); + component.ngOnInit(); expect(component.result).toEqual(25); }); - it("should not display a value if one of the two values is not a number", async () => { + it("should not display a value if one of the two values is not a number", () => { component.entity["totalValue"] = 15; - await component.ngOnInit(); - expect(component.result).toBeUndefined; + component.ngOnInit(); + expect(component.result).toBe(undefined); }); - it("should not display a value if totalValue is 0", async () => { + it("should not display a value if totalValue is 0", () => { component.entity["totalValue"] = 0; component.entity["actualValue"] = 15; - await component.ngOnInit(); - expect(component.result).toBeUndefined; + component.ngOnInit(); + expect(component.result).toBe(undefined); }); }); From 5bc0b083e5ecb5e2e45b1080a8dfd9e569a1a376 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 8 Sep 2023 14:21:15 +0200 Subject: [PATCH 11/19] feat(core): configurable styles for whole platform (#1953) this enables user-configured "white-label" design of the system in custom colors MIGRATION NECESSARY: see PR #1953 closes #1949 --------- Co-authored-by: Sebastian --- package-lock.json | 498 +++++++----------- package.json | 1 + .../roll-call-setup.component.scss | 5 +- .../child-block/child-block.component.html | 11 +- .../child-block/child-block.component.scss | 8 +- .../child-block/child-block.component.spec.ts | 28 +- .../child-block/child-block.component.ts | 12 +- .../notes/model/interaction-type.interface.ts | 3 - src/app/core/alerts/_alert-style-classes.scss | 2 - .../core/analytics/analytics.service.spec.ts | 25 +- src/app/core/analytics/analytics.service.ts | 7 +- .../configurable-enum.datatype.spec.ts | 13 +- .../configurable-enum.directive.ts | 16 + .../configurable-enum-ordering.ts | 2 +- .../configurable-enum.interface.ts | 16 +- .../configurable-enum.service.ts | 3 +- .../configurable-enum/configurable-enum.ts | 4 + src/app/core/config/config-fix.ts | 29 + src/app/core/config/config.service.spec.ts | 8 +- src/app/core/config/config.service.ts | 36 +- src/app/core/config/testing-config-service.ts | 2 + src/app/core/demo-data/demo-data.module.ts | 2 + src/app/core/entity/latest-entity-loader.ts | 52 ++ .../entity/schema/entity-schema.service.ts | 7 +- .../core/import/import/import.component.scss | 2 +- .../core/language/langauge.service.spec.ts | 26 +- .../language-select.component.html | 8 +- .../language-select.component.spec.ts | 12 +- .../language-select.component.ts | 19 +- src/app/core/language/language.service.ts | 31 +- src/app/core/language/languages.ts | 13 + .../permissions/ability/ability.service.ts | 39 +- .../password-form.component.spec.ts | 6 +- .../session/login/login.component.spec.ts | 3 +- .../demo-site-settings-generator.service.ts | 22 + .../site-settings.service.spec.ts | 161 ++++++ .../site-settings/site-settings.service.ts | 161 ++++++ src/app/core/site-settings/site-settings.ts | 66 +++ src/app/core/ui/ui/ui.component.html | 46 +- src/app/core/ui/ui/ui.component.ts | 29 +- .../user-account.component.spec.ts | 9 +- .../user-security.component.spec.ts | 7 +- .../file/couchdb-file.service.spec.ts | 6 + src/app/features/file/couchdb-file.service.ts | 9 +- .../display-img/display-img.component.html | 3 + .../display-img/display-img.component.scss | 7 + .../display-img/display-img.component.spec.ts | 49 ++ .../file/display-img/display-img.component.ts | 39 ++ .../file/edit-photo/edit-photo.component.html | 10 +- .../file/edit-photo/edit-photo.component.scss | 1 - .../file/edit-photo/edit-photo.component.ts | 3 +- src/app/features/file/file.service.ts | 11 +- .../features/file/mock-file.service.spec.ts | 8 +- src/app/features/file/mock-file.service.ts | 4 +- .../matching-entities.component.scss | 1 - .../public-form/public-form.component.spec.ts | 2 +- src/app/utils/storybook-base.module.ts | 5 - src/index.html | 235 +++------ src/styles/mdc_overwrites/mdc_overwrites.scss | 27 + src/styles/styles.scss | 3 + src/styles/themes/ndb-theme.scss | 2 +- src/styles/variables/_colors.scss | 2 + src/styles/variables/_ndb-light-theme.scss | 83 ++- 63 files changed, 1219 insertions(+), 741 deletions(-) create mode 100644 src/app/core/entity/latest-entity-loader.ts create mode 100644 src/app/core/language/languages.ts create mode 100644 src/app/core/site-settings/demo-site-settings-generator.service.ts create mode 100644 src/app/core/site-settings/site-settings.service.spec.ts create mode 100644 src/app/core/site-settings/site-settings.service.ts create mode 100644 src/app/core/site-settings/site-settings.ts create mode 100644 src/app/features/file/display-img/display-img.component.html create mode 100644 src/app/features/file/display-img/display-img.component.scss create mode 100644 src/app/features/file/display-img/display-img.component.spec.ts create mode 100644 src/app/features/file/display-img/display-img.component.ts create mode 100644 src/styles/mdc_overwrites/mdc_overwrites.scss diff --git a/package-lock.json b/package-lock.json index 9d49ba0ff3..8c5f70fd12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^16.2.1", "@angular/router": "^16.2.1", "@angular/service-worker": "^16.2.1", + "@aytek/material-color-picker": "^1.0.4", "@casl/ability": "^6.5.0", "@casl/angular": "^8.2.1", "@faker-js/faker": "^8.0.2", @@ -481,9 +482,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.1.tgz", - "integrity": "sha512-XVabK9fRKJaYPhW5wn8ySL4KL45N5Np+xOssWhLPDRDBdZjl62MExfpvMkamdkos6E1n1IGsy9wSemjnR4WKhg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.2.tgz", + "integrity": "sha512-p0QefudkPGXjq9inZDrtW6WJrDcSeL+Nkc8lxubjg5fLQATKWKpsUBb+u2xEVu8OvWqj8BvrZUDnXYLyTdM4vw==", "dependencies": { "tslib": "^2.3.0" }, @@ -491,7 +492,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.1" + "@angular/core": "16.2.2" } }, "node_modules/@angular/cdk": { @@ -545,9 +546,9 @@ } }, "node_modules/@angular/common": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.1.tgz", - "integrity": "sha512-druackA5JQpvfS8cD8DFtPRXGRKbhx3mQ778t1n6x3fXpIdGaAX+nSAgAKhIoF7fxWmu0KuHGzb+3BFlZRyTXw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.2.tgz", + "integrity": "sha512-2ww8/heDHkfJEBwjakbQeleq610ljcvytNs6ZN1xiXib060xMP+xx17Oa9I3onhi369JsKCHkMR5Qs2U5af1uA==", "dependencies": { "tslib": "^2.3.0" }, @@ -555,14 +556,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.1", + "@angular/core": "16.2.2", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.1.tgz", - "integrity": "sha512-dPauu+ESn79d66U9nBvnunNuBk/UMqnm7iL9Q31J8OKYN/4vrKbsO57pmULOft/GRAYsE3FdLBH0NkocFZKIMQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.2.tgz", + "integrity": "sha512-0X9i5NsqjX++0gmFy0fy2Uc5dHJMxDq6Yu/j1L3RdbvycL1GW+P8GgPfIvD/+v/YiDqpOHQswQXLbkcHw1+svA==", "dependencies": { "tslib": "^2.3.0" }, @@ -570,7 +571,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.1" + "@angular/core": "16.2.2" }, "peerDependenciesMeta": { "@angular/core": { @@ -579,9 +580,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.1.tgz", - "integrity": "sha512-A5SyNZTZnXSCL5JVXHKbYj9p2dRYoeFnb6hGQFt2AuCcpUjVIIdwHtre3YzkKe5sFwepPctdoRe2fRXlTfTRjA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.2.tgz", + "integrity": "sha512-+4i7o0yBc6xSljO8rdYL1G9AiZr2OW5dJAHfPuO21yNhp9BjIJ/TW+Sw1+o/WH4Gnim9adtnonL18UM+vuYeXg==", "dependencies": { "@babel/core": "7.22.5", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -601,7 +602,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.1", + "@angular/compiler": "16.2.2", "typescript": ">=4.9.3 <5.2" } }, @@ -643,9 +644,9 @@ } }, "node_modules/@angular/core": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.1.tgz", - "integrity": "sha512-Y+0jssQnJPovxMv9cDKYlp6BBHeFBLOHd/+FPv5IIGD1c7NwBP/TImJxCaIV78a57xnO8L0SFacDg/kULzvKrg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.2.tgz", + "integrity": "sha512-l6nJlppguroov7eByBIpbxn/mEPcQrL//Ru1TSPzTtXOLR1p41VqPMaeJXj7xYVx7im57YLTDPAjhtLzkUT/Ow==", "dependencies": { "tslib": "^2.3.0" }, @@ -658,9 +659,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.1.tgz", - "integrity": "sha512-cCygiLfBAsVHdtKmNptlk2IgXu0wjRc8kSiiSnJkfK6U/NiNg8ADMiN7iYgKW2TD1ZRw+7dYZV856lxEy2n0+A==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.2.tgz", + "integrity": "sha512-Q3GmOCLSD5BXSjvlLkMsJLXWXb4SO0gA2Aya8JaG1y0doQT/CdGcYXrsCrCT3ot13wqp0HdGQ/ATNd0cNjmz2A==", "dependencies": { "tslib": "^2.3.0" }, @@ -668,16 +669,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.1", - "@angular/core": "16.2.1", - "@angular/platform-browser": "16.2.1", + "@angular/common": "16.2.2", + "@angular/core": "16.2.2", + "@angular/platform-browser": "16.2.2", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.1.tgz", - "integrity": "sha512-IMlDEuDNYtVTZ135ATm+YAksdCaFjkOsrtTPu3aIg08Dsyqw7awZ1lEmmmSpiflOqEfPjgHScLWhUMhER70aUg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.2.tgz", + "integrity": "sha512-6WO8icVzOGAjZd0Zm4mXisg1ljhmB1+UFSjUdHWrXd0QxAKKhHuI2P91v8J+5j1wl27JIKzTVA7+/gnNQMmGsw==", "dependencies": { "@babel/core": "7.22.5", "fast-glob": "3.3.0", @@ -692,8 +693,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.1", - "@angular/compiler-cli": "16.2.1" + "@angular/compiler": "16.2.2", + "@angular/compiler-cli": "16.2.2" } }, "node_modules/@angular/localize/node_modules/@babel/core": { @@ -826,9 +827,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.1.tgz", - "integrity": "sha512-SH8zRiRAcw0B5/tVlEc5U/lN5F8g+JizSuu7BQvpCAQEDkM6IjF9LP36Bjav7JuadItbWLfT6peWYa1sJvax2w==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.2.tgz", + "integrity": "sha512-9RwUiHYCAmEirXqwWL/rPfXHMkU9PnpGinok6tmHF8agAmJs1kMWZedxG0GnreTzpTlBu/dI/4v6VDfR9S/D6Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -836,9 +837,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.2.1", - "@angular/common": "16.2.1", - "@angular/core": "16.2.1" + "@angular/animations": "16.2.2", + "@angular/common": "16.2.2", + "@angular/core": "16.2.2" }, "peerDependenciesMeta": { "@angular/animations": { @@ -847,9 +848,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.1.tgz", - "integrity": "sha512-dKMCSrbD/joOMXM1mhDOKNDZ1BxwO9r9uu5ZxY0L/fWm/ousgMucNikLr38vBudgWM8CN6BuabzkxWKcqi3k4g==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.2.tgz", + "integrity": "sha512-EOGDZ+oABB/aNiBR//wxc6McycjF99/9ds74Q6WoHiNy8CYkzH3plr5pHoy4zkriSyqzoETg2tCu7jSiiMbjRg==", "dependencies": { "tslib": "^2.3.0" }, @@ -857,16 +858,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.1", - "@angular/compiler": "16.2.1", - "@angular/core": "16.2.1", - "@angular/platform-browser": "16.2.1" + "@angular/common": "16.2.2", + "@angular/compiler": "16.2.2", + "@angular/core": "16.2.2", + "@angular/platform-browser": "16.2.2" } }, "node_modules/@angular/router": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.1.tgz", - "integrity": "sha512-C0WfcktsC25G37unxdH/5I7PbkVBSEB1o+0DJK9/HG97r1yzEkptF6fbRIzDBTS7dX0NfWN/PTAKF0ep7YlHvA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.2.tgz", + "integrity": "sha512-r4KMVUVEWqjOZK0ZUsY8jRqscseGvgcigcikvYJwfxPqtCGYY7RoVAFY7HUtmXC0GAv1aIybK5o/MKTLaecD5Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -874,16 +875,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.1", - "@angular/core": "16.2.1", - "@angular/platform-browser": "16.2.1", + "@angular/common": "16.2.2", + "@angular/core": "16.2.2", + "@angular/platform-browser": "16.2.2", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.1.tgz", - "integrity": "sha512-9AYBYQ19aMQN3AoZgpd4T3qmHVM7nHvjqotSATwwWU/+sbcfdaasdJE4mRP3z6cRbIwYHTbNQJl6pJT/2jDWbw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.2.tgz", + "integrity": "sha512-0r7DNY3MCQcSYw+lC7V3fljRA1eNl9RI4OS122s9gBUTR9/24D4WGulGzPtSRs11NfnK7v6+m1YDrNtsh0i4PQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -894,8 +895,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.1", - "@angular/core": "16.2.1" + "@angular/common": "16.2.2", + "@angular/core": "16.2.2" } }, "node_modules/@assemblyscript/loader": { @@ -916,6 +917,11 @@ "x-default-browser": "bin/x-default-browser.js" } }, + "node_modules/@aytek/material-color-picker": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@aytek/material-color-picker/-/material-color-picker-1.0.4.tgz", + "integrity": "sha512-Z22MksLzyf1bQAPftRouF58sOoXH14mlp9iqe9LqmhA8DcCBkPHyyWzzYFq9E9eELNSiMT2Arm/mDwZ8bYy88g==" + }, "node_modules/@babel/code-frame": { "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", @@ -3709,9 +3715,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.7.0.tgz", + "integrity": "sha512-+HencqxU7CFJnQb7IKtuNBqS6Yx3Tz4kOL8BJXo+JyeiBm5MEX6pO8onXDkjrkCRlfYXS1Axro15ZjVFe9YgsA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -4209,9 +4215,9 @@ } }, "node_modules/@jest/schemas": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", - "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -4221,22 +4227,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz", - "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", + "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", + "jest-haste-map": "^29.6.4", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -4305,12 +4311,12 @@ } }, "node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -5647,114 +5653,12 @@ } }, "node_modules/@oasisdigital/angular-typed-forms-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@oasisdigital/angular-typed-forms-helpers/-/angular-typed-forms-helpers-1.3.2.tgz", - "integrity": "sha512-dLATZYh+mspxdYz9UQRXfsctSMlHNjmxUxOoHXYrdxXm7kgSXEbv/gpogtSuwQg26WQM0lNRtixoLvqO7LNwWg==", - "dev": true, - "dependencies": { - "@angular/forms": "^14.0.1" - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/@angular/animations": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.3.0.tgz", - "integrity": "sha512-QoBcIKy1ZiU+4qJsAh5Ls20BupWiXiZzKb0s6L9/dntPt5Msr4Ao289XR2P6O1L+kTsCprH9Kt41zyGQ/bkRqg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/core": "14.3.0" - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/@angular/common": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.3.0.tgz", - "integrity": "sha512-pV9oyG3JhGWeQ+TFB0Qub6a1VZWMNZ6/7zEopvYivdqa5yDLLDSBRWb6P80RuONXyGnM1pa7l5nYopX+r/23GQ==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/core": "14.3.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/@angular/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.3.0.tgz", - "integrity": "sha512-wYiwItc0Uyn4FWZ/OAx/Ubp2/WrD3EgUJ476y1XI7yATGPF8n9Ld5iCXT08HOvc4eBcYlDfh90kTXR6/MfhzdQ==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.11.4 || ~0.12.0" - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/@angular/forms": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.3.0.tgz", - "integrity": "sha512-fBZZC2UFMom2AZPjGQzROPXFWO6kvCsPDKctjJwClVC8PuMrkm+RRyiYRdBbt2qxWHEqOZM2OCQo73xUyZOYHw==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/common": "14.3.0", - "@angular/core": "14.3.0", - "@angular/platform-browser": "14.3.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/@angular/platform-browser": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.3.0.tgz", - "integrity": "sha512-w9Y3740UmTz44T0Egvc+4QV9sEbO61L+aRHbpkLTJdlEGzHByZvxJmJyBYmdqeyTPwc/Zpy7c02frlpfAlyB7A==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0" - }, - "peerDependencies": { - "@angular/animations": "14.3.0", - "@angular/common": "14.3.0", - "@angular/core": "14.3.0" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@oasisdigital/angular-typed-forms-helpers/node_modules/zone.js": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", - "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@oasisdigital/angular-typed-forms-helpers/-/angular-typed-forms-helpers-1.5.0.tgz", + "integrity": "sha512-xI5ScZ1rcYiWtV98H3rTAJs+BTjWpawtJteP5xNrn41bG3SU/w/qc4k2d6Q1zWzPxV+gjnKvX+wq2NP/qcQ0aQ==", "dev": true, - "peer": true, "dependencies": { - "tslib": "^2.3.0" + "@angular/forms": "^16.2.1" } }, "node_modules/@parcel/watcher": { @@ -7214,15 +7118,15 @@ } }, "node_modules/@storybook/angular/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/@storybook/angular/node_modules/@types/react": { - "version": "16.14.45", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.45.tgz", - "integrity": "sha512-XFtKkY3yuPO5VJSE6Lru9yLkVQvYE+l6NbmLp6IWCg4jo5S8Ijbpke8wC9q4NmQ5pJErT8KKboG5eY7n5n718A==", + "version": "16.14.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", + "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -7363,9 +7267,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/@storybook/channels": { @@ -7648,9 +7552,9 @@ } }, "node_modules/@storybook/core-common/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/@storybook/core-common/node_modules/ansi-styles": { @@ -7770,9 +7674,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/@storybook/core-server/node_modules/ansi-styles": { @@ -7845,9 +7749,9 @@ } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/@storybook/csf": { @@ -8801,9 +8705,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", - "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "version": "4.17.36", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz", + "integrity": "sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==", "dev": true, "dependencies": { "@types/node": "*", @@ -8938,9 +8842,9 @@ "dev": true }, "node_modules/@types/mdx": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.6.tgz", - "integrity": "sha512-sVcwEG10aFU2KcM7cIA0M410UPv/DesOPyG8zMVk0QUDexHA3lYmGucpEpZ2dtWWhi2ip3CG+5g/iH0PwoW4Fw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.7.tgz", + "integrity": "sha512-BG4tyr+4amr3WsSEmHn/fXPqaCba/AYZ7dsaQTiavihQunHSIxk+uAtqsjvicNpyHN6cm+B9RVrUOtW9VzIKHw==", "dev": true }, "node_modules/@types/mime": { @@ -8962,9 +8866,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", - "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==" + "version": "20.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.4.tgz", + "integrity": "sha512-Y9vbIAoM31djQZrPYjpTLo0XlaSwOIsrlfE3LpulZeRblttsLQRFRlBAppW0LOxyT3ALj2M5vU1ucQQayQH3jA==" }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -9193,9 +9097,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", - "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", + "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -9213,9 +9117,9 @@ } }, "node_modules/@types/react-dom/node_modules/@types/react": { - "version": "16.14.45", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.45.tgz", - "integrity": "sha512-XFtKkY3yuPO5VJSE6Lru9yLkVQvYE+l6NbmLp6IWCg4jo5S8Ijbpke8wC9q4NmQ5pJErT8KKboG5eY7n5n718A==", + "version": "16.14.46", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", + "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -9351,16 +9255,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.0.tgz", - "integrity": "sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.1.tgz", + "integrity": "sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.4.0", - "@typescript-eslint/type-utils": "6.4.0", - "@typescript-eslint/utils": "6.4.0", - "@typescript-eslint/visitor-keys": "6.4.0", + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/type-utils": "6.4.1", + "@typescript-eslint/utils": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -9386,13 +9290,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.4.0.tgz", - "integrity": "sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.4.1.tgz", + "integrity": "sha512-7ON8M8NXh73SGZ5XvIqWHjgX2f+vvaOarNliGhjrJnv1vdjG0LVIz+ToYfPirOoBi56jxAKLfsLm40+RvxVVXA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.4.0", - "@typescript-eslint/utils": "6.4.0", + "@typescript-eslint/typescript-estree": "6.4.1", + "@typescript-eslint/utils": "6.4.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -9413,17 +9317,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.4.0.tgz", - "integrity": "sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.4.1.tgz", + "integrity": "sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.4.0", - "@typescript-eslint/types": "6.4.0", - "@typescript-eslint/typescript-estree": "6.4.0", + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/typescript-estree": "6.4.1", "semver": "^7.5.4" }, "engines": { @@ -9438,15 +9342,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", - "integrity": "sha512-I1Ah1irl033uxjxO9Xql7+biL3YD7w9IU8zF+xlzD/YxY6a4b7DYA08PXUUCbm2sEljwJF6ERFy2kTGAGcNilg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.1.tgz", + "integrity": "sha512-610G6KHymg9V7EqOaNBMtD1GgpAmGROsmfHJPXNLCU9bfIuLrkdOygltK784F6Crboyd5tBFayPB7Sf0McrQwg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.4.0", - "@typescript-eslint/types": "6.4.0", - "@typescript-eslint/typescript-estree": "6.4.0", - "@typescript-eslint/visitor-keys": "6.4.0", + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/typescript-estree": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", "debug": "^4.3.4" }, "engines": { @@ -9466,13 +9370,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz", - "integrity": "sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.1.tgz", + "integrity": "sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.4.0", - "@typescript-eslint/visitor-keys": "6.4.0" + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9567,9 +9471,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz", - "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.1.tgz", + "integrity": "sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9580,13 +9484,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz", - "integrity": "sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.1.tgz", + "integrity": "sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.4.0", - "@typescript-eslint/visitor-keys": "6.4.0", + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9729,12 +9633,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz", - "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.1.tgz", + "integrity": "sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.4.0", + "@typescript-eslint/types": "6.4.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -12600,9 +12504,9 @@ } }, "node_modules/cypress/node_modules/@types/node": { - "version": "16.18.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.41.tgz", - "integrity": "sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "node_modules/cypress/node_modules/ansi-styles": { @@ -13927,9 +13831,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.496", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.496.tgz", - "integrity": "sha512-qeXC3Zbykq44RCrBa4kr8v/dWzYJA8rAwpyh9Qd+NKWoJfjG5vvJqy9XOJ9H4P/lqulZBCgUWAYi+FeK5AuJ8g==" + "version": "1.4.500", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.500.tgz", + "integrity": "sha512-P38NO8eOuWOKY1sQk5yE0crNtrjgjJj6r3NrbIKtG18KzCHmHE2Bt+aQA7/y0w3uYsHWxDa6icOohzjLJ4vJ4A==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -15342,9 +15246,9 @@ } }, "node_modules/flag-icons": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.10.0.tgz", - "integrity": "sha512-rP44fcHBWwCQasGbZM9QxZJBPFe9vCR3GIuL981BJa3Z0S4Q6I/BNttpbW6iKgTQIHdn96cK3ne1lBVYB/h81Q==" + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.11.0.tgz", + "integrity": "sha512-oK+QhV5UMWq+lmyOnfXUfVhSgHy29gQ0gQpmORdNP6ucAsKzIGm39ncvyThJyvt76Au2qsepDTE9waHRuaYPUw==" }, "node_modules/flat": { "version": "5.0.2", @@ -15375,9 +15279,9 @@ "dev": true }, "node_modules/flow-parser": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.214.0.tgz", - "integrity": "sha512-RW1Dh6BuT14DA7+gtNRKzgzvG3GTPdrceHCi4ddZ9VFGQ9HtO5L8wzxMGsor7XtInIrbWZZCSak0oxnBF7tApw==", + "version": "0.215.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.215.1.tgz", + "integrity": "sha512-qq3rdRToqwesrddyXf+Ml8Tuf7TdoJS+EMbJgC6fHAVoBCXjb4mHelNd3J+jD8ts0bSHX81FG3LN7Qn/dcl6pA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -15712,9 +15616,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -16738,9 +16642,9 @@ "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" }, "node_modules/immutable": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.2.tgz", - "integrity": "sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.3.tgz", + "integrity": "sha512-808ZFYMsIRAjLAu5xkKo0TsbY9LBy9H5MazTKIEHerNkg0ymgilGfBPMR/3G7d/ihGmuK2Hw8S1izY2d3kd3wA==", "dev": true }, "node_modules/import-fresh": { @@ -17691,20 +17595,20 @@ } }, "node_modules/jest-haste-map": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz", - "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", + "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -17716,21 +17620,21 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz", - "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -17794,13 +17698,13 @@ } }, "node_modules/jest-worker": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz", - "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", + "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.6.2", + "jest-util": "^29.6.3", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -19969,9 +19873,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" @@ -19989,9 +19893,9 @@ } }, "node_modules/node-fetch-native": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.2.0.tgz", - "integrity": "sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.4.0.tgz", + "integrity": "sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==", "dev": true }, "node_modules/node-fetch/node_modules/tr46": { @@ -23027,9 +22931,9 @@ "optional": true }, "node_modules/rollup": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz", - "integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==", + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" diff --git a/package.json b/package.json index 66f19d6afc..396cff68ce 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@angular/platform-browser-dynamic": "^16.2.1", "@angular/router": "^16.2.1", "@angular/service-worker": "^16.2.1", + "@aytek/material-color-picker": "^1.0.4", "@casl/ability": "^6.5.0", "@casl/angular": "^8.2.1", "@faker-js/faker": "^8.0.2", diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.scss b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.scss index 6711416e6e..acfcd9cce9 100644 --- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.scss +++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.scss @@ -1,7 +1,6 @@ @use "src/styles/mixins/grid-layout"; @use "src/styles/variables/sizes"; -@use "@angular/material" as mat; -@use "src/styles/variables/ndb-light-theme" as theme; +@use "src/styles/variables/colors"; .top-control { position: sticky; @@ -19,7 +18,7 @@ padding-right: sizes.$margin-main-view-right; margin-top: - sizes.$margin-main-view-top; padding-top: sizes.$margin-main-view-top; - background-color: mat.get-color-from-palette(theme.$primary, 50); + background-color: colors.$background; } .cards-list { diff --git a/src/app/child-dev-project/children/child-block/child-block.component.html b/src/app/child-dev-project/children/child-block/child-block.component.html index 64c7e26b7a..8b3c1815f1 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.html +++ b/src/app/child-dev-project/children/child-block/child-block.component.html @@ -5,16 +5,9 @@ [class.inactive]="!entity.isActive" class="truncate-text container" > - - + {{ entity?.toString() }} - - ({{ entity?.projectNumber }}) + ({{ entity?.projectNumber }}) diff --git a/src/app/child-dev-project/children/child-block/child-block.component.scss b/src/app/child-dev-project/children/child-block/child-block.component.scss index 175380e9bd..a77a266b19 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.scss +++ b/src/app/child-dev-project/children/child-block/child-block.component.scss @@ -7,13 +7,7 @@ object-fit: cover; margin-right: 4px; vertical-align: middle; -} - -.child-pic-large { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; + overflow: hidden; } .inactive { diff --git a/src/app/child-dev-project/children/child-block/child-block.component.spec.ts b/src/app/child-dev-project/children/child-block/child-block.component.spec.ts index e215c2942c..f847898ea8 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.spec.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.spec.ts @@ -3,29 +3,25 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { ChildBlockComponent } from "./child-block.component"; import { ChildrenService } from "../children.service"; import { Child } from "../model/child"; -import { FileService } from "app/features/file/file.service"; -import { of } from "rxjs"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { FileService } from "../../../features/file/file.service"; describe("ChildBlockComponent", () => { let component: ChildBlockComponent; let fixture: ComponentFixture; let mockChildrenService: jasmine.SpyObj; - let mockFileService: jasmine.SpyObj; beforeEach(waitForAsync(() => { mockChildrenService = jasmine.createSpyObj("mockChildrenService", [ "getChild", ]); mockChildrenService.getChild.and.resolveTo(new Child("")); - mockFileService = jasmine.createSpyObj(["loadFile"]); - mockFileService.loadFile.and.returnValue(of("success")); TestBed.configureTestingModule({ imports: [ChildBlockComponent, FontAwesomeTestingModule], providers: [ { provide: ChildrenService, useValue: mockChildrenService }, - { provide: FileService, useValue: mockFileService }, + { provide: FileService, useValue: undefined }, ], }).compileComponents(); })); @@ -40,24 +36,4 @@ describe("ChildBlockComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); - - it("should reset picture if child has none", async () => { - const withPicture = new Child(); - withPicture.photo = "some-picture"; - component.entity = withPicture; - - await component.ngOnChanges({ entity: undefined }); - - expect(mockFileService.loadFile).toHaveBeenCalled(); - expect(component.imgPath).toBeDefined(); - - mockFileService.loadFile.calls.reset(); - // without picture - component.entity = new Child(); - - await component.ngOnChanges({ entity: undefined }); - - expect(mockFileService.loadFile).not.toHaveBeenCalled(); - expect(component.imgPath).toBeUndefined(); - }); }); diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index de1874f283..90eb5b97f1 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -11,9 +11,8 @@ import { DynamicComponent } from "../../../core/config/dynamic-components/dynami import { NgIf } from "@angular/common"; import { TemplateTooltipDirective } from "../../../core/common-components/template-tooltip/template-tooltip.directive"; import { ChildBlockTooltipComponent } from "./child-block-tooltip/child-block-tooltip.component"; -import { SafeUrl } from "@angular/platform-browser"; import { FileService } from "../../../features/file/file.service"; -import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynamic-icon/fa-dynamic-icon.component"; +import { DisplayImgComponent } from "../../../features/file/display-img/display-img.component"; @DynamicComponent("ChildBlock") @Component({ @@ -24,7 +23,7 @@ import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynam NgIf, TemplateTooltipDirective, ChildBlockTooltipComponent, - FaDynamicIconComponent, + DisplayImgComponent, ], standalone: true, }) @@ -38,7 +37,6 @@ export class ChildBlockComponent implements OnChanges { /** prevent additional details to be displayed in a tooltip on mouse over */ @Input() tooltipDisabled: boolean; - imgPath: SafeUrl; icon = Child.icon; constructor( @@ -50,11 +48,5 @@ export class ChildBlockComponent implements OnChanges { if (changes.hasOwnProperty("entityId")) { this.entity = await this.childrenService.getChild(this.entityId); } - this.imgPath = undefined; - if (this.entity?.photo) { - this.fileService - .loadFile(this.entity, "photo") - .subscribe((res) => (this.imgPath = res)); - } } } diff --git a/src/app/child-dev-project/notes/model/interaction-type.interface.ts b/src/app/child-dev-project/notes/model/interaction-type.interface.ts index dcf0f3eb03..c1745fdeb7 100644 --- a/src/app/child-dev-project/notes/model/interaction-type.interface.ts +++ b/src/app/child-dev-project/notes/model/interaction-type.interface.ts @@ -5,9 +5,6 @@ import { ConfigurableEnumValue } from "../../../core/basic-datatypes/configurabl * providing special context for {@link Note} categories. */ export interface InteractionType extends ConfigurableEnumValue { - /** color highlighting the individual category */ - color?: string; - /** whether the Note is a group type category that stores attendance details for each related person */ isMeeting?: boolean; } diff --git a/src/app/core/alerts/_alert-style-classes.scss b/src/app/core/alerts/_alert-style-classes.scss index 791083771b..9a2e7d314f 100644 --- a/src/app/core/alerts/_alert-style-classes.scss +++ b/src/app/core/alerts/_alert-style-classes.scss @@ -1,5 +1,3 @@ -@use "@angular/material" as mat; -@use "src/styles/variables/ndb-light-theme" as theme; @use "src/styles/variables/colors"; .ndb-alert { diff --git a/src/app/core/analytics/analytics.service.spec.ts b/src/app/core/analytics/analytics.service.spec.ts index d5bbc721df..97dbca34ec 100644 --- a/src/app/core/analytics/analytics.service.spec.ts +++ b/src/app/core/analytics/analytics.service.spec.ts @@ -1,12 +1,17 @@ import { TestBed } from "@angular/core/testing"; import { AnalyticsService } from "./analytics.service"; -import { Angulartics2Matomo, Angulartics2Module } from "angulartics2"; +import { + Angulartics2, + Angulartics2Matomo, + Angulartics2Module, +} from "angulartics2"; import { RouterTestingModule } from "@angular/router/testing"; import { ConfigService } from "../config/config.service"; import { UsageAnalyticsConfig } from "./usage-analytics-config"; import { Subject } from "rxjs"; import { Config } from "../config/config"; +import { SiteSettingsService } from "../site-settings/site-settings.service"; describe("AnalyticsService", () => { let service: AnalyticsService; @@ -14,6 +19,8 @@ describe("AnalyticsService", () => { let mockConfigService: jasmine.SpyObj; const configUpdates = new Subject(); let mockMatomo: jasmine.SpyObj; + let mockAngulartics: jasmine.SpyObj; + let siteNameSubject = new Subject(); beforeEach(() => { mockConfigService = jasmine.createSpyObj( @@ -25,6 +32,9 @@ describe("AnalyticsService", () => { "setUsername", "startTracking", ]); + mockAngulartics = jasmine.createSpyObj([], { + setUserProperties: { next: jasmine.createSpy() }, + }); TestBed.configureTestingModule({ imports: [Angulartics2Module.forRoot(), RouterTestingModule], @@ -32,6 +42,11 @@ describe("AnalyticsService", () => { AnalyticsService, { provide: ConfigService, useValue: mockConfigService }, { provide: Angulartics2Matomo, useValue: mockMatomo }, + { provide: Angulartics2, useValue: mockAngulartics }, + { + provide: SiteSettingsService, + useValue: { siteName: siteNameSubject }, + }, ], }); service = TestBed.inject(AnalyticsService); @@ -108,4 +123,12 @@ describe("AnalyticsService", () => { testAnalyticsConfig2.url + "matomo.php", ]); }); + + it("should set the hostname as the organisation", () => { + service.init(); + + expect(mockAngulartics.setUserProperties.next).toHaveBeenCalledWith({ + dimension2: location.hostname, + }); + }); }); diff --git a/src/app/core/analytics/analytics.service.ts b/src/app/core/analytics/analytics.service.ts index e6977279b0..9642301b7c 100644 --- a/src/app/core/analytics/analytics.service.ts +++ b/src/app/core/analytics/analytics.service.ts @@ -7,7 +7,6 @@ import { } from "./usage-analytics-config"; import { Angulartics2, Angulartics2Matomo } from "angulartics2"; import md5 from "md5"; -import { UiConfig } from "../ui/ui-config"; /** * Track usage analytics data and report it to a backend server like Matomo. @@ -48,6 +47,7 @@ export class AnalyticsService { window["_paq"].push(["trackPageView"]); window["_paq"].push(["enableLinkTracking"]); this.setVersion(); + this.setOrganization(location.hostname); this.setUser(undefined); this.configService.configUpdates.subscribe(() => this.setConfigValues()); } @@ -97,11 +97,6 @@ export class AnalyticsService { if (site_id) { window["_paq"].push(["setSiteId", site_id]); } - const { site_name } = - this.configService.getConfig("appConfig") || {}; - if (site_name) { - this.setOrganization(site_name); - } } /** diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype.spec.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype.spec.ts index 044e20493b..302041dd51 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype.spec.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype.spec.ts @@ -32,7 +32,12 @@ describe("Schema data type: configurable-enum", () => { const TEST_CONFIG: ConfigurableEnumConfig = [ { id: "NONE", label: "" }, { id: "TEST_1", label: "Category 1" }, - { id: "TEST_3", label: "Category 3", color: "#FFFFFF", isMeeting: true }, + { + id: "TEST_3", + label: "Category 3", + color: "#FFFFFF", + isMeeting: true, + } as ConfigurableEnumValue, ]; @DatabaseEntity("ConfigurableEnumDatatypeTestEntity") @@ -53,7 +58,11 @@ describe("Schema data type: configurable-enum", () => { let enumService: jasmine.SpyObj; beforeEach(waitForAsync(() => { - enumService = jasmine.createSpyObj(["getEnumValues", "preLoadEnums"]); + enumService = jasmine.createSpyObj([ + "getEnumValues", + "preLoadEnums", + "cacheEnum", + ]); enumService.getEnumValues.and.returnValue(TEST_CONFIG); TestBed.configureTestingModule({ diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts index eceb624ed5..c1855eee14 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-directive/configurable-enum.directive.ts @@ -1,5 +1,6 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core"; import { ConfigurableEnumService } from "../configurable-enum.service"; +import { ConfigurableEnumValue } from "../configurable-enum.interface"; /** * Enumerate over all {@link ConfigurableEnumConfig} values for the given enum config id. @@ -36,4 +37,19 @@ export class ConfigurableEnumDirective { private viewContainerRef: ViewContainerRef, private enumService: ConfigurableEnumService, ) {} + + /** + * Make sure the template checker knows the type of the context with which the + * template of this directive will be rendered + * See {@link https://angular.io/guide/structural-directives#typing-the-directives-context} + * @param directive + * @param context + */ + + static ngTemplateContextGuard( + directive: ConfigurableEnumDirective, + context: unknown, + ): context is { $implicit: ConfigurableEnumValue } { + return true; + } } diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-ordering.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-ordering.ts index 4ee1059349..143d001060 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum-ordering.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum-ordering.ts @@ -19,7 +19,7 @@ export namespace Ordering { * of 'greater' or 'less' than is dependent on the concrete enum. */ export interface HasOrdinal { - _ordinal: number; + _ordinal?: number; } export function hasOrdinalValue(value: any): value is HasOrdinal { diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.interface.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.interface.ts index c71f0fcd04..45fb86e8cc 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.interface.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.interface.ts @@ -1,3 +1,6 @@ +import { Ordering } from "./configurable-enum-ordering"; +import HasOrdinal = Ordering.HasOrdinal; + /** * Interface specifying overall object representing an enum with all its options * as stored in the config database @@ -10,7 +13,7 @@ export type ConfigurableEnumConfig< * Mandatory properties of each option of an configurable enum * the actual object can contain additional properties in the specific context of that enum (e.g. a `color` property) */ -export interface ConfigurableEnumValue { +export interface ConfigurableEnumValue extends HasOrdinal { /** * identifier that is unique among all values of the same enum and does not change even when label or other things are edited */ @@ -33,19 +36,12 @@ export interface ConfigurableEnumValue { isInvalidOption?: boolean; /** - * Optionally any number of additional properties specific to a certain enum collection. + * optional styling class that should be applied when displaying this value */ - [x: string]: any; + style?: string; } export const EMPTY: ConfigurableEnumValue = { id: "", label: "", }; - -/** - * The prefix of all enum collection entries in the config database. - * - * This prefix is concatenated with the individual enum collection's id, resulting in the full config object id. - */ -export const CONFIGURABLE_ENUM_CONFIG_PREFIX = "enum:"; diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts index faf34cd5b3..0d117ea76c 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.service.ts @@ -7,7 +7,7 @@ import { EntityAbility } from "../../permissions/ability/entity-ability"; @Injectable({ providedIn: "root" }) export class ConfigurableEnumService { - private enums: Map; + private enums = new Map(); constructor( private entityMapper: EntityMapperService, @@ -20,7 +20,6 @@ export class ConfigurableEnumService { async preLoadEnums() { const allEnums = await this.entityMapper.loadType(ConfigurableEnum); - this.enums = new Map(); allEnums.forEach((entity) => this.cacheEnum(entity)); } diff --git a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.ts b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.ts index 032cfb134f..2a8704f1a3 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configurable-enum.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configurable-enum.ts @@ -6,4 +6,8 @@ import { DatabaseField } from "../../entity/database-field.decorator"; @DatabaseEntity("ConfigurableEnum") export class ConfigurableEnum extends Entity { @DatabaseField() values: ConfigurableEnumValue[] = []; + constructor(id?: string, values: ConfigurableEnumValue[] = []) { + super(id); + this.values = values; + } } diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 3d531283cd..b179821a6d 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -54,6 +54,11 @@ export const defaultJsonConfig = { "icon": "wrench", "link": "/admin" }, + { + "name": $localize`:Menu item:Site settings`, + "icon": "wrench", + "link": "/site-settings/global" + }, { "name": $localize`:Menu item:Import`, "icon": "file-import", @@ -273,6 +278,30 @@ export const defaultJsonConfig = { } ] }, + "view:site-settings/:id": { + "component": "EntityDetails", + "config": { + "entity": "SiteSettings", + "panels": [ + { + "title": $localize`Site Settings`, + "components": [ + { + "component": "Form", + "config": { + "cols": [ + ["logo", "favicon"], + ["siteName", "defaultLanguage", "displayLanguageSelect"], + ["primary", "secondary", "error", "font"], + ] + } + } + ] + } + ] + }, + "permittedUserRoles": ["admin_app"] + }, "view:admin": { "component": "Admin", "permittedUserRoles": ["admin_app"] diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 3ca4657613..1c7d3384e8 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -30,7 +30,7 @@ describe("ConfigService", () => { testConfig.data = { testKey: "testValue" }; entityMapper.load.and.resolveTo(testConfig); - service.loadConfig(); + service.loadOnce(); expect(entityMapper.load).toHaveBeenCalled(); tick(); expect(service.getConfig("testKey")).toEqual("testValue"); @@ -40,7 +40,7 @@ describe("ConfigService", () => { entityMapper.load.and.rejectWith("No config found"); const configLoaded = firstValueFrom(service.configUpdates); - service.loadConfig(); + service.loadOnce(); tick(); expect(() => service.getConfig("testKey")).toThrowError(); @@ -61,7 +61,7 @@ describe("ConfigService", () => { "test:2": { name: "second" }, }; entityMapper.load.and.resolveTo(testConfig); - service.loadConfig(); + service.loadOnce(); tick(); const result = service.getAllConfigs("test:"); expect(result).toHaveSize(2); @@ -74,7 +74,7 @@ describe("ConfigService", () => { const testConfig = new Config(); testConfig.data = { first: "correct", second: "wrong" }; entityMapper.load.and.resolveTo(testConfig); - service.loadConfig(); + service.loadOnce(); tick(); const result = service.getConfig("first"); expect(result).toBe("correct"); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 5007da2bfa..c30c1b88fc 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -1,45 +1,29 @@ import { Injectable } from "@angular/core"; import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service"; import { Config } from "./config"; -import { Observable, ReplaySubject } from "rxjs"; -import { filter } from "rxjs/operators"; +import { LoggingService } from "../logging/logging.service"; +import { LatestEntityLoader } from "../entity/latest-entity-loader"; +import { shareReplay } from "rxjs/operators"; /** * Access dynamic app configuration retrieved from the database * that defines how the interface and data models should look. */ @Injectable({ providedIn: "root" }) -export class ConfigService { +export class ConfigService extends LatestEntityLoader { /** * Subscribe to receive the current config and get notified whenever the config is updated. */ - private _configUpdates = new ReplaySubject(1); private currentConfig: Config; - get configUpdates(): Observable { - return this._configUpdates.asObservable(); - } - - constructor(private entityMapper: EntityMapperService) { - this.loadConfig(); - this.entityMapper - .receiveUpdates(Config) - .pipe(filter(({ entity }) => entity.getId() === Config.CONFIG_KEY)) - .subscribe(({ entity }) => this.updateConfigIfChanged(entity)); - } + configUpdates = this.entityUpdated.pipe(shareReplay(1)); - async loadConfig(): Promise { - return this.entityMapper - .load(Config, Config.CONFIG_KEY) - .then((config) => this.updateConfigIfChanged(config)) - .catch(() => {}); - } - - private updateConfigIfChanged(config: Config) { - if (!this.currentConfig || config._rev !== this.currentConfig?._rev) { + constructor(entityMapper: EntityMapperService, logger: LoggingService) { + super(Config, Config.CONFIG_KEY, entityMapper, logger); + super.startLoading(); + this.entityUpdated.subscribe(async (config) => { this.currentConfig = config; - this._configUpdates.next(config); - } + }); } public saveConfig(config: any): Promise { diff --git a/src/app/core/config/testing-config-service.ts b/src/app/core/config/testing-config-service.ts index 5dcac09537..d119c65d75 100644 --- a/src/app/core/config/testing-config-service.ts +++ b/src/app/core/config/testing-config-service.ts @@ -1,5 +1,6 @@ import { defaultJsonConfig } from "./config-fix"; import { mockEntityMapper } from "../entity/entity-mapper/mock-entity-mapper-service"; +import { LoggingService } from "../logging/logging.service"; import { Config } from "./config"; import { ConfigService } from "./config.service"; @@ -8,5 +9,6 @@ export function createTestingConfigService( ): ConfigService { return new ConfigService( mockEntityMapper([new Config(Config.CONFIG_KEY, configsObject)]), + new LoggingService(), ); } diff --git a/src/app/core/demo-data/demo-data.module.ts b/src/app/core/demo-data/demo-data.module.ts index fd5c7e15a4..f45c7da74b 100644 --- a/src/app/core/demo-data/demo-data.module.ts +++ b/src/app/core/demo-data/demo-data.module.ts @@ -38,10 +38,12 @@ import { DemoPermissionGeneratorService } from "../permissions/demo-permission-g import { DemoTodoGeneratorService } from "../../features/todos/model/demo-todo-generator.service"; import { DemoConfigurableEnumGeneratorService } from "../basic-datatypes/configurable-enum/demo-configurable-enum-generator.service"; import { DemoPublicFormGeneratorService } from "../../features/public-form/demo-public-form-generator.service"; +import { DemoSiteSettingsGeneratorService } from "../site-settings/demo-site-settings-generator.service"; const demoDataGeneratorProviders = [ ...DemoConfigGeneratorService.provider(), ...DemoPermissionGeneratorService.provider(), + ...DemoSiteSettingsGeneratorService.provider(), ...DemoPublicFormGeneratorService.provider(), ...DemoUserGeneratorService.provider(), ...DemoConfigurableEnumGeneratorService.provider(), diff --git a/src/app/core/entity/latest-entity-loader.ts b/src/app/core/entity/latest-entity-loader.ts new file mode 100644 index 0000000000..18755a262f --- /dev/null +++ b/src/app/core/entity/latest-entity-loader.ts @@ -0,0 +1,52 @@ +import { EntityMapperService } from "./entity-mapper/entity-mapper.service"; +import { LoggingService } from "../logging/logging.service"; +import { filter } from "rxjs/operators"; +import { Entity, EntityConstructor } from "./model/entity"; +import { HttpStatusCode } from "@angular/common/http"; +import { Subject } from "rxjs"; + +/** + * Implement an Angular Service extending this base class + * when you need to work with continuous updates of a specific entity from the database. + * (e.g. SiteSettings & SiteSettingsService) + */ +export abstract class LatestEntityLoader { + /** subscribe to this and execute any actions required when the entity changes */ + entityUpdated = new Subject(); + + protected constructor( + private entityCtor: EntityConstructor, + private entityID: string, + protected entityMapper: EntityMapperService, + protected logger: LoggingService, + ) {} + + /** + * Initialize the loader to make the entity available and emit continuous updates + * through the `entityUpdated` property + */ + startLoading() { + this.loadOnce(); + this.entityMapper + .receiveUpdates(this.entityCtor) + .pipe(filter(({ entity }) => entity.getId() === this.entityID)) + .subscribe(({ entity }) => this.entityUpdated.next(entity)); + } + + /** + * Do an initial load of the entity to be available through the `entityUpdated` property + * (without watching for continuous updates). + */ + loadOnce() { + return this.entityMapper + .load(this.entityCtor, this.entityID) + .then((entity) => this.entityUpdated.next(entity)) + .catch((err) => { + if (err?.status !== HttpStatusCode.NotFound) { + this.logger.error( + `Loading entity "${this.entityCtor.ENTITY_TYPE}:${this.entityID}" failed: ${err} `, + ); + } + }); + } +} diff --git a/src/app/core/entity/schema/entity-schema.service.ts b/src/app/core/entity/schema/entity-schema.service.ts index 4161dbc1ed..5b04b8e23c 100644 --- a/src/app/core/entity/schema/entity-schema.service.ts +++ b/src/app/core/entity/schema/entity-schema.service.ts @@ -94,7 +94,10 @@ export class EntitySchemaService { * @param data The database object that will be transformed to the given entity format * @param schema A schema defining the transformation */ - public transformDatabaseToEntityFormat(data: any, schema: EntitySchema) { + public transformDatabaseToEntityFormat( + data: any, + schema: EntitySchema, + ): T { const transformed = {}; for (const key of schema.keys()) { const schemaField: EntitySchemaField = schema.get(key); @@ -113,7 +116,7 @@ export class EntitySchemaService { } } - return transformed; + return transformed as T; } /** diff --git a/src/app/core/import/import/import.component.scss b/src/app/core/import/import/import.component.scss index 20b6658776..7b3d1aab8a 100644 --- a/src/app/core/import/import/import.component.scss +++ b/src/app/core/import/import/import.component.scss @@ -35,7 +35,7 @@ mat-stepper { position: sticky; top: 0; z-index: 100; - background-color: #fff3e0; + background-color: colors.$background; padding-top: sizes.$x-small; padding-bottom: sizes.$regular; diff --git a/src/app/core/language/langauge.service.spec.ts b/src/app/core/language/langauge.service.spec.ts index cdfd554511..4eb6369237 100644 --- a/src/app/core/language/langauge.service.spec.ts +++ b/src/app/core/language/langauge.service.spec.ts @@ -2,30 +2,32 @@ import { TestBed } from "@angular/core/testing"; import { LanguageService } from "./language.service"; import { LOCALE_ID } from "@angular/core"; -import { ConfigService } from "../config/config.service"; import { WINDOW_TOKEN } from "../../utils/di-tokens"; import { LANGUAGE_LOCAL_STORAGE_KEY } from "./language-statics"; -import { of } from "rxjs"; +import { Subject } from "rxjs"; +import { SiteSettingsService } from "../site-settings/site-settings.service"; +import { ConfigurableEnumValue } from "../basic-datatypes/configurable-enum/configurable-enum.interface"; -describe("TranslationServiceService", () => { +describe("LanguageService", () => { let service: LanguageService; - let mockConfigService: jasmine.SpyObj; let reloadSpy: jasmine.Spy; + let languageSubject: Subject; beforeEach(() => { - mockConfigService = jasmine.createSpyObj("ConfigService", ["getConfig"], { - configUpdates: of({}), - }); reloadSpy = jasmine.createSpy(); const mockWindow: Partial = { localStorage: window.localStorage, location: { reload: reloadSpy } as any, }; + languageSubject = new Subject(); TestBed.configureTestingModule({ providers: [ { provide: LOCALE_ID, useValue: "en-US" }, - { provide: ConfigService, useValue: mockConfigService }, { provide: WINDOW_TOKEN, useValue: mockWindow }, + { + provide: SiteSettingsService, + useValue: { defaultLanguage: languageSubject }, + }, ], }); service = TestBed.inject(LanguageService); @@ -43,24 +45,24 @@ describe("TranslationServiceService", () => { expect(service.currentRegionCode()).toBe("us"); }); - it("should set use the default locale if no locale is set", () => { - mockConfigService.getConfig.and.returnValue({ default_language: "de" }); + it("should use the default locale if no locale is set", () => { service.initDefaultLanguage(); + languageSubject.next({ id: "de", label: "de" }); expect(window.localStorage.getItem(LANGUAGE_LOCAL_STORAGE_KEY)).toBe("de"); expect(reloadSpy).toHaveBeenCalled(); }); it("should not change language if a different locale is set", () => { window.localStorage.setItem(LANGUAGE_LOCAL_STORAGE_KEY, "fr"); - mockConfigService.getConfig.and.returnValue({ default_language: "de" }); service.initDefaultLanguage(); + languageSubject.next({ id: "de", label: "de" }); expect(window.localStorage.getItem(LANGUAGE_LOCAL_STORAGE_KEY)).toBe("fr"); expect(reloadSpy).not.toHaveBeenCalled(); }); it("should not reload, if the current locale is the same as the default", () => { - mockConfigService.getConfig.and.returnValue({ default_language: "en-US" }); service.initDefaultLanguage(); + languageSubject.next({ id: "en-US", label: "us" }); expect(reloadSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/core/language/language-select/language-select.component.html b/src/app/core/language/language-select/language-select.component.html index d0736808c3..4a7fe60653 100644 --- a/src/app/core/language/language-select/language-select.component.html +++ b/src/app/core/language/language-select/language-select.component.html @@ -4,11 +4,11 @@ - - {{ lang.regionCode }} + + {{ lang.label }} diff --git a/src/app/core/language/language-select/language-select.component.spec.ts b/src/app/core/language/language-select/language-select.component.spec.ts index 95830cfc94..e3bf917135 100644 --- a/src/app/core/language/language-select/language-select.component.spec.ts +++ b/src/app/core/language/language-select/language-select.component.spec.ts @@ -5,16 +5,18 @@ import { RouterTestingModule } from "@angular/router/testing"; import { LANGUAGE_LOCAL_STORAGE_KEY } from "../language-statics"; import { LOCATION_TOKEN } from "../../../utils/di-tokens"; import { LanguageService } from "../language.service"; +import { ConfigurableEnumService } from "../../basic-datatypes/configurable-enum/configurable-enum.service"; +import { availableLocales } from "../languages"; describe("LanguageSelectComponent", () => { let component: LanguageSelectComponent; let fixture: ComponentFixture; let mockLocation: jasmine.SpyObj; - let mockTranslationService: jasmine.SpyObj; + let mockLanguageService: jasmine.SpyObj; beforeEach(async () => { mockLocation = jasmine.createSpyObj("Location", ["reload"]); - mockTranslationService = jasmine.createSpyObj("LanguageService", [ + mockLanguageService = jasmine.createSpyObj("LanguageService", [ "currentRegionCode", "initDefaultLanguage", ]); @@ -22,7 +24,11 @@ describe("LanguageSelectComponent", () => { imports: [LanguageSelectComponent, RouterTestingModule], providers: [ { provide: LOCATION_TOKEN, useValue: mockLocation }, - { provide: LanguageService, useValue: mockTranslationService }, + { provide: LanguageService, useValue: mockLanguageService }, + { + provide: ConfigurableEnumService, + useValue: { getEnumValues: () => availableLocales.values }, + }, ], }).compileComponents(); }); diff --git a/src/app/core/language/language-select/language-select.component.ts b/src/app/core/language/language-select/language-select.component.ts index a25fe51094..36f02929eb 100644 --- a/src/app/core/language/language-select/language-select.component.ts +++ b/src/app/core/language/language-select/language-select.component.ts @@ -6,6 +6,8 @@ import { MatButtonModule } from "@angular/material/button"; import { MatIconModule } from "@angular/material/icon"; import { MatMenuModule } from "@angular/material/menu"; import { NgForOf } from "@angular/common"; +import { LOCALE_ENUM_ID } from "../languages"; +import { ConfigurableEnumDirective } from "../../basic-datatypes/configurable-enum/configurable-enum-directive/configurable-enum.directive"; /** * Shows a dropdown-menu of available languages @@ -14,22 +16,27 @@ import { NgForOf } from "@angular/common"; selector: "app-language-select", templateUrl: "./language-select.component.html", styleUrls: ["./language-select.component.scss"], - imports: [MatButtonModule, MatIconModule, MatMenuModule, NgForOf], + imports: [ + MatButtonModule, + MatIconModule, + MatMenuModule, + NgForOf, + ConfigurableEnumDirective, + ], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) export class LanguageSelectComponent { + localeEnumId = LOCALE_ENUM_ID; /** * The region code of the currently selected language/region */ - siteRegionCode: string; + siteRegionCode = this.translationService.currentRegionCode(); constructor( - public translationService: LanguageService, + private translationService: LanguageService, @Inject(LOCATION_TOKEN) private location: Location, - ) { - this.siteRegionCode = translationService.currentRegionCode(); - } + ) {} changeLocale(lang: string) { localStorage.setItem(LANGUAGE_LOCAL_STORAGE_KEY, lang); diff --git a/src/app/core/language/language.service.ts b/src/app/core/language/language.service.ts index 70dcaccfdf..8f48f57626 100644 --- a/src/app/core/language/language.service.ts +++ b/src/app/core/language/language.service.ts @@ -1,33 +1,19 @@ import { Inject, Injectable, LOCALE_ID } from "@angular/core"; import { LANGUAGE_LOCAL_STORAGE_KEY } from "./language-statics"; -import { UiConfig } from "../ui/ui-config"; -import { ConfigService } from "../config/config.service"; import { WINDOW_TOKEN } from "../../utils/di-tokens"; +import { SiteSettingsService } from "../site-settings/site-settings.service"; /** - * Service that contains - *
  • The currently selected language - *
  • All available languages + * Service that provides the currently active locale and applies a newly selected one. */ @Injectable({ providedIn: "root", }) export class LanguageService { - /** - * A readonly array of all locales available - * TODO: Hardcoded - */ - readonly availableLocales: { locale: string; regionCode: string }[] = [ - { locale: "de", regionCode: "de" }, - { locale: "en-US", regionCode: "us" }, - { locale: "fr", regionCode: "fr" }, - { locale: "it", regionCode: "it" }, - ]; - constructor( @Inject(LOCALE_ID) private baseLocale: string, @Inject(WINDOW_TOKEN) private window: Window, - private configService: ConfigService, + private siteSettings: SiteSettingsService, ) {} initDefaultLanguage(): void { @@ -36,15 +22,10 @@ export class LanguageService { ); if (!languageSelected) { - this.configService.configUpdates.subscribe(() => { - const { default_language } = - this.configService.getConfig("appConfig") ?? {}; - if (default_language && default_language !== this.baseLocale) { + this.siteSettings.defaultLanguage.subscribe(({ id }) => { + if (id !== this.baseLocale) { // Reload app with default language from config - this.window.localStorage.setItem( - LANGUAGE_LOCAL_STORAGE_KEY, - default_language, - ); + this.window.localStorage.setItem(LANGUAGE_LOCAL_STORAGE_KEY, id); this.window.location.reload(); } }); diff --git a/src/app/core/language/languages.ts b/src/app/core/language/languages.ts new file mode 100644 index 0000000000..4e1261f2cf --- /dev/null +++ b/src/app/core/language/languages.ts @@ -0,0 +1,13 @@ +import { ConfigurableEnum } from "../basic-datatypes/configurable-enum/configurable-enum"; + +export const LOCALE_ENUM_ID = "locales"; + +/** + * A readonly array of all locales available + */ +export const availableLocales = new ConfigurableEnum(LOCALE_ENUM_ID, [ + { id: "de", label: "de" }, + { id: "en-US", label: "us" }, + { id: "fr", label: "fr" }, + { id: "it", label: "it" }, +]); diff --git a/src/app/core/permissions/ability/ability.service.ts b/src/app/core/permissions/ability/ability.service.ts index 3607afa85f..098f79b3a4 100644 --- a/src/app/core/permissions/ability/ability.service.ts +++ b/src/app/core/permissions/ability/ability.service.ts @@ -1,7 +1,6 @@ import { Injectable } from "@angular/core"; import { SessionService } from "../../session/session-service/session.service"; -import { filter } from "rxjs/operators"; -import { Observable, Subject } from "rxjs"; +import { shareReplay } from "rxjs/operators"; import { DatabaseRule, DatabaseRules } from "../permission-types"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { PermissionEnforcerService } from "../permission-enforcer/permission-enforcer.service"; @@ -10,47 +9,36 @@ import { Config } from "../../config/config"; import { LoggingService } from "../../logging/logging.service"; import { get } from "lodash-es"; import { AuthUser } from "../../session/session-service/auth-user"; +import { LatestEntityLoader } from "../../entity/latest-entity-loader"; /** * This service sets up the `EntityAbility` injectable with the JSON defined rules for the currently logged in user. */ @Injectable() -export class AbilityService { - private _abilityUpdated = new Subject(); - +export class AbilityService extends LatestEntityLoader> { /** * Get notified whenever the permissions of the current user are updated. * Use this to re-evaluate the permissions of the currently logged-in user. */ - get abilityUpdated(): Observable { - return this._abilityUpdated.asObservable(); - } + abilityUpdated = this.entityUpdated.pipe(shareReplay(1)); constructor( private ability: EntityAbility, private sessionService: SessionService, - private entityMapper: EntityMapperService, private permissionEnforcer: PermissionEnforcerService, - private logger: LoggingService, - ) {} - - initializeRules() { - // TODO this setup is very similar to `ConfigService` - this.loadRules(); - this.entityMapper - .receiveUpdates>(Config) - .pipe(filter(({ entity }) => entity.getId() === Config.PERMISSION_KEY)) - .subscribe(({ entity }) => this.updateAbilityWithUserRules(entity.data)); + entityMapper: EntityMapperService, + logger: LoggingService, + ) { + super(Config, Config.PERMISSION_KEY, entityMapper, logger); } - private loadRules(): Promise { + initializeRules() { // Initially allow everything until permission document could be fetched - // TODO somehow this rules is used if no other is found even after update this.ability.update([{ action: "manage", subject: "all" }]); - return this.entityMapper - .load>(Config, Config.PERMISSION_KEY) - .then((config) => this.updateAbilityWithUserRules(config.data)) - .catch(() => undefined); + super.startLoading(); + this.entityUpdated.subscribe((config) => + this.updateAbilityWithUserRules(config.data), + ); } private updateAbilityWithUserRules(rules: DatabaseRules): Promise { @@ -104,6 +92,5 @@ export class AbilityService { private updateAbilityWithRules(rules: DatabaseRule[]) { this.ability.update(rules); - this._abilityUpdated.next(); } } diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts index db9dbf8eaf..8f6888b834 100644 --- a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts +++ b/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts @@ -11,6 +11,7 @@ import { MockedTestingModule } from "../../../../../utils/mocked-testing.module" import { SessionService } from "../../../session-service/session.service"; import { CouchdbAuthService } from "../couchdb-auth.service"; import { AuthService } from "../../auth.service"; +import { NEVER } from "rxjs"; describe("PasswordFormComponent", () => { let component: PasswordFormComponent; @@ -19,7 +20,10 @@ describe("PasswordFormComponent", () => { let mockCouchDBAuth: jasmine.SpyObj; beforeEach(async () => { - mockSessionService = jasmine.createSpyObj(["login", "checkPassword"]); + mockSessionService = jasmine.createSpyObj(["login", "checkPassword"], { + syncState: NEVER, + loginState: NEVER, + }); mockCouchDBAuth = jasmine.createSpyObj(["changePassword"]); await TestBed.configureTestingModule({ diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts index 42a964424a..470a2d4258 100644 --- a/src/app/core/session/login/login.component.spec.ts +++ b/src/app/core/session/login/login.component.spec.ts @@ -29,7 +29,7 @@ import { SessionService } from "../session-service/session.service"; import { LoginState } from "../session-states/login-state.enum"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { AuthService } from "../auth/auth.service"; -import { Subject } from "rxjs"; +import { NEVER, Subject } from "rxjs"; import { ActivatedRoute, Router } from "@angular/router"; import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; import { HarnessLoader } from "@angular/cdk/testing"; @@ -45,6 +45,7 @@ describe("LoginComponent", () => { beforeEach(waitForAsync(() => { mockSessionService = jasmine.createSpyObj(["login", "getCurrentUser"], { loginState, + syncState: NEVER, }); mockSessionService.getCurrentUser.and.returnValue({ name: "", roles: [] }); TestBed.configureTestingModule({ diff --git a/src/app/core/site-settings/demo-site-settings-generator.service.ts b/src/app/core/site-settings/demo-site-settings-generator.service.ts new file mode 100644 index 0000000000..3a7ef6309a --- /dev/null +++ b/src/app/core/site-settings/demo-site-settings-generator.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; +import { DemoDataGenerator } from "../demo-data/demo-data-generator"; +import { SiteSettings } from "./site-settings"; + +/** + * Generates SiteSettings entity. Defaults are defined in the SiteSettings class. + */ +@Injectable() +export class DemoSiteSettingsGeneratorService extends DemoDataGenerator { + static provider() { + return [ + { + provide: DemoSiteSettingsGeneratorService, + useClass: DemoSiteSettingsGeneratorService, + }, + ]; + } + + protected generateEntities(): SiteSettings[] { + return [new SiteSettings()]; + } +} diff --git a/src/app/core/site-settings/site-settings.service.spec.ts b/src/app/core/site-settings/site-settings.service.spec.ts new file mode 100644 index 0000000000..c62230f7d2 --- /dev/null +++ b/src/app/core/site-settings/site-settings.service.spec.ts @@ -0,0 +1,161 @@ +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; + +import { SiteSettingsService } from "./site-settings.service"; +import { FileService } from "../../features/file/file.service"; +import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service"; +import { + mockEntityMapper, + MockEntityMapperService, +} from "../entity/entity-mapper/mock-entity-mapper-service"; +import { SiteSettings } from "./site-settings"; +import { of } from "rxjs"; +import { Title } from "@angular/platform-browser"; +import { availableLocales } from "../language/languages"; +import { CoreModule } from "../core.module"; +import { ComponentRegistry } from "../../dynamic-components"; +import { ConfigurableEnumModule } from "../basic-datatypes/configurable-enum/configurable-enum.module"; +import { EntityAbility } from "../permissions/ability/entity-ability"; +import { FileModule } from "../../features/file/file.module"; +import { EntitySchemaService } from "../entity/schema/entity-schema.service"; +import { LoggingService } from "../logging/logging.service"; +import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; + +describe("SiteSettingsService", () => { + let service: SiteSettingsService; + let entityMapper: MockEntityMapperService; + let mockFileService: jasmine.SpyObj; + + beforeEach(() => { + entityMapper = mockEntityMapper(); + mockFileService = jasmine.createSpyObj(["loadFile"]); + TestBed.configureTestingModule({ + imports: [CoreModule, ConfigurableEnumModule, FileModule], + providers: [ + ComponentRegistry, + { provide: FileService, useValue: mockFileService }, + { provide: EntityMapperService, useValue: entityMapper }, + EntityAbility, + ], + }); + service = TestBed.inject(SiteSettingsService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should only publish changes if property has changed", () => { + const titleSpy = spyOn(TestBed.inject(Title), "setTitle"); + const settings = new SiteSettings(); + + entityMapper.add(settings); + + expect(titleSpy).not.toHaveBeenCalled(); + + settings.displayLanguageSelect = false; + entityMapper.add(settings); + + expect(titleSpy).not.toHaveBeenCalled(); + + settings.siteName = "New name"; + entityMapper.add(settings); + + expect(titleSpy).toHaveBeenCalled(); + + titleSpy.calls.reset(); + settings.displayLanguageSelect = true; + entityMapper.add(settings); + + expect(titleSpy).not.toHaveBeenCalled(); + + settings.displayLanguageSelect = false; + settings.siteName = "Another new name"; + entityMapper.add(settings); + + expect(titleSpy).toHaveBeenCalled(); + }); + + it("should reset favicon when deleted", fakeAsync(() => { + const siteSettings = SiteSettings.create({ favicon: "some.icon" }); + mockFileService.loadFile.and.returnValue(of({ url: "icon.url" })); + const mockIconEl = { href: "initial" }; + spyOn(document, "querySelector").and.returnValue(mockIconEl as any); + entityMapper.add(siteSettings); + tick(); + + expect(mockFileService.loadFile).toHaveBeenCalledWith( + siteSettings, + "favicon", + ); + expect(mockIconEl.href).toBe("icon.url"); + + mockFileService.loadFile.calls.reset(); + delete siteSettings.favicon; + entityMapper.add(siteSettings); + tick(); + + expect(mockFileService.loadFile).not.toHaveBeenCalled(); + expect(mockIconEl.href).toBe("favicon.ico"); + })); + + function expectStyleSetProperty(siteSettingsProperty, cssVariable, value) { + spyOn(document.documentElement.style, "setProperty"); + + entityMapper.add(SiteSettings.create({ [siteSettingsProperty]: value })); + + expect(document.documentElement.style.setProperty).toHaveBeenCalledWith( + cssVariable, + value, + ); + } + + it("should set font family once defined", () => { + expectStyleSetProperty("font", "--font-family", "comic sans"); + }); + + it("should update the color palette if a color is changed", () => { + expectStyleSetProperty("primary", "--primary-50", "#ffffff"); + }); + + it("should store any settings update in localStorage", fakeAsync(() => { + const localStorageSetItemSpy = spyOn(localStorage, "setItem"); + + const settings = SiteSettings.create({ + siteName: "test", + defaultLanguage: availableLocales.values[0], + }); + + entityMapper.save(settings); + tick(); + + expect(localStorageSetItemSpy).toHaveBeenCalledWith( + service.SITE_SETTINGS_LOCAL_STORAGE_KEY, + jasmine.any(String), + ); + expect(localStorageSetItemSpy.calls.mostRecent().args[1]).toMatch( + `"siteName":"${settings.siteName}"`, + ); + expect(localStorageSetItemSpy.calls.mostRecent().args[1]).toMatch( + `"defaultLanguage":"${settings.defaultLanguage.id}"`, + ); + })); + + it("should init settings from localStorage during startup", fakeAsync(() => { + const settings = SiteSettings.create({ siteName: "local storage test" }); + const localStorageGetItemSpy = spyOn(localStorage, "getItem"); + localStorageGetItemSpy.and.returnValue(JSON.stringify(settings)); + + const titleSpy = spyOn(TestBed.inject(Title), "setTitle"); + + service = new SiteSettingsService( + TestBed.inject(Title), + TestBed.inject(FileService), + TestBed.inject(EntitySchemaService), + TestBed.inject(ConfigurableEnumService), + TestBed.inject(EntityMapperService), + TestBed.inject(LoggingService), + ); + + expect(titleSpy).toHaveBeenCalledWith(settings.siteName); + })); +}); diff --git a/src/app/core/site-settings/site-settings.service.ts b/src/app/core/site-settings/site-settings.service.ts new file mode 100644 index 0000000000..5dd801c130 --- /dev/null +++ b/src/app/core/site-settings/site-settings.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from "@angular/core"; +import { SiteSettings } from "./site-settings"; +import { delay, firstValueFrom, Observable, skipWhile } from "rxjs"; +import { distinctUntilChanged, map, shareReplay } from "rxjs/operators"; +import { Title } from "@angular/platform-browser"; +import { FileService } from "../../features/file/file.service"; +import materialColours from "@aytek/material-color-picker"; +import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service"; +import { LatestEntityLoader } from "../entity/latest-entity-loader"; +import { LoggingService } from "../logging/logging.service"; +import { Entity } from "../entity/model/entity"; +import { EntitySchemaService } from "../entity/schema/entity-schema.service"; +import { availableLocales } from "../language/languages"; +import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; + +/** + * Access to site settings stored in the database, like styling, site name and logo. + */ +@Injectable({ + providedIn: "root", +}) +export class SiteSettingsService extends LatestEntityLoader { + readonly DEFAULT_FAVICON = "favicon.ico"; + readonly SITE_SETTINGS_LOCAL_STORAGE_KEY = Entity.createPrefixedId( + SiteSettings.ENTITY_TYPE, + SiteSettings.ENTITY_ID, + ); + + siteSettings = this.entityUpdated.pipe(shareReplay(1)); + + siteName = this.getPropertyObservable("siteName"); + defaultLanguage = this.getPropertyObservable("defaultLanguage"); + displayLanguageSelect = this.getPropertyObservable("displayLanguageSelect"); + + constructor( + private title: Title, + private fileService: FileService, + private schemaService: EntitySchemaService, + private enumService: ConfigurableEnumService, + entityMapper: EntityMapperService, + logger: LoggingService, + ) { + super(SiteSettings, SiteSettings.ENTITY_ID, entityMapper, logger); + + this.initAvailableLocales(); + + this.siteName.subscribe((name) => this.title.setTitle(name)); + this.subscribeFontChanges(); + this.subscribeFaviconChanges(); + this.subscribeColorChanges("primary"); + this.subscribeColorChanges("secondary"); + this.subscribeColorChanges("error"); + + this.initFromLocalStorage(); + this.cacheInLocalStorage(); + + super.startLoading(); + } + + /** + * Making locales enum available at runtime + * so that UI can show dropdown options + * @private + */ + private initAvailableLocales() { + this.enumService["cacheEnum"](availableLocales); + } + + /** + * Do an initial loading of settings from localStorage, if available. + * @private + */ + private initFromLocalStorage() { + let localStorageSettings: SiteSettings; + + try { + const stored = localStorage.getItem(this.SITE_SETTINGS_LOCAL_STORAGE_KEY); + if (stored) { + localStorageSettings = + this.schemaService.transformDatabaseToEntityFormat( + JSON.parse(stored), + SiteSettings.schema, + ); + } + } catch (e) { + this.logger.debug( + "SiteSettingsService: could not parse settings from localStorage: " + e, + ); + } + + if (localStorageSettings) { + this.entityUpdated.next(localStorageSettings); + } + } + + /** + * Store the latest SiteSettings in localStorage to be available before login also. + * @private + */ + private cacheInLocalStorage() { + this.entityUpdated.subscribe((settings) => { + const dbFormat = + this.schemaService.transformEntityToDatabaseFormat(settings); + localStorage.setItem( + this.SITE_SETTINGS_LOCAL_STORAGE_KEY, + JSON.stringify(dbFormat), + ); + }); + } + + private subscribeFontChanges() { + this.getPropertyObservable("font").subscribe((font) => + document.documentElement.style.setProperty("--font-family", font), + ); + } + + private subscribeFaviconChanges() { + this.getPropertyObservable("favicon") + .pipe(delay(0)) + .subscribe(async (icon) => { + const favIcon: HTMLLinkElement = document.querySelector("#appIcon"); + if (icon) { + const entity = await firstValueFrom(this.siteSettings); + const imgUrl = await firstValueFrom( + this.fileService.loadFile(entity, "favicon"), + ); + favIcon.href = Object.values(imgUrl)[0]; + } else { + favIcon.href = this.DEFAULT_FAVICON; + } + }); + } + + private subscribeColorChanges(property: "primary" | "secondary" | "error") { + this.getPropertyObservable(property).subscribe((color) => { + if (color) { + const palette = materialColours(color); + palette["A100"] = palette["200"]; + palette["A200"] = palette["300"]; + palette["A400"] = palette["500"]; + palette["A700"] = palette["800"]; + Object.entries(palette).forEach(([key, value]) => + document.documentElement.style.setProperty( + `--${property}-${key}`, + `#${value}`, + ), + ); + } + }); + } + + getPropertyObservable

    ( + property: P, + ): Observable { + return this.siteSettings.pipe( + skipWhile((v) => !v[property]), + map((s) => s[property]), + distinctUntilChanged(), + ); + } +} diff --git a/src/app/core/site-settings/site-settings.ts b/src/app/core/site-settings/site-settings.ts new file mode 100644 index 0000000000..d5f2fc51a9 --- /dev/null +++ b/src/app/core/site-settings/site-settings.ts @@ -0,0 +1,66 @@ +import { Entity } from "../entity/model/entity"; +import { DatabaseEntity } from "../entity/database-entity.decorator"; +import { DatabaseField } from "../entity/database-field.decorator"; +import { availableLocales, LOCALE_ENUM_ID } from "../language/languages"; +import { ConfigurableEnumValue } from "../basic-datatypes/configurable-enum/configurable-enum.interface"; + +/** + * Global settings like styling and title to customize an instance of the app. + * The settings are applied at runtime. + */ +@DatabaseEntity("SiteSettings") +export class SiteSettings extends Entity { + static ENTITY_ID = "global"; + static label = $localize`Site settings`; + + static create(value: Partial): SiteSettings { + return Object.assign(new SiteSettings(), value); + } + + @DatabaseField({ label: $localize`Site name` }) siteName: string = + "Aam Digital - Demo"; + + @DatabaseField({ + label: $localize`Default language`, + description: $localize`This will only be applied once the app is reloaded`, + dataType: "configurable-enum", + innerDataType: LOCALE_ENUM_ID, + }) + defaultLanguage: ConfigurableEnumValue = availableLocales.values.find( + ({ id }) => id === "en-US", + ); + + @DatabaseField({ + label: $localize`Display language select`, + }) + displayLanguageSelect: boolean = true; + + @DatabaseField({ + label: $localize`Logo`, + dataType: "file", + editComponent: "EditPhoto", + additional: 300, + }) + logo: string; + + @DatabaseField({ + label: $localize`App favicon`, + dataType: "file", + editComponent: "EditPhoto", + additional: 256, + }) + favicon: string; + + @DatabaseField({ label: $localize`Primary color` }) primary: string; + @DatabaseField({ label: $localize`Secondary color` }) secondary: string; + @DatabaseField({ label: $localize`Error color` }) error: string; + @DatabaseField({ label: $localize`Text font` }) font: string; + + constructor() { + super(SiteSettings.ENTITY_ID); + } + + toString() { + return this.getConstructor().label; + } +} diff --git a/src/app/core/ui/ui/ui.component.html b/src/app/core/ui/ui/ui.component.html index 4a0523cf69..1d4442a954 100644 --- a/src/app/core/ui/ui/ui.component.html +++ b/src/app/core/ui/ui/ui.component.html @@ -26,34 +26,32 @@ - {{ title }} + {{ siteSettings.siteName }} -

    - +
    + - +
    - + -
    - - +
    + -
    - - this.deleteFilesOfDeletedEntities()); } private deleteFilesOfDeletedEntities() { diff --git a/src/app/features/file/mock-file.service.spec.ts b/src/app/features/file/mock-file.service.spec.ts index a3fc1110c4..c67a60b212 100644 --- a/src/app/features/file/mock-file.service.spec.ts +++ b/src/app/features/file/mock-file.service.spec.ts @@ -3,11 +3,13 @@ import { TestBed } from "@angular/core/testing"; import { MockFileService } from "./mock-file.service"; import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service"; import { Entity } from "../../core/entity/model/entity"; -import { firstValueFrom, NEVER } from "rxjs"; +import { firstValueFrom, NEVER, of } from "rxjs"; import { entityRegistry, EntityRegistry, } from "../../core/entity/database-entity.decorator"; +import { SessionService } from "../../core/session/session-service/session.service"; +import { SyncState } from "../../core/session/session-states/sync-state.enum"; describe("MockFileService", () => { let service: MockFileService; @@ -21,6 +23,10 @@ describe("MockFileService", () => { useValue: { receiveUpdates: () => NEVER }, }, { provide: EntityRegistry, useValue: entityRegistry }, + { + provide: SessionService, + useValue: { syncState: of(SyncState.COMPLETED) }, + }, ], }); service = TestBed.inject(MockFileService); diff --git a/src/app/features/file/mock-file.service.ts b/src/app/features/file/mock-file.service.ts index 8bb3ed47c4..66e8df7b4f 100644 --- a/src/app/features/file/mock-file.service.ts +++ b/src/app/features/file/mock-file.service.ts @@ -6,6 +6,7 @@ import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapp import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { LoggingService } from "../../core/logging/logging.service"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { SessionService } from "../../core/session/session-service/session.service"; /** * A mock implementation of the file service which only stores the file temporarily in the browser. @@ -20,9 +21,10 @@ export class MockFileService extends FileService { entityMapper: EntityMapperService, entities: EntityRegistry, logger: LoggingService, + session: SessionService, private sanitizer: DomSanitizer, ) { - super(entityMapper, entities, logger); + super(entityMapper, entities, logger, session); } removeFile(entity: Entity, property: string): Observable { diff --git a/src/app/features/matching-entities/matching-entities/matching-entities.component.scss b/src/app/features/matching-entities/matching-entities/matching-entities.component.scss index 5b6beddef4..56589383ad 100644 --- a/src/app/features/matching-entities/matching-entities/matching-entities.component.scss +++ b/src/app/features/matching-entities/matching-entities/matching-entities.component.scss @@ -29,7 +29,6 @@ .selection-header { font-weight: 500; font-size: medium; - font-family: Roboto, sans-serif; } .highlighted-name { diff --git a/src/app/features/public-form/public-form.component.spec.ts b/src/app/features/public-form/public-form.component.spec.ts index 445187d879..dcd11fb592 100644 --- a/src/app/features/public-form/public-form.component.spec.ts +++ b/src/app/features/public-form/public-form.component.spec.ts @@ -109,6 +109,6 @@ describe("PublicFormComponent", () => { function initComponent() { TestBed.inject(EntityMapperService).save(testFormConfig); const configService = TestBed.inject(ConfigService); - configService["_configUpdates"].next(configService["currentConfig"]); + configService.entityUpdated.next(configService["currentConfig"]); } }); diff --git a/src/app/utils/storybook-base.module.ts b/src/app/utils/storybook-base.module.ts index 9c43bbe550..7ab847a729 100644 --- a/src/app/utils/storybook-base.module.ts +++ b/src/app/utils/storybook-base.module.ts @@ -69,11 +69,6 @@ export const entityFormStorybookDefaultParameters = { indicesRegistered: EMPTY, }, }, - { provide: ConfigService, useValue: createTestingConfigService() }, - { - provide: ConfigurableEnumService, - useValue: createTestingConfigurableEnumService(), - }, { provide: EntityMapperService, useValue: mockEntityMapper() }, ], }) diff --git a/src/index.html b/src/index.html index b414b692a0..702f4b74d9 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,4 @@ - - - - - Aam Digital - + + + Aam Digital + - - + + - - + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - -
    -

    Aam Digital

    -

    ... is loading for the first time.

    -

    - This may take a few seconds or even minutes depending on your internet - connection. -

    -

    - Browser Details: - -

    -
    -
    - + + + + + + + + +
    +

    Aam Digital

    +

    ... is loading for the first time.

    +

    + This may take a few seconds or even minutes depending on your internet + connection. +

    +

    + Browser Details: + +

    +
    +
    + + diff --git a/src/styles/mdc_overwrites/mdc_overwrites.scss b/src/styles/mdc_overwrites/mdc_overwrites.scss new file mode 100644 index 0000000000..dfdb96682c --- /dev/null +++ b/src/styles/mdc_overwrites/mdc_overwrites.scss @@ -0,0 +1,27 @@ +@use "src/styles/variables/ndb-light-theme" as theme; +@use "@angular/material" as mat; + +/* + * This file holds the required overwrites for styles broken due to css variables. + * more background: https://github.com/angular/components/issues/25981#issuecomment-1515332869 + */ + +// Text color for fab buttons otherwise defaults to black +.mat-mdc-fab.mat-accent, .mat-mdc-mini-fab.mat-accent { + --mat-mdc-fab-color: #{mat.get-contrast-color-from-palette(theme.$accent, A200)} !important; +} + +// Text color for buttons with accent color otherwise defaults to black +.mat-mdc-raised-button:not(:disabled)[color="accent"] { + --mdc-protected-button-label-text-color: #{mat.get-contrast-color-from-palette(theme.$accent, A200)} !important +} + +// Progress bar background color should have opacity to indicate progress +.mat-mdc-progress-bar .mdc-linear-progress__buffer-bar { + opacity: 25%; +} + +// Color for enabled checkbox otherwise defaults to black +.mdc-checkbox .mdc-checkbox__native-control:enabled~.mdc-checkbox__background .mdc-checkbox__checkmark { + --mdc-checkbox-selected-checkmark-color: #{mat.get-contrast-color-from-palette(theme.$accent, A200)} !important +} diff --git a/src/styles/styles.scss b/src/styles/styles.scss index 62e6ca4053..64b81e6d21 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -31,6 +31,9 @@ // such as material components or native html components @use "resets"; +// Overwrite some MDC (Material) styles to work with variables +@use "./mdc_overwrites/mdc_overwrites"; + // Include the styles for directives here to make them available as global style (currently, it // is not possible to use styles in a directive) @use "../app/core/common-components/border-highlight/border-highlight.directive"; diff --git a/src/styles/themes/ndb-theme.scss b/src/styles/themes/ndb-theme.scss index 0b149be0ef..346ba9e07f 100644 --- a/src/styles/themes/ndb-theme.scss +++ b/src/styles/themes/ndb-theme.scss @@ -3,5 +3,5 @@ // Include non-theme styles for core. @include mat.core(); +@include mat.typography-hierarchy(ndb-theme.$typography); @include mat.all-component-themes(ndb-theme.$theme); -@include mat.all-component-typographies(); diff --git a/src/styles/variables/_colors.scss b/src/styles/variables/_colors.scss index fd84323996..d52942c23f 100644 --- a/src/styles/variables/_colors.scss +++ b/src/styles/variables/_colors.scss @@ -15,6 +15,7 @@ * along with ndb-core. If not, see . */ @use "@angular/material" as mat; +@use "src/styles/variables/ndb-light-theme" as theme; $err-palette: mat.define-palette(mat.$red-palette); $warn-palette: mat.define-palette(mat.$orange-palette); @@ -26,6 +27,7 @@ $error: mat.get-color-from-palette($err-palette); $muted: rgba(0, 0, 0, 0.54); $disabled: rgba(0, 0, 0, 0.38); +$background: mat.get-color-from-palette(theme.$primary, 50); /* * Quantized warning-levels. Each level represents a color from green (all is OK) diff --git a/src/styles/variables/_ndb-light-theme.scss b/src/styles/variables/_ndb-light-theme.scss index 64793065a7..8a7e10de2e 100644 --- a/src/styles/variables/_ndb-light-theme.scss +++ b/src/styles/variables/_ndb-light-theme.scss @@ -3,18 +3,77 @@ */ @use "@angular/material" as mat; +$primary-palette: ( + 50: var(--primary-50, map-get(mat.$orange-palette, 50)), + 100: var(--primary-100, map-get(mat.$orange-palette, 100)), + 200: var(--primary-200, map-get(mat.$orange-palette, 200)), + 300: var(--primary-300, map-get(mat.$orange-palette, 300)), + 400: var(--primary-400, map-get(mat.$orange-palette, 400)), + //500: map-get(mat.$orange-palette, 500), + // TODO somehow this CSS variable breaks the progress bars + 500: var(--primary-500, map-get(mat.$orange-palette, 500)), + 600: var(--primary-600, map-get(mat.$orange-palette, 600)), + 700: var(--primary-700, map-get(mat.$orange-palette, 700)), + 800: var(--primary-800, map-get(mat.$orange-palette, 800)), + 900: var(--primary-900, map-get(mat.$orange-palette, 900)), + A100: var(--primary-A100, map-get(mat.$orange-palette, A100)), + A200: var(--primary-A200, map-get(mat.$orange-palette, A200)), + A400: var(--primary-A400, map-get(mat.$orange-palette, A400)), + A700: var(--primary-A700, map-get(mat.$orange-palette, A700)), + contrast: map-get(mat.$orange-palette, contrast) +); + +$secondary-palette: ( + 50: var(--secondary-50, map-get(mat.$blue-palette, 50)), + 100: var(--secondary-100, map-get(mat.$blue-palette, 100)), + 200: var(--secondary-200, map-get(mat.$blue-palette, 200)), + 300: var(--secondary-300, map-get(mat.$blue-palette, 300)), + 400: var(--secondary-400, map-get(mat.$blue-palette, 400)), + 500: var(--secondary-500, map-get(mat.$blue-palette, 500)), + 600: var(--secondary-600, map-get(mat.$blue-palette, 600)), + 700: var(--secondary-700, map-get(mat.$blue-palette, 700)), + 800: var(--secondary-800, map-get(mat.$blue-palette, 800)), + 900: var(--secondary-900, map-get(mat.$blue-palette, 900)), + A100: var(--secondary-A100, map-get(mat.$blue-palette, A100)), + // TODO this breaks the primary action button text color + A200: var(--secondary-A200, map-get(mat.$blue-palette, A200)), + A400: var(--secondary-A400, map-get(mat.$blue-palette, A400)), + A700: var(--secondary-A700, map-get(mat.$blue-palette, A700)), + contrast: map-get(mat.$blue-palette, contrast) +); + +$error-palette: ( + 50: var(--error-50, map-get(mat.$red-palette, 50)), + 100: var(--error-100, map-get(mat.$red-palette, 100)), + 200: var(--error-200, map-get(mat.$red-palette, 200)), + 300: var(--error-300, map-get(mat.$red-palette, 300)), + 400: var(--error-400, map-get(mat.$red-palette, 400)), + 500: var(--error-500, map-get(mat.$red-palette, 500)), + 600: var(--error-600, map-get(mat.$red-palette, 600)), + 700: var(--error-700, map-get(mat.$red-palette, 700)), + 800: var(--error-800, map-get(mat.$red-palette, 800)), + 900: var(--error-900, map-get(mat.$red-palette, 900)), + A200: var(--error-A200, map-get(mat.$red-palette, A200)), + A400: var(--error-A400, map-get(mat.$red-palette, A400)), + A700: var(--error-A700, map-get(mat.$red-palette, A700)), + contrast: map-get(mat.$red-palette, contrast) +); + // Define the applications theme. -$primary: mat.define-palette(mat.$orange-palette); -$accent: mat.define-palette(mat.$blue-palette, A200, A100, A400); -$warn: mat.define-palette(mat.$red-palette); +$primary : mat.define-palette($primary-palette); +$accent : mat.define-palette($secondary-palette, A200, A100, A400); +$warn : mat.define-palette($error-palette); -// Create the theme object (a Sass map containing all of the palettes). -$theme: mat.define-light-theme( - ( - color: ( - primary: $primary, - accent: $accent, - warn: $warn, - ), - ) +$typography: mat.define-typography-config( + $font-family: var(--font-family, sans-serif), ); + +// Create the theme object (a Sass map containing all of the palettes). +$theme: mat.define-light-theme(( + color: ( + primary: $primary, + accent: $accent, + warn: $warn + ), + typography: $typography +)); From e4e5dda984782b3eb6ad20c8811aebe1a67067ef Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 8 Sep 2023 14:45:54 +0200 Subject: [PATCH 12/19] rename --- .../display-percentage/display-percentage.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts index 4cbaac1199..c7f878b575 100644 --- a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts @@ -6,7 +6,7 @@ import { CommonModule } from "@angular/common"; @DynamicComponent("DisplayPercentage") @Component({ selector: "app-display-percentage", - template: "{{ value ? (value | number : decimalPipe) + '%' : '-' }}", + template: "{{ value ? (value | number : decimalPlaces) + '%' : '-' }}", standalone: true, imports: [CommonModule], }) @@ -15,7 +15,7 @@ export class DisplayPercentageComponent implements OnInit { @HostBinding("style") style = {}; - decimalPipe: string; + decimalPlaces: string; /** * returns a css-compatible color value from green to red using the given @@ -35,7 +35,7 @@ export class DisplayPercentageComponent } ngOnInit() { - this.decimalPipe = + this.decimalPlaces = "1." + (this.config && this.config.decimalPlaces ? this.config.decimalPlaces + "-" + this.config.decimalPlaces From 7dfa6f754bb5f6a9fbca87da34b36f5105237c6d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 8 Sep 2023 14:46:58 +0200 Subject: [PATCH 13/19] Update src/app/core/basic-datatypes/entity/display-entity/display-entity.stories.ts --- .../entity/display-entity/display-entity.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/basic-datatypes/entity/display-entity/display-entity.stories.ts b/src/app/core/basic-datatypes/entity/display-entity/display-entity.stories.ts index 63b3382fe7..ed1d41f424 100644 --- a/src/app/core/basic-datatypes/entity/display-entity/display-entity.stories.ts +++ b/src/app/core/basic-datatypes/entity/display-entity/display-entity.stories.ts @@ -24,7 +24,7 @@ const Template: StoryFn = ( }); const testChild = new Child(); -testChild.name = "Test NameXXX"; +testChild.name = "Test Name"; testChild.projectNumber = "10"; export const ChildComponent = Template.bind({}); ChildComponent.args = { From 530a95505a024b08539aca244362043b1659e8b1 Mon Sep 17 00:00:00 2001 From: christophscheuing <47225324+christophscheuing@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:06:16 +0200 Subject: [PATCH 14/19] Removal of this test Co-authored-by: Sebastian --- .../health-checkup.component.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts index 4495c53066..1b7f284d14 100644 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts @@ -32,16 +32,6 @@ export class HealthCheckupComponent implements OnInit { tooltip: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, additional: (entity: HealthCheck) => this.getBMI(entity), }, - { - id: "display-percentage", - label: "display-percentage", - view: "DisplayDynamicPercentage", - additional: { - actual: "weight", - total: "height", - decimalPlaces: 0, - }, - }, ], }; @Input() entity: Child; From fe686ea109dd0bd937530276b8efb014cec33e4e Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Sep 2023 16:14:36 +0200 Subject: [PATCH 15/19] calculating result on every change detection --- .../display-dynamic-percentage.component.ts | 25 ++++++++----------- .../display-percentage.component.ts | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts index 485eec5fde..552401aae4 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { ViewDirective } from "app/core/entity/default-datatype/view.directive"; import { DynamicComponent } from "app/core/config/dynamic-components/dynamic-component.decorator"; import { DisplayPercentageComponent } from "../display-percentage/display-percentage.component"; @@ -7,28 +7,23 @@ import { DisplayPercentageComponent } from "../display-percentage/display-percen @Component({ selector: "app-display-dynamic-percentage", template: - "", + "", standalone: true, imports: [DisplayPercentageComponent], }) -export class DisplayDynamicPercentageComponent - extends ViewDirective< - number, - { total: string; actual: string; numberOfDigits?: number } - > - implements OnInit -{ - public result: number; - - ngOnInit() { +export class DisplayDynamicPercentageComponent extends ViewDirective< + number, + { total: string; actual: string; decimalPlaces?: number } +> { + calculateValue() { if ( Number.isFinite(this.entity[this.config.actual]) && Number.isFinite(this.entity[this.config.total]) && this.entity[this.config.total] != 0 ) { - this.result = - (this.entity[this.config.actual] / this.entity[this.config.total]) * - 100; + return ( + (this.entity[this.config.actual] / this.entity[this.config.total]) * 100 + ); } } } diff --git a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts index c7f878b575..c4630bf141 100644 --- a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts @@ -11,7 +11,7 @@ import { CommonModule } from "@angular/common"; imports: [CommonModule], }) export class DisplayPercentageComponent - extends ViewDirective + extends ViewDirective implements OnInit { @HostBinding("style") style = {}; From ca57d118aadea5d024a96083a5e42bcb10f6293a Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 18 Sep 2023 16:14:43 +0200 Subject: [PATCH 16/19] added demo showcase --- .../health-checkup.component.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts index 1b7f284d14..ee2b7d9f6d 100644 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts @@ -32,6 +32,16 @@ export class HealthCheckupComponent implements OnInit { tooltip: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, additional: (entity: HealthCheck) => this.getBMI(entity), }, + + { + id: "percentage", + view: "DisplayDynamicPercentage", + additional: { + total: "height", + actual: "weight", + }, + label: "Percentage", + }, ], }; @Input() entity: Child; From c81d2025564f3b66ba1e884dd2976470daefa114 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 21 Sep 2023 16:47:03 +0200 Subject: [PATCH 17/19] better property name for numberFormat --- .../display-percentage/display-percentage.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts index c4630bf141..c3b54bab8f 100644 --- a/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-percentage/display-percentage.component.ts @@ -6,7 +6,7 @@ import { CommonModule } from "@angular/common"; @DynamicComponent("DisplayPercentage") @Component({ selector: "app-display-percentage", - template: "{{ value ? (value | number : decimalPlaces) + '%' : '-' }}", + template: "{{ value ? (value | number : numberFormat) + '%' : '-' }}", standalone: true, imports: [CommonModule], }) @@ -15,7 +15,7 @@ export class DisplayPercentageComponent implements OnInit { @HostBinding("style") style = {}; - decimalPlaces: string; + numberFormat: string; /** * returns a css-compatible color value from green to red using the given @@ -35,7 +35,7 @@ export class DisplayPercentageComponent } ngOnInit() { - this.decimalPlaces = + this.numberFormat = "1." + (this.config && this.config.decimalPlaces ? this.config.decimalPlaces + "-" + this.config.decimalPlaces From 621e337d112f26b2232404198adc968121c0c49f Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 21 Sep 2023 16:53:17 +0200 Subject: [PATCH 18/19] cleanup --- .../health-checkup.component.ts | 10 ---------- .../display-dynamic-percentage.component.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts index ee2b7d9f6d..1b7f284d14 100644 --- a/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts +++ b/src/app/child-dev-project/children/health-checkup/health-checkup-component/health-checkup.component.ts @@ -32,16 +32,6 @@ export class HealthCheckupComponent implements OnInit { tooltip: $localize`:Tooltip for BMI info:This is calculated using the height and the weight measure`, additional: (entity: HealthCheck) => this.getBMI(entity), }, - - { - id: "percentage", - view: "DisplayDynamicPercentage", - additional: { - total: "height", - actual: "weight", - }, - label: "Percentage", - }, ], }; @Input() entity: Child; diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts index 552401aae4..49c3c33f80 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.ts @@ -3,6 +3,10 @@ import { ViewDirective } from "app/core/entity/default-datatype/view.directive"; import { DynamicComponent } from "app/core/config/dynamic-components/dynamic-component.decorator"; import { DisplayPercentageComponent } from "../display-percentage/display-percentage.component"; +/** + * Dynamically calculate the ratio between two properties of the entity, + * as configured. + */ @DynamicComponent("DisplayDynamicPercentage") @Component({ selector: "app-display-dynamic-percentage", @@ -15,6 +19,10 @@ export class DisplayDynamicPercentageComponent extends ViewDirective< number, { total: string; actual: string; decimalPlaces?: number } > { + /** + * dynamically calculate the ratio of the actual / total values. + * This is defined as a function to re-calculate on every change detection cycle as the value remains outdated otherwise. + */ calculateValue() { if ( Number.isFinite(this.entity[this.config.actual]) && From e5d81234f080b58083622c34b35128fd1e310a9f Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Thu, 21 Sep 2023 17:14:37 +0200 Subject: [PATCH 19/19] fix tests --- .../display-dynamic-percentage.component.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts index 28f10f2375..d170c7c932 100644 --- a/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts +++ b/src/app/core/basic-datatypes/number/display-dyanamic-percentage/display-dynamic-percentage.component.spec.ts @@ -32,20 +32,17 @@ describe("DisplayDynamicPercentageComponent", () => { it("should display the correct percentage value", () => { component.entity["totalValue"] = 200; component.entity["actualValue"] = 50; - component.ngOnInit(); - expect(component.result).toEqual(25); + expect(component.calculateValue()).toEqual(25); }); it("should not display a value if one of the two values is not a number", () => { component.entity["totalValue"] = 15; - component.ngOnInit(); - expect(component.result).toBe(undefined); + expect(component.calculateValue()).toBe(undefined); }); it("should not display a value if totalValue is 0", () => { component.entity["totalValue"] = 0; component.entity["actualValue"] = 15; - component.ngOnInit(); - expect(component.result).toBe(undefined); + expect(component.calculateValue()).toBe(undefined); }); });