diff --git a/.eslintrc.json b/.eslintrc.json index f93cd6e7bf..97d9fbec73 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,7 +50,12 @@ } } ], + "rules": { + "prettier/prettier": "error" + }, + "plugins": ["prettier"], "extends": [ - "plugin:storybook/recommended" + "plugin:storybook/recommended", + "prettier" ] } diff --git a/.github/workflows/master-push.yml b/.github/workflows/master-push.yml index 5436506d5c..8e2b17ee24 100644 --- a/.github/workflows/master-push.yml +++ b/.github/workflows/master-push.yml @@ -65,18 +65,3 @@ jobs: uses: github/codeql-action/upload-sarif@v2 with: sarif_file: snyk.sarif - run-e2e-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Cypress run - uses: cypress-io/github-action@v4 - with: - command: npm run e2e - env: - # pass the Dashboard record key as an environment variable - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - # pass GitHub token to allow accurately detecting a build vs a re-run build - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # pass the project ID from the secrets through environment variable - CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} diff --git a/.github/workflows/pull-request-update.yml b/.github/workflows/pull-request-update.yml index a205fcff60..a93fbca683 100644 --- a/.github/workflows/pull-request-update.yml +++ b/.github/workflows/pull-request-update.yml @@ -73,15 +73,3 @@ jobs: cd /var/docker/pr-${{ github.event.number }} docker compose pull docker compose up -d - run-e2e-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Cypress run - uses: cypress-io/github-action@v5 - with: - command: npm run e2e - env: - COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} - # pass GitHub token to allow accurately detecting a build vs a re-run build - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..0567403f9e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,16 @@ +{ + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "html" + } + }, + { + "files": "*.component.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/build/Dockerfile b/build/Dockerfile index 5407c2439a..71006776cc 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -70,7 +70,7 @@ RUN if [ "$SENTRY_AUTH_TOKEN" != "" ] ; then \ ### PROD image -FROM nginx:1.25.2-alpine +FROM nginx:1.25.3-alpine COPY ./build/default.conf /etc/nginx/templates/default.conf COPY --from=builder /app/dist/ /usr/share/nginx/html # The port on which the app will run in the Docker container diff --git a/doc/compodoc_sources/concepts/configuration.md b/doc/compodoc_sources/concepts/configuration.md index 442393ebdc..863f5fc87e 100644 --- a/doc/compodoc_sources/concepts/configuration.md +++ b/doc/compodoc_sources/concepts/configuration.md @@ -35,7 +35,9 @@ The config service provides a behavior subject which will notify all subscribers This can be used for core tasks like setting up the routes or creating the navigation bar. Top-level "view" components (i.e. components that are used to define a whole page, not just some building block for a part or section) -receive their config data through the standard Angular router and can access it by injecting `ActivatedRoute`. +receive their config data automatically assigned as `@Input()` properties mapped from config object property name to an identical component class property. +This is handled by the `RoutedViewComponent` internally. +(If needed for special cases, you can also access it through the standard Angular router and can access it by injecting `ActivatedRoute`.) ### Storing config in DB diff --git a/doc/compodoc_sources/concepts/entities.md b/doc/compodoc_sources/concepts/entities.md new file mode 100644 index 0000000000..0a26de0dae --- /dev/null +++ b/doc/compodoc_sources/concepts/entities.md @@ -0,0 +1,10 @@ +# Entities & Entity Schema +----- +For us, an "Entity" is an object in the database (and a representation of something in the user's real world, e.g. a "Child" or "School"). +Entities are at the core of the Aam Digital platform and the primary way to customize the system is to adapt and add new entity types. + +The Entity Schema defines the data structure as well as how it is displayed in the UI. +Entity instances also have some generic functionality inherited from the `Entity` base class. + +------ +_see the sub-pages here for details of the various concepts related to the Entity system_ diff --git a/doc/compodoc_sources/concepts/entity-anonymization.md b/doc/compodoc_sources/concepts/entity-anonymization.md new file mode 100644 index 0000000000..421b176ea0 --- /dev/null +++ b/doc/compodoc_sources/concepts/entity-anonymization.md @@ -0,0 +1,39 @@ +# Archive / Anonymize Entities +----- +Any entity can be archived (i.e. marked as inactive and hidden from UI by default) or anonymized (i.e. discarding most data and keeping a few selected properties for statistical reports). +This is often preferable to deleting a record completely. Deleting data also affects statistical reports, even for previous time periods. +By anonymizing records, all personal identifiable data can be removed and the remaining stub record can be stored indefinitely, as it is not subject to data protection regulations like GDPR anymore. + +Anonymization is configured as part of the entity schema. +Data of fields that are not explicitly marked to be retained during anonymization is always deleted (anonymization by default). + +To keep some data even after the user "anonymized" a record, configure the `anonymize` property of the `@DatabaseField` decorator: +- `anonymize: "retain"` will keep this field unchanged and prevent it from being deleted +- `anonymize: "retain-anonymized"` will trigger a special "partial" deletion that depends on the dataType (e.g. date types will be changed to 1st July of the given year, thereby largely removing details but keeping data to calculate a rough age) + + +## Cascading anonymization / deletion +Relationships between entities are automatically handled when the user anonymizes or deletes an entity. +Any related entities that reference the anonymized/deleted entity are checked +and - depending on their configured role - may be updated or anonymized as well. + +The logic follows the scenarios shown below: +![](../../images/cascading-delete.png) + + +## Data Protection & GDPR regarding anonymization / pseudonomyzation +The "anonymize" function is implemented specifically for data protection rules requiring to delete personal data. +According to the EU's "General Data Protection Regulation" (GDPR) "anonymous" data does not fall under its regulations: + +- GDPR is not applicable to anonymous data: "The principles of data protection should therefore not apply to [...] personal data rendered anonymous in such a manner that the data subject is not or no longer identifiable." [GDPR Recital 26](https://gdpr-info.eu/recitals/no-26/) + - "To determine whether a natural person is identifiable, account should be taken of all the means reasonably likely to be used, such as singling out, either by the controller or by another person to identify the natural person directly or indirectly." + - "To ascertain whether means are reasonably likely to be used to identify the natural person, account should be taken of all objective factors, such as the costs of and the amount of time required for identification, taking into consideration the available technology at the time of the processing and technological developments." +- "Pseudonymisation enables the personal data to become unidentifiable unless more information is available whereas anonymization allows the processing of personal data to irreversibly prevent re-identification." [source](https://www.privacycompany.eu/blogpost-en/what-are-the-differences-between-anonymisation-and-pseudonymisation) +- _also see this [good overview of anonymization misunderstandings and considerations](https://edps.europa.eu/system/files/2021-04/21-04-27_aepd-edps_anonymisation_en_5.pdf)_ + +In the case of records being retained "anonymized" in Aam Digital, we provide a context that makes re-identification even harder: +- only authorized users of the system can access even the anonymized record (where only a few properties have been retained). Unless the organisation actively shares the data, it remains as securely protected as the personal data managed in Aam Digital. +- those authorized users with access to the anonymized records (and therefor a theoretical chance to attempt re-identification) are team members of an organization. They have been screened to be responsible persons and are usually legally bound to keep information confidential. +- by default only a few, explicitly selected properties in anonymized records are retained (data minimization by default). As such, both re-identification likelihood and the impact in case of re-identification are reduced as far as possible. + +--> If our anonymization process is configured thoughfully on a case by case basis to only retain a few data fields that are not easy indirect identifiers, it seems reasonably unlikely that the person can be identified after the anonymization process. Therefore, GDPR should not apply to these records and it is legitimate to retain these for statistical reporting. diff --git a/doc/compodoc_sources/concepts/entity-schema-system.md b/doc/compodoc_sources/concepts/entity-schema-system.md index cd9a715ae1..9d55ab7aad 100644 --- a/doc/compodoc_sources/concepts/entity-schema-system.md +++ b/doc/compodoc_sources/concepts/entity-schema-system.md @@ -1,8 +1,6 @@ # Entity Schema ----- The Entity Schema defines details of the properties of an entity type. -(An "entity" is an object of a certain type that users work with and save to the database, like "Child" or "School") - We define an entity type and its schema in code through a plain TypeScript class and some custom annotations. Read more on the background and practical considerations in [How to create a new Entity Type](../how-to-guides/create-a-new-entity-type.html). @@ -45,9 +43,16 @@ providing these through Angular dependency injection using `multi: true`. Also see: [How to create a new Datatype](../how-to-guides/create-a-new-datatype.html). -## Schema options +### Schema options The schema definitions contains information regarding the schema transformation as well as how a property can be displayed. The [EntitySchemaField](../../interfaces/EntitySchemaField.html) interface shows all configuration options. If the `editComponent` and the `viewComponent` are not set, the default components of this property's datatype will be used. The `description` field allows adding further explanation which will be displayed as a tooltip. + + +## Generic Entity functionalities + +### Metadata (created, updated) +Each record automatically holds basic data of timestamp and user who created and last updated the record. +(see `Entity` class) diff --git a/doc/compodoc_sources/concepts/extendability.md b/doc/compodoc_sources/concepts/extendability.md index ac6b8a8c29..de01065883 100644 --- a/doc/compodoc_sources/concepts/extendability.md +++ b/doc/compodoc_sources/concepts/extendability.md @@ -19,6 +19,7 @@ The following aspects are specifically designed to be extended: - defining a screen completely, including data loaded, etc. and hook it into the platforms navigation and overall layout - **Sub-Views** - defining a screen to display custom details for the entity currently loaded in the active route. The core platform takes care of passing the current entity and config details to the view as inputs. + - *also see [How to create an Entity Details Panel](../how-to-guides/create-an-entity-details-panel.html)* - **Dashboard Widgets** - filling the given card template with custom data and visualization - **Filters** diff --git a/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md b/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md new file mode 100644 index 0000000000..c288856047 --- /dev/null +++ b/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md @@ -0,0 +1,46 @@ +# How to create an Entity Details Panel Component +Aam Digital as a platform takes care of loading the relevant entity and configs for an "Entity Details View", +which displays the information about a single entity (e.g. one specific student). +The `EntityDetailsComponent` handles this initialization from the route. +To display some details in a customized way, you can create "panel components" that receive a reference to the current entity and their config parameters as `@Input()` automatically. + +![](../../images/entity-details-panels.png) +The tabs (in the config called "panels") within the Entity Details view can display one or more "panel components", as shown above. + +To make these components as re-usable and simple as possible, we do not load config from the route here. +Instead, the components are initialized from config by the `DynamicComponentDirective`. +This automatically sets `@Input()` properties to the value with the same name in the config object. + +Those background details aside, what that means for your implementation is: + +## Implementing a new Panel Component + +1. Create a new component class +2. Add an `@Input() entity: Entity;`. This will always contain the entity object, whose Details View is currently being displayed. You should not load this yourself from the database. +3. (If needed) Add more inputs for aspects that should be configurable about your component. +(e.g. `@Input() showDescription: boolean;`, which you can use in your template or code to adapt the component.) +These values are automatically set to whatever value is specified in the config object for your component at runtime in the database. +4. Register the new component in its parent module, so that it can be loaded under its name through the config. + +An example config for the above: +```json +{ + "component": "MySubView", + "config": { "showDescription": true } +} +``` + +Use the `ComponentRegistry` to register your component, +e.g. in its Module: +```javascript +export class MyModule { + constructor(components: ComponentRegistry) { + components.addAll([ + [ + "MySubView", // this is the name to use in the config document + () => import("./my-sub-view/my-sub-view.component").then((c) => c.MySubViewComponent), + ], + ]); + } +} +``` diff --git a/doc/compodoc_sources/summary.json b/doc/compodoc_sources/summary.json index 90c9d2796e..244192ddaa 100644 --- a/doc/compodoc_sources/summary.json +++ b/doc/compodoc_sources/summary.json @@ -38,8 +38,18 @@ "file": "concepts/extendability.md" }, { - "title": "Entity Schema", - "file": "concepts/entity-schema-system.md" + "title": "Entity System", + "file": "concepts/entities.md", + "children": [ + { + "title": "Entity Schema", + "file": "concepts/entity-schema-system.md" + }, + { + "title": "Archiving, Anonymizing and Deleting Entities", + "file": "concepts/entity-anonymization.md" + } + ] }, { "title": "Configuration", @@ -75,6 +85,28 @@ "title": "How-To Guides", "file": "how-to-guides/_index.md", "children": [ + { + "title": "Development Processes", + "file": "how-to-guides/contribute-code.md", + "children": [ + { + "title": "Write Automated Unit Tests", + "file": "how-to-guides/write-unit-tests.md" + }, + { + "title": "Write E2E Tests", + "file": "how-to-guides/write-e2e-tests.md" + }, + { + "title": "Document Code", + "file": "how-to-guides/write-documentation.md" + }, + { + "title": "Review a Pull Request", + "file": "how-to-guides/review-pull-request.md" + } + ] + }, { "title": "Configure and Customize a System", "file": "how-to-guides/configure-custom-system.md" @@ -83,14 +115,14 @@ "title": "Navigate the Code Structure", "file": "how-to-guides/navigate-code-structure.md" }, - { - "title": "Contribute Code to the Project", - "file": "how-to-guides/contribute-code.md" - }, { "title": "Create a New Entity Type", "file": "how-to-guides/create-new-entity-type.md" }, + { + "title": "Create an Entity Details Panel", + "file": "how-to-guides/create-entity-details-panel.md" + }, { "title": "Create a New Datatype", "file": "how-to-guides/create-new-datatype.md" @@ -115,18 +147,6 @@ "title": "Log Errors", "file": "how-to-guides/log-errors.md" }, - { - "title": "Write Automated Unit Tests", - "file": "how-to-guides/write-unit-tests.md" - }, - { - "title": "Write E2E Tests", - "file": "how-to-guides/write-e2e-tests.md" - }, - { - "title": "Document Code", - "file": "how-to-guides/write-documentation.md" - }, { "title": "Use Queries and Indices", "file": "how-to-guides/use-queries-and-indices.md" @@ -135,25 +155,23 @@ "title": "Generate Demo Data", "file": "how-to-guides/generate-demo-data.md" }, - { - "title": "Review a Pull Request", - "file": "how-to-guides/review-pull-request.md" - }, { "title": "Format Data Export", "file": "how-to-guides/exports.md" }, { - "title": "Build Localizable Components", - "file": "how-to-guides/build-localizable-components.md" - }, - { - "title": "Work with XLF", - "file": "how-to-guides/work-with-xlf.md" - }, - { - "title": "Add Another Language", - "file": "how-to-guides/add-another-language.md" + "title": "Build localizable (translatable) UI", + "file": "how-to-guides/build-localizable-components.md", + "children": [ + { + "title": "Work with XLF", + "file": "how-to-guides/work-with-xlf.md" + }, + { + "title": "Add Another Language", + "file": "how-to-guides/add-another-language.md" + } + ] } ] }, diff --git a/doc/images/cascading-delete.png b/doc/images/cascading-delete.png new file mode 100644 index 0000000000..178781d676 Binary files /dev/null and b/doc/images/cascading-delete.png differ diff --git a/doc/images/entity-details-panels.png b/doc/images/entity-details-panels.png new file mode 100644 index 0000000000..ab843106f4 Binary files /dev/null and b/doc/images/entity-details-panels.png differ diff --git a/e2e/integration/MarkingChildAsDropout.cy.ts b/e2e/integration/MarkingChildAsDropout.cy.ts deleted file mode 100644 index a0176beade..0000000000 --- a/e2e/integration/MarkingChildAsDropout.cy.ts +++ /dev/null @@ -1,42 +0,0 @@ -describe("Scenario: Marking a child as dropout - E2E test", function () { - before("GIVEN I am on the details page of a specific child", function () { - // go to a child - cy.visit("child"); - cy.get("tr").eq(2).click(); - }); - - it("WHEN I select a dropout date for this child", () => { - // click on "Dropout" menu - cy.contains("div", "Dropout").click(); - // click on button with the content "Edit" in Dropout menu. - cy.get(".form-buttons-wrapper:visible").contains("button", "Edit").click(); - // select today as the dropout date (which is initially marked as active) - cy.get('[aria-label="Open calendar"]').filter(":visible").click(); - // without wait, sometimes the panel is still open after clicking - cy.wait(100); - cy.get(".mat-calendar-body-active:visible").click(); - // click on button with the content "Save" - cy.get(".form-buttons-wrapper:visible").contains("button", "Save").click(); - // save the name of this Child to the variable - cy.get(".mat-title > .remove-margin-bottom").invoke("text").as("childName"); - }); - - it("THEN I should not see this child in the list of all children at first", function () { - // click on "Children" menu in navigation - cy.get('[ng-reflect-angulartics-label="Children"]').click(); - // type to the input "Filter" the name of child - cy.get('[placeholder="e.g. name, age"]').type(this.childName, { - force: true, - }); - // find at this table the name of child and it should not exist - cy.get("table").contains(this.childName.trim()).should("not.exist"); - }); - - it("AND I should see the child when I activate the 'inactive' filter", function () { - // click on the button with the content "Inactive" - cy.contains("span", "Active").click(); - cy.contains("span", "Inactive").should("be.visible").click(); - // find at this table the name of child and it should exist - cy.get("table").contains(this.childName.trim()).should("exist"); - }); -}); diff --git a/e2e/integration/RecordingAttendanceOfChild.cy.ts b/e2e/integration/RecordingAttendanceOfChild.cy.ts index be757f8c59..b9737967ef 100644 --- a/e2e/integration/RecordingAttendanceOfChild.cy.ts +++ b/e2e/integration/RecordingAttendanceOfChild.cy.ts @@ -9,7 +9,6 @@ describe("Scenario: Recording attendance of a child - E2E test", function () { }); it("AND I set the attendance of the specific child to 'present'", function () { - cy.contains("button", "Show more").click({ scrollBehavior: "center" }); cy.contains("mat-card", "School Class") .eq(0) .click({ scrollBehavior: "center" }); @@ -30,7 +29,7 @@ describe("Scenario: Recording attendance of a child - E2E test", function () { cy.get(".mat-calendar-body-active").should( "have.css", "background-color", - "rgb(200, 230, 201)" + "rgb(200, 230, 201)", ); }); }); diff --git a/package-lock.json b/package-lock.json index 25c1411d36..9fe2449906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,14 @@ "license": "GPL-3.0", "dependencies": { "@angular/animations": "^16.2.1", - "@angular/cdk": "^16.2.1", + "@angular/cdk": "^16.2.10", "@angular/common": "^16.2.1", "@angular/compiler": "^16.2.1", "@angular/core": "^16.2.1", "@angular/forms": "^16.2.1", "@angular/localize": "^16.2.1", - "@angular/material": "^16.2.1", - "@angular/material-moment-adapter": "^16.2.1", + "@angular/material": "^16.2.10", + "@angular/material-moment-adapter": "^16.2.10", "@angular/platform-browser": "^16.2.1", "@angular/platform-browser-dynamic": "^16.2.1", "@angular/router": "^16.2.1", @@ -31,15 +31,15 @@ "@fortawesome/free-regular-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ngneat/until-destroy": "^10.0.0", - "@sentry/browser": "^7.64.0", + "@sentry/browser": "^7.76.0", "angulartics2": "^12.2.1", "assert": "^2.0.0", - "crypto-es": "^2.0.4", + "crypto-es": "^2.1.0", "deep-object-diff": "^1.1.9", - "flag-icons": "^6.10.0", + "flag-icons": "^6.11.2", "hammerjs": "^2.0.8", "json-query": "^2.2.2", - "keycloak-js": "^22.0.1", + "keycloak-js": "^22.0.5", "leaflet": "^1.9.4", "lodash-es": "^4.17.21", "md5": "^2.3.0", @@ -91,6 +91,8 @@ "babel-loader": "^9.1.3", "cypress": "13.1.0", "eslint": "^8.47.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-storybook": "^0.6.13", "jasmine-core": "^5.1.0", "jasmine-spec-reporter": "^7.0.0", @@ -139,12 +141,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.2.tgz", - "integrity": "sha512-JFIeKKW7V2+/C8+pTReM6gfQkVU9l1IR1OCb9vvHWTRvuTr7E5h2L1rUInnmLiRWkEvkYfG29B+UPpYlkVl9oQ==", + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.6.tgz", + "integrity": "sha512-b1NNV3yNg6Rt86ms20bJIroWUI8ihaEwv5k+EoijEXLoMs4eNs5PhqL+QE8rTj+q9pa1gSrWf2blXor2JGwf1g==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.2", + "@angular-devkit/core": "16.2.6", "rxjs": "7.8.1" }, "engines": { @@ -154,15 +156,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.2.tgz", - "integrity": "sha512-j2lni4mN6NaMLT85sJUPSz/pNuaTCAYG3EYUeuMRNkC5keH/f4W0Tiuq6DxY4OLEF1JnEnfkp+k0Z84mEti/xA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.6.tgz", + "integrity": "sha512-QdU/q77K1P8CPEEZGxw1QqLcnA9ofboDWS7vcLRBmFmk2zydtLTApbK0P8GNDRbnmROOKkoaLo+xUTDJz9gvPA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1602.2", - "@angular-devkit/build-webpack": "0.1602.2", - "@angular-devkit/core": "16.2.2", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/build-webpack": "0.1602.6", + "@angular-devkit/core": "16.2.6", "@babel/core": "7.22.9", "@babel/generator": "7.22.9", "@babel/helper-annotate-as-pure": "7.22.5", @@ -174,7 +176,7 @@ "@babel/runtime": "7.22.6", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.2.2", + "@ngtools/webpack": "16.2.6", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -204,7 +206,7 @@ "parse5-html-rewriting-stream": "7.0.0", "picomatch": "2.3.1", "piscina": "4.0.0", - "postcss": "8.4.27", + "postcss": "8.4.31", "postcss-loader": "7.3.3", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", @@ -321,12 +323,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.2.tgz", - "integrity": "sha512-V9+tsBgNrXJPeabq9vJzN3Cfz9joaNOxs6l6M4XItcMGmAtzvxxGZ7qS5uRH1RE+SOMpYyh9uPY4QMHRNRD/gA==", + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.6.tgz", + "integrity": "sha512-BJPR6xdq7gRJ6bVWnZ81xHyH75j7lyLbegCXbvUNaM8TWVBkwWsSdqr2NQ717dNLLn5umg58SFpU/pWMq6CxMQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.2", + "@angular-devkit/architect": "0.1602.6", "rxjs": "7.8.1" }, "engines": { @@ -340,9 +342,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.2.tgz", - "integrity": "sha512-6H4FsvP3rLJaGiWpIhCFPS7ZeNoM4sSrnFtRhhecu6s7MidzE4aqzuGdzJpzLammw1KL+DuTlN0gpLtM1Bvcwg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.6.tgz", + "integrity": "sha512-iez/8NYXQT6fqVQLlKmZUIRkFUEZ88ACKbTwD4lBmk0+hXW+bQBxI7JOnE3C4zkcM2YeuTXIYsC5SebTKYiR4Q==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -367,12 +369,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.2.tgz", - "integrity": "sha512-KeXIlibVrQEwIKbR9GViLKc3m1SXi/xuSXgIvSv+22FNu5i91ScsAhYLe65sDUL6m6MM1XQQMS46XN1Z9bRqQw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.6.tgz", + "integrity": "sha512-PhpRYHCJ3WvZXmng6Qk8TXeQf83jeBMAf7AIzI8h0fgeBocOl97Xf7bZpLg6GymiU+rVn15igQ4Rz9rKAay8bQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.2", + "@angular-devkit/core": "16.2.6", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -385,9 +387,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.1.2.tgz", - "integrity": "sha512-Y95IBEWqzWA7SyIh5nlPuFasw/4lOILrAdY5Ji6tOpIJgNFoiR9K1UcH46i34r3384ApN8GEQJ7FlK6D6qCOJA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.2.0.tgz", + "integrity": "sha512-SZjXOi3YIjuX2CocuRsR2QH6k1ca9lRO6IMm0YIYMmBPFCRP2KFHkL6aQnXM6DSaymQNN2TXfpuvUd45NxhU1w==", "dev": true, "dependencies": { "@nx/devkit": "16.5.1", @@ -399,18 +401,18 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.1.2.tgz", - "integrity": "sha512-wDiHPFsKTijMcQUPNcoHOJ5kezIPCCbmDK6LHH7hAdAC/eDY9NHL5e4zQ2Xkf3/r1PFuwVLGTwwreEHlmeENDw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz", + "integrity": "sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.1.2.tgz", - "integrity": "sha512-lYVvoKUIOg/ez15yfN4zY2A++vnIeJe1xh2ADNTmmjSh2PFV24K9YOgrTbgrY3Ul9kzGDTBkvYqslq+IvMGdIw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.2.0.tgz", + "integrity": "sha512-zdiAIox1T+B71HL+A8m+1jWdU34nvPGLhCRw/uZNwHzknsF4tYzNQ9W7T/SC/g/2s1yT2yNosEVNJSGSFvunJg==", "dev": true, "dependencies": { - "@angular-eslint/utils": "16.1.2", + "@angular-eslint/utils": "16.2.0", "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { @@ -419,17 +421,17 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.1.2.tgz", - "integrity": "sha512-2qsoUgPg9Qp4EVUJRwWcJ+8JMxBb0ma3pNBjFmY6LOd59igRYorJKfWep4Nln1EicYRDRsCLzeLHO976+b1yaQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.2.0.tgz", + "integrity": "sha512-YFdQ6hHX6NlQj0lfogZwfyKjU8pqkJU+Zsk0ehjlXP8VfKFVmDeQT5/Xr6Df9C8pveC3hvq6Jgd8vo67S9Enxg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.2", - "@angular-eslint/utils": "16.1.2", + "@angular-eslint/bundled-angular-compiler": "16.2.0", + "@angular-eslint/utils": "16.2.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "aria-query": "5.3.0", - "axobject-query": "3.1.1" + "axobject-query": "3.2.1" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -437,13 +439,13 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.1.2.tgz", - "integrity": "sha512-319i47NU6nfaAaQTQYN7k320proTIBCueWGt+fbT11210CMqQriFmD+B85AatCwQgMgLd8Rhs1/F7YL2OOhegA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.2.0.tgz", + "integrity": "sha512-2JUVR7hAKx37mgWeDjvyWEMH5uSeeksYuaQT5wwlgIzgrO4BNFuqs6Rgyp2jiYa7BFMX/qHULSa+bSq5J5ceEA==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "16.1.2", - "@angular-eslint/eslint-plugin-template": "16.1.2", + "@angular-eslint/eslint-plugin": "16.2.0", + "@angular-eslint/eslint-plugin-template": "16.2.0", "@nx/devkit": "16.5.1", "ignore": "5.2.4", "nx": "16.5.1", @@ -455,12 +457,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.1.2.tgz", - "integrity": "sha512-vIkPOShVJLBEHYY3jISCVvJF3lXL//Y70J8T9lY2CBowgqp6AzzJ6cZU7JxrORN6b64rBUVvUtCGo8L36GvfuA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.2.0.tgz", + "integrity": "sha512-v2jVKTy2wN7iM9nHpBkxLn2wfL8jSl4IlPrXThIqj8No2VHtpLQZPKuXbGPUXQX05VS2Mj5feScQ36ZVGS8Rbw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.2", + "@angular-eslint/bundled-angular-compiler": "16.2.0", "eslint-scope": "^7.0.0" }, "peerDependencies": { @@ -469,12 +471,12 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.1.2.tgz", - "integrity": "sha512-2yfEK3BPSJsUhP4JCz0EB6ktu4E4+/zc9qdtZvPWNF/eww2J/oYVPjY47C/HVg4MXpjJTI8vbdkvcnxrICIkfw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.2.0.tgz", + "integrity": "sha512-NxMRwnlIgzmbJQfWkfd9y3Sz0hzjFdK5LH44i+3D5NhpPdZ6SzwHAjMYWoYsmmNQX5tlDXoicYF9Mz9Wz8DJ/A==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.1.2", + "@angular-eslint/bundled-angular-compiler": "16.2.0", "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { @@ -483,9 +485,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.5.tgz", - "integrity": "sha512-2reD50S9zWvhewRvwl320iuRICN9s0fI+3nKULlwcyJ0praLRhJ1SnaAK3NEEu7MWo3n9sb3iVTzA6S9qZRJ4g==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.9.tgz", + "integrity": "sha512-J+nsc2x/ZQuh+YwwTzxXUrV+7SBpJq6DDStfTFkZls9PWGRj9fjqQeRCWrfNLllpxopAEjhFkoyK06oSjcwqAw==", "dependencies": { "tslib": "^2.3.0" }, @@ -493,13 +495,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.5" + "@angular/core": "16.2.9" } }, "node_modules/@angular/cdk": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.4.tgz", - "integrity": "sha512-Hnh7Gs+gAkBnRYIMkDXRElEPAmBFas37isIfOtiqEmkgmSPFxsPpDOXK1soXeDk8U+yNmDWnO0fcHPp/pobHCw==", + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.10.tgz", + "integrity": "sha512-kOQrPxSMPi66aM9XfwZIjQXhH+q0PkhK4BNMHB9RkvaaQ34ovOrKaGsT7t0+sjlVhiwTiy2mB1Qgz6NlIB0ZZw==", "dependencies": { "tslib": "^2.3.0" }, @@ -513,15 +515,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.2.tgz", - "integrity": "sha512-PmhR/NMVVCiATXxHLkVCV781Q5aa5DaYye9+plZGX3rdKTilEunRNIfT13w7IuRfa0K/pKZj6PJU1S6yb7sqZg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.6.tgz", + "integrity": "sha512-9poPvUEmlufOAW1Cjk+aA5e2x3mInLtbYYSL/EYviDN2ugmavsSIvxAE/WLnxq6cPWqhNDbHDaqvcmqkcFM3Cw==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.2", - "@angular-devkit/core": "16.2.2", - "@angular-devkit/schematics": "16.2.2", - "@schematics/angular": "16.2.2", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", + "@schematics/angular": "16.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -547,9 +549,9 @@ } }, "node_modules/@angular/common": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.5.tgz", - "integrity": "sha512-MCPSZfPXTEqdkswPczivwjqV117YeVjObtyxZsDAwrTZHzYBtfQreQG1XJ1IRRgDncznP6ke0mdH9LyD2LgZKQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.9.tgz", + "integrity": "sha512-5Lh5KsxCkaoBDeSAghKNF5lCi0083ug4X2X7wnafsSd6Z3xt/rDjH9hDOP5SF5IDLtCVjJgHfs3cCLSTjRuNwg==", "dependencies": { "tslib": "^2.3.0" }, @@ -557,14 +559,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.5", + "@angular/core": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.5.tgz", - "integrity": "sha512-DpLfWWZFk4lbr81W7sLRt15+/nbyyqTvz+UmGcrSfKBTSbV0VSoUjC3XZeIdPWoIgQXiKUCpaC0YXw0BjaOl0g==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.9.tgz", + "integrity": "sha512-lh799pnbdvzTVShJHOY1JC6c1pwBsZC4UIgB3Itklo9dskGybQma/gP+lE6RhqM4FblNfaaBXGlCMUuY8HkmEQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -572,7 +574,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.5" + "@angular/core": "16.2.9" }, "peerDependenciesMeta": { "@angular/core": { @@ -581,9 +583,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.5.tgz", - "integrity": "sha512-6TtyFxro4iukVXhLlzxz7sVCMfAlNQhSYnizIJRSW31uQ0Uku8rjlUmX1tCAmhW6CacLumiz2tcy04Xn/QFWyw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz", + "integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==", "dependencies": { "@babel/core": "7.22.5", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -603,7 +605,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.5", + "@angular/compiler": "16.2.9", "typescript": ">=4.9.3 <5.2" } }, @@ -645,9 +647,9 @@ } }, "node_modules/@angular/core": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.5.tgz", - "integrity": "sha512-Po2LMUnPg23D2qI7EYaoA4x3lRswx9nxfpwROzfFPbMNJ3JVbTK0HkTD2dFPGxRua2UjfJTb1um23tEGO4OGMQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.9.tgz", + "integrity": "sha512-chvPX29ZBcMDuh7rLIgb0Cru6oJ/0FaqRzfOI3wT4W2F9W1HOlCtipovzmPYaUAmXBWfVP4EBO9TOWnpog0S0w==", "dependencies": { "tslib": "^2.3.0" }, @@ -660,9 +662,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.5.tgz", - "integrity": "sha512-iYJImRji1OiYIcC2mDBcXhtvPfAoEGT+HqZpivu+/ZPLuf+QegC9+ktJw90SQXR+xccmpkUb9MsJ52SN2MgkPA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.9.tgz", + "integrity": "sha512-rxlg2iNJNBH/uc7b5YqybfYc8BkLzzPv1d/nMsQUlY0O2UV2zwNRpcIiWbWd7+ZaKjcyPynVe9FsXC8wgWIABw==", "dependencies": { "tslib": "^2.3.0" }, @@ -670,16 +672,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.5", - "@angular/core": "16.2.5", - "@angular/platform-browser": "16.2.5", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.5.tgz", - "integrity": "sha512-vDtrBlbWOqtATqaBv3gmxBT0e8TfxwW+4J47S8u5Pbi1ZAnQfDkD9MNivC6/CAifFMcxN1pH8NALwLXOUga1PA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.9.tgz", + "integrity": "sha512-t5002NgBj+wjd81IXwg+yc2ypaBk6OWLAka1GXmWua3x7hwGw1yMtPFmzOE1cCNdXgWlluLxWclFjCUrAbGEww==", "dependencies": { "@babel/core": "7.22.5", "fast-glob": "3.3.0", @@ -694,8 +696,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.5", - "@angular/compiler-cli": "16.2.5" + "@angular/compiler": "16.2.9", + "@angular/compiler-cli": "16.2.9" } }, "node_modules/@angular/localize/node_modules/@babel/core": { @@ -751,9 +753,9 @@ } }, "node_modules/@angular/material": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.4.tgz", - "integrity": "sha512-TIZ/0MKObn5YU9n/VReghJJKqgkqyzrWVNEJ8UgOP6MV5o+kAbqLSmlDJEyjLIwJF0vPnJ3UP6qbEOfEi1OLaA==", + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.10.tgz", + "integrity": "sha512-0XhMwbcxpEESL11mVO8ycwxa+Jlh+8egOSRleD30zFUesqBA5EhtRpH8cqtna03f/xxtRq00Q315igMIMNiOSg==", "dependencies": { "@material/animation": "15.0.0-canary.bc9ae6c9c.0", "@material/auto-init": "15.0.0-canary.bc9ae6c9c.0", @@ -806,7 +808,7 @@ }, "peerDependencies": { "@angular/animations": "^16.0.0 || ^17.0.0", - "@angular/cdk": "16.2.4", + "@angular/cdk": "16.2.10", "@angular/common": "^16.0.0 || ^17.0.0", "@angular/core": "^16.0.0 || ^17.0.0", "@angular/forms": "^16.0.0 || ^17.0.0", @@ -815,22 +817,22 @@ } }, "node_modules/@angular/material-moment-adapter": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-16.2.4.tgz", - "integrity": "sha512-2kmSsgrSOzWf7B8lU6xJYYdvr0h++/BbyPWODK8g3cC9odtvIDxxZYnixmgBSgpnproLKwW/XGvbLlMUVzqc7Q==", + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-16.2.10.tgz", + "integrity": "sha512-Gus+TPy/RRuB+48fNeT9eMLfx0ZkQHw4Z9o0V3FoTFKa5JTfS4pHf+HoYFpcn6Ge+Y3MMt17dp+fyKAQSV9FSA==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "^16.0.0 || ^17.0.0", - "@angular/material": "16.2.4", + "@angular/material": "16.2.10", "moment": "^2.18.1" } }, "node_modules/@angular/platform-browser": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.5.tgz", - "integrity": "sha512-p+1GH/M4Vwoyp7brKkNBcMTxscoZxA1zehetFlNr8kArXWiISgPolyqOVzvT6cycYKu5uSRLnvHOTDss6xrAuA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.9.tgz", + "integrity": "sha512-9Je7+Jmx0AOyRzBBumraVJG3M0R6YbT4c9jTUbLGJCcPxwDI3/u2ZzvW3rBqpmrDaqLxN5f1LcZeTZx287QeqQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -838,9 +840,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.2.5", - "@angular/common": "16.2.5", - "@angular/core": "16.2.5" + "@angular/animations": "16.2.9", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9" }, "peerDependenciesMeta": { "@angular/animations": { @@ -849,9 +851,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.5.tgz", - "integrity": "sha512-kzC4z/KmLss8Du9uM1Q16r+3EqDExKKHnrb3G3tuEQ1jTvYCysdWoooVSBmtIlQUw13znpBm1B7XLoyviFvnwA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.9.tgz", + "integrity": "sha512-ztpo0939vTZ/5CWVSvo41Yl6YPoTZ0If+yTrs7dk1ce0vFgaZXMlc+y5ZwjJIiMM5CvHbhL48Uk+HJNIojP98A==", "dependencies": { "tslib": "^2.3.0" }, @@ -859,16 +861,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.5", - "@angular/compiler": "16.2.5", - "@angular/core": "16.2.5", - "@angular/platform-browser": "16.2.5" + "@angular/common": "16.2.9", + "@angular/compiler": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9" } }, "node_modules/@angular/router": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.5.tgz", - "integrity": "sha512-5IXhe6G7zYFUwHSfUgPw+I/q6M1AcfSyaOVcjMFQ94bVSWEMq5KrGCDc8HQtkdw7GqJ4txwbyQKSKp7khpqShQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.9.tgz", + "integrity": "sha512-5vrJNMblTDx3WC3dtaqLddWNtR0P9iwpqffeZL1uobBIwP4hbJx+8Dos3TwxGR4hnopFKahoDQ5nC0NOQslyog==", "dependencies": { "tslib": "^2.3.0" }, @@ -876,16 +878,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.5", - "@angular/core": "16.2.5", - "@angular/platform-browser": "16.2.5", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.5.tgz", - "integrity": "sha512-rHSFkrzyOunWwAQNtTC01ry2inrutlCad8MChK+fHCAhD2maWbNHtIelXR5ylojx7EyTUY0TPL30D60z2mXbwA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-16.2.9.tgz", + "integrity": "sha512-f7CRhSyOkSW2SeV0uQTigcprA2mi3Xw5xfl/ywEYqpVvgJI0dNyoclgF9pVA37geozhKBK7mhmw/FKK22BNvlg==", "dependencies": { "tslib": "^2.3.0" }, @@ -896,8 +898,8 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.5", - "@angular/core": "16.2.5" + "@angular/common": "16.2.9", + "@angular/core": "16.2.9" } }, "node_modules/@assemblyscript/loader": { @@ -936,29 +938,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.17.tgz", - "integrity": "sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", + "@babel/generator": "^7.23.0", "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.22.17", - "@babel/helpers": "^7.22.15", - "@babel/parser": "^7.22.16", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.17", - "@babel/types": "^7.22.17", - "convert-source-map": "^1.7.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", @@ -973,11 +975,11 @@ } }, "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -999,6 +1001,11 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1127,9 +1134,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1143,20 +1150,33 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1174,12 +1194,12 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", - "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -1197,15 +1217,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz", - "integrity": "sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.15" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1236,14 +1256,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz", - "integrity": "sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.17" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1253,13 +1273,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -1312,9 +1332,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz", - "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -1328,14 +1348,14 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz", - "integrity": "sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", - "@babel/types": "^7.22.17" + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" @@ -1356,13 +1376,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", - "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -1382,11 +1402,11 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -1395,9 +1415,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1508,6 +1528,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1833,14 +1870,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", + "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -1883,9 +1920,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", - "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", + "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1970,9 +2007,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", - "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", + "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2174,12 +2211,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", + "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2190,12 +2227,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", - "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", + "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -2207,15 +2244,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", - "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", + "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -2355,9 +2392,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", - "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", + "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2789,15 +2826,15 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.15.tgz", - "integrity": "sha512-HblhNmh6yM+cU4VwbBRpxFhxsTdfS1zsvH9W+gEjD0ARV9+8B4sNfpI6GuhePti84nuvhiwKS539jKPFHskA9A==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.2.tgz", + "integrity": "sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", "@babel/plugin-transform-typescript": "^7.22.15" }, "engines": { @@ -2976,18 +3013,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.17.tgz", - "integrity": "sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dependencies": { "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.17", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2996,11 +3033,11 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -3010,12 +3047,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", - "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.15", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3060,47 +3097,47 @@ } }, "node_modules/@compodoc/compodoc": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.21.tgz", - "integrity": "sha512-/FDlwRgKzmkGuorDnURuCzoGY6rZ0KY7Mj5/PwnEjMs0y1CRRPHBJC9I0yVIjn8j8gxVy8PPc6dna0lY1MWwhg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.22.tgz", + "integrity": "sha512-NmcEe9GDP90jtDKPlBU/PMyvVt6RjsGvraniVOH6Qgrm472FZu6tQotGd6mFQyqBabWS0uI2X3isuFJz1/Yj5Q==", "dev": true, "hasInstallScript": true, "dependencies": { - "@angular-devkit/schematics": "^16.0.1", - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", + "@angular-devkit/schematics": "14.2.12", + "@babel/core": "^7.23.2", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/preset-env": "^7.23.2", "@compodoc/live-server": "^1.2.3", "@compodoc/ngd-transformer": "^2.1.3", "chalk": "4.1.2", "cheerio": "^1.0.0-rc.12", "chokidar": "^3.5.3", "colors": "1.4.0", - "commander": "^10.0.1", - "cosmiconfig": "^8.1.3", - "decache": "^4.6.1", + "commander": "^11.1.0", + "cosmiconfig": "^8.3.6", + "decache": "^4.6.2", "fancy-log": "^2.0.0", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.1", "fs-extra": "^11.1.1", - "glob": "^10.2.4", - "handlebars": "^4.7.7", - "html-entities": "^2.3.3", - "i18next": "^22.4.15", - "inside": "^1.0.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "html-entities": "^2.4.0", + "i18next": "^23.5.1", "json5": "^2.2.3", "lodash": "^4.17.21", "loglevel": "^1.8.1", "loglevel-plugin-prefix": "^0.8.4", "lunr": "^2.3.9", - "marked": "4.3.0", + "marked": "7.0.3", "minimist": "^1.2.8", "opencollective-postinstall": "^2.0.3", "os-name": "4.0.1", "pdfjs-dist": "2.12.313", "pdfmake": "^0.2.7", - "semver": "^7.5.1", + "semver": "^7.5.4", "traverse": "^0.6.7", - "ts-morph": "^18.0.0", - "uuid": "^9.0.0" + "ts-morph": "^20.0.0", + "uuid": "^9.0.1" }, "bin": { "compodoc": "bin/index-cli.js" @@ -3109,6 +3146,183 @@ "node": ">= 14.0.0" } }, + "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/core": { + "version": "14.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.12.tgz", + "integrity": "sha512-tg1+deEZdm3fgk2BQ6y7tujciL6qhtN5Ums266lX//kAZeZ4nNNXTBT+oY5xgfjvmLbW+xKg0XZrAS0oIRKY5g==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics": { + "version": "14.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.12.tgz", + "integrity": "sha512-MN5yGR+SSSPPBBVMf4cifDJn9u0IYvxiHst+HWokH2AkBYy+vB1x8jYES2l1wkiISD7nvjTixfqX+Y95oMBoLg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.12", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@compodoc/compodoc/node_modules/@babel/preset-env": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", + "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.2", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.23.2", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.23.0", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.23.0", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.23.0", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-modules-systemjs": "^7.23.0", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.23.0", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.23.0", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@compodoc/compodoc/node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@compodoc/compodoc/node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@compodoc/compodoc/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@compodoc/compodoc/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3149,6 +3363,48 @@ "node": ">=8" } }, + "node_modules/@compodoc/compodoc/node_modules/jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", + "dev": true + }, + "node_modules/@compodoc/compodoc/node_modules/magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@compodoc/compodoc/node_modules/marked": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.3.tgz", + "integrity": "sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@compodoc/compodoc/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, "node_modules/@compodoc/compodoc/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3161,6 +3417,12 @@ "node": ">=8" } }, + "node_modules/@compodoc/compodoc/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/@compodoc/live-server": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz", @@ -3761,9 +4023,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", - "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -3815,9 +4077,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3860,18 +4122,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@faker-js/faker": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", - "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.2.0.tgz", + "integrity": "sha512-VacmzZqVxdWdf9y64lDOMZNDMM/FQdtM9IsaOPKOm2suYwEatb8VkdHqOzXcDnZbk7YDE2BmsJmy/2Hmkn563g==", "funding": [ { "type": "opencollective", @@ -3890,22 +4152,22 @@ "dev": true }, "node_modules/@floating-ui/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", - "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", "dev": true, "dependencies": { - "@floating-ui/utils": "^0.1.1" + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.2.tgz", - "integrity": "sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "dev": true, "dependencies": { - "@floating-ui/core": "^1.4.1", - "@floating-ui/utils": "^0.1.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/react-dom": { @@ -3922,9 +4184,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.2.tgz", - "integrity": "sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", "dev": true }, "node_modules/@foliojs-fork/fontkit": { @@ -3945,23 +4207,6 @@ "unicode-trie": "^2.0.0" } }, - "node_modules/@foliojs-fork/fontkit/node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@foliojs-fork/linebreak": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.1.tgz", @@ -4469,9 +4714,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5282,9 +5527,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.2.tgz", - "integrity": "sha512-BDZ2yyXdzVE8kILOM0lhRpmKlvfLMluuZvqVa1r5dHkjCLbyOo1jXoYTCXvrQ2JU5GXc/MBBLXwmIHgtPWk8/A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.6.tgz", + "integrity": "sha512-d8ZlZL6dOtWmHdjG9PTGBkdiJMcsXD2tp6WeFRVvTEuvCI3XvKsUXBvJDE+mZOhzn5pUEYt+1TR5DHjDZbME3w==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -5726,20 +5971,20 @@ } }, "node_modules/@percy/cli": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.27.1.tgz", - "integrity": "sha512-2rU4NFe8TshV2sF+fs8bseGNohpLuRILW4t/Is7PCFYAitla1nvO0mndy0INct5VWc2KKewnq+2ZQONx3iNXLw==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.27.3.tgz", + "integrity": "sha512-GyAIk9wm32cheYAeVyY6nOkDcGgSeG7wgLhZJWzi0gVhFE9W04sudWa++7akEqpGLMnTM2Q4OZv5P3cy1F9iPA==", "dev": true, "dependencies": { - "@percy/cli-app": "1.27.1", - "@percy/cli-build": "1.27.1", - "@percy/cli-command": "1.27.1", - "@percy/cli-config": "1.27.1", - "@percy/cli-exec": "1.27.1", - "@percy/cli-snapshot": "1.27.1", - "@percy/cli-upload": "1.27.1", - "@percy/client": "1.27.1", - "@percy/logger": "1.27.1" + "@percy/cli-app": "1.27.3", + "@percy/cli-build": "1.27.3", + "@percy/cli-command": "1.27.3", + "@percy/cli-config": "1.27.3", + "@percy/cli-exec": "1.27.3", + "@percy/cli-snapshot": "1.27.3", + "@percy/cli-upload": "1.27.3", + "@percy/client": "1.27.3", + "@percy/logger": "1.27.3" }, "bin": { "percy": "bin/run.cjs" @@ -5749,39 +5994,39 @@ } }, "node_modules/@percy/cli-app": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.27.1.tgz", - "integrity": "sha512-Pat2BogPUmyciURUWbI1PQtUdPlhUrnS6aEVO1gNrulDeYF6j4LgD4SycCTj5L/xJijMXa3qFkZwW1VSdx97Yg==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.27.3.tgz", + "integrity": "sha512-RUSzWTRkG4hjUy7dzu+zlKVlbXEcj3yoiFTpjrmUxoK5QMVQtZfDdkHfJNSI51w1ZRzuxmO5x5uqFRpgUKh8QQ==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1", - "@percy/cli-exec": "1.27.1" + "@percy/cli-command": "1.27.3", + "@percy/cli-exec": "1.27.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-build": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.27.1.tgz", - "integrity": "sha512-EtktlYj4kzsBkrBSOVLKrvZWew/nq9mw/Z2m8mU4Jf7Pc779rQGUVCuI8PBhOI59c3JTjnY8KVaaj4Y+z9X+fg==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.27.3.tgz", + "integrity": "sha512-F7vWMmXipINDO9kvDyaPaIeUZlZLfIbg74oRaAJUOVH2nkf0FNSPy7Dy/MEsFWAiKNeW7/UwHjuYl+I/yMBZPw==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1" + "@percy/cli-command": "1.27.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-command": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.27.1.tgz", - "integrity": "sha512-LK9YwE7qr67EtShVVjc20aJaRF8BFjZ8VWneLVoN73IYgpwBq96J4AUomoqfRpPNkpKvv6wDZFpr30xFokbrHA==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.27.3.tgz", + "integrity": "sha512-aU76lu2a2/N/BUSsXpLXRzeJeXzmAcA3s9EFDeMNDMZY/7dV6wlLX9r7n931UD3bIhdlkl1o7COXIIATtPymJQ==", "dev": true, "dependencies": { - "@percy/config": "1.27.1", - "@percy/core": "1.27.1", - "@percy/logger": "1.27.1" + "@percy/config": "1.27.3", + "@percy/core": "1.27.3", + "@percy/logger": "1.27.3" }, "bin": { "percy-cli-readme": "bin/readme.js" @@ -5791,24 +6036,24 @@ } }, "node_modules/@percy/cli-config": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.27.1.tgz", - "integrity": "sha512-bEzXfdP+RRxHekZ5pULsuuKHL0K9E81IFcEqZ8DcE3gnrpWQnIFPP2fTHF6WgWuXYHzJnsoIIm90LaWYc4+BJQ==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.27.3.tgz", + "integrity": "sha512-+gUZUlbpgllux0yJnCFYMuNn1kItfIP9F1AIYqdqFM9KLixodMCWAXDUQ4V4EVLDjOR7fD3NKmyoFUFJgwcIww==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1" + "@percy/cli-command": "1.27.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-exec": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.27.1.tgz", - "integrity": "sha512-8xB7Iq3fh+QfuViUeZQXZeJNoFzDf0IhYUhhlEDXPcL2sgjrLiy9QHBdq6Yn3/8gHlDJyzpkej6hzFqERYZs1g==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.27.3.tgz", + "integrity": "sha512-PuSxfZKWx9BoeCbt5U1Edc+fv5kb/mYr+9sgoBsoHmwe5iNIgYKx6D+wmNK7FHgc82mycNLaEguew2JwJXlPew==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1", + "@percy/cli-command": "1.27.3", "cross-spawn": "^7.0.3", "which": "^2.0.2" }, @@ -5817,12 +6062,12 @@ } }, "node_modules/@percy/cli-snapshot": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.27.1.tgz", - "integrity": "sha512-hVYATkHSN6Mz67LuP5QqRvdblg+kLb9dqGlaMJZI0CtuQYRtkbrlPz5yII9SJGMyUYMQHbZKsc68eT7+LZHKbA==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.27.3.tgz", + "integrity": "sha512-HU7p5FLJ1W+QM3mbq8MFyqukthFnxPXrBAC7O0Dh4GfxXfdThV1HOdXqCEZ1pj1b3XkEM5SvvLKc7mClnc4D1Q==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1", + "@percy/cli-command": "1.27.3", "yaml": "^2.0.0" }, "engines": { @@ -5830,12 +6075,12 @@ } }, "node_modules/@percy/cli-upload": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.27.1.tgz", - "integrity": "sha512-4Kcft6ceuWy+Q5T4PKJXI63/QxCRFtCJUoU0QYrWo6TKKsescdE7/zWy6YESqHIm+XzBhZFYHwdYRsIbnjqUqQ==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.27.3.tgz", + "integrity": "sha512-sqzOAMqFKXhpuXDGAK5zUf5Aw/xgBbAP4o4y43dRBi6gIgf8DiiLuc+avyfcKm+2lW5cUTG9SPETVCpu03WozQ==", "dev": true, "dependencies": { - "@percy/cli-command": "1.27.1", + "@percy/cli-command": "1.27.3", "fast-glob": "^3.2.11", "image-size": "^1.0.0" }, @@ -5844,25 +6089,25 @@ } }, "node_modules/@percy/client": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.27.1.tgz", - "integrity": "sha512-GvjrGUaVdjMwx8ODDja3Kdb6tXYxRxv4PLXfXg0Wbn5jIfcjicojOc0mSGxcLvRGu5tmKrE3fusMhYYtKlLihg==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.27.3.tgz", + "integrity": "sha512-AuVpiuknK8OiDIN7BpkDRTYx6yAQVI4TjLFy/QmfZDK0PQMH+2dNIBi/pXooC5Ac64I7BYVqdaUk2IbdxEXbqQ==", "dev": true, "dependencies": { - "@percy/env": "1.27.1", - "@percy/logger": "1.27.1" + "@percy/env": "1.27.3", + "@percy/logger": "1.27.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/config": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.27.1.tgz", - "integrity": "sha512-O7GgSrpjPeRmUmPWpFrg7368lWTJj8BtKY18Ztpq2j7bef5HCFi0AxvuOjwIH/GEwzGm/cI7ZK2kA/XGTOtpsg==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.27.3.tgz", + "integrity": "sha512-hxp/zQz/3IQcyBbMkmvvnlkwMn9tgYFAOAFtZqECruqQ1M9XE10uYtWjvVwt9s4rlJEB4PWFvAV4c04/CT6ugQ==", "dev": true, "dependencies": { - "@percy/logger": "1.27.1", + "@percy/logger": "1.27.3", "ajv": "^8.6.2", "cosmiconfig": "^8.0.0", "yaml": "^2.0.0" @@ -5872,17 +6117,17 @@ } }, "node_modules/@percy/core": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.27.1.tgz", - "integrity": "sha512-/XsOCk/XHYf9MAqcituMGrYeITiav9BW1oGM0LfmU1Gne/YnEvur4JwpJ4uJ0OrNennwIqMuAVM9Y5y/xfZ39w==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.27.3.tgz", + "integrity": "sha512-LK+7dVLRQdVEQUG6Qn61awLbdYmazLwXWYg2UHqEWrbRIygkhoe+n3pPTwzxjiJxJsYYlelx/gDzGwjYzvV4aA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@percy/client": "1.27.1", - "@percy/config": "1.27.1", - "@percy/dom": "1.27.1", - "@percy/logger": "1.27.1", - "@percy/webdriver-utils": "1.27.1", + "@percy/client": "1.27.3", + "@percy/config": "1.27.3", + "@percy/dom": "1.27.3", + "@percy/logger": "1.27.3", + "@percy/webdriver-utils": "1.27.3", "content-disposition": "^0.5.4", "cross-spawn": "^7.0.3", "extract-zip": "^2.0.1", @@ -5898,45 +6143,45 @@ } }, "node_modules/@percy/dom": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.27.1.tgz", - "integrity": "sha512-duYA3ATjADtn05VuGIfezgdCq+8ASwcY6Mzk857DzFIlJd/6T2P8v1dy66RRp78efzmIMtRroGy9SqWonlaAEA==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.27.3.tgz", + "integrity": "sha512-QVSmogZhlVjy5mzqSesWbS5hjV8+1gMtAXSLtfRjdkZGQ/h2rnDbRZUW4PoJQ9ZpxBZegU5/W9LFvEO3FMVelA==", "dev": true }, "node_modules/@percy/env": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.27.1.tgz", - "integrity": "sha512-Og0vQfV9zJftYYOY3PVtu+r7Ut/xr72BP3jH3rkeQJHnFKLkwemGbJpgSpMz7IbzWXSxYONkNfUtLvXwEsRpAw==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.27.3.tgz", + "integrity": "sha512-f9NwjAmcN6HEfJ2/32fmBwUJ3xjn/JXmGUKqmSvw0L3WZuvKaYRpYtaI+n0B6NsujZ4R+KUEGr0jczEoQ62UEg==", "dev": true, "dependencies": { - "@percy/logger": "1.27.1" + "@percy/logger": "1.27.3" }, "engines": { "node": ">=14" } }, "node_modules/@percy/logger": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.27.1.tgz", - "integrity": "sha512-jH++paKzIY94fQYQBEedVTfO36I8WBqm2RNl/xzsnLuAwNEX5+JUsaPcbLrwRzhn/tCv+8h9xEBuE9nj23krIA==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.27.3.tgz", + "integrity": "sha512-Z/XoEakUHVBivZqmRwFTwiOWT6v5nJgQ6V9aNs2FzjcpyFlJAFZTmHv8+craDKehmnND3tp8FGdPe34Ty0JhEQ==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@percy/sdk-utils": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.27.1.tgz", - "integrity": "sha512-I12rBQYySyt8VILgYnI19obYtkaIPuFR07HVBrHLsRHiLUf92XzAtTI482qrnYwybCgU9mb9o3Kb7KtMJ0nalA==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.27.3.tgz", + "integrity": "sha512-IhNx5VaYsKiHaFYFbX6BsVb4bNWojNrKyDa9aRF1afAWAeVWHd1Jq/XeVfMDNCrYcsrA+pYDPSXfb+N5cpAeGQ==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@percy/storybook": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@percy/storybook/-/storybook-4.3.6.tgz", - "integrity": "sha512-tLAVH86schBfq4NTMNIEDVABTnuVbrx9IKL7s5/2ONZqKsgJfgVBPBjSF38A1HyomSnD2mwYzZ1xR76G3usyxA==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@percy/storybook/-/storybook-4.3.7.tgz", + "integrity": "sha512-8Tu0EZfZ7Y0suw5iElXjJSMzcyO9pE9IQfzlbbE2tHj8yeQqokOeVNYKPcuKNR21iJfBSFH1fYQZAE1qGuOv+w==", "dev": true, "dependencies": { "@percy/cli-command": "^1.24.0", @@ -5948,13 +6193,13 @@ } }, "node_modules/@percy/webdriver-utils": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.27.1.tgz", - "integrity": "sha512-bPj16YmvV84egtMHyYEhlGYjwJTk0f6vopcbCYWgMgvQp78Y/RKwRm8ZpWZfW2rTqElNRudrhKdqcEeaG6qd9g==", + "version": "1.27.3", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.27.3.tgz", + "integrity": "sha512-yV+7nc8aUPQHP1KnWwTndVHqvgs4xzCA8ObzRFuAaKSTC/hBgpI5ceCDv3aSM03ZtXk4f06AWtsgpU5BYpoP1A==", "dev": true, "dependencies": { - "@percy/config": "1.27.1", - "@percy/sdk-utils": "1.27.1" + "@percy/config": "1.27.3", + "@percy/sdk-utils": "1.27.3" }, "engines": { "node": ">=14" @@ -5970,6 +6215,56 @@ "node": ">=14" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@pkgr/utils/node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -6634,13 +6929,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.2.tgz", - "integrity": "sha512-OqPhpodkQx9pzSz7H2AGeEbf3ut6WOkJFP2YlX2JIGholfG/0FQMJmfTEyRoFXCBeVIDGt3sOmlfK7An0PS8uA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.6.tgz", + "integrity": "sha512-fM09WPqST+nhVGV5Q3fhG7WKo96kgSVMsbz3wGS0DmTn4zge7ZWnrW3VvbxnMapmGoKa9DFPqdqNln4ADcdIMQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.2", - "@angular-devkit/schematics": "16.2.2", + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", "jsonc-parser": "3.2.0" }, "engines": { @@ -6650,76 +6945,73 @@ } }, "node_modules/@sentry-internal/tracing": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.69.0.tgz", - "integrity": "sha512-4BgeWZUj9MO6IgfO93C9ocP3+AdngqujF/+zB2rFdUe+y9S6koDyUC7jr9Knds/0Ta72N/0D6PwhgSCpHK8s0Q==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.76.0.tgz", + "integrity": "sha512-QQVIv+LS2sbGf/e5P2dRisHzXpy02dAcLqENLPG4sZ9otRaFNjdFYEqnlJ4qko+ORpJGQEQp/BX7Q/qzZQHlAg==", "dependencies": { - "@sentry/core": "7.69.0", - "@sentry/types": "7.69.0", - "@sentry/utils": "7.69.0", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/core": "7.76.0", + "@sentry/types": "7.76.0", + "@sentry/utils": "7.76.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/browser": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.69.0.tgz", - "integrity": "sha512-5ls+zu2PrMhHCIIhclKQsWX5u6WH0Ez5/GgrCMZTtZ1d70ukGSRUvpZG9qGf5Cw1ezS1LY+1HCc3whf8x8lyPw==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.76.0.tgz", + "integrity": "sha512-83xA+cWrBhhkNuMllW5ucFsEO2NlUh2iBYtmg07lp3fyVW+6+b1yMKRnc4RFArJ+Wcq6UO+qk2ZEvrSAts1wEw==", "dependencies": { - "@sentry-internal/tracing": "7.69.0", - "@sentry/core": "7.69.0", - "@sentry/replay": "7.69.0", - "@sentry/types": "7.69.0", - "@sentry/utils": "7.69.0", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry-internal/tracing": "7.76.0", + "@sentry/core": "7.76.0", + "@sentry/replay": "7.76.0", + "@sentry/types": "7.76.0", + "@sentry/utils": "7.76.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/core": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.69.0.tgz", - "integrity": "sha512-V6jvK2lS8bhqZDMFUtvwe2XvNstFQf5A+2LMKCNBOV/NN6eSAAd6THwEpginabjet9dHsNRmMk7WNKvrUfQhZw==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.76.0.tgz", + "integrity": "sha512-M+ptkCTeCNf6fn7p2MmEb1Wd9/JXUWxIT/0QEc+t11DNR4FYy1ZP2O9Zb3Zp2XacO7ORrlL3Yc+VIfl5JTgjfw==", "dependencies": { - "@sentry/types": "7.69.0", - "@sentry/utils": "7.69.0", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/types": "7.76.0", + "@sentry/utils": "7.76.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/replay": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.69.0.tgz", - "integrity": "sha512-oUqWyBPFUgShdVvgJtV65EQH9pVDmoYVQMOu59JI6FHVeL3ald7R5Mvz6GaNLXsirvvhp0yAkcAd2hc5Xi6hDw==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.76.0.tgz", + "integrity": "sha512-OACT7MfMHC/YGKnKST8SF1d6znr3Yu8fpUpfVVh2t9TNeh3+cQJVTOliHDqLy+k9Ljd5FtitgSn4IHtseCHDLQ==", "dependencies": { - "@sentry/core": "7.69.0", - "@sentry/types": "7.69.0", - "@sentry/utils": "7.69.0" + "@sentry-internal/tracing": "7.76.0", + "@sentry/core": "7.76.0", + "@sentry/types": "7.76.0", + "@sentry/utils": "7.76.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/types": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.69.0.tgz", - "integrity": "sha512-zPyCox0mzitzU6SIa1KIbNoJAInYDdUpdiA+PoUmMn2hFMH1llGU/cS7f4w/mAsssTlbtlBi72RMnWUCy578bw==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.76.0.tgz", + "integrity": "sha512-vj6z+EAbVrKAXmJPxSv/clpwS9QjPqzkraMFk2hIdE/kii8s8kwnkBwTSpIrNc8GnzV3qYC4r3qD+BXDxAGPaw==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.69.0.tgz", - "integrity": "sha512-4eBixe5Y+0EGVU95R4NxH3jkkjtkE4/CmSZD4In8SCkWGSauogePtq6hyiLsZuP1QHdpPb9Kt0+zYiBb2LouBA==", + "version": "7.76.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.76.0.tgz", + "integrity": "sha512-40jFD+yfQaKpFYINghdhovzec4IEpB7aAuyH/GtE7E0gLpcqnC72r55krEIVILfqIR2Mlr5OKUzyeoCyWAU/yw==", "dependencies": { - "@sentry/types": "7.69.0", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/types": "7.76.0" }, "engines": { "node": ">=8" @@ -6786,19 +7078,19 @@ "dev": true }, "node_modules/@storybook/addon-actions": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.4.1.tgz", - "integrity": "sha512-ZCrBUpCAxgMCrcMGvBOhh+8uUZ9HhoCIOfV1XiaTXpE9Y2lqIqfRsc18E/ST3zN25Waf/LcJPJF2Dp/VSSoGpA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.5.0.tgz", + "integrity": "sha512-eeHIFpZXGyhkfmrbHRf3rndL+ppFqlKTgN74y+UfFaAWNUhV3caXxRbHV3BbcPSLkRAsNShBH9hTNTlUAHSVjA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "dequal": "^2.0.2", "lodash": "^4.17.21", "polished": "^4.2.2", @@ -6826,19 +7118,19 @@ } }, "node_modules/@storybook/addon-backgrounds": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.4.1.tgz", - "integrity": "sha512-srmY6S9RAYkApjy49lYwKMFDpRp1XCws0pwHV0QoRBl7zibqUwr3PexkryK0uopPDhnfZRtRykPG5gzePNntmA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.5.0.tgz", + "integrity": "sha512-Yu/eFHZIfyAhK28GKKcIBwj/9+hRem8pSdI3N0FJuOhErmaE0zg6VDUBzkgLa/Fn9SwC5PNyAeLAtxssg1KSNg==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "memoizerific": "^1.11.3", "ts-dedent": "^2.0.0" }, @@ -6860,21 +7152,21 @@ } }, "node_modules/@storybook/addon-controls": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.4.1.tgz", - "integrity": "sha512-KlCYprhBerAKItVQKpexR1oParTbNDOZpJbonG+uldZ12FV7kkrTEGD1vwoLtYTLy+QXIGg4MI1cmUpd39LrLg==", - "dev": true, - "dependencies": { - "@storybook/blocks": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/core-events": "7.4.1", - "@storybook/manager-api": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.5.0.tgz", + "integrity": "sha512-X56Pd+0GH1A8ddVsziJQaJ8qCaxsWK0aLCKH5li9GLtnyIGHvd5+KvvfYEbjTkeJv3d9J7X0D4uTAH1/dsmI8w==", + "dev": true, + "dependencies": { + "@storybook/blocks": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/core-events": "7.5.0", + "@storybook/manager-api": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "lodash": "^4.17.21", "ts-dedent": "^2.0.0" }, @@ -6896,26 +7188,26 @@ } }, "node_modules/@storybook/addon-docs": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.4.1.tgz", - "integrity": "sha512-rhLeIX30Z/UsCp7tKtUJyGXWJ2Wggtkl+n6hyaW3orQlSQbsndqJ1rGIs0lHScrDv0dKwT2Dcp2WaEXWHRmgEw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.5.0.tgz", + "integrity": "sha512-lgrum81iJT+i85kO3uOR4wR1t05x4SmJLCB2cyYohCIafiOiV4FuyYFhvT9N6UhHByOfrWgpipKgKg6zsmV2eg==", "dev": true, "dependencies": { "@jest/transform": "^29.3.1", "@mdx-js/react": "^2.1.5", - "@storybook/blocks": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/csf-plugin": "7.4.1", - "@storybook/csf-tools": "7.4.1", + "@storybook/blocks": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/csf-plugin": "7.5.0", + "@storybook/csf-tools": "7.5.0", "@storybook/global": "^5.0.0", "@storybook/mdx2-csf": "^1.0.0", - "@storybook/node-logger": "7.4.1", - "@storybook/postinstall": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/react-dom-shim": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/node-logger": "7.5.0", + "@storybook/postinstall": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/react-dom-shim": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "fs-extra": "^11.1.0", "remark-external-links": "^8.0.0", "remark-slug": "^6.0.0", @@ -6931,24 +7223,24 @@ } }, "node_modules/@storybook/addon-essentials": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.4.1.tgz", - "integrity": "sha512-Ma63h7gQ2uQgMBvMYlrevurqtzbXFfyuHgYp1PZrhFUCuiC7f1yKkxp5X+jLcfXrG2IsPIuBxLBMYtSpRu6izA==", - "dev": true, - "dependencies": { - "@storybook/addon-actions": "7.4.1", - "@storybook/addon-backgrounds": "7.4.1", - "@storybook/addon-controls": "7.4.1", - "@storybook/addon-docs": "7.4.1", - "@storybook/addon-highlight": "7.4.1", - "@storybook/addon-measure": "7.4.1", - "@storybook/addon-outline": "7.4.1", - "@storybook/addon-toolbars": "7.4.1", - "@storybook/addon-viewport": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/manager-api": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/preview-api": "7.4.1", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.5.0.tgz", + "integrity": "sha512-CKPHdQBP6psTVb3NHsP8cWSUcAA4kwzT8SrJxKddn4ecqmWJWeZo5g5y3WuqVQHlv3edpluJLQYehcVibcljag==", + "dev": true, + "dependencies": { + "@storybook/addon-actions": "7.5.0", + "@storybook/addon-backgrounds": "7.5.0", + "@storybook/addon-controls": "7.5.0", + "@storybook/addon-docs": "7.5.0", + "@storybook/addon-highlight": "7.5.0", + "@storybook/addon-measure": "7.5.0", + "@storybook/addon-outline": "7.5.0", + "@storybook/addon-toolbars": "7.5.0", + "@storybook/addon-viewport": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/manager-api": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/preview-api": "7.5.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -6961,14 +7253,14 @@ } }, "node_modules/@storybook/addon-highlight": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.4.1.tgz", - "integrity": "sha512-7fD3//+FHOankINRhPnAuW2gLNC7oJMT0eFD0sHrQPG5qMpR+T7u8mqyI05kPszyiY9U72LRfjrf8GL1Hac8gQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.5.0.tgz", + "integrity": "sha512-6SlEkGCZ/LnEcbN6oE2Au3fgI9VfULErWQ36bx+sV6WWTb1EoooiD7ZJJzobrcOAriSyfWoctO5DF4W+X9I8lg==", "dev": true, "dependencies": { - "@storybook/core-events": "7.4.1", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/preview-api": "7.4.1" + "@storybook/preview-api": "7.5.0" }, "funding": { "type": "opencollective", @@ -6976,18 +7268,18 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.4.1.tgz", - "integrity": "sha512-OFRBGlA8Bs04vJe2dAP2KK+Juus0JrdfLeeW0wm1RQGYCHJZb0awiI59wQ3rJLyS9IEDl95VaNgWrsyCu5YnIw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.5.0.tgz", + "integrity": "sha512-zzHrQpn+burEr37hV1QV7yA1M33wBa38dUe+RLNYkS9g22BXYYZ/uVUhljpmA9DhZCUNJqYbXWi+ad4XMPE6+Q==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/types": "7.5.0", "tiny-invariant": "^1.3.1" }, "funding": { @@ -7008,18 +7300,18 @@ } }, "node_modules/@storybook/addon-outline": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.4.1.tgz", - "integrity": "sha512-HnBQbHLTEHFzeuzNu39Hjol5cCOsXpb406oeD+u8wv6udfDYClg1QmVEaVKddgPooTy9Gv9ztpYlAaMBfSjnmQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.5.0.tgz", + "integrity": "sha512-iVcyFi2N2NEZRytUg8wSiXS9UE9wA8/prs/sIsQ7Y34vHm1UaqAd8KxCE/fhHFNYw4UyHEEDUyTfci/jNrNQYA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/types": "7.5.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -7040,16 +7332,16 @@ } }, "node_modules/@storybook/addon-toolbars": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.4.1.tgz", - "integrity": "sha512-CWHMBCKomQ5JkoFmFD66uo5A2Xa4ER+DX2Kb0oX62s35mBaNOfJVois++i/2Or8BwOUl61x5/3UdPgN2rWHeSw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.5.0.tgz", + "integrity": "sha512-RLONWIJE7myVL3DzWZDWnnmb53C1OitCiO3mDt678xyK5ZrFCOV9cznckXASx1wNJVt3P9OOW1N2UY7wul72+Q==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1" + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0" }, "funding": { "type": "opencollective", @@ -7069,18 +7361,18 @@ } }, "node_modules/@storybook/addon-viewport": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.4.1.tgz", - "integrity": "sha512-3bdRPIFAqZcdGe3XSS9X4T3is6DP8FGytpU96SwnAllG3rI7kQHxmC7pn6mrdNMpLBHq47ZSABoRZZLq8bT/AA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.5.0.tgz", + "integrity": "sha512-NXnjHQFKgeFsWOaJE0fl2THgejxDqx8axy4Prtc3ePcoVa/UrMu11G3iEcCaLhDJU7RDNM6CODgifYpH6gyKWg==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0", "memoizerific": "^1.11.3", "prop-types": "^15.7.2" }, @@ -7101,46 +7393,27 @@ } } }, - "node_modules/@storybook/addons": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-7.4.1.tgz", - "integrity": "sha512-sedMROyWFwlV6gtPHeOhps2/9UpcCmMLnYDhIueu2fAw/Djz0nYVNY2N6ZNiP/eqZISTLr9RzeBfAcymyjAJ2A==", - "dev": true, - "dependencies": { - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/types": "7.4.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@storybook/angular": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-7.4.1.tgz", - "integrity": "sha512-z4FUcNyLWlheXttfukshnGKMyW29NYmL/Uk7O5lq3DNb54WPZe6Q5QV9418L8jcnwe54LlEJZXlP6AlNNslxjQ==", - "dev": true, - "dependencies": { - "@storybook/builder-webpack5": "7.4.1", - "@storybook/cli": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/core-events": "7.4.1", - "@storybook/core-server": "7.4.1", - "@storybook/core-webpack": "7.4.1", - "@storybook/docs-tools": "7.4.1", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-7.5.0.tgz", + "integrity": "sha512-TJdPTXaKazZjA+KaRzbzM8QET48J/Lqfk0wwaNamAY/KvXI6akKgeXJ3Y9SUsmNA9G3QnEMeFAFYvj95XGOwWQ==", + "dev": true, + "dependencies": { + "@storybook/builder-webpack5": "7.5.0", + "@storybook/cli": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/core-events": "7.5.0", + "@storybook/core-server": "7.5.0", + "@storybook/core-webpack": "7.5.0", + "@storybook/docs-tools": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/telemetry": "7.4.1", - "@storybook/types": "7.4.1", - "@types/node": "^16.0.0", + "@storybook/manager-api": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/telemetry": "7.5.0", + "@storybook/types": "7.5.0", + "@types/node": "^18.0.0", "@types/react": "^16.14.34", "@types/react-dom": "^16.9.14", "@types/semver": "^7.3.4", @@ -7178,7 +7451,7 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "rxjs": "^6.0.0 || ^7.4.0", "typescript": "^4.0.0 || ^5.0.0", - "zone.js": "^0.8.29 || >= 0.9.0 < 1.0.0" + "zone.js": ">= 0.11.1 < 1.0.0" }, "peerDependenciesMeta": { "@angular/cli": { @@ -7187,15 +7460,15 @@ } }, "node_modules/@storybook/angular/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@storybook/angular/node_modules/@types/react": { - "version": "16.14.46", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", - "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", + "version": "16.14.50", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.50.tgz", + "integrity": "sha512-7TWZ/HjhXsRK3BbhSFxTinbSft3sUXJAU3ONngT0rpcKJaIOlxkRke4bidqQTopUbEv1ApC5nlSEkIpX43MkTg==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -7204,22 +7477,22 @@ } }, "node_modules/@storybook/blocks": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.4.1.tgz", - "integrity": "sha512-allNTTuFcFK/DzGGQqFGPu/bH53wjM7lO9m/yHBtJv8Mi1aP745JqW0ucJMVb/aO2Y8vjkTIVa+meVIl02bfrg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.5.0.tgz", + "integrity": "sha512-4poS7lQVKhitWKl0TPECMszOMtNamsbNvZdAZ188U/p1EzTrqLg+RT9HtsB8q8Y0owx29Nh5LdfhNOddpx23ig==", "dev": true, "dependencies": { - "@storybook/channels": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/channels": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/components": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/csf": "^0.1.0", - "@storybook/docs-tools": "7.4.1", + "@storybook/docs-tools": "7.5.0", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager-api": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "@types/lodash": "^4.14.167", "color-convert": "^2.0.1", "dequal": "^2.0.2", @@ -7243,15 +7516,15 @@ } }, "node_modules/@storybook/builder-manager": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.4.1.tgz", - "integrity": "sha512-5zD10jO+vxpbkz9yPdPy0ysRRd+81GmZ1yf12xARREy2hp+KeIIC228QDVA1OAsYcfnqREgCAnQslzhR57739A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.5.0.tgz", + "integrity": "sha512-nj+n36i7Mds4RIyGJqvOB+Z47zfgbMes+6Gd6reT1vC22Yda5nAITnd2vxbYfv/sUPhIBBfuFZ/eogomgYCjKg==", "dev": true, "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.4.1", - "@storybook/manager": "7.4.1", - "@storybook/node-logger": "7.4.1", + "@storybook/core-common": "7.5.0", + "@storybook/manager": "7.5.0", + "@storybook/node-logger": "7.5.0", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^3.2.1", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", @@ -7271,30 +7544,22 @@ } }, "node_modules/@storybook/builder-webpack5": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.4.1.tgz", - "integrity": "sha512-1Z4eod4a6Q2LbiTvI+sipM/ehQb9dtTD5W/3y8js2tQ8L8PE9q7CP3J64mJvEJ3Zh2Uc5S+CKqHoH6/Px3ZNcw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.22.9", - "@storybook/addons": "7.4.1", - "@storybook/channels": "7.4.1", - "@storybook/client-api": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/components": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/core-events": "7.4.1", - "@storybook/core-webpack": "7.4.1", - "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/preview": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/router": "7.4.1", - "@storybook/store": "7.4.1", - "@storybook/theming": "7.4.1", - "@swc/core": "^1.3.49", - "@types/node": "^16.0.0", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.5.0.tgz", + "integrity": "sha512-bZRIkJCLdwiPZIUIE5NxEHF+gTO/+4AB/t5/w7UnBhuydDgFStY3cBJHC7wp3crgRuNd4eZJYb2YCqcD/hAgVQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.0", + "@storybook/channels": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/core-events": "7.5.0", + "@storybook/core-webpack": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/preview": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@swc/core": "^1.3.82", + "@types/node": "^18.0.0", "@types/semver": "^7.3.4", "babel-loader": "^9.0.0", "babel-plugin-named-exports-order": "^0.0.2", @@ -7325,10 +7590,6 @@ "type": "opencollective", "url": "https://opencollective.com/storybook" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -7336,19 +7597,19 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@storybook/channels": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.4.1.tgz", - "integrity": "sha512-gnE1mNrRF+9oCVRMq6MS/tLXJbYmf9P02PCC3KpMLcSsABdH5jcrACejzJVo/kE223knFH7NJc4BBj7+5h0uXA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.5.0.tgz", + "integrity": "sha512-/7QJS1UA7TX3uhZqCpjv4Ib8nfMnDOJrBWvjiXiUONaRcSk/he5X+W1Zz/c7dgt+wkYuAh+evjc7glIaBhVNVQ==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -7360,23 +7621,23 @@ } }, "node_modules/@storybook/cli": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.4.1.tgz", - "integrity": "sha512-G1oM1Egs5Z/5FOBcqfACJy2u5cDPl8FMFr3CETkn15a5MXzX3qxH8FD8GmZnXIsEDsGH5WvhnXYbCw+43R6GKg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.5.0.tgz", + "integrity": "sha512-f14q6sqHhDf7bFS0o/ZTgN2tM00Q0cMGMmGFXTQSCh0HXJUS4ujy/FADL+x62wUylIdr1HkIw+ONWMMqHuenEA==", "dev": true, "dependencies": { "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.9", "@babel/types": "^7.22.5", "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/core-events": "7.4.1", - "@storybook/core-server": "7.4.1", - "@storybook/csf-tools": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/telemetry": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/codemod": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/core-events": "7.5.0", + "@storybook/core-server": "7.5.0", + "@storybook/csf-tools": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/telemetry": "7.5.0", + "@storybook/types": "7.5.0", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", @@ -7492,24 +7753,10 @@ "node": ">=8" } }, - "node_modules/@storybook/client-api": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.4.1.tgz", - "integrity": "sha512-B1sQhVv1vq7L1xcvzBT3n4gjG63GTD2KcexJVxcdkVaWNHxE6/QrX+gtsHFevkYI5sjs+sR9fdljALxMf2I0mQ==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/preview-api": "7.4.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/client-logger": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.1.tgz", - "integrity": "sha512-2j0DQlKlPNY8XAaEZv+mUYEUm4dOWg6/Q92UNbvYPRK5qbXUvbMiQco5nmvg4LvMT6y99LhRSW2xrwEx5xKAKw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.5.0.tgz", + "integrity": "sha512-JV7J9vc69f9Il4uW62NIeweUU7O38VwFWxtCkhd0bcBA/9RG0go4M2avzxYYEAe9kIOX9IBBk8WGzMacwW4gKQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -7520,18 +7767,18 @@ } }, "node_modules/@storybook/codemod": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.4.1.tgz", - "integrity": "sha512-KlN2oImqc45RLNRJDWJObvYcLzdtkk4fH40nBIP1/nem8AEbyjEbC5c1OtZilEV47Vn8IdAxqGRPQFXW8GVFEQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.5.0.tgz", + "integrity": "sha512-QdjFdD1OK+LqhYwNMh60/kgSt9VZIgH2TBUeXrPlCK6gfcZBrCB0ktgtuM8Zk/ROktq09pZoVDxqFi0AbEUPew==", "dev": true, "dependencies": { "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.9", "@babel/types": "^7.22.5", "@storybook/csf": "^0.1.0", - "@storybook/csf-tools": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/csf-tools": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/types": "7.5.0", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^11.0.2", @@ -7561,18 +7808,18 @@ } }, "node_modules/@storybook/components": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.4.1.tgz", - "integrity": "sha512-hCuKmMB0+d3/apHjC8G0vMks1cE1aeoKu09gQ40YT+cBxKWj2+lNVKxDd6wJpaR6bU/wrAL1S6eaIQ/T9QpqRA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.5.0.tgz", + "integrity": "sha512-6lmZ6PbS27xN32vTJ/NvgaiKkFIQRzZuBeBIg2u+FoAEgCiCwRXjZKe/O8NZC2Xr0uf97+7U2P0kD4Hwr9SNhw==", "dev": true, "dependencies": { "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.4.1", + "@storybook/client-logger": "7.5.0", "@storybook/csf": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "memoizerific": "^1.11.3", "use-resize-observer": "^9.1.0", "util-deprecate": "^1.0.2" @@ -7587,21 +7834,21 @@ } }, "node_modules/@storybook/core-common": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.4.1.tgz", - "integrity": "sha512-dvHY515l9yyH3Yki9CuGF/LG85yWDmhjtlbHJ7mrMSreaAgvDs7O5Q2iVh6DXg3oMspQvKlLii/ZLzu+3uxMbg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.5.0.tgz", + "integrity": "sha512-Gw3/rzRb5+XbwqBcr2ZNaIYGEp+WNTwaBOnMs4yp2SCrNIb0P+i3BxlVQdgABaq43EI3/bksowT6hei0jyhGhw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/core-events": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/types": "7.5.0", "@types/find-cache-dir": "^3.2.1", - "@types/node": "^16.0.0", + "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", "@types/pretty-hrtime": "^1.0.0", "chalk": "^4.1.0", "esbuild": "^0.18.0", - "esbuild-register": "^3.4.0", + "esbuild-register": "^3.5.0", "file-system-cache": "2.3.0", "find-cache-dir": "^3.0.0", "find-up": "^5.0.0", @@ -7622,9 +7869,9 @@ } }, "node_modules/@storybook/core-common/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@storybook/core-common/node_modules/ansi-styles": { @@ -7680,9 +7927,9 @@ } }, "node_modules/@storybook/core-events": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.4.1.tgz", - "integrity": "sha512-F1tGb32XZ4FRfbtXdi4b+zdzWUjFz5rn3TF18mSuBGGXvxKU+4tywgjGQ3dKGdvuP754czn3poSdz2ZW08bLsQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.5.0.tgz", + "integrity": "sha512-FsD+clTzayqprbVllnL8LLch+uCslJFDgsv7Zh99/zoi7OHtHyauoCZkdLBSiDzgc84qS41dY19HqX1/y7cnOw==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -7693,28 +7940,28 @@ } }, "node_modules/@storybook/core-server": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.4.1.tgz", - "integrity": "sha512-8JJGci8eyNSfiHJ+Xr46Jv95fqQbjrd+ecQJvpyRqwN1LFdCM6QtHYmjt6LzuK16/by5jYXJ7+f8SA+gvW8SbQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.5.0.tgz", + "integrity": "sha512-7QT8uzwSJOsv9PASQ6ywepYkcEYFB7+S7Cj/0nFMh3Vl9vW96LXvEHLAo9CUhSxdEKWeTnD8DS5+j90dLhQFCA==", "dev": true, "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.4.1", - "@storybook/channels": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/builder-manager": "7.5.0", + "@storybook/channels": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/csf": "^0.1.0", - "@storybook/csf-tools": "7.4.1", + "@storybook/csf-tools": "7.5.0", "@storybook/docs-mdx": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/manager": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/telemetry": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/manager": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/telemetry": "7.5.0", + "@storybook/types": "7.5.0", "@types/detect-port": "^1.3.0", - "@types/node": "^16.0.0", + "@types/node": "^18.0.0", "@types/pretty-hrtime": "^1.0.0", "@types/semver": "^7.3.4", "better-opn": "^3.0.2", @@ -7732,7 +7979,6 @@ "prompts": "^2.4.0", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "serve-favicon": "^2.5.0", "telejson": "^7.2.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0", @@ -7747,9 +7993,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@storybook/core-server/node_modules/ansi-styles": { @@ -7805,15 +8051,15 @@ } }, "node_modules/@storybook/core-webpack": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.4.1.tgz", - "integrity": "sha512-fXkOU3IRbXNWmd4o6lNqoNq5s/iE07Bp86P8scE6a3aU309L4lHaIqbxVQy7afvTYqz3X/xtcf0DsuXQi25wXA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.5.0.tgz", + "integrity": "sha512-TI83kG5k2PIjOc+QAOHy0GxOjeLE/TQKFji/+40QJzS7h2+eJTRrO7q32Vo+IyXrkHYlwHk4KrF2LqgYQL8HaQ==", "dev": true, "dependencies": { - "@storybook/core-common": "7.4.1", - "@storybook/node-logger": "7.4.1", - "@storybook/types": "7.4.1", - "@types/node": "^16.0.0", + "@storybook/core-common": "7.5.0", + "@storybook/node-logger": "7.5.0", + "@storybook/types": "7.5.0", + "@types/node": "^18.0.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -7822,9 +8068,9 @@ } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", "dev": true }, "node_modules/@storybook/csf": { @@ -7837,12 +8083,12 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.4.1.tgz", - "integrity": "sha512-TnvDS2szwwzoqn3WbnB57w1Q+rZ+EFFwpLdjvocsiosLQglMQdPNhDvl1U5uDgwTzVhs4MEiEHJ1LxTkeizxhA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.5.0.tgz", + "integrity": "sha512-kghaEFYvQISdAjQddeicSuvBFMeuuLNtpmMkuoLQzULF7e/Tws6zLCYsjGevqlnqXD0iW2XM/j9q4M5L/mWc5A==", "dev": true, "dependencies": { - "@storybook/csf-tools": "7.4.1", + "@storybook/csf-tools": "7.5.0", "unplugin": "^1.3.1" }, "funding": { @@ -7851,9 +8097,9 @@ } }, "node_modules/@storybook/csf-tools": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.4.1.tgz", - "integrity": "sha512-mzzsAtB9CYSgxCvZJ4xQrC7QIhMR5MXGBohADiNhnuRXLdZ6wXBhWkRi/sY7Wh5Uh8DdgHkGPJHJxcyYG+FYQw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.5.0.tgz", + "integrity": "sha512-KOHbFNSwwc7KTdNz/6yO7S2pxbr7sH6nqfolS6/l+pod45WvRH3VhyqlDIIeX7ESIhfCw87ExC96hNDL3TojCw==", "dev": true, "dependencies": { "@babel/generator": "^7.22.9", @@ -7861,7 +8107,7 @@ "@babel/traverse": "^7.22.8", "@babel/types": "^7.22.5", "@storybook/csf": "^0.1.0", - "@storybook/types": "7.4.1", + "@storybook/types": "7.5.0", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -7878,14 +8124,14 @@ "dev": true }, "node_modules/@storybook/docs-tools": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.4.1.tgz", - "integrity": "sha512-4PRsib2hDQjGhT2CnnPgzNZ5pVrpQ6wtb5l0TG4lDDc0F9Tal0EbrooXWwMsc7SxYslHKIEgxd+Nll66FWILFw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.5.0.tgz", + "integrity": "sha512-NFhqbXj6Wv5YypMwDkt0z9xcfWD7M3wZhr8Z9XcXDlUUPjBrdv0cHt3rfHwEXpTfFyunbK41KQZZ3JkjiAjgTg==", "dev": true, "dependencies": { - "@storybook/core-common": "7.4.1", - "@storybook/preview-api": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/core-common": "7.5.0", + "@storybook/preview-api": "7.5.0", + "@storybook/types": "7.5.0", "@types/doctrine": "^0.0.3", "doctrine": "^3.0.0", "lodash": "^4.17.21" @@ -7902,9 +8148,9 @@ "dev": true }, "node_modules/@storybook/manager": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.4.1.tgz", - "integrity": "sha512-LaORUHqfinhKk6Ysz7LyBYqblr/Oj+H5jXeMidSWYor+cJ6AZp1BtCUwWAqtjBliZ8vfASxME1CCImENG11eSA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.5.0.tgz", + "integrity": "sha512-M4h4b0Y4aZ1sRGaZuJXgvPZHqu7vN/wgWB5yPcSwJqH1+DlPxYXYnPKGERgaEUUVKJV3oWQD2qZ+UpDeTgI5UQ==", "dev": true, "funding": { "type": "opencollective", @@ -7912,19 +8158,19 @@ } }, "node_modules/@storybook/manager-api": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.4.1.tgz", - "integrity": "sha512-nzYasETW20uDWpfST6JFf6c/GSFB/dj7xVtg5EpvAYF8GkErCk9TvNKdLNroRrIYm5VJxHWC2V+CJ07RuX3Glw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.5.0.tgz", + "integrity": "sha512-n9EaJTThsuFiBDs+GcmNBHnvLhH0znJQprhIQqHNVnosCs/7sloYUzWZzZvPwfnfPvRR7ostEEMXvriaYXYdJQ==", "dev": true, "dependencies": { - "@storybook/channels": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/channels": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/csf": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/router": "7.4.1", - "@storybook/theming": "7.4.1", - "@storybook/types": "7.4.1", + "@storybook/router": "7.5.0", + "@storybook/theming": "7.5.0", + "@storybook/types": "7.5.0", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", @@ -7949,9 +8195,9 @@ "dev": true }, "node_modules/@storybook/node-logger": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.4.1.tgz", - "integrity": "sha512-P7rR/WoHCR2zdDo8bDowIBlB3wRrVNHHIfyWxubbzj/AA2uPv7cpdjDA+NDHAIq8MkuxZqfqhatjrHLFwMHDBg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.5.0.tgz", + "integrity": "sha512-Og3hdB1bjpVCXhmlhvpgVxUfCQGd0DCguXf5qhn2kX4a+D++dxJ8YqzVJ5JQCacI9bCKITV6W9JSGseWcBaXBg==", "dev": true, "funding": { "type": "opencollective", @@ -7959,9 +8205,9 @@ } }, "node_modules/@storybook/postinstall": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.4.1.tgz", - "integrity": "sha512-nzSAS2kKhYFdeQHOb+mwk6LCiSBx8vigiRActRWMpoUSntlrLFdYKXoYfPQtUQcE7cHDLv5hutD31Kcl7pIazw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.5.0.tgz", + "integrity": "sha512-SHpBItwar7qDZO7BBSqTNQK0yNy+RUROZUhW6wlVvsgVhIGF1bgA4pgpW1iMyfPmmGyNekE1BJjN+v8rjq9s6A==", "dev": true, "funding": { "type": "opencollective", @@ -7969,9 +8215,9 @@ } }, "node_modules/@storybook/preview": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.4.1.tgz", - "integrity": "sha512-KqHbS5jVKSvFESrwU3iLJE5ciIJicdV3ZducL9t+hNJOdchzV3ezEwMn6gApEin3dm3Ts7InN+W7nBc+MzaXmA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.5.0.tgz", + "integrity": "sha512-KPhx43pRgIb6UhqjsF0sUG5c3GG2dwzTzjN1/sj0QbPMghZ3b7xKGrCu6VSlsXoWQtcwisMHETFnowk0Ba/AMg==", "dev": true, "funding": { "type": "opencollective", @@ -7979,17 +8225,17 @@ } }, "node_modules/@storybook/preview-api": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.4.1.tgz", - "integrity": "sha512-swmosWK73lP0CXDKMOwYIaaId28+muPDYX2V/0JmIOA+45HFXimeXZs3XsgVgQMutVF51QqnDA0pfrNgRofHgQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.5.0.tgz", + "integrity": "sha512-+DubgKwYFk532FKDB6sEGaG47wr0t137aIQSjbNwVmXXxj0QY0zIAThtERx7w6eHS7ZjOs6xlLEZhzC4FI525g==", "dev": true, "dependencies": { - "@storybook/channels": "7.4.1", - "@storybook/client-logger": "7.4.1", - "@storybook/core-events": "7.4.1", + "@storybook/channels": "7.5.0", + "@storybook/client-logger": "7.5.0", + "@storybook/core-events": "7.5.0", "@storybook/csf": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/types": "7.4.1", + "@storybook/types": "7.5.0", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", @@ -8005,9 +8251,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.4.1.tgz", - "integrity": "sha512-LUxmXyAFZB61kFWtZZA5WCHgFfUI5Jtn0d2HVOfpIYK1OcGwW8K4ya0lbMVrYvMgL37e5ShPurjj32U2YBeiJA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.5.0.tgz", + "integrity": "sha512-OzJhXg1En/9D9vKvD2t0EcYcuHFzrLTA9kEUWt/eP3Ww41kndfJoZca33JZr17iuKksVAZ8ucETMnkL3yO+ybA==", "dev": true, "funding": { "type": "opencollective", @@ -8019,12 +8265,12 @@ } }, "node_modules/@storybook/router": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.4.1.tgz", - "integrity": "sha512-7tE1B18jb+5+ujXd3BHcub85QnytIVBNA0iAo+o8MNwArISyodqp12y2D3w+QpXkg0GtPhAp/CMhzpyxotPhRQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.5.0.tgz", + "integrity": "sha512-NzPwjndmOEOUL8jK5kUrSvRUIcN5Z+h+l0Z8g4I56RoEhNTcKeOW4jbcT4WKnR9H455dti8HAcTV/4x59GpgxQ==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", + "@storybook/client-logger": "7.5.0", "memoizerific": "^1.11.3", "qs": "^6.10.0" }, @@ -8037,29 +8283,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/store": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/store/-/store-7.4.1.tgz", - "integrity": "sha512-Z132AFO+mgc4W09zBmYBSY1w3q/bjYsf93dD29gyRP3i5+LrXmS3cFMkg0+buDTXarM+AoyEJ6vnia4MAYL+6A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/preview-api": "7.4.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/telemetry": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.4.1.tgz", - "integrity": "sha512-53eQPm22Fa7qzjXFSE++bJv5qNG/89rRLU5xywuSYmjQgtaS6HKLPjIRtNPPbU50gRvklVedDDxD8UqN73mD3w==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.5.0.tgz", + "integrity": "sha512-dvc1cjxHYGNfLEvh8eQI/R2KtMft0kUs6TJ2uXZdIX4+WqWG6mfn75sP8eyC1tcjkdslS6AmFWTfgt9EVcIPQA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.4.1", - "@storybook/core-common": "7.4.1", - "@storybook/csf-tools": "7.4.1", + "@storybook/client-logger": "7.5.0", + "@storybook/core-common": "7.5.0", + "@storybook/csf-tools": "7.5.0", "chalk": "^4.1.0", "detect-package-manager": "^2.0.1", "fetch-retry": "^5.0.2", @@ -8124,24 +8356,24 @@ } }, "node_modules/@storybook/testing-library": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.0.tgz", - "integrity": "sha512-Ff6jNnrsosmDshgCf0Eb5Cz7IA34p/1Ps5N3Kp3598kfXpBSccSkQQvVFUXC3kIHw/isIXWPqntZuKqnWUz7Gw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.2.tgz", + "integrity": "sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==", "dev": true, "dependencies": { "@testing-library/dom": "^9.0.0", - "@testing-library/user-event": "^14.0.0", + "@testing-library/user-event": "^14.4.0", "ts-dedent": "^2.2.0" } }, "node_modules/@storybook/theming": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.1.tgz", - "integrity": "sha512-a4QajZbnYumq8ovtn7nW7BeNrk/TaWyKmUrIz4w08I6ghzESJA4aCWZ6394awbrruiIOzCCKOUq4mfWEsc8W6A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.5.0.tgz", + "integrity": "sha512-uTo97oh+pvmlfsZocFq5qae0zGo0VGk7oiBqNSSw6CiTqE1rIuSxoPrMAY+oCTWCUZV7DjONIGvpnGl2QALsAw==", "dev": true, "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.4.1", + "@storybook/client-logger": "7.5.0", "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3" }, @@ -8155,12 +8387,12 @@ } }, "node_modules/@storybook/types": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.1.tgz", - "integrity": "sha512-bjt1YDG9AocFBhIFRvGGbYZPlD223p+qAFcFgYdezU16fFE4ZGFUzUuq2ERkOofL7a2+OzLTCQ/SKe1jFkXCxQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.5.0.tgz", + "integrity": "sha512-fiOUnHKFi/UZSfvc53F0WEQCiquqcSqslL3f5EffwQRiXfeXlGavJb0kU03BO+CvOXcliRn6qKSF2dL0Rgb7Xw==", "dev": true, "dependencies": { - "@storybook/channels": "7.4.1", + "@storybook/channels": "7.5.0", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -8171,13 +8403,14 @@ } }, "node_modules/@swc/core": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.84.tgz", - "integrity": "sha512-UPKUiDwG7HOdPfOb1VFeEJ76JDgU2w80JLewzx6tb0fk9TIjhr9yxKBzPbzc/QpjGHDu5iaEuNeZcu27u4j63g==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.93.tgz", + "integrity": "sha512-690GRr1wUGmGYZHk7fUduX/JUwViMF2o74mnZYIWEcJaCcd9MQfkhsxPBtjeg6tF+h266/Cf3RPYhsFBzzxXcA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@swc/types": "^0.1.4" + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" }, "engines": { "node": ">=10" @@ -8187,16 +8420,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.84", - "@swc/core-darwin-x64": "1.3.84", - "@swc/core-linux-arm-gnueabihf": "1.3.84", - "@swc/core-linux-arm64-gnu": "1.3.84", - "@swc/core-linux-arm64-musl": "1.3.84", - "@swc/core-linux-x64-gnu": "1.3.84", - "@swc/core-linux-x64-musl": "1.3.84", - "@swc/core-win32-arm64-msvc": "1.3.84", - "@swc/core-win32-ia32-msvc": "1.3.84", - "@swc/core-win32-x64-msvc": "1.3.84" + "@swc/core-darwin-arm64": "1.3.93", + "@swc/core-darwin-x64": "1.3.93", + "@swc/core-linux-arm-gnueabihf": "1.3.93", + "@swc/core-linux-arm64-gnu": "1.3.93", + "@swc/core-linux-arm64-musl": "1.3.93", + "@swc/core-linux-x64-gnu": "1.3.93", + "@swc/core-linux-x64-musl": "1.3.93", + "@swc/core-win32-arm64-msvc": "1.3.93", + "@swc/core-win32-ia32-msvc": "1.3.93", + "@swc/core-win32-x64-msvc": "1.3.93" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -8208,9 +8441,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.84.tgz", - "integrity": "sha512-mqK0buOo+toF2HoJ/gWj2ApZbvbIiNq3mMwSTHCYJHlQFQfoTWnl9aaD5GSO4wfNFVYfEZ1R259o5uv5NlVtoA==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.93.tgz", + "integrity": "sha512-gEKgk7FVIgltnIfDO6GntyuQBBlAYg5imHpRgLxB1zSI27ijVVkksc6QwISzFZAhKYaBWIsFSVeL9AYSziAF7A==", "cpu": [ "arm64" ], @@ -8224,9 +8457,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.84.tgz", - "integrity": "sha512-cyuQZz62C43EDZqtnptUTlfDvAjgG3qu139m5zsfIK6ltXA5inKFbDWV3a/M5c18dFzA2Xh21Q46XZezmtQ9Tg==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.93.tgz", + "integrity": "sha512-ZQPxm/fXdDQtn3yrYSL/gFfA8OfZ5jTi33yFQq6vcg/Y8talpZ+MgdSlYM0FkLrZdMTYYTNFiuBQuuvkA+av+Q==", "cpu": [ "x64" ], @@ -8240,9 +8473,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.84.tgz", - "integrity": "sha512-dmt/ECQrp3ZPWnK27p4E4xRIRHOoJhgGvxC5t5YaWzN20KcxE9ykEY2oLGSoeceM/A+4D11aRYGwF/EM7yOkvA==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.93.tgz", + "integrity": "sha512-OYFMMI2yV+aNe3wMgYhODxHdqUB/jrK0SEMHHS44GZpk8MuBXEF+Mcz4qjkY5Q1EH7KVQqXb/gVWwdgTHpjM2A==", "cpu": [ "arm" ], @@ -8256,9 +8489,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.84.tgz", - "integrity": "sha512-PgVfrI3NVg2z/oeg3GWLb9rFLMqidbdPwVH5nRyHVP2RX/BWP6qfnYfG+gJv4qrKzIldb9TyCGH7y8VWctKLxw==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.93.tgz", + "integrity": "sha512-BT4dT78odKnJMNiq5HdjBsv29CiIdcCcImAPxeFqAeFw1LL6gh9nzI8E96oWc+0lVT5lfhoesCk4Qm7J6bty8w==", "cpu": [ "arm64" ], @@ -8272,9 +8505,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.84.tgz", - "integrity": "sha512-hcuEa8/vin4Ns0P+FpcDHQ4f3jmhgGKQhqw0w+TovPSVTIXr+nrFQ2AGhs9nAxS6tSQ77C53Eb5YRpK8ToFo1A==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.93.tgz", + "integrity": "sha512-yH5fWEl1bktouC0mhh0Chuxp7HEO4uCtS/ly1Vmf18gs6wZ8DOOkgAEVv2dNKIryy+Na++ljx4Ym7C8tSJTrLw==", "cpu": [ "arm64" ], @@ -8288,9 +8521,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.84.tgz", - "integrity": "sha512-IvyimSbwGdu21jBBEqR1Up8Jhvl8kIAf1k3e5Oy8oRfgojdUfmW1EIwgGdoUeyQ1VHlfquiWaRGfsnHQUKl35g==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.93.tgz", + "integrity": "sha512-OFUdx64qvrGJhXKEyxosHxgoUVgba2ztYh7BnMiU5hP8lbI8G13W40J0SN3CmFQwPP30+3oEbW7LWzhKEaYjlg==", "cpu": [ "x64" ], @@ -8304,9 +8537,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.84.tgz", - "integrity": "sha512-hdgVU/O5ufDCe+p5RtCjU7PRNwd0WM+eWJS+GNY4QWL6O8y2VLM+i4+6YzwSUjeBk0xd+1YElMxbqz7r5tSZhw==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.93.tgz", + "integrity": "sha512-4B8lSRwEq1XYm6xhxHhvHmKAS7pUp1Q7E33NQ2TlmFhfKvCOh86qvThcjAOo57x8DRwmpvEVrqvpXtYagMN6Ig==", "cpu": [ "x64" ], @@ -8320,9 +8553,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.84.tgz", - "integrity": "sha512-rzH6k2BF0BFOFhUTD+bh0oCiUCZjFfDfoZoYNN/CM0qbtjAcFH21hzMh/EH8ZaXq8k/iQmUNNa5MPNPZ4SOMNw==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.93.tgz", + "integrity": "sha512-BHShlxtkven8ZjjvZ5QR6sC5fZCJ9bMujEkiha6W4cBUTY7ce7qGFyHmQd+iPC85d9kD/0cCiX/Xez8u0BhO7w==", "cpu": [ "arm64" ], @@ -8336,9 +8569,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.84.tgz", - "integrity": "sha512-Y+Dk7VLLVwwsAzoDmjkNW/sTmSPl9PGr4Mj1nhc5A2NNxZ+hz4SxFMclacDI03SC5ikK8Qh6WOoE/+nwUDa3uA==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.93.tgz", + "integrity": "sha512-nEwNWnz4JzYAK6asVvb92yeylfxMYih7eMQOnT7ZVlZN5ba9WF29xJ6kcQKs9HRH6MvWhz9+wRgv3FcjlU6HYA==", "cpu": [ "ia32" ], @@ -8352,9 +8585,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.84.tgz", - "integrity": "sha512-WmpaosqCWMX7DArLdU8AJcj96hy0PKlYh1DaMVikSrrDHbJm2dZ8rd27IK3qUB8DgPkrDYHmLAKNZ+z3gWXgRQ==", + "version": "1.3.93", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.93.tgz", + "integrity": "sha512-jibQ0zUr4kwJaQVwgmH+svS04bYTPnPw/ZkNInzxS+wFAtzINBYcU8s2PMWbDb2NGYiRSEeoSGyAvS9H+24JFA==", "cpu": [ "x64" ], @@ -8367,16 +8600,22 @@ "node": ">=10" } }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true + }, "node_modules/@swc/types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.4.tgz", - "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "dev": true }, "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -8432,6 +8671,35 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@testing-library/dom/node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8454,9 +8722,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.4.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", - "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==", + "version": "14.5.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz", + "integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==", "dev": true, "engines": { "node": ">=12", @@ -8476,9 +8744,9 @@ } }, "node_modules/@ts-morph/common": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", - "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.21.0.tgz", + "integrity": "sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==", "dev": true, "dependencies": { "fast-glob": "^3.2.12", @@ -8597,15 +8865,15 @@ } }, "node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.3.tgz", + "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", - "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", + "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -8616,18 +8884,18 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", + "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", + "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -8635,18 +8903,18 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", - "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", + "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", + "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", "dev": true, "dependencies": { "@types/connect": "*", @@ -8654,27 +8922,27 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.12.tgz", + "integrity": "sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "version": "3.4.37", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", + "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", - "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz", + "integrity": "sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -8688,36 +8956,36 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.15.tgz", + "integrity": "sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/cross-spawn": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.3.tgz", - "integrity": "sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.4.tgz", + "integrity": "sha512-GGLpeThc2Bu8FBGmVn76ZU3lix17qZensEI4/MPty0aZpm2CHfgEMis31pf5X5EiudYKcPAsWciAsCALoPo5dw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/debug": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", - "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.10.tgz", + "integrity": "sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==", "dev": true, "dependencies": { "@types/ms": "*" } }, "node_modules/@types/detect-port": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz", - "integrity": "sha512-bV/jQlAJ/nPY3XqSatkGpu+nGzou+uSwrH1cROhn+jBFg47yaNH+blW4C7p9KhopC7QxCv/6M86s37k8dMk0Yg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.4.tgz", + "integrity": "sha512-HveFGabu3IwATqwLelcp6UZ1MIzSFwk+qswC9luzzHufqAwhs22l7KkINDLWRfXxIPTYnSZ1DuQBEgeVPgUOSA==", "dev": true }, "node_modules/@types/doctrine": { @@ -8727,44 +8995,44 @@ "dev": true }, "node_modules/@types/ejs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.2.tgz", - "integrity": "sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.4.tgz", + "integrity": "sha512-fnM/NjByiWdSRJRrmGxgqOSAnmOnsvX1QcNYk5TVyIIj+7ZqOKMb9gQa4OIl/lil2w/8TiTWV+nz3q8yqxez/w==", "dev": true }, "node_modules/@types/emscripten": { - "version": "1.39.7", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.7.tgz", - "integrity": "sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==", + "version": "1.39.9", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.9.tgz", + "integrity": "sha512-ILdWj4XYtNOqxJaW22NEQx2gJsLfV5ncxYhhGX1a1H1lXl2Ta0gUz7QOnOoF1xQbJwWDjImi8gXN9mKdIf6n9g==", "dev": true }, "node_modules/@types/eslint": { - "version": "8.44.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", - "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "version": "8.44.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.5.tgz", + "integrity": "sha512-Ol2eio8LtD/tGM4Ga7Jb83NuFwEv3NqvssSlifXL9xuFpSyQZw0ecmm2Kux6iU0KxQmp95hlPmGCzGJ0TCFeRA==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", + "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", + "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", + "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -8774,9 +9042,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "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==", + "version": "4.17.38", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.38.tgz", + "integrity": "sha512-hXOtc0tuDHZPFwwhuBJXPbjemWtXnJjbvuuyNH2Y5Z6in+iXc63c4eXYDc7GGGqHy+iwYqAJMdaItqdnbcBKmg==", "dev": true, "dependencies": { "@types/node": "*", @@ -8792,24 +9060,24 @@ "dev": true }, "node_modules/@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "version": "7946.0.12", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.12.tgz", + "integrity": "sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA==", "dev": true }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", + "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/hammerjs": { - "version": "2.0.41", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", - "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==", + "version": "2.0.43", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.43.tgz", + "integrity": "sha512-wqxfwHk83RS7+6OpytGdo5wqkqtvx+bGaIs1Rwm5NrtQHUfL4OgWs/5p0OipmjmT+fexePh37Ek+mqIpdNjQKA==", "dev": true }, "node_modules/@types/html-minifier-terser": { @@ -8819,130 +9087,133 @@ "dev": true }, "node_modules/@types/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", + "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.11", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", - "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "version": "1.17.13", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.13.tgz", + "integrity": "sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", "dev": true }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jasmine": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.5.tgz", - "integrity": "sha512-9YHUdvuNDDRJYXZwHqSsO72Ok0vmqoJbNn73ttyITQp/VA60SarnZ+MPLD37rJAhVoKp+9BWOvJP5tHIRfZylQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", "dev": true }, "node_modules/@types/json-query": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@types/json-query/-/json-query-2.2.3.tgz", - "integrity": "sha512-ygE4p8lyKzTBo9LF2K/u6MHnxPxbHY6wGvwM7TdAKhbP3SvEf+Y9aeVWedDiP8SMIPowTl9R/6awQYjiUTHz2g==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@types/json-query/-/json-query-2.2.5.tgz", + "integrity": "sha512-DuoPfJ2lDdVWa1KSsJKOx1m1EXl2yIGwxymr2Hgpzilzzatmq6Ws3+7E9lo0Vk3EYBoXW95oAlQumYCsLKAa4g==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" }, "node_modules/@types/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-kfwgQf4eOxoe/tD9CaKQrBKHbc7VpyfJOG5sxsQtkH+ML9xYa8hUC3UMa0wU1pKfciJtO0pU9g9XbWhPo7iBCA==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.7.tgz", + "integrity": "sha512-FOfKB1ALYUDnXkH7LfTFreWiZr9R7GErqGP+8lYQGWr2GFq5+jy3Ih0M7e9j41cvRN65kLALJ4dc43yZwyl/6g==", "dev": true, "dependencies": { "@types/geojson": "*" } }, "node_modules/@types/lodash": { - "version": "4.14.198", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", - "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==", + "version": "4.14.200", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", + "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", "dev": true }, "node_modules/@types/lodash-es": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.9.tgz", - "integrity": "sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ==", + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.10.tgz", + "integrity": "sha512-YJP+w/2khSBwbUSFdGsSqmDvmnN3cCKoPOL7Zjle6s30ZtemkkqhjVfFqGwPN7ASil5VyjE2GtyU/yqYY6mC0A==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/marked": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.1.tgz", - "integrity": "sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", "peer": true }, "node_modules/@types/md5": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz", - "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.4.tgz", + "integrity": "sha512-e/L4hvpCK8GavKXmP02QlNilZOj8lpmZGGA9QGMMPZjCUoKgi1B4BvhXcbruIi6r+PqzpcjLfda/tocpHFKqDA==", "dev": true }, "node_modules/@types/mdx": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.7.tgz", - "integrity": "sha512-BG4tyr+4amr3WsSEmHn/fXPqaCba/AYZ7dsaQTiavihQunHSIxk+uAtqsjvicNpyHN6cm+B9RVrUOtW9VzIKHw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.8.tgz", + "integrity": "sha512-r7/zWe+f9x+zjXqGxf821qz++ld8tp6Z4jUS6qmPZUXH6tfh4riXOhAqb12tWGWAevCFtMt1goLWkQMqIJKpsA==", "dev": true }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", "dev": true }, "node_modules/@types/mime-types": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", - "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.3.tgz", + "integrity": "sha512-bvxCbHeeS7quxS7uOJShyoOQj/BfLabhF6mk9Rmr+2MRfW8W1yxyyL/0GTxLFTHen41GrIw4K3D4DrLouhb8vg==", "dev": true }, "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.33.tgz", + "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==", "dev": true }, "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==" + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "dependencies": { + "undici-types": "~5.25.1" + } }, "node_modules/@types/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-OZsUlr2nxvkqUFLSaY2ZbA+P1q22q+KrlxWOn/38RX+u5kTkYL2mTujEpzUhGkS+K/QCYp9oagfXG39XOzyySg==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-lX17GZVpJ/fuCjguZ5b3TjEbSENxmEk1B2z02yoXSK9WMEWRivhdSY73wWMn6bpcCDAOh6qAdktpKHIlkDk2lg==", "dev": true, "dependencies": { "@types/node": "*", @@ -8950,21 +9221,21 @@ } }, "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz", + "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==", "dev": true }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", + "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==", "dev": true }, "node_modules/@types/pouchdb": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@types/pouchdb/-/pouchdb-6.4.0.tgz", - "integrity": "sha512-eGCpX+NXhd5VLJuJMzwe3L79fa9+IDTrAG3CPaf4s/31PD56hOrhDJTSmRELSXuiqXr6+OHzzP0PldSaWsFt7w==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@types/pouchdb/-/pouchdb-6.4.1.tgz", + "integrity": "sha512-GvTXWOup2REK9WttO6nHM81S+ICTssBzpMZ/waD+HBWvbNLGPeg89rOhsKm7YuUjSPRZwYkfldqh8epvaR9JdQ==", "dev": true, "dependencies": { "@types/pouchdb-adapter-cordova-sqlite": "*", @@ -8985,72 +9256,72 @@ } }, "node_modules/@types/pouchdb-adapter-cordova-sqlite": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.1.tgz", - "integrity": "sha512-nqlXpW1ho3KBg1mUQvZgH2755y3z/rw4UA7ZJCPMRTHofxGMY8izRVw5rHBL4/7P615or0J2udpRYxgkT3D02g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.3.tgz", + "integrity": "sha512-xQTzjPW5AgtUzobByDY2VgIXeQ5QRN0BZumufvj6Y2c0lw9UZFHGgHQPe6eCVbGmZneaWhTKw+HutS4gILFzqw==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-fruitdown": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-fruitdown/-/pouchdb-adapter-fruitdown-6.1.3.tgz", - "integrity": "sha512-Wz1Z1JLOW1hgmFQjqnSkmyyfH7by/iWb4abKn684WMvQfmxx6BxKJpJ4+eulkVPQzzgMMSgU1MpnQOm9FgRkbw==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-fruitdown/-/pouchdb-adapter-fruitdown-6.1.5.tgz", + "integrity": "sha512-p3BsBmYLbv8gVH9Ca1B45+Ua/hhX1V9o/UCdeAkcjC0Cx8aMnBz5B1oEflVhvuInpTfrwA8v1biLb/CaWBOeMw==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-http": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-http/-/pouchdb-adapter-http-6.1.3.tgz", - "integrity": "sha512-9Z4TLbF/KJWy/D2sWRPBA+RNU0odQimfdvlDX+EY7rGcd3aVoH8qjD/X0Xcd/0dfBH5pKrNIMFFQgW/TylRCmA==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-http/-/pouchdb-adapter-http-6.1.5.tgz", + "integrity": "sha512-FC84xpDBL4rcJ86CjHiEFYG99sHyca7MJOPFoyePe+JxQd48TFWJ6hWcO7QyyBpIvF2h7Divg8jnaxnyHDSQ4w==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-idb": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-idb/-/pouchdb-adapter-idb-6.1.4.tgz", - "integrity": "sha512-KIAXbkF4uYUz0ZwfNEFLtEkK44mEWopAsD76UhucH92XnJloBysav+TjI4FFfYQyTjoW3S1s6V+Z14CUJZ0F6w==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-idb/-/pouchdb-adapter-idb-6.1.6.tgz", + "integrity": "sha512-hS/QtHDwMeZbOS7D2epua1Ks/dabYPIqbJPEr9UOAtHiGhTZbQss7Flb66zB+we2cBYYPPUOYOwHbXs61zRBnQ==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-leveldb": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-leveldb/-/pouchdb-adapter-leveldb-6.1.3.tgz", - "integrity": "sha512-ex8NFqQGFwEpFi7AaZ5YofmuemfZNsL3nTFZBUCAKYMBkazQij1pe2ILLStSvJr0XS0qxgXjCEW19T5Wqiiskg==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-leveldb/-/pouchdb-adapter-leveldb-6.1.5.tgz", + "integrity": "sha512-a0lZTPSEfojLabv9sOoksKh9zDfW8Q0N5aDAxfNGRH8BtdQ8wSHr3+V8igTdg9kT+OMhdU+G9xC543M2CY5edg==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-localstorage": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-localstorage/-/pouchdb-adapter-localstorage-6.1.3.tgz", - "integrity": "sha512-oor040tye1KKiGLWYtIy7rRT7C2yoyX3Tf6elEJRpjOA7Ja/H8lKc4LaSh9ATbptIcES6MRqZDxtp7ly9hsW3Q==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-localstorage/-/pouchdb-adapter-localstorage-6.1.5.tgz", + "integrity": "sha512-qeXAYHm1avjgjS9tu6RgB7I9pQdNJ207kfYX6Nj0VsUGRtH2JjWPR16oIPqwvjdO6gVnNArS2MI4K+G6qzobLQ==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-memory": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-memory/-/pouchdb-adapter-memory-6.1.3.tgz", - "integrity": "sha512-gVbsIMzDzgZYThFVT4eVNsmuZwVm/4jDxP1sjlgc3qtDIxbtBhGgyNfcskwwz9Zu5Lv1avkDsIWvcxQhnvRlHg==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-memory/-/pouchdb-adapter-memory-6.1.5.tgz", + "integrity": "sha512-n0Ps/SoHgTJ584cwD3YZyOriQa9m0ed4Taleq8deFWp2cV9ZLvt1ehV+5Or82VMSvqYK315RGosuDgSkPsUDng==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-adapter-node-websql": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-node-websql/-/pouchdb-adapter-node-websql-6.1.3.tgz", - "integrity": "sha512-F/P+os6Jsa7CgHtH64+Z0HfwIcj0hIRB5z8gNhF7L7dxPWoAfkopK5H2gydrP3sQrlGyN4WInF+UJW/Zu1+FKg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-node-websql/-/pouchdb-adapter-node-websql-6.1.4.tgz", + "integrity": "sha512-Jpt9a0jv0RHPwPMaaAfZvSxUkuburTSmZL/ksWkm7Efx/Ui8VFPEA/bT8ulHkkWtKWjoCCI79pceeY/xEpeYqw==", "dev": true, "dependencies": { "@types/pouchdb-adapter-websql": "*", @@ -9058,18 +9329,18 @@ } }, "node_modules/@types/pouchdb-adapter-websql": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-websql/-/pouchdb-adapter-websql-6.1.4.tgz", - "integrity": "sha512-zMJQCtXC40hBsIDRn0GhmpeGMK0f9l/OGWfLguvczROzxxcOD7REI+e6SEmX7gJKw5JuMvlfuHzkQwjmvSJbtg==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-websql/-/pouchdb-adapter-websql-6.1.6.tgz", + "integrity": "sha512-Yx0Ek7LiAEc5maD5W4d9yD/tmKyr7yFK+NZNSvPsIM6PH9Lc4Xd7b+bQdiiwvyLBBfCkWyYFrhzkGcHD5m/1YA==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-browser": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-browser/-/pouchdb-browser-6.1.3.tgz", - "integrity": "sha512-EdYowrWxW9SWBMX/rux2eq7dbHi5Zeyzz+FF/IAsgQKnUxgeCO5VO2j4zTzos0SDyJvAQU+EYRc11r7xGn5tvA==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/pouchdb-browser/-/pouchdb-browser-6.1.4.tgz", + "integrity": "sha512-yo/igvZ5JBZTkqEZoIOYXkIwyP1+hZdFY3Ts3ot/zZWmpXyySKvbtfNnhmtLeyTDWkc6ZL/U3rP/ta2U33WWvA==", "dev": true, "dependencies": { "@types/pouchdb-adapter-http": "*", @@ -9081,9 +9352,9 @@ } }, "node_modules/@types/pouchdb-core": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/pouchdb-core/-/pouchdb-core-7.0.11.tgz", - "integrity": "sha512-KTKj0Taf8pLTj8eW3qBtCd1Fh+/yhEyVo6/1czTN46MnseobdmnqgXtzaKgHMbuLouh+SHK1vM++aMPaG1qTTA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/pouchdb-core/-/pouchdb-core-7.0.13.tgz", + "integrity": "sha512-DAuu60a/Y+An30+thA6BoucTyb4IdeIzqvz4qBfJRoUkKRIsFo7qMB4SUm1/wIq7ppyd9mljnjGRUIgCxjnS4w==", "dev": true, "dependencies": { "@types/debug": "*", @@ -9091,18 +9362,18 @@ } }, "node_modules/@types/pouchdb-find": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@types/pouchdb-find/-/pouchdb-find-7.3.0.tgz", - "integrity": "sha512-sFPli5tBjGX9UfXioik1jUzPdcN84eV82n0lmEFuoPepWqkLjQcyri0eOa++HYOaNPyMDhKFBqEALEZivK2dRg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@types/pouchdb-find/-/pouchdb-find-7.3.2.tgz", + "integrity": "sha512-H4D+2gpGfoJCXURDJR6dIg6bKpFN3X46CQZEmEOeYSy5xGguEXokU+oKQ3HrIlD9p2eA4aonuh5I8uSTMSLprw==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-http": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-http/-/pouchdb-http-6.1.3.tgz", - "integrity": "sha512-0e9E5SqNOyPl/3FnEIbENssB4FlJsNYuOy131nxrZk36S+y1R/6qO7ZVRypWpGTqBWSuVd7gCsq2UDwO/285+w==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/pouchdb-http/-/pouchdb-http-6.1.4.tgz", + "integrity": "sha512-Sx7qnc9HaTJr15UDZTnxsxiqxeG47LyWdCX+KUoVhjWMw3cBbY1Wv4UuEAAlsNTdVK1t/GrmgC2K1rH+nRvIxg==", "dev": true, "dependencies": { "@types/pouchdb-adapter-http": "*", @@ -9110,18 +9381,18 @@ } }, "node_modules/@types/pouchdb-mapreduce": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@types/pouchdb-mapreduce/-/pouchdb-mapreduce-6.1.7.tgz", - "integrity": "sha512-WzBwm7tmO9QhfRzVaWT4v6JQSS/fG2OoUDrWrhX87rPe2Pn6laPvdK5li6myNRxCoI/l5e8Jd+oYBAFnaiFucA==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@types/pouchdb-mapreduce/-/pouchdb-mapreduce-6.1.9.tgz", + "integrity": "sha512-dsgLocavfxj4exjUETKU5ffKYhjzdBfkTDSctVSWLUqjKsUuAqHCcot6ECxkUVuD6Tejv0Y8rGi3If5xzGb5Vw==", "dev": true, "dependencies": { "@types/pouchdb-core": "*" } }, "node_modules/@types/pouchdb-node": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/pouchdb-node/-/pouchdb-node-6.1.4.tgz", - "integrity": "sha512-wnTCH8X1JOPpNOfVhz8HW0AvmdHh6pt40MuRj0jQnK7QEHsHS79WujsKTKSOF8QXtPwpvCNSsI7ut7H7tfxxJQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@types/pouchdb-node/-/pouchdb-node-6.1.6.tgz", + "integrity": "sha512-5t3p4YsbAp3sxx4OVhBdsfwfuM3o9XNnjXOEj10up/Z6ykIqDLfFyy2kPWW5c9txj7Qxl07EQlwm9CbIWNEhBg==", "dev": true, "dependencies": { "@types/pouchdb-adapter-http": "*", @@ -9132,9 +9403,9 @@ } }, "node_modules/@types/pouchdb-replication": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/@types/pouchdb-replication/-/pouchdb-replication-6.4.4.tgz", - "integrity": "sha512-BsE5LKpjJK4iAf6Fx5kyrMw+33V+Ip7uWldUnU2BYrrvtR+MLD22dcImm7DZN1st2wPPb91i0XEnQzvP0w1C/Q==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@types/pouchdb-replication/-/pouchdb-replication-6.4.6.tgz", + "integrity": "sha512-DYsh3X5fnbmMOZ1u0CAZBD8iPkDmHQIJwxjez8S60SHQyFqLUzd4A/OV7C4nNszdcdtGNwhSwoNdfjZe4JX16Q==", "dev": true, "dependencies": { "@types/pouchdb-core": "*", @@ -9142,33 +9413,33 @@ } }, "node_modules/@types/pretty-hrtime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", - "integrity": "sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.2.tgz", + "integrity": "sha512-vyv9knII8XeW8TnXDcGH7HqG6FeR56ESN6ExM34d/U8Zvs3xuG34euV6CVyB7KEYI7Ts4lQM8b4NL72e7UadnA==", "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==", "dev": true }, "node_modules/@types/qs": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", - "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "version": "6.9.9", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", + "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", + "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", "dev": true }, "node_modules/@types/react": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", - "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", + "version": "18.2.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.29.tgz", + "integrity": "sha512-Z+ZrIRocWtdD70j45izShRwDuiB4JZqDegqMFW/I8aG5DxxLKOzVNoq62UIO82v9bdgi+DO1jvsb9sTEZUSm+Q==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -9177,18 +9448,18 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.19.tgz", - "integrity": "sha512-xC8D280Bf6p0zguJ8g62jcEOKZiUbx9sIe6O3tT/lKfR87A7A6g65q13z6D5QUMIa/6yFPkNhqjF5z/VVZEYqQ==", + "version": "16.9.20", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.20.tgz", + "integrity": "sha512-sYJBek61QO1qeZOnGy79jOaQnQK/sT5CHK0gmwEhMzbhrgpRWoxdEXRaaR96vGfRttWliKG82SVrWbc6WRNwng==", "dev": true, "dependencies": { "@types/react": "^16" } }, "node_modules/@types/react-dom/node_modules/@types/react": { - "version": "16.14.46", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.46.tgz", - "integrity": "sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw==", + "version": "16.14.50", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.50.tgz", + "integrity": "sha512-7TWZ/HjhXsRK3BbhSFxTinbSft3sUXJAU3ONngT0rpcKJaIOlxkRke4bidqQTopUbEv1ApC5nlSEkIpX43MkTg==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -9203,21 +9474,21 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", "dev": true }, "node_modules/@types/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -9225,18 +9496,18 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-asaEIoc6J+DbBKXtO7p2shWUpKacZOoMBEGBgPG91P8xhO53ohzHWGCs4ScZo5pQMf5ukQzVT9fhX1WzpHihig==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", - "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", "dev": true, "dependencies": { "@types/http-errors": "*", @@ -9251,15 +9522,15 @@ "dev": true }, "node_modules/@types/sizzle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.4.tgz", + "integrity": "sha512-jA2llq2zNkg8HrALI7DtWzhALcVH0l7i89yhY3iBdOz6cBPeACoFq+fkQrjHA39t1hnSFOboZ7A/AY5MMZSlag==", "dev": true }, "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.34.tgz", + "integrity": "sha512-R+n7qBFnm/6jinlteC9DBL5dGiDGjWAvjo4viUanpnc/dG1y7uDoacXPIQ/PQEg1fI912SMHIa014ZjRpvDw4g==", "dev": true, "dependencies": { "@types/node": "*" @@ -9272,21 +9543,21 @@ "dev": true }, "node_modules/@types/uuid": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", - "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.5.tgz", + "integrity": "sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ==", "dev": true }, "node_modules/@types/webpack-env": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", - "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.2.tgz", + "integrity": "sha512-BFqcTHHTrrI8EBmIzNAmLPP3IqtEG9J1IPFWbPeS/F0/TGNmo0pI5svOa7JbMF9vSCXQCvJWT2gxLJNVuf9blw==", "dev": true }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", + "integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -9299,24 +9570,24 @@ "dev": true }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.28", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz", + "integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==", "dev": true, "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==", "dev": true }, "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", + "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", "dev": true, "optional": true, "dependencies": { @@ -9324,16 +9595,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", - "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", + "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.0", - "@typescript-eslint/type-utils": "6.7.0", - "@typescript-eslint/utils": "6.7.0", - "@typescript-eslint/visitor-keys": "6.7.0", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/type-utils": "6.8.0", + "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -9359,13 +9630,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz", - "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", + "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.0", - "@typescript-eslint/utils": "6.7.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/utils": "6.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -9386,17 +9657,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz", - "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", + "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", "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.7.0", - "@typescript-eslint/types": "6.7.0", - "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", "semver": "^7.5.4" }, "engines": { @@ -9411,15 +9682,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz", - "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", + "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.7.0", - "@typescript-eslint/types": "6.7.0", - "@typescript-eslint/typescript-estree": "6.7.0", - "@typescript-eslint/visitor-keys": "6.7.0", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4" }, "engines": { @@ -9439,13 +9710,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz", - "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", + "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.0", - "@typescript-eslint/visitor-keys": "6.7.0" + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9540,9 +9811,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz", - "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", + "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9553,13 +9824,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz", - "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", + "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.0", - "@typescript-eslint/visitor-keys": "6.7.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9702,12 +9973,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz", - "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", + "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/types": "6.8.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -10773,9 +11044,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", - "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -10790,12 +11061,12 @@ "dev": true }, "node_modules/axobject-query": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/babel-core": { @@ -10960,13 +11231,13 @@ "dev": true }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", + "@babel/helper-define-polyfill-provider": "^0.4.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -10983,25 +11254,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", - "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", + "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.31.0" + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.32.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" + "@babel/helper-define-polyfill-provider": "^0.4.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -11327,9 +11598,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -11345,10 +11616,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -11421,6 +11692,21 @@ "semver": "^7.0.0" } }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -11521,9 +11807,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001534", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz", - "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==", + "version": "1.0.30001550", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001550.tgz", + "integrity": "sha512-p82WjBYIypO0ukTsd/FG3Xxs+4tFeaY9pfT4amQL8KWtYH7H9nYwReGAbMTJ0hsmRO8IfDtsS6p3ZWj8+1c2RQ==", "funding": [ { "type": "opencollective", @@ -11672,9 +11958,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -11892,12 +12178,12 @@ } }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/common-path-prefix": { @@ -12209,12 +12495,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", - "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "dev": true, "dependencies": { - "browserslist": "^4.21.10" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", @@ -12388,14 +12674,14 @@ } }, "node_modules/crypto-es": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/crypto-es/-/crypto-es-2.0.4.tgz", - "integrity": "sha512-GJxgJJ7HjGzwT7iQF0K9RertKkzgSKUXd8X2aQ7RGDS8yMVDzIBFMNHID9hnj48Ep8NAICHWV1CFIETlVNZ/Lg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/crypto-es/-/crypto-es-2.1.0.tgz", + "integrity": "sha512-C5Dbuv4QTPGuloy5c5Vv/FZHtmK+lobLAypFfuRaBbwCsk3qbCWWESCH3MUcBsrgXloRNMrzwUAiPg4U6+IaKA==" }, "node_modules/crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "dev": true }, "node_modules/crypto-random-string": { @@ -12568,9 +12854,9 @@ } }, "node_modules/cypress/node_modules/@types/node": { - "version": "16.18.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.50.tgz", - "integrity": "sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==", + "version": "16.18.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.59.tgz", + "integrity": "sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ==", "dev": true }, "node_modules/cypress/node_modules/ansi-styles": { @@ -13247,9 +13533,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", - "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", "devOptional": true }, "node_modules/debug": { @@ -13284,29 +13570,17 @@ "dev": true }, "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "regexp.prototype.flags": "^1.2.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13332,6 +13606,24 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -13348,6 +13640,116 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -13400,9 +13802,9 @@ } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -13909,9 +14311,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.520", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz", - "integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g==" + "version": "1.4.557", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.557.tgz", + "integrity": "sha512-6x0zsxyMXpnMJnHrondrD3SuAeKcwij9S+83j2qHAQPXbGTDDfgImzzwgGlzrIcXbHQ42tkG4qA6U860cImNhw==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -13979,9 +14381,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz", + "integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -14347,15 +14749,15 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -14400,10 +14802,51 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-storybook": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.13.tgz", - "integrity": "sha512-smd+CS0WH1jBqUEJ3znGS7DU4ayBE9z6lkQAK2yrSUv1+rq8BT/tiI5C/rKE7rmiqiAfojtNYZRhzo5HrulccQ==", + "version": "0.6.15", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.15.tgz", + "integrity": "sha512-lAGqVAJGob47Griu29KXYowI4G7KwMoJDOkEip8ujikuDLxU+oWJ1l0WL6F2oDO4QiyUFXvtDkEkISMOPzo+7w==", "dev": true, "dependencies": { "@storybook/csf": "^0.0.1", @@ -14533,9 +14976,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -15007,6 +15450,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -15319,9 +15768,9 @@ } }, "node_modules/flag-icons": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.11.0.tgz", - "integrity": "sha512-oK+QhV5UMWq+lmyOnfXUfVhSgHy29gQ0gQpmORdNP6ucAsKzIGm39ncvyThJyvt76Au2qsepDTE9waHRuaYPUw==" + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.11.2.tgz", + "integrity": "sha512-xEVPdyDkL28GG0H+x7OP89uFXDgCduNX5DEJ9ZJuxpg3dnDCioBoHfhl4kjC4ixnOnVh7ajL/PNFg7my8qBxtQ==" }, "node_modules/flat": { "version": "5.0.2", @@ -15333,12 +15782,12 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" }, @@ -15347,24 +15796,24 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/flow-parser": { - "version": "0.216.1", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.216.1.tgz", - "integrity": "sha512-wstw46/C/8bRv/8RySCl15lK376j8DHxm41xFjD9eVL+jSS1UmVpbdLdA0LzGuS2v5uGgQiBLEj6mgSJQwW+MA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.219.0.tgz", + "integrity": "sha512-f1RKw+2QW4HCwCQ7qw8fTrlWmQnPIHmWDYbrMhXSSAuDbQbncY63I3Y/vwgimChGF2PT4qtXusu04R3wtCh4hw==", "dev": true, "engines": { "node": ">=0.4.0" } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "dev": true, "funding": [ { @@ -15618,9 +16067,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "engines": { "node": "*" @@ -15678,9 +16127,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", - "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, "node_modules/fs.realpath": { @@ -15703,9 +16152,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -15846,23 +16298,48 @@ } }, "node_modules/giget": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.1.2.tgz", - "integrity": "sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.1.3.tgz", + "integrity": "sha512-zHuCeqtfgqgDwvXlR84UNgnJDuUHQcNI5OqWqFxxuk2BshuKbYhJWdxBsEo4PvKqoGh23lUAIvBNpChMLv7/9Q==", "dev": true, "dependencies": { - "colorette": "^2.0.19", + "colorette": "^2.0.20", "defu": "^6.1.2", - "https-proxy-agent": "^5.0.1", + "https-proxy-agent": "^7.0.2", "mri": "^1.2.0", - "node-fetch-native": "^1.0.2", - "pathe": "^1.1.0", - "tar": "^6.1.13" + "node-fetch-native": "^1.4.0", + "pathe": "^1.1.1", + "tar": "^6.2.0" }, "bin": { "giget": "dist/cli.mjs" } }, + "node_modules/giget/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/giget/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-slugger": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", @@ -15870,19 +16347,19 @@ "dev": true }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", + "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -16133,12 +16610,9 @@ "dev": true }, "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "engines": { "node": ">= 0.4.0" } @@ -16585,9 +17059,9 @@ } }, "node_modules/i18next": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", - "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "version": "23.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.5.1.tgz", + "integrity": "sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==", "dev": true, "funding": [ { @@ -16604,7 +17078,7 @@ } ], "dependencies": { - "@babel/runtime": "^7.20.6" + "@babel/runtime": "^7.22.5" } }, "node_modules/iconv-lite": { @@ -16866,13 +17340,6 @@ "node": ">=8" } }, - "node_modules/inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/inside/-/inside-1.0.0.tgz", - "integrity": "sha512-tvFwvS4g7q6iDot/4FjtWFHwwpv6TVvEumbTdLQilk1F07ojakbXPQcvf3kMAlyNDpzKRzn+d33O3RuXODuxZQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true - }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -17129,6 +17596,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -17566,9 +18066,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -18363,9 +18863,9 @@ } }, "node_modules/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==", + "version": "0.16.9", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", + "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -18388,27 +18888,27 @@ } }, "node_modules/keycloak-js": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-22.0.3.tgz", - "integrity": "sha512-QV96sDngWMlJ8Mq5l+A0BNuzZqJT+d8UM9vvGsPXFT+TDfPjXuFjtrpHH26X6YMY3JaCvH2mH9PXb3DGvEkuOw==", + "version": "22.0.5", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-22.0.5.tgz", + "integrity": "sha512-a7ZwCZeHl8tpeJBy102tZtAnHslDUOA1Nf/sHNF3HYLchKpwoDuaitwIUiS2GnNUe+tlNKLlCqZS+Mi5K79m1w==", "dependencies": { "base64-js": "^1.5.1", "js-sha256": "^0.9.0" } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/khroma": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", - "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", "optional": true }, "node_modules/kind-of": { @@ -18439,13 +18939,13 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/layout-base": { @@ -19177,6 +19677,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -19429,9 +19930,9 @@ } }, "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -20165,9 +20666,9 @@ } }, "node_modules/npm-install-checks": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.2.0.tgz", - "integrity": "sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", "dev": true, "dependencies": { "semver": "^7.1.1" @@ -20557,9 +21058,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz", + "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21304,9 +21805,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -21685,6 +22186,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -22513,9 +23026,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -23033,9 +23546,9 @@ "optional": true }, "node_modules/rollup": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", - "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -23048,6 +23561,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -23164,9 +23692,9 @@ } }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", "dev": true }, "node_modules/saxes": { @@ -23343,34 +23871,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/serve-favicon": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", - "integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==", - "dev": true, - "dependencies": { - "etag": "~1.8.1", - "fresh": "0.5.2", - "ms": "2.1.1", - "parseurl": "~1.3.2", - "safe-buffer": "5.1.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-favicon/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "node_modules/serve-favicon/node_modules/safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true - }, "node_modules/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -23869,9 +24369,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/spdy": { @@ -24241,12 +24741,12 @@ "dev": true }, "node_modules/storybook": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.4.1.tgz", - "integrity": "sha512-b90jq0CYqBMl2JAbC1lInGAoadkPkeGg4Vh8C9Bv7dGhl6M9uei3yEMQTDrj3HvCsktdeZqztGfrkGs2scK+LA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.5.0.tgz", + "integrity": "sha512-dmvQNSuoHq1KrPcK8siApBi5n5reSf6RFAlLHYD+nhM+EP6SL2fXdVjP6ZynTUMRu1NQ5YR/oJhz/SsBzJNkcA==", "dev": true, "dependencies": { - "@storybook/cli": "7.4.1" + "@storybook/cli": "7.5.0" }, "bin": { "sb": "index.js", @@ -24561,6 +25061,22 @@ "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", "dev": true }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -25015,6 +25531,18 @@ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "dev": true }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -25053,9 +25581,9 @@ } }, "node_modules/tocbot": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.21.1.tgz", - "integrity": "sha512-IfajhBTeg0HlMXu1f+VMbPef05QpDTsZ9X2Yn1+8npdaXsXg/+wrm9Ze1WG5OS1UDC3qJ5EQN/XOZ3gfXjPFCw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.21.2.tgz", + "integrity": "sha512-R5Muhi/TUu4i4snWVrMgNoXyJm2f8sJfdgIkQvqb+cuIXQEIMAiWGWgCgYXHqX4+XiS/Bnm7IYZ9Zy6NVe6lhw==", "dev": true }, "node_modules/toidentifier": { @@ -25147,12 +25675,12 @@ } }, "node_modules/ts-morph": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", - "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-20.0.0.tgz", + "integrity": "sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==", "dev": true, "dependencies": { - "@ts-morph/common": "~0.19.0", + "@ts-morph/common": "~0.21.0", "code-block-writer": "^12.0.0" } }, @@ -25449,6 +25977,11 @@ "node": ">=0.8.0" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -25609,12 +26142,12 @@ } }, "node_modules/unplugin": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.4.0.tgz", - "integrity": "sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.5.0.tgz", + "integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.10.0", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.5.0" @@ -25630,9 +26163,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -25667,9 +26200,9 @@ } }, "node_modules/url": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.2.tgz", - "integrity": "sha512-7yIgNnrST44S7PJ5+jXbdIupfU1nWUdQJBFBeJRclPXiWgCvrSq5Frw8lr/i//n5sqDfzoKmBymMS81l4U/7cg==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dev": true, "dependencies": { "punycode": "^1.4.1", @@ -26573,9 +27106,9 @@ } }, "node_modules/ws": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", - "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "dev": true, "engines": { "node": ">=10.0.0" @@ -26667,9 +27200,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", "dev": true, "engines": { "node": ">= 14" diff --git a/package.json b/package.json index af9ad8b5b3..9aaaf0368d 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,14 @@ "private": true, "dependencies": { "@angular/animations": "^16.2.1", - "@angular/cdk": "^16.2.1", + "@angular/cdk": "^16.2.10", "@angular/common": "^16.2.1", "@angular/compiler": "^16.2.1", "@angular/core": "^16.2.1", "@angular/forms": "^16.2.1", "@angular/localize": "^16.2.1", - "@angular/material": "^16.2.1", - "@angular/material-moment-adapter": "^16.2.1", + "@angular/material": "^16.2.10", + "@angular/material-moment-adapter": "^16.2.10", "@angular/platform-browser": "^16.2.1", "@angular/platform-browser-dynamic": "^16.2.1", "@angular/router": "^16.2.1", @@ -42,15 +42,15 @@ "@fortawesome/free-regular-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ngneat/until-destroy": "^10.0.0", - "@sentry/browser": "^7.64.0", + "@sentry/browser": "^7.76.0", "angulartics2": "^12.2.1", "assert": "^2.0.0", - "crypto-es": "^2.0.4", + "crypto-es": "^2.1.0", "deep-object-diff": "^1.1.9", - "flag-icons": "^6.10.0", + "flag-icons": "^6.11.2", "hammerjs": "^2.0.8", "json-query": "^2.2.2", - "keycloak-js": "^22.0.1", + "keycloak-js": "^22.0.5", "leaflet": "^1.9.4", "lodash-es": "^4.17.21", "md5": "^2.3.0", @@ -102,6 +102,8 @@ "babel-loader": "^9.1.3", "cypress": "13.1.0", "eslint": "^8.47.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-storybook": "^0.6.13", "jasmine-core": "^5.1.0", "jasmine-spec-reporter": "^7.0.0", diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.html b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.html deleted file mode 100644 index 0cb187e2f0..0000000000 --- a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.spec.ts b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.spec.ts deleted file mode 100644 index c5881318bb..0000000000 --- a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from "@angular/core/testing"; -import { RecurringActivity } from "../model/recurring-activity"; -import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; -import { - mockEntityMapper, - MockEntityMapperService, -} from "../../../core/entity/entity-mapper/mock-entity-mapper-service"; -import { UpdatedEntity } from "../../../core/entity/model/entity-update"; -import { Subject } from "rxjs"; -import { School } from "../../schools/model/school"; - -import { ActivitiesOverviewComponent } from "./activities-overview.component"; -import { MockedTestingModule } from "../../../utils/mocked-testing.module"; -import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; - -describe("ActivitiesOverviewComponent", () => { - let component: ActivitiesOverviewComponent; - let fixture: ComponentFixture; - - let entityMapper: MockEntityMapperService; - - beforeEach(waitForAsync(() => { - entityMapper = mockEntityMapper(); - TestBed.configureTestingModule({ - imports: [ActivitiesOverviewComponent, MockedTestingModule.withState()], - providers: [ - { provide: EntityMapperService, useValue: entityMapper }, - { provide: FormDialogService, useValue: null }, - ], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ActivitiesOverviewComponent); - component = fixture.componentInstance; - component.entity = new School(); - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should fetch all and only recurring activities having the selected school as a linkedGroup", async () => { - const school1 = new School("school1"); - const activity1 = new RecurringActivity(); - activity1.linkedGroups = ["school1"]; - const activity2 = new RecurringActivity(); - activity2.linkedGroups = ["school1", "school2"]; - const activity3 = new RecurringActivity(); - activity3.linkedGroups = ["school3"]; - entityMapper.addAll([activity1, activity2, activity3]); - - component.entity = school1; - await component.ngOnInit(); - expect(component.records).toEqual([activity1, activity2]); - }); - - it("should create a new recurring activity having the current school as a linkedGroup", () => { - component.entity = new School("school1"); - const newRecurringActivity = component.generateNewRecordFactory(); - expect(newRecurringActivity().linkedGroups).toEqual(["school1"]); - }); - - it("should remove the recurring activity from the table view if the current school is removed as a group of this recurring activity", fakeAsync(() => { - const school1 = new School("school1"); - const activity1 = new RecurringActivity(); - activity1.linkedGroups = ["school1"]; - const activity2 = new RecurringActivity(); - activity2.linkedGroups = ["school1", "school2", "school3"]; - entityMapper.addAll([activity1, activity2]); - const subject = new Subject>(); - spyOn(entityMapper, "receiveUpdates").and.returnValue(subject); - component.entity = school1; - component.ngOnInit(); - tick(); - - expect(component.records).toEqual([activity1, activity2]); - - activity2.linkedGroups = ["school2", "school3"]; - subject.next({ entity: activity2, type: "update" }); - tick(); - - expect(component.records).toEqual([activity1]); - })); -}); diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts index e67e89f7e3..4d5e5e6e0d 100644 --- a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts +++ b/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.ts @@ -1,23 +1,28 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { RecurringActivity } from "../model/recurring-activity"; -import { FormFieldConfig } from "../../../core/common-components/entity-form/entity-form/FormConfig"; -import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; -import { Entity } from "../../../core/entity/model/entity"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; -import { delay } from "rxjs"; +import { RelatedEntitiesComponent } from "../../../core/entity-details/related-entities/related-entities.component"; import { EntitySubrecordComponent } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { ColumnConfig } from "../../../core/common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; -@UntilDestroy() +/** + * @deprecated configure a RelatedEntitiesComponent instead + */ @DynamicComponent("ActivitiesOverview") @Component({ selector: "app-activities-overview", - templateUrl: "./activities-overview.component.html", - styleUrls: ["./activities-overview.component.scss"], + templateUrl: + "../../../core/entity-details/related-entities/related-entities.component.html", imports: [EntitySubrecordComponent], standalone: true, }) -export class ActivitiesOverviewComponent implements OnInit { +export class ActivitiesOverviewComponent + extends RelatedEntitiesComponent + implements OnInit +{ + entityType = RecurringActivity.ENTITY_TYPE; + property = "linkedGroups"; + titleColumn = { id: "title", edit: "EditTextWithAutocomplete", @@ -27,49 +32,16 @@ export class ActivitiesOverviewComponent implements OnInit { relevantValue: "", }, }; - @Input() columns: FormFieldConfig[] = [ + _columns: ColumnConfig[] = [ this.titleColumn, - { id: "type" }, - { id: "assignedTo" }, - { id: "linkedGroups" }, - { id: "excludedParticipants" }, + "type", + "assignedTo", + "linkedGroups", + "excludedParticipants", ]; - @Input() entity: Entity; - records: RecurringActivity[] = []; - - constructor(private entityMapper: EntityMapperService) {} - async ngOnInit() { this.titleColumn.additional.relevantValue = this.entity.getId(); - await this.initLinkedActivities(); - - this.entityMapper - .receiveUpdates(RecurringActivity) - // using short delay to make sure the EntitySubrecord's `receiveUpdates` code is executed before this - .pipe(delay(0), untilDestroyed(this)) - .subscribe((updateEntity) => { - if (updateEntity.type === "update") { - this.initLinkedActivities(); - } - }); - } - - private async initLinkedActivities() { - this.records = await this.entityMapper - .loadType(RecurringActivity) - .then((activities) => - activities.filter((activity) => - activity.linkedGroups.includes(this.entity.getId()), - ), - ); - } - - generateNewRecordFactory(): () => RecurringActivity { - return () => { - const newRecurringActivity = new RecurringActivity(); - newRecurringActivity.linkedGroups.push(this.entity.getId()); - return newRecurringActivity; - }; + await super.ngOnInit(); } } diff --git a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.html b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.html index de9808bda5..07fc624d8c 100644 --- a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.html +++ b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.html @@ -29,7 +29,7 @@

{{ event.subject }}

diff --git a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts index ad53d635fc..f465cc1a85 100644 --- a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts +++ b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts @@ -1,11 +1,9 @@ -import { Component, ViewChild } from "@angular/core"; +import { Component, Input, ViewChild } from "@angular/core"; import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; import { Note } from "../../notes/model/note"; import { ConfirmationDialogService } from "../../../core/common-components/confirmation-dialog/confirmation-dialog.service"; import { ConfirmationDialogButton } from "../../../core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component"; import { RollCallComponent } from "./roll-call/roll-call.component"; -import { ActivatedRoute } from "@angular/router"; -import { RouteData } from "../../../core/config/dynamic-routing/view-config.interface"; import { RouteTarget } from "../../../app.routing"; import { NgIf } from "@angular/common"; import { MatButtonModule } from "@angular/material/button"; @@ -14,14 +12,6 @@ import { MatTooltipModule } from "@angular/material/tooltip"; import { RollCallSetupComponent } from "./roll-call-setup/roll-call-setup.component"; import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component"; -/** - * additional config specifically for AddDayAttendanceComponent - */ -export interface AddDayAttendanceConfig { - /** (optional) property name of the participant entities by which they are sorted for the roll call */ - sortParticipantsBy?: string; -} - @RouteTarget("AddDayAttendance") @Component({ selector: "app-add-day-attendance", @@ -39,7 +29,8 @@ export interface AddDayAttendanceConfig { standalone: true, }) export class AddDayAttendanceComponent { - config?: AddDayAttendanceConfig; + /** (optional) property name of the participant entities by which they are sorted for the roll call */ + @Input() sortParticipantsBy: string; currentStage = 0; @@ -75,13 +66,8 @@ export class AddDayAttendanceComponent { constructor( private entityMapper: EntityMapperService, - private route: ActivatedRoute, private confirmationDialog: ConfirmationDialogService, - ) { - this.route.data.subscribe((data: RouteData) => { - this.config = data.config; - }); - } + ) {} finishBasicInformationStage(event: Note) { this.event = event; diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html index 8a9617e58d..ecdd75d394 100644 --- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html +++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html @@ -5,7 +5,7 @@ [value]="(currentIndex / children.length) * 100" > -
+
{{ currentIndex + 1 }} / {{ children.length }}
-
+
+ +
+ +
{ let component: AttendanceWeekDashboardComponent; @@ -90,28 +91,38 @@ describe("AttendanceWeekDashboardComponent", () => { ]); }); - it("should correctly use the offset", () => { - // default case: last week monday till saturday - const mondayLastWeek = moment().startOf("isoWeek").subtract(7, "days"); - const saturdayLastWeek = mondayLastWeek.clone().add("5", "days"); + function expectTimePeriodCalled(from: moment.Moment, to: moment.Moment) { mockAttendanceService.getAllActivityAttendancesForPeriod.calls.reset(); component.ngOnInit(); expect( mockAttendanceService.getAllActivityAttendancesForPeriod, - ).toHaveBeenCalledWith(mondayLastWeek.toDate(), saturdayLastWeek.toDate()); + ).toHaveBeenCalledWith(from.toDate(), to.toDate()); + } - // with offset: this week monday till saturday - const mondayThisWeek = moment().startOf("isoWeek"); - const saturdayThisWeek = mondayThisWeek.clone().add(5, "days"); - mockAttendanceService.getAllActivityAttendancesForPeriod.calls.reset(); + it("should correctly use the offset", () => { + // default case: last week monday till saturday + // on Monday, that's the first day of the current period + MockDate.set(moment("2023-11-20").toDate()); + const mondayLastWeek = moment("2023-11-13"); + const saturdayLastWeek = moment("2023-11-18"); + expectTimePeriodCalled(mondayLastWeek, saturdayLastWeek); + + // on Sunday, that's the still the last day of the currently ending period + MockDate.set(moment("2023-11-26").toDate()); + const mondayLastWeek2 = moment("2023-11-13"); + const saturdayLastWeek2 = moment("2023-11-18"); + expectTimePeriodCalled(mondayLastWeek2, saturdayLastWeek2); + + // with offset: this week monday till saturday + MockDate.set(moment("2023-11-20").toDate()); + const mondayThisWeek = moment("2023-11-20"); + const saturdayThisWeek = moment("2023-11-25"); component.daysOffset = 7; - component.ngOnInit(); + expectTimePeriodCalled(mondayThisWeek, saturdayThisWeek); - expect( - mockAttendanceService.getAllActivityAttendancesForPeriod, - ).toHaveBeenCalledWith(mondayThisWeek.toDate(), saturdayThisWeek.toDate()); + MockDate.reset(); }); }); diff --git a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts index be585ee430..2b1db14b1f 100644 --- a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts +++ b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.ts @@ -99,22 +99,16 @@ export class AttendanceWeekDashboardComponent implements OnInit, AfterViewInit { } private async loadAttendanceOfAbsentees() { - const today = new Date(); - const previousMonday = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() - today.getDay() - 6 + this.daysOffset, - ); - const previousSaturday = new Date( - previousMonday.getFullYear(), - previousMonday.getMonth(), - previousMonday.getDate() + 5, - ); + const previousMonday = moment() + .startOf("isoWeek") + .subtract(1, "week") + .add(this.daysOffset, "days"); + const previousSaturday = moment(previousMonday).add(5, "days"); const activityAttendances = await this.attendanceService.getAllActivityAttendancesForPeriod( - previousMonday, - previousSaturday, + previousMonday.toDate(), + previousSaturday.toDate(), ); const lowAttendanceCases = new Set(); const records: AttendanceWeekRow[] = []; diff --git a/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.spec.ts b/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.spec.ts index 069ca990ce..bee7d4c417 100644 --- a/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.spec.ts +++ b/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.spec.ts @@ -85,9 +85,10 @@ describe("EditAttendanceComponent", () => { categoryForm.setValue(defaultInteractionTypes.find((c) => c.isMeeting)); fixture.detectChanges(); - const inputElements = await TestbedHarnessEnvironment.loader( - fixture, - ).getAllHarnesses(MatInputHarness); + const inputElements = + await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses( + MatInputHarness, + ); const firstRemarkInput = inputElements[1]; await firstRemarkInput.setValue("new remarks"); diff --git a/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.ts b/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.ts index 7fe25e7f1a..58ba70a4c3 100644 --- a/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.ts +++ b/src/app/child-dev-project/attendance/edit-attendance/edit-attendance.component.ts @@ -90,12 +90,13 @@ export class EditAttendanceComponent const index = children.indexOf(id); children.splice(index, 1); this.attendanceForm.value.delete(id); - this.formControl.setValue([...children]); this.formControl.markAsDirty(); + this.formControl.setValue([...children]); } updateAttendanceValue(childId, property: "status" | "remarks", newValue) { - this.getAttendance(childId)[property] = newValue; this.formControl.markAsDirty(); + this.getAttendance(childId)[property] = newValue; + this.attendanceForm.setValue(this.attendanceForm.value); } } diff --git a/src/app/child-dev-project/children/aser/model/aser.ts b/src/app/child-dev-project/children/aser/model/aser.ts index 045bf2b32d..19ecb55a87 100644 --- a/src/app/child-dev-project/children/aser/model/aser.ts +++ b/src/app/child-dev-project/children/aser/model/aser.ts @@ -22,15 +22,26 @@ import { SkillLevel } from "./skill-levels"; import { WarningLevel } from "../../../warning-level"; import { ConfigurableEnumDatatype } from "../../../../core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype"; import { PLACEHOLDERS } from "../../../../core/entity/schema/entity-schema-field"; +import { Child } from "../../model/child"; @DatabaseEntity("Aser") export class Aser extends Entity { - @DatabaseField() child: string; // id of Child entity + static override hasPII = true; + + @DatabaseField({ + dataType: "entity", + additional: Child.ENTITY_TYPE, + entityReferenceRole: "composite", + }) + child: string; + @DatabaseField({ label: $localize`:Label for date of the ASER results:Date`, defaultValue: PLACEHOLDERS.NOW, + anonymize: "retain-anonymized", }) date: Date; + @DatabaseField({ label: $localize`:Label of the Hindi ASER result:Hindi`, dataType: "configurable-enum", @@ -55,6 +66,7 @@ export class Aser extends Entity { innerDataType: "math-levels", }) math: SkillLevel; + @DatabaseField({ label: $localize`:Label for the remarks of a ASER result:Remarks`, }) 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 8b3c1815f1..a5e2166e74 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,9 +5,16 @@ [class.inactive]="!entity.isActive" class="truncate-text container" > - + {{ entity?.toString() }} - ({{ entity?.projectNumber }}) + + ({{ entity?.projectNumber }}) diff --git a/src/app/child-dev-project/children/children-components.ts b/src/app/child-dev-project/children/children-components.ts index 569caea244..c39c2287e0 100644 --- a/src/app/child-dev-project/children/children-components.ts +++ b/src/app/child-dev-project/children/children-components.ts @@ -44,13 +44,6 @@ export const childrenComponents: ComponentTuple[] = [ "./health-checkup/children-bmi-dashboard/children-bmi-dashboard.component" ).then((c) => c.ChildrenBmiDashboardComponent), ], - [ - "RelatedEntitiesWithSummary", - () => - import( - "../../core/entity-details/related-entities-with-summary/related-entities-with-summary.component" - ).then((c) => c.RelatedEntitiesWithSummaryComponent), - ], [ "BmiBlock", () => diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts index 97b121db14..5153552570 100644 --- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts +++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts @@ -17,7 +17,6 @@ describe("ChildrenListComponent", () => { let fixture: ComponentFixture; const routeData: EntityListConfig = { title: "Children List", - filterPlaceholder: "e.g. participant name", columns: [ { view: "DisplayText", label: "PN", id: "projectNumber" }, { view: "ChildBlock", label: "Name", id: "name" }, diff --git a/src/app/child-dev-project/children/children-list/children-list.component.ts b/src/app/child-dev-project/children/children-list/children-list.component.ts index f692d2d3f4..03fd7dc774 100644 --- a/src/app/child-dev-project/children/children-list/children-list.component.ts +++ b/src/app/child-dev-project/children/children-list/children-list.component.ts @@ -34,6 +34,8 @@ export class ChildrenListComponent implements OnInit { async ngOnInit() { this.route.data.subscribe( + // TODO replace this use of route and rely on the RoutedViewComponent instead + // see that flattens the config option, assigning individual properties as inputs however, so we can't easily pass on (data: RouteData) => (this.listConfig = data.config), ); this.childrenList = await this.childrenService.getChildren(); diff --git a/src/app/child-dev-project/children/children.service.spec.ts b/src/app/child-dev-project/children/children.service.spec.ts index 371750d700..09e60059ce 100644 --- a/src/app/child-dev-project/children/children.service.spec.ts +++ b/src/app/child-dev-project/children/children.service.spec.ts @@ -11,7 +11,7 @@ import { genders } from "./model/genders"; import { DatabaseTestingModule } from "../../utils/database-testing.module"; import { sortByAttribute } from "../../utils/utils"; import { expectEntitiesToMatch } from "../../utils/expect-entity-data.spec"; -import { DateWithAge } from "./model/dateWithAge"; +import { DateWithAge } from "../../core/basic-datatypes/date-with-age/dateWithAge"; describe("ChildrenService", () => { let service: ChildrenService; diff --git a/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts b/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts index 522bd4e305..f10e8d1a66 100644 --- a/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts +++ b/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts @@ -8,7 +8,7 @@ import { faker } from "../../../core/demo-data/faker"; import { centersWithProbability } from "./fixtures/centers"; import { genders } from "../model/genders"; import { calculateAge } from "../../../utils/utils"; -import { DateWithAge } from "../model/dateWithAge"; +import { DateWithAge } from "../../../core/basic-datatypes/date-with-age/dateWithAge"; export class DemoChildConfig { count: number; diff --git a/src/app/child-dev-project/children/health-checkup/model/health-check.ts b/src/app/child-dev-project/children/health-checkup/model/health-check.ts index 56e639a030..b8ad26bcca 100644 --- a/src/app/child-dev-project/children/health-checkup/model/health-check.ts +++ b/src/app/child-dev-project/children/health-checkup/model/health-check.ts @@ -19,6 +19,7 @@ import { Entity } from "../../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../../core/entity/database-field.decorator"; import { WarningLevel } from "../../../warning-level"; +import { Child } from "../../model/child"; /** * Model Class for the Health Checks that are taken for a Child. @@ -26,12 +27,24 @@ import { WarningLevel } from "../../../warning-level"; */ @DatabaseEntity("HealthCheck") export class HealthCheck extends Entity { + static override hasPII = true; + static create(contents: Partial) { return Object.assign(new HealthCheck(), contents); } - @DatabaseField() child: string; - @DatabaseField({ label: $localize`:Label for date of a health check:Date` }) + @DatabaseField({ + dataType: "entity", + additional: Child.ENTITY_TYPE, + entityReferenceRole: "composite", + anonymize: "retain", + }) + child: string; + + @DatabaseField({ + label: $localize`:Label for date of a health check:Date`, + anonymize: "retain-anonymized", + }) date: Date; /** height measurement in cm **/ diff --git a/src/app/child-dev-project/children/model/child.spec.ts b/src/app/child-dev-project/children/model/child.spec.ts index ab4f350743..6f35d39f6f 100644 --- a/src/app/child-dev-project/children/model/child.spec.ts +++ b/src/app/child-dev-project/children/model/child.spec.ts @@ -57,5 +57,13 @@ describe("Child", () => { const testEntity3 = new Child(); testEntity3["status"] = "Dropout"; expect(testEntity3.isActive).withContext("Dropout").toBeFalse(); + + // always give "inactive" precedence over other logic (as it is trigger in UI) + const testEntityPrec = new Child(); + testEntityPrec["inactive"] = false; + testEntityPrec["status"] = "Dropout"; + expect(testEntityPrec.isActive) + .withContext("inactive taking precedence") + .toBeTrue(); }); }); diff --git a/src/app/child-dev-project/children/model/child.ts b/src/app/child-dev-project/children/model/child.ts index 79f8fe2fad..4cd743f56c 100644 --- a/src/app/child-dev-project/children/model/child.ts +++ b/src/app/child-dev-project/children/model/child.ts @@ -19,8 +19,8 @@ import { Entity } from "../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../core/entity/database-field.decorator"; import { ConfigurableEnumValue } from "../../../core/basic-datatypes/configurable-enum/configurable-enum.interface"; -import { DateWithAge } from "./dateWithAge"; import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { DateWithAge } from "../../../core/basic-datatypes/date-with-age/dateWithAge"; export type Center = ConfigurableEnumValue; @@ -31,6 +31,7 @@ export class Child extends Entity { static label = $localize`:label for entity:Participant`; static labelPlural = $localize`:label (plural) for entity:Participants`; static color = "#1565C0"; + static override hasPII = true; static create(name: string): Child { const instance = new Child(); @@ -54,12 +55,14 @@ export class Child extends Entity { label: $localize`:Label for the project number of a child:Project Number`, labelShort: $localize`:Short label for the project number:PN`, searchable: true, + anonymize: "retain", }) projectNumber: string; @DatabaseField({ label: $localize`:Label for the date of birth of a child:Date of birth`, labelShort: $localize`:Short label for the date of birth:DoB`, + anonymize: "retain-anonymized", }) dateOfBirth: DateWithAge; @@ -67,6 +70,7 @@ export class Child extends Entity { dataType: "configurable-enum", label: $localize`:Label for the gender of a child:Gender`, innerDataType: "genders", + anonymize: "retain", }) gender: ConfigurableEnumValue; @@ -74,11 +78,13 @@ export class Child extends Entity { dataType: "configurable-enum", innerDataType: "center", label: $localize`:Label for the center of a child:Center`, + anonymize: "retain", }) center: Center; @DatabaseField({ label: $localize`:Label for the admission date of a child:Admission`, + anonymize: "retain-anonymized", }) admissionDate: Date; @@ -89,11 +95,13 @@ export class Child extends Entity { @DatabaseField({ label: $localize`:Label for the dropout date of a child:Dropout Date`, + anonymize: "retain-anonymized", }) dropoutDate: Date; @DatabaseField({ label: $localize`:Label for the type of dropout of a child:Dropout Type`, + anonymize: "retain", }) dropoutType: string; @@ -120,6 +128,11 @@ export class Child extends Entity { phone: string; get isActive(): boolean { + if (this.inactive !== undefined) { + // explicit property set through UI has to take precedence + return super.isActive; + } + return ( this.status !== "Dropout" && !this["dropoutDate"] && diff --git a/src/app/child-dev-project/children/model/childSchoolRelation.ts b/src/app/child-dev-project/children/model/childSchoolRelation.ts index fdb8119efe..4362b98af5 100644 --- a/src/app/child-dev-project/children/model/childSchoolRelation.ts +++ b/src/app/child-dev-project/children/model/childSchoolRelation.ts @@ -9,13 +9,17 @@ import { TimePeriod } from "../../../core/entity-details/related-time-period-ent */ @DatabaseEntity("ChildSchoolRelation") export class ChildSchoolRelation extends TimePeriod { + static override hasPII = true; + @DatabaseField({ label: $localize`:Label for the child of a relation:Child`, dataType: "entity", additional: Child.ENTITY_TYPE, + entityReferenceRole: "composite", validators: { required: true, }, + anonymize: "retain", }) childId: string; @@ -23,9 +27,11 @@ export class ChildSchoolRelation extends TimePeriod { label: $localize`:Label for the school of a relation:School`, dataType: "entity", additional: School.ENTITY_TYPE, + entityReferenceRole: "aggregate", validators: { required: true, }, + anonymize: "retain", }) schoolId: string; @@ -36,6 +42,7 @@ export class ChildSchoolRelation extends TimePeriod { dataType: "date-only", label: $localize`:Label for the start date of a relation:Start date`, description: $localize`:Description of the start date of a relation:The date a child joins a school`, + anonymize: "retain", }) start: Date; @@ -43,6 +50,7 @@ export class ChildSchoolRelation extends TimePeriod { dataType: "date-only", label: $localize`:Label for the end date of a relation:End date`, description: $localize`:Description of the end date of a relation:The date of a child leaving the school`, + anonymize: "retain", }) end: Date; diff --git a/src/app/child-dev-project/notes/model/note.ts b/src/app/child-dev-project/notes/model/note.ts index 11bf224ada..378815bb0d 100644 --- a/src/app/child-dev-project/notes/model/note.ts +++ b/src/app/child-dev-project/notes/model/note.ts @@ -39,6 +39,7 @@ export class Note extends Entity { static toStringAttributes = ["subject"]; static label = $localize`:label for entity:Note`; static labelPlural = $localize`:label (plural) for entity:Notes`; + static override hasPII = true; static create( date: Date, @@ -76,7 +77,9 @@ export class Note extends Entity { label: $localize`:Label for the children of a note:Children`, dataType: "entity-array", additional: Child.ENTITY_TYPE, + entityReferenceRole: "composite", editComponent: "EditAttendance", + anonymize: "retain", }) children: string[] = []; @@ -85,28 +88,37 @@ export class Note extends Entity { * * No direct access to change this property. Use the `.getAttendance()` method to have safe access. */ - @DatabaseField({ innerDataType: "schema-embed", additional: EventAttendance }) + @DatabaseField({ + innerDataType: "schema-embed", + additional: EventAttendance, + anonymize: "retain", + }) private childrenAttendance: Map = new Map(); @DatabaseField({ label: $localize`:Label for the date of a note:Date`, dataType: "date-only", defaultValue: PLACEHOLDERS.NOW, + anonymize: "retain", }) date: Date; + @DatabaseField({ label: $localize`:Label for the subject of a note:Subject` }) subject: string; + @DatabaseField({ label: $localize`:Label for the actual notes of a note:Notes`, editComponent: "EditLongText", }) text: string; + /** IDs of users that authored this note */ @DatabaseField({ label: $localize`:Label for the social worker(s) who created the note:SW`, dataType: "entity-array", additional: User.ENTITY_TYPE, defaultValue: PLACEHOLDERS.CURRENT_USER, + anonymize: "retain", }) authors: string[] = []; @@ -114,13 +126,23 @@ export class Note extends Entity { label: $localize`:Label for the category of a note:Category`, dataType: "configurable-enum", innerDataType: INTERACTION_TYPE_CONFIG_ID, + anonymize: "retain", }) category: InteractionType; + @DatabaseField({ + label: $localize`Attachment`, + dataType: "file", + }) + attachment: string; + /** * id referencing a different entity (e.g. a recurring activity) this note is related to */ - @DatabaseField() relatesTo: string; + @DatabaseField({ + anonymize: "retain", + }) + relatesTo: string; /** * other records (e.g. a recurring activity, group membership, ...) to which this note is related in some way, @@ -134,6 +156,7 @@ export class Note extends Entity { editComponent: "EditEntityArray", // by default no additional relatedEntities can be linked apart from children and schools, overwrite this in config to display (e.g. additional: "ChildSchoolRelation") additional: undefined, + anonymize: "retain", }) relatedEntities: string[] = []; @@ -144,6 +167,8 @@ export class Note extends Entity { label: $localize`:label for the linked schools:Groups`, dataType: "entity-array", additional: School.ENTITY_TYPE, + entityReferenceRole: "composite", + anonymize: "retain", }) schools: string[] = []; @@ -151,6 +176,7 @@ export class Note extends Entity { label: $localize`:Status of a note:Status`, dataType: "configurable-enum", innerDataType: "warning-levels", + anonymize: "retain", }) warningLevel: Ordering.EnumValue; diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.html b/src/app/child-dev-project/notes/note-details/note-details.component.html index 7832f0da99..2649422078 100644 --- a/src/app/child-dev-project/notes/note-details/note-details.component.html +++ b/src/app/child-dev-project/notes/note-details/note-details.component.html @@ -18,51 +18,55 @@

{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}

- - - + +
+ - -
-
- - -
+ + + + +
+
+ + +
-
- - +
+ + +
-
- + +
diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.ts b/src/app/child-dev-project/notes/note-details/note-details.component.ts index 368ec94a47..060add770f 100644 --- a/src/app/child-dev-project/notes/note-details/note-details.component.ts +++ b/src/app/child-dev-project/notes/note-details/note-details.component.ts @@ -26,6 +26,7 @@ import { DynamicComponentDirective } from "../../../core/config/dynamic-componen import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog"; import { DialogButtonsComponent } from "../../../core/form-dialog/dialog-buttons/dialog-buttons.component"; import { DialogCloseComponent } from "../../../core/common-components/dialog-close/dialog-close.component"; +import { EntityArchivedInfoComponent } from "../../../core/entity-details/entity-archived-info/entity-archived-info.component"; /** * Component responsible for displaying the Note creation/view window @@ -46,6 +47,7 @@ import { DialogCloseComponent } from "../../../core/common-components/dialog-clo DialogButtonsComponent, MatMenuModule, DialogCloseComponent, + EntityArchivedInfoComponent, ], standalone: true, encapsulation: ViewEncapsulation.None, @@ -56,9 +58,9 @@ export class NoteDetailsComponent implements OnInit { /** export format for notes to be used for downloading the individual details */ exportConfig: ExportColumnConfig[]; - topForm = ["date", "warningLevel", "category", "authors"].map((field) => [ - toFormFieldConfig(field), - ]); + topForm = ["date", "warningLevel", "category", "authors", "attachment"].map( + (field) => [toFormFieldConfig(field)], + ); middleForm = ["subject", "text"].map(toFormFieldConfig); bottomForm = ["children", "schools"].map(toFormFieldConfig); form: EntityForm; diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts index 92422aa17a..0c1238b8fa 100644 --- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts +++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts @@ -1,7 +1,4 @@ -import { - NotesManagerComponent, - NotesManagerConfig, -} from "./notes-manager.component"; +import { NotesManagerComponent } from "./notes-manager.component"; import { ComponentFixture, fakeAsync, @@ -191,11 +188,9 @@ describe("NotesManagerComponent", () => { entityMapper.save(eventNote); tick(); + component.includeEventNotes = true; routeMock.data.next({ - config: Object.assign( - { includeEventNotes: true } as NotesManagerConfig, - routeData, - ), + config: routeData, }); flush(); diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts index e38943d871..a7ae7e67b7 100644 --- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts +++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts @@ -4,7 +4,6 @@ import { NoteDetailsComponent } from "../note-details/note-details.component"; import { ActivatedRoute } from "@angular/router"; import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; import { FilterSelectionOption } from "../../../core/filter/filters/filters"; -import { SessionService } from "../../../core/session/session-service/session.service"; import { FormDialogService } from "../../../core/form-dialog/form-dialog.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { LoggingService } from "../../../core/logging/logging.service"; @@ -92,7 +91,6 @@ export class NotesManagerComponent implements OnInit { constructor( private formDialog: FormDialogService, - private sessionService: SessionService, private entityMapperService: EntityMapperService, private route: ActivatedRoute, private log: LoggingService, @@ -101,11 +99,9 @@ export class NotesManagerComponent implements OnInit { async ngOnInit() { this.route.data.subscribe( async (data: RouteData) => { + // TODO replace this use of route and rely on the RoutedViewComponent instead this.config = data.config; this.addPrebuiltFilters(); - - this.includeEventNotes = data.config.includeEventNotes; - this.showEventNotesToggle = data.config.showEventNotesToggle; this.notes = await this.loadEntities(); }, ); diff --git a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts index d4bea18e49..2ac5ade5e0 100644 --- a/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts +++ b/src/app/child-dev-project/schools/child-school-overview/child-school-overview.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; import { Child } from "../../children/model/child"; import { School } from "../model/school"; @@ -41,6 +41,7 @@ export class ChildSchoolOverviewComponent implements OnInit { mode: "child" | "school" = "child"; + @Input() showInactive = false; constructor(private childrenService: ChildrenService) { super(null, null); @@ -60,7 +61,7 @@ export class ChildSchoolOverviewComponent this.switchRelatedEntityColumnForMode(); await this.loadData(); - super.filterActiveInactive(); + super.onIsActiveFilterChange(this.showInactive); } private inferMode(entity: Entity): "child" | "school" { diff --git a/src/app/core/basic-datatypes/array/array.datatype.ts b/src/app/core/basic-datatypes/array/array.datatype.ts index 25d0aff32e..0d036455a7 100644 --- a/src/app/core/basic-datatypes/array/array.datatype.ts +++ b/src/app/core/basic-datatypes/array/array.datatype.ts @@ -97,6 +97,25 @@ export class ArrayDatatype< ), ); } + + async anonymize( + value: EntityType[], + schemaField: EntitySchemaField, + parent, + ): Promise { + const arrayElementDatatype: DefaultDatatype = + this.schemaService.getDatatypeOrDefault(schemaField.innerDataType); + + const mappedPromises = value.map(async (el) => + arrayElementDatatype.anonymize( + el, + generateSubSchemaField(schemaField), + parent, + ), + ); + + return Promise.all(mappedPromises); + } } /** diff --git a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.html b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.html index f0e54c54cc..599b384fc6 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.html +++ b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.html @@ -2,6 +2,7 @@

Edit dropdown options

+ matIconPrefix class="grab-icon margin-right-small" > + + + + + + Add new option + + + diff --git a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts index f1d171a757..32bd4619d7 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.spec.ts @@ -17,6 +17,7 @@ import { entityRegistry, EntityRegistry, } from "../../../entity/database-entity.decorator"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; describe("ConfigureEnumPopupComponent", () => { let component: ConfigureEnumPopupComponent; @@ -26,7 +27,11 @@ describe("ConfigureEnumPopupComponent", () => { beforeEach(async () => { entityMapper = mockEntityMapper(); await TestBed.configureTestingModule({ - imports: [ConfigureEnumPopupComponent, FontAwesomeTestingModule], + imports: [ + ConfigureEnumPopupComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], providers: [ { provide: MAT_DIALOG_DATA, useValue: new ConfigurableEnum() }, { provide: MatDialogRef, useValue: { afterClosed: () => EMPTY } }, diff --git a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts index 949bb3f46d..bffccff20a 100644 --- a/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts +++ b/src/app/core/basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component.ts @@ -43,6 +43,8 @@ import { Entity } from "../../../entity/model/entity"; standalone: true, }) export class ConfigureEnumPopupComponent { + newOptionInput: string; + constructor( @Inject(MAT_DIALOG_DATA) public enumEntity: ConfigurableEnum, private dialog: MatDialogRef, @@ -125,4 +127,13 @@ export class ConfigureEnumPopupComponent { ), ); } + + createNewOption() { + this.enumEntity.values.push({ + id: this.newOptionInput, + label: this.newOptionInput, + }); + this.newOptionInput = ""; + } + mynewFun() {} } diff --git a/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.spec.ts b/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.spec.ts index c198494af8..101e1b19d0 100644 --- a/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.spec.ts +++ b/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.spec.ts @@ -1,7 +1,7 @@ import { DateWithAgeDatatype } from "./date-with-age.datatype"; -import { DateWithAge } from "../../../child-dev-project/children/model/dateWithAge"; import { testDatatype } from "../../entity/schema/entity-schema.service.spec"; import moment from "moment"; +import { DateWithAge } from "./dateWithAge"; describe("Schema data type: date-with-age", () => { testDatatype( diff --git a/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.ts b/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.ts index 49d6ab46fe..50ab219e90 100644 --- a/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.ts +++ b/src/app/core/basic-datatypes/date-with-age/date-with-age.datatype.ts @@ -1,6 +1,6 @@ import { DateOnlyDatatype } from "../date-only/date-only.datatype"; -import { DateWithAge } from "../../../child-dev-project/children/model/dateWithAge"; import { Injectable } from "@angular/core"; +import { DateWithAge } from "./dateWithAge"; /** * Similar to the 'date-only' datatype but it uses the `DateWithAge` class which provides the `age` function. diff --git a/src/app/child-dev-project/children/model/dateWithAge.ts b/src/app/core/basic-datatypes/date-with-age/dateWithAge.ts similarity index 100% rename from src/app/child-dev-project/children/model/dateWithAge.ts rename to src/app/core/basic-datatypes/date-with-age/dateWithAge.ts diff --git a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.spec.ts b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.spec.ts index ad4ad52c92..f8cd943f29 100644 --- a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.spec.ts +++ b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.spec.ts @@ -2,8 +2,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { DisplayAgeComponent } from "./display-age.component"; import { Child } from "../../../../child-dev-project/children/model/child"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import moment from "moment"; +import { DateWithAge } from "../dateWithAge"; describe("DisplayAgeComponent", () => { let component: DisplayAgeComponent; diff --git a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.ts b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.ts index abe9f9a770..377224d8d0 100644 --- a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.ts +++ b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { ViewDirective } from "../../../entity/default-datatype/view.directive"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator"; +import { DateWithAge } from "../dateWithAge"; /** * A component which displays the age of an entity with a DateOfBirth property. diff --git a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.stories.ts b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.stories.ts index 247756345a..33e9af6d93 100644 --- a/src/app/core/basic-datatypes/date-with-age/display-age/display-age.stories.ts +++ b/src/app/core/basic-datatypes/date-with-age/display-age/display-age.stories.ts @@ -1,8 +1,8 @@ import { applicationConfig, Meta, StoryFn } from "@storybook/angular"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { DisplayAgeComponent } from "./display-age.component"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { importProvidersFrom } from "@angular/core"; +import { DateWithAge } from "../dateWithAge"; export default { title: "Core/Entities/Properties/date/DisplayAge", diff --git a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.html b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.html index f9cd7c35e8..9ef10c03a9 100644 --- a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.html +++ b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.html @@ -1,5 +1,10 @@
- + {{ label }} + Age { let component: EditAgeComponent; diff --git a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.ts b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.ts index 4d692054e0..77241695bd 100644 --- a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.ts +++ b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.component.ts @@ -5,7 +5,6 @@ import { MatDatepickerInputEvent, MatDatepickerModule, } from "@angular/material/datepicker"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { ReactiveFormsModule } from "@angular/forms"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatInputModule } from "@angular/material/input"; @@ -13,6 +12,7 @@ import { ErrorHintComponent } from "../../../common-components/error-hint/error- import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { NgIf } from "@angular/common"; import { MatTooltipModule } from "@angular/material/tooltip"; +import { DateWithAge } from "../dateWithAge"; @DynamicComponent("EditAge") @Component({ diff --git a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.stories.ts b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.stories.ts index c70dd08798..0bbdce871f 100644 --- a/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.stories.ts +++ b/src/app/core/basic-datatypes/date-with-age/edit-age/edit-age.stories.ts @@ -1,6 +1,6 @@ import { generateFormFieldStory } from "../../../entity/default-datatype/edit-component-story-utils"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { StoryFn } from "@storybook/angular"; +import { DateWithAge } from "../dateWithAge"; const formFieldStory = generateFormFieldStory( "EditAge", diff --git a/src/app/core/basic-datatypes/date/date.datatype.spec.ts b/src/app/core/basic-datatypes/date/date.datatype.spec.ts index c3f8214c62..a630ffc912 100644 --- a/src/app/core/basic-datatypes/date/date.datatype.spec.ts +++ b/src/app/core/basic-datatypes/date/date.datatype.spec.ts @@ -7,4 +7,12 @@ describe("Schema data type: date", () => { new Date(2023, 10, 25), new Date(2023, 10, 25), ); + + it("should anonymize dates and only retain year", async () => { + const datatype = new DateDatatype(); + const testDate = new Date(2023, 10, 25); + + const actualAnonymized = await datatype.anonymize(testDate); + expect(actualAnonymized).toEqual(new Date(2023, 6, 1)); + }); }); diff --git a/src/app/core/basic-datatypes/date/date.datatype.ts b/src/app/core/basic-datatypes/date/date.datatype.ts index a176ffc55c..d18777f913 100644 --- a/src/app/core/basic-datatypes/date/date.datatype.ts +++ b/src/app/core/basic-datatypes/date/date.datatype.ts @@ -71,4 +71,10 @@ export class DateDatatype extends DefaultDatatype< return undefined; } } + + async anonymize(value: Date): Promise { + // normalize to 01.06. of the year, which has less statistical distortion than 01.01. + // (roughly half the dates before anonymization will be earlier and half will be later) + return new Date(value.getFullYear(), 6, 1); + } } diff --git a/src/app/core/basic-datatypes/date/display-date/display-date.component.html b/src/app/core/basic-datatypes/date/display-date/display-date.component.html new file mode 100644 index 0000000000..78fd5b6411 --- /dev/null +++ b/src/app/core/basic-datatypes/date/display-date/display-date.component.html @@ -0,0 +1,14 @@ + + {{ value | date: config }} + + + + {{ value | date: "YYYY" }} + + diff --git a/src/app/core/basic-datatypes/date/display-date/display-date.component.ts b/src/app/core/basic-datatypes/date/display-date/display-date.component.ts index 76001a89f0..7db5675d5e 100644 --- a/src/app/core/basic-datatypes/date/display-date/display-date.component.ts +++ b/src/app/core/basic-datatypes/date/display-date/display-date.component.ts @@ -1,7 +1,9 @@ -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { ViewDirective } from "../../../entity/default-datatype/view.directive"; import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator"; -import { DatePipe } from "@angular/common"; +import { DatePipe, NgIf } from "@angular/common"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { MatTooltipModule } from "@angular/material/tooltip"; /** * This component displays a date attribute using the shortDate format. @@ -12,8 +14,13 @@ import { DatePipe } from "@angular/common"; @DynamicComponent("DisplayDate") @Component({ selector: "app-display-date", - template: `{{ value | date: config }}`, + templateUrl: "./display-date.component.html", standalone: true, - imports: [DatePipe], + imports: [DatePipe, NgIf, FontAwesomeModule, MatTooltipModule], }) -export class DisplayDateComponent extends ViewDirective {} +export class DisplayDateComponent extends ViewDirective { + @Input() displayAsAnonymized: boolean; + + /** formatting string for date pipe */ + @Input() config: string; +} diff --git a/src/app/core/basic-datatypes/date/edit-date/edit-date.component.html b/src/app/core/basic-datatypes/date/edit-date/edit-date.component.html index caf141aebe..31f53b3378 100644 --- a/src/app/core/basic-datatypes/date/edit-date/edit-date.component.html +++ b/src/app/core/basic-datatypes/date/edit-date/edit-date.component.html @@ -5,6 +5,15 @@ [formControlName]="formControlName" [matDatepicker]="datepickerComp" /> + + + { - const mockEntitySchemaService: EntitySchemaService = { - getDatatypeOrDefault: () => new EntityDatatype(undefined), + const mockEntityDatatype = new EntityDatatype(null, null); + const mockEntitySchemaService = { + getDatatypeOrDefault: () => mockEntityDatatype, } as any; - testDatatype( - new EntityArrayDatatype(mockEntitySchemaService), - ["1", "User:5"], - ["1", "User:5"], - "User", - ); + const datatype = new EntityArrayDatatype(mockEntitySchemaService); + + testDatatype(datatype, ["1", "User:5"], ["1", "User:5"], "User"); testDatatype( - new EntityArrayDatatype(mockEntitySchemaService), + datatype, ["User:1", "Child:1"], ["User:1", "Child:1"], ["User", "Child", "School"], ); + it("should anonymize entities recursively", async () => { + const testValue = ["Entity:ref-1", "Entity:ref-2"]; + spyOn(mockEntityDatatype, "anonymize").and.callFake(async (x) => x); + + const anonymizedValue = await datatype.anonymize(testValue, null, null); + + expect(anonymizedValue).toEqual(testValue); + expect(mockEntityDatatype.anonymize).toHaveBeenCalledTimes(2); + expect(mockEntityDatatype.anonymize).toHaveBeenCalledWith( + "Entity:ref-1", + jasmine.objectContaining({ dataType: "entity" }), + null, + ); + }); + xit("adds prefix to ids when a definite entity type is given in schema", () => { // TODO discuss whether we want to switch to prefixed ids always (also see #1526) const data = { diff --git a/src/app/core/basic-datatypes/entity-array/entity-array.datatype.ts b/src/app/core/basic-datatypes/entity-array/entity-array.datatype.ts index 0840a37baa..543873e13e 100644 --- a/src/app/core/basic-datatypes/entity-array/entity-array.datatype.ts +++ b/src/app/core/basic-datatypes/entity-array/entity-array.datatype.ts @@ -63,4 +63,12 @@ export class EntityArrayDatatype extends ArrayDatatype { return value; }*/ } + + async anonymize(value, schema: EntitySchemaField, parent) { + return super.anonymize( + value, + { ...schema, innerDataType: EntityDatatype.dataType }, + parent, + ); + } } diff --git a/src/app/core/basic-datatypes/entity/entity-import-config/entity-import-config.component.html b/src/app/core/basic-datatypes/entity/entity-import-config/entity-import-config.component.html index 0f80445fcf..d7a6e0cc7b 100644 --- a/src/app/core/basic-datatypes/entity/entity-import-config/entity-import-config.component.html +++ b/src/app/core/basic-datatypes/entity/entity-import-config/entity-import-config.component.html @@ -1,19 +1,25 @@ -

Select matching {{ entity.label }} property

- - - {{ property.label }} - - - +

+ Select matching {{ entity.label }} property +

+ + + {{ property.label }} + + +
- - + + diff --git a/src/app/core/basic-datatypes/entity/entity-reference-role.ts b/src/app/core/basic-datatypes/entity/entity-reference-role.ts new file mode 100644 index 0000000000..e629dda389 --- /dev/null +++ b/src/app/core/basic-datatypes/entity/entity-reference-role.ts @@ -0,0 +1,10 @@ +/** + * Specifies the role of an entity reference where + * "aggregate" = "has a" relationship where both entities have meaning independent of each other; + * "composite" = "part of" relationship where the referenced entity should not exist without the referenced entity. + * + * Default is treated as "aggregate". + * + * (role names following the UML association types) + */ +export type EntityReferenceRole = "aggregate" | "composite"; diff --git a/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts b/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts index 133f37454e..99b11747c0 100644 --- a/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts +++ b/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts @@ -20,16 +20,17 @@ import { EntityDatatype } from "./entity.datatype"; import { mockEntityMapper } from "../../entity/entity-mapper/mock-entity-mapper-service"; import { Child } from "../../../child-dev-project/children/model/child"; import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; describe("Schema data type: entity", () => { - testDatatype(new EntityDatatype(undefined), "1", "1", "User"); + testDatatype(new EntityDatatype(null, null), "1", "1", "User"); it("should map to the referenced entity", async () => { const c1 = Child.create("first"); const c2 = new Child(); c2.projectNumber = "123"; const entityMapper = mockEntityMapper([c1, c2]); - const dataType = new EntityDatatype(entityMapper); + const dataType = new EntityDatatype(entityMapper, null); const schema = ChildSchoolRelation.schema.get("childId"); await expectAsync( @@ -42,4 +43,27 @@ describe("Schema data type: entity", () => { dataType.importMapFunction("345", schema, "projectNumber"), ).toBeResolvedTo(undefined); }); + + it("should anonymize entity recursively", async () => { + const referencedEntity = new Child("ref-1"); + referencedEntity.name = "test"; + + const entityMapper = mockEntityMapper([referencedEntity]); + spyOn(entityMapper, "save"); + const mockRemoveService: jasmine.SpyObj = + jasmine.createSpyObj("EntityRemoveService", ["anonymize"]); + const dataType = new EntityDatatype(entityMapper, mockRemoveService); + + const testValue = referencedEntity.getId(); + const testSchemaField = { additional: "Child", dataType: "entity" }; + + const anonymizedValue = await dataType.anonymize( + testValue, + testSchemaField, + null, + ); + + expect(anonymizedValue).toEqual(testValue); + expect(mockRemoveService.anonymize).toHaveBeenCalledWith(referencedEntity); + }); }); diff --git a/src/app/core/basic-datatypes/entity/entity.datatype.ts b/src/app/core/basic-datatypes/entity/entity.datatype.ts index 1e9c81a7a2..eb1281c586 100644 --- a/src/app/core/basic-datatypes/entity/entity.datatype.ts +++ b/src/app/core/basic-datatypes/entity/entity.datatype.ts @@ -20,6 +20,7 @@ import { StringDatatype } from "../string/string.datatype"; import { EntitySchemaField } from "../../entity/schema/entity-schema-field"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { ColumnMapping } from "../../import/column-mapping"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; /** * Datatype for the EntitySchemaService to handle a single reference to another entity @@ -37,7 +38,10 @@ export class EntityDatatype extends StringDatatype { viewComponent = "DisplayEntity"; importConfigComponent = "EntityImportConfig"; - constructor(private entityMapper: EntityMapperService) { + constructor( + private entityMapper: EntityMapperService, + private removeService: EntityActionsService, + ) { super(); } @@ -57,4 +61,29 @@ export class EntityDatatype extends StringDatatype { importIncompleteAdditionalConfigBadge(col: ColumnMapping): string { return col.additional ? undefined : "?"; } + + /** + * Recursively calls anonymize on the referenced entity and saves it. + * @param value + * @param schemaField + * @param parent + */ + async anonymize( + value, + schemaField: EntitySchemaField, + parent, + ): Promise { + const referencedEntity = await this.entityMapper.load( + schemaField.additional, + value, + ); + + if (!referencedEntity) { + // TODO: remove broken references? + return value; + } + + await this.removeService.anonymize(referencedEntity); + return value; + } } diff --git a/src/app/core/basic-datatypes/month/display-month/display-month.component.ts b/src/app/core/basic-datatypes/month/display-month/display-month.component.ts index ecdf6591e5..8eddf4dd3a 100644 --- a/src/app/core/basic-datatypes/month/display-month/display-month.component.ts +++ b/src/app/core/basic-datatypes/month/display-month/display-month.component.ts @@ -1,11 +1,16 @@ import { Component } from "@angular/core"; -import { DatePipe } from "@angular/common"; +import { DatePipe, NgIf } from "@angular/common"; import { ViewDirective } from "../../../entity/default-datatype/view.directive"; +import { DisplayDateComponent } from "../../date/display-date/display-date.component"; @Component({ selector: "app-display-month", standalone: true, - template: `{{ value | date: "YYYY-MM" }}`, - imports: [DatePipe], + template: ``, + imports: [DatePipe, NgIf, DisplayDateComponent], }) export class DisplayMonthComponent extends ViewDirective {} diff --git a/src/app/core/basic-datatypes/month/edit-month/edit-month.component.html b/src/app/core/basic-datatypes/month/edit-month/edit-month.component.html index 56239701c7..ab01510f80 100644 --- a/src/app/core/basic-datatypes/month/edit-month/edit-month.component.html +++ b/src/app/core/basic-datatypes/month/edit-month/edit-month.component.html @@ -5,6 +5,15 @@ [formControl]="formControl" [matDatepicker]="datepickerComp" /> + + + { setMonthAndYear(date: Moment, datepicker: MatDatepicker) { - this.formControl.setValue(date.toDate()); this.formControl.markAsDirty(); + this.formControl.setValue(date.toDate()); datepicker.close(); } } diff --git a/src/app/core/common-components/basic-autocomplete/custom-form-control.directive.ts b/src/app/core/common-components/basic-autocomplete/custom-form-control.directive.ts index 48263da39b..00fc6d0a23 100644 --- a/src/app/core/common-components/basic-autocomplete/custom-form-control.directive.ts +++ b/src/app/core/common-components/basic-autocomplete/custom-form-control.directive.ts @@ -4,6 +4,7 @@ import { FormGroupDirective, NgControl, NgForm, + Validators, } from "@angular/forms"; import { MatFormFieldControl } from "@angular/material/form-field"; import { @@ -28,7 +29,16 @@ export abstract class CustomFormControlDirective // eslint-disable-next-line @angular-eslint/no-input-rename @Input("aria-describedby") userAriaDescribedBy: string; @Input() placeholder: string; - @Input() required = false; + + @Input() + get required() { + return this._required; + } + set required(req: boolean) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + private _required = false; abstract inputElement: { _elementRef: ElementRef }; stateChanges = new Subject(); @@ -123,16 +133,22 @@ export abstract class CustomFormControlDirective this.disabled = isDisabled; } + ngDoCheck() { + const control = this.ngControl + ? (this.ngControl.control as AbstractControl) + : null; + + this.checkUpdateErrorState(control); + this.checkUpdateRequired(control); + } + /** * Updates the error state based on the form control * Taken from {@link https://github.com/angular/components/blob/a1d5614f18066c0c2dc2580c7b5099e8f68a8e74/src/material/core/common-behaviors/error-state.ts#L59} */ - ngDoCheck() { + private checkUpdateErrorState(control: AbstractControl | null) { const oldState = this.errorState; const parent = this.parentFormGroup || this.parentForm; - const control = this.ngControl - ? (this.ngControl.control as AbstractControl) - : null; const newState = this.errorStateMatcher.isErrorState(control, parent); if (newState !== oldState) { @@ -140,4 +156,17 @@ export abstract class CustomFormControlDirective this.stateChanges.next(); } } + + private checkUpdateRequired(control: AbstractControl | null) { + if (!control) { + return; + } + + if ( + this.required !== + coerceBooleanProperty(control.hasValidator(Validators.required)) + ) { + this.required = control.hasValidator(Validators.required); + } + } } diff --git a/src/app/core/common-components/confirmation-dialog/confirmation-dialog.service.ts b/src/app/core/common-components/confirmation-dialog/confirmation-dialog.service.ts index f7fd6c9cb5..b4352403db 100644 --- a/src/app/core/common-components/confirmation-dialog/confirmation-dialog.service.ts +++ b/src/app/core/common-components/confirmation-dialog/confirmation-dialog.service.ts @@ -6,6 +6,7 @@ import { YesNoButtons, } from "./confirmation-dialog/confirmation-dialog.component"; import { firstValueFrom } from "rxjs"; +import { ProgressDialogComponent } from "./progress-dialog/progress-dialog.component"; /** * Inject this service instead of MatDialog if you need a simple, configurable confirmation dialog box @@ -63,4 +64,17 @@ export class ConfirmationDialogService { $localize`:Discard changes message:You have unsaved changes. Do you really want to leave this page? All unsaved changes will be lost.`, ); } + + /** + * Show an (indeterminate) progress bar modal that cannot be closed by the user. + * Use the returned dialogRef to close the dialog once your processing is completed. + * @param message + */ + showProgressDialog(message: string) { + return this.dialog.open(ProgressDialogComponent, { + data: { message }, + minWidth: "50vh", + disableClose: true, + }); + } } diff --git a/src/app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component.html b/src/app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component.html index f1a2a1861e..4b31f2b57c 100644 --- a/src/app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component.html +++ b/src/app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component.html @@ -6,7 +6,7 @@

>

-

{{ data.text }}

+

{{ para }}

diff --git a/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.html b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.html new file mode 100644 index 0000000000..8481907b58 --- /dev/null +++ b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.html @@ -0,0 +1,7 @@ +

+ {{ data.message }} +

+ +
+ +
diff --git a/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.spec.ts b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.spec.ts new file mode 100644 index 0000000000..8926eec000 --- /dev/null +++ b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.spec.ts @@ -0,0 +1,32 @@ +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; + +import { ProgressDialogComponent } from "./progress-dialog.component"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; + +describe("ProgressDialogComponent", () => { + let component: ProgressDialogComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ProgressDialogComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { + provide: MAT_DIALOG_DATA, + useValue: { message: "test title" }, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProgressDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.ts b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.ts new file mode 100644 index 0000000000..3c213d838c --- /dev/null +++ b/src/app/core/common-components/confirmation-dialog/progress-dialog/progress-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, Inject } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog"; +import { MatProgressBarModule } from "@angular/material/progress-bar"; + +/** + * A simple progress indicator dialog + * used via the {@link ConfirmationDialogService}. + */ +@Component({ + templateUrl: "./progress-dialog.component.html", + imports: [MatProgressBarModule, MatDialogModule], + standalone: true, +}) +export class ProgressDialogComponent { + constructor(@Inject(MAT_DIALOG_DATA) public data: { message: string }) {} +} diff --git a/src/app/core/common-components/entity-form/entity-form.service.spec.ts b/src/app/core/common-components/entity-form/entity-form.service.spec.ts index c0ca1e9bda..d945c641d4 100644 --- a/src/app/core/common-components/entity-form/entity-form.service.spec.ts +++ b/src/app/core/common-components/entity-form/entity-form.service.spec.ts @@ -173,7 +173,7 @@ describe("EntityFormService", () => { schema.defaultValue = PLACEHOLDERS.NOW; form = service.createFormGroup([{ id: "test" }], new Entity()); - expect(form.get("test")).toHaveValue(new Date()); + expect(form.get("test").value).toEqual(new Date()); schema.defaultValue = PLACEHOLDERS.CURRENT_USER; form = service.createFormGroup([{ id: "test" }], new Entity()); diff --git a/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts b/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts index e57589da85..5d81b59e78 100644 --- a/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts +++ b/src/app/core/common-components/entity-form/entity-form/entity-form.component.spec.ts @@ -6,7 +6,7 @@ import { MockedTestingModule } from "../../../../utils/mocked-testing.module"; import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; import { EntityFormService } from "../entity-form.service"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; +import { DateWithAge } from "../../../basic-datatypes/date-with-age/dateWithAge"; describe("EntityFormComponent", () => { let component: EntityFormComponent; diff --git a/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts b/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts index fd806f216b..7cdc2713ae 100644 --- a/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts +++ b/src/app/core/common-components/entity-form/entity-form/entity-form.component.ts @@ -85,6 +85,7 @@ export class EntityFormComponent } if (changes.form && this.form) { this.initialFormValues = this.form.getRawValue(); + this.disableForLockedEntity(); } } @@ -142,4 +143,14 @@ export class EntityFormComponent JSON.stringify(entityValue) === JSON.stringify(formValue) ); } + + /** + * Disable the form for certain states of the entity, like it being already anonymized. + * @private + */ + private disableForLockedEntity() { + if (this.entity?.anonymized) { + this.form.disable(); + } + } } diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html index 487abc8be4..a2908b24f1 100644 --- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html +++ b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.html @@ -1,8 +1,24 @@
+
- +
+ + + + +
+ + -
+ + +
diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss index 81846c3786..a79b44d627 100644 --- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss +++ b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.scss @@ -24,7 +24,17 @@ justify-content: center; } -:host { +.table { + @include mat-elevation.elevation(2); +} + +.table-footer { @include mat-elevation.elevation(1); - display: block; + border-top: colors.$grey-darker solid 2px; +} + +.filter-inactive-toggle { + padding: sizes.$regular; + background-color: white; + border-top: none; } diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts index 806079e239..827eab3465 100644 --- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts +++ b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts @@ -27,8 +27,8 @@ import { UpdatedEntity } from "../../../entity/model/entity-update"; import { EntityAbility } from "../../../permissions/ability/entity-ability"; import { ScreenWidthObserver } from "../../../../utils/media/screen-size-observer.service"; import { WINDOW_TOKEN } from "../../../../utils/di-tokens"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { FormDialogService } from "../../../form-dialog/form-dialog.service"; +import { DateWithAge } from "../../../basic-datatypes/date-with-age/dateWithAge"; describe("EntitySubrecordComponent", () => { let component: EntitySubrecordComponent; @@ -410,4 +410,17 @@ describe("EntitySubrecordComponent", () => { expect(component.recordsDataSource.data).toEqual([]); })); + + it("should only show active relations by default", async () => { + const active1 = new Entity(); + active1.inactive = false; + const inactive = new Entity(); + inactive.inactive = true; + + component.records = [active1, inactive]; + + component.ngOnChanges({ records: undefined, filter: undefined }); + + expect(component.recordsDataSource.data).toEqual([{ record: active1 }]); + }); }); diff --git a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts index e63338eb51..b76a780380 100644 --- a/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts +++ b/src/app/core/common-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts @@ -24,7 +24,7 @@ import { } from "../../entity-form/entity-form.service"; import { LoggingService } from "../../../logging/logging.service"; import { AnalyticsService } from "../../../analytics/analytics.service"; -import { EntityRemoveService } from "../../../entity/entity-remove.service"; +import { EntityActionsService } from "../../../entity/entity-actions/entity-actions.service"; import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service"; import { tableSort } from "./table-sort"; import { @@ -50,6 +50,12 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { DisableEntityOperationDirective } from "../../../permissions/permission-directive/disable-entity-operation.directive"; import { Angulartics2Module } from "angulartics2"; import { ListPaginatorComponent } from "../list-paginator/list-paginator.component"; +import { + MatCheckboxChange, + MatCheckboxModule, +} from "@angular/material/checkbox"; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { applyUpdate } from "../../../entity/model/entity-update"; export interface TableRow { record: T; @@ -87,14 +93,28 @@ export interface TableRow { DisableEntityOperationDirective, Angulartics2Module, ListPaginatorComponent, + MatCheckboxModule, + MatSlideToggleModule, ], standalone: true, }) export class EntitySubrecordComponent implements OnChanges { @Input() isLoading: boolean; - @Input() clickMode: "popup" | "navigate" | "none" = "popup"; + /** + * outputs an event containing an array of currently selected records (checkmarked by the user) + * + * Checkboxes to select rows are only displayed if you set "selectable" also. + */ + @Output() selectedRecordsChange: EventEmitter = new EventEmitter(); + @Input() selectedRecords: T[] = []; + readonly COLUMN_ROW_SELECT = "_selectRows"; + @Input() selectable: boolean = false; + + @Input() showInactive = false; + @Output() showInactiveChange = new EventEmitter(); + /** configuration what kind of columns to be generated for the table */ @Input() set columns(columns: ColumnConfig[]) { if (columns) { @@ -110,6 +130,9 @@ export class EntitySubrecordComponent implements OnChanges { /** data to be displayed, can also be used as two-way-binding */ @Input() records: T[] = []; + /** output the currently displayed records, whenever filters for the user change */ + @Output() filteredRecordsChange = new EventEmitter(true); + /** * factory method to create a new instance of the displayed Entity type * used when the user adds a new entity to the list. @@ -163,7 +186,7 @@ export class EntitySubrecordComponent implements OnChanges { private router: Router, private analyticsService: AnalyticsService, private loggingService: LoggingService, - public entityRemoveService: EntityRemoveService, + public entityRemoveService: EntityActionsService, private entityMapper: EntityMapperService, private filterService: FilterService, ) { @@ -180,6 +203,10 @@ export class EntitySubrecordComponent implements OnChanges { @Input() getBackgroundColor?: (rec: T) => string = (rec: T) => rec.getColor(); private initDataSource() { + this.filter = this.filter ?? ({} as DataFilter); + this.filterActiveInactive(); + this.predicate = this.filterService.getFilterPredicate(this.filter); + this.recordsDataSource.data = this.records .filter(this.predicate) .map((record) => ({ record })); @@ -228,8 +255,10 @@ export class EntitySubrecordComponent implements OnChanges { } } } - if (changes.hasOwnProperty("filter") && this.filter) { - this.predicate = this.filterService.getFilterPredicate(this.filter); + if ( + (changes.hasOwnProperty("filter") && this.filter) || + changes.hasOwnProperty("showInactive") + ) { reinitDataSource = true; } if (changes.hasOwnProperty("columns")) { @@ -240,6 +269,13 @@ export class EntitySubrecordComponent implements OnChanges { } if (changes.hasOwnProperty("columnsToDisplay")) { this.mediaSubscription.unsubscribe(); + resetupTable = true; + } + if ( + changes.hasOwnProperty("editable") || + changes.hasOwnProperty("selectable") + ) { + resetupTable = true; } if (reinitDataSource) { @@ -255,6 +291,9 @@ export class EntitySubrecordComponent implements OnChanges { this.sortDefault(); } + this.filteredRecordsChange.emit( + this.recordsDataSource.filteredData.map((item) => item.record), + ); this.listenToEntityUpdates(); } @@ -262,7 +301,7 @@ export class EntitySubrecordComponent implements OnChanges { if (this.entityConstructorIsAvailable()) { try { this.entityFormService.extendFormFieldConfig( - this._columns, + this.filteredColumns, this.getEntityConstructor(), true, ); @@ -275,7 +314,7 @@ export class EntitySubrecordComponent implements OnChanges { private sortDefault() { if ( this.records.length === 0 || - this._columns.length === 0 || + this.filteredColumns.length === 0 || this.sort.active ) { // do not overwrite existing sort @@ -301,11 +340,10 @@ export class EntitySubrecordComponent implements OnChanges { private inferDefaultSort(): Sort { // initial sorting by first column, ensure that not the 'action' column is used - const sortBy = - this.columnsToDisplay[0] === "actions" - ? this.columnsToDisplay[1] - : this.columnsToDisplay[0]; - const sortByColumn = this._columns.find((c) => c.id === sortBy); + const sortBy = this.columnsToDisplay.filter( + (c) => c !== "actions" && c !== this.COLUMN_ROW_SELECT, + )[0]; + const sortByColumn = this.filteredColumns.find((c) => c.id === sortBy); let sortDirection: SortDirection = "asc"; if ( @@ -324,19 +362,12 @@ export class EntitySubrecordComponent implements OnChanges { this.updateSubscription = this.entityMapper .receiveUpdates(this.getEntityConstructor()) .pipe(untilDestroyed(this)) - .subscribe(({ entity, type }) => { - if (type === "new") { - this.addToTable(entity); - } else if (type === "remove") { - this.removeFromDataTable(entity); - } else if ( - type === "update" && - !this.records.find((rec) => rec.getId() === entity.getId()) - ) { - this.addToTable(entity); - } + .subscribe((next) => { + this.records = applyUpdate(this.records, next, true); - if (!this.predicate(entity)) { + if (this.predicate(next.entity)) { + this.initDataSource(); + } else { // hide after a short delay to give a signal in the UI why records disappear by showing the changed values first setTimeout(() => this.initDataSource(), 5000); } @@ -348,7 +379,7 @@ export class EntitySubrecordComponent implements OnChanges { if (this.screenWidthObserver.isDesktop()) { if (!row.formGroup) { row.formGroup = this.entityFormService.createFormGroup( - this._columns, + this.filteredColumns, row.record, true, ); @@ -385,20 +416,6 @@ export class EntitySubrecordComponent implements OnChanges { row.formGroup = null; } - private removeFromDataTable(deleted: T) { - // use setter so datasource is also updated - this.records = this.records.filter( - (rec) => rec.getId() !== deleted.getId(), - ); - this.initDataSource(); - } - - private addToTable(record: T) { - // use setter so datasource is also updated - this.records = [record].concat(this.records); - this.initDataSource(); - } - /** * Create a new entity. * The entity is only written to the database when the user saves this record which is newly added in edit mode. @@ -443,12 +460,32 @@ export class EntitySubrecordComponent implements OnChanges { * resets columnsToDisplay depending on current screensize */ private setupTable() { - if (this._columns !== undefined && this.screenWidth !== undefined) { - this.columnsToDisplay = this._columns - .filter((col) => this.isVisible(col)) - .map((col) => col.id); - this.columnsToDisplay.unshift("actions"); + let columns = + this.columnsToDisplay?.filter((c) => + this.filteredColumns.some((column) => column.id === c), + ) ?? []; + + if ( + !(columns.length > 0) && + this.filteredColumns !== undefined && + this.screenWidth !== undefined + ) { + columns = [ + ...this._columns + .filter((col) => this.isVisible(col)) + .map((col) => col.id), + ]; + } + + if (this.editable) { + columns.unshift("actions"); + } + if (this.selectable) { + // only show selection checkboxes if Output is used in parent + columns.unshift(this.COLUMN_ROW_SELECT); } + + this.columnsToDisplay = [...columns]; } /** @@ -468,4 +505,34 @@ export class EntitySubrecordComponent implements OnChanges { } return this.screenWidthObserver.currentScreenSize() >= numericValue; } + + selectRow(row: TableRow, event: MatCheckboxChange) { + if (event.checked) { + this.selectedRecords.push(row.record); + } else { + const index = this.selectedRecords.indexOf(row.record); + if (index > -1) { + this.selectedRecords.splice(index, 1); + } + } + + this.selectedRecordsChange.emit(this.selectedRecords); + } + + filterActiveInactive() { + if (this.showInactive) { + // @ts-ignore type has issues with getters + delete this.filter.isActive; + } else { + this.filter["isActive"] = true; + } + } + + setActiveInactiveFilter(newValue: boolean) { + if (newValue !== this.showInactive) { + this.showInactive = newValue; + this.showInactiveChange.emit(newValue); + } + this.initDataSource(); + } } diff --git a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.html b/src/app/core/common-components/entity-subrecord/row-details/row-details.component.html index 1a414fccfa..9f55fc6fac 100644 --- a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.html +++ b/src/app/core/common-components/entity-subrecord/row-details/row-details.component.html @@ -1,28 +1,34 @@ + - - +
+ + + + -
- - {{ col.label }}:  - - +
+ + {{ col.label }}:  + + +
+ diff --git a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts b/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts index 9046cf1cd9..4f84211622 100644 --- a/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts +++ b/src/app/core/common-components/entity-subrecord/row-details/row-details.component.ts @@ -14,6 +14,7 @@ import { DynamicComponentDirective } from "../../../config/dynamic-components/dy import { MatTooltipModule } from "@angular/material/tooltip"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DialogButtonsComponent } from "../../../form-dialog/dialog-buttons/dialog-buttons.component"; +import { EntityArchivedInfoComponent } from "../../../entity-details/entity-archived-info/entity-archived-info.component"; /** * Data interface that must be given when opening the dialog @@ -44,6 +45,7 @@ export interface DetailsComponentData { NgIf, DynamicComponentDirective, DialogButtonsComponent, + EntityArchivedInfoComponent, ], standalone: true, }) diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 0c160bf969..a7db8c1390 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -258,20 +258,6 @@ export const defaultJsonConfig = { ] } }, - "appConfig:note-details": { - "topForm": ["date", "warningLevel", "category", "authors", "attachment"] - }, - "entity:Note": { - "attributes": [ - { - "name": "attachment", - "schema": { - "label": $localize`Attachment`, - "dataType": "file" - } - } - ] - }, "view:site-settings/:id": { "component": "EntityDetails", "config": { @@ -556,15 +542,6 @@ export const defaultJsonConfig = { ] }, "filters": [ - { - "id": "isActive", - "type": "boolean", - "default": "true", - "label": $localize`Children`, - "true": $localize`:Active children filter label - true case:Active`, - "false": $localize`:Active children filter label - false case:Inactive`, - "all": $localize`:Active children unselect option:All` - }, { "id": "center" }, @@ -573,6 +550,13 @@ export const defaultJsonConfig = { "type": "School", "label": $localize`:Label of schools filter:School` } + ], + "exportConfig": [ + { "label": "Name", "query": "name" }, + { "label": "Gender", "query": "gender" }, + { "label": "Date of Birth", "query": "dateOfBirth" }, + { "label": "School", "query": ".schoolId:toEntities(School).name" }, + { "label": "more fields can be configured - or all data exported", "query": "projectNumber" } ] } }, @@ -771,17 +755,6 @@ export const defaultJsonConfig = { "type", "assignedTo" ], - "filters": [ - { - "id": "isActive", - "type": "boolean", - "default": "true", - "label": $localize`Status`, - "true": $localize`:Active records filter label - true case:Active`, - "false": $localize`:Active records filter label - false case:Inactive`, - "all": $localize`:Active records unselect option:All` - }, - ], "exportConfig": [ { "label": "Title", "query": "title" }, { "label": "Type", "query": "type" }, @@ -909,6 +882,19 @@ export const defaultJsonConfig = { "label": $localize`:Name of a column of a report:Name`, "query": `.participant:toEntities(Child).name` }, + { + "query": ".participant:toEntities(Child):getRelated(ChildSchoolRelation, childId)[*isActive=true]", + "subQueries": [ + { + "label": "Class", + "query": ".schoolClass" + }, + { + "label": "School", + "query": ".schoolId:toEntities(School).name" + }, + ] + }, { "label": $localize`:Name of a column of a report:Total`, "query": `total` diff --git a/src/app/core/config/dynamic-routing/router.service.spec.ts b/src/app/core/config/dynamic-routing/router.service.spec.ts index e78ab00c14..d4b4dcca37 100644 --- a/src/app/core/config/dynamic-routing/router.service.spec.ts +++ b/src/app/core/config/dynamic-routing/router.service.spec.ts @@ -10,9 +10,9 @@ import { ViewConfig } from "./view-config.interface"; import { UserRoleGuard } from "../../permissions/permission-guard/user-role.guard"; import { ApplicationLoadingComponent } from "./empty/application-loading.component"; import { NotFoundComponent } from "./not-found/not-found.component"; -import { componentRegistry } from "../../../dynamic-components"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { AuthGuard } from "../../session/auth.guard"; +import { RoutedViewComponent } from "../../ui/routed-view/routed-view.component"; class TestComponent extends Component {} @@ -63,24 +63,24 @@ describe("RouterService", () => { const expectedRoutes = [ { path: "child", - loadComponent: componentRegistry.get("ChildrenList"), - data: {}, + component: RoutedViewComponent, + data: { component: "ChildrenList" }, canDeactivate: [jasmine.any(Function)], canActivate: [AuthGuard], }, { path: "child/:id", - loadComponent: componentRegistry.get("EntityDetails"), - data: { config: testViewConfig }, + component: RoutedViewComponent, + data: { component: "EntityDetails", config: testViewConfig }, canDeactivate: [jasmine.any(Function)], canActivate: [AuthGuard], }, { path: "list", - loadComponent: componentRegistry.get("EntityList"), + component: RoutedViewComponent, + data: { component: "EntityList", permittedUserRoles: ["user_app"] }, canActivate: [AuthGuard, UserRoleGuard], canDeactivate: [jasmine.any(Function)], - data: { permittedUserRoles: ["user_app"] }, }, ]; @@ -144,9 +144,11 @@ describe("RouterService", () => { const router = TestBed.inject(Router); expect(router.config.find((r) => r.path === "child").data).toEqual({ + component: "ChildrenList", config: { foo: 1 }, }); expect(router.config.find((r) => r.path === "child2").data).toEqual({ + component: "ChildrenList", config: { foo: 2 }, }); }); @@ -162,10 +164,10 @@ describe("RouterService", () => { const expectedRoutes = [ { path: "list", - loadComponent: componentRegistry.get("EntityList"), + component: RoutedViewComponent, + data: { component: "EntityList", permittedUserRoles: ["admin"] }, canActivate: [AuthGuard, UserRoleGuard], canDeactivate: [jasmine.any(Function)], - data: { permittedUserRoles: ["admin"] }, }, ]; const router = TestBed.inject(Router); @@ -186,14 +188,4 @@ describe("RouterService", () => { expect(wildcardRoute).toEqual({ path: "**", component: NotFoundComponent }); }); - - it("should log a warning if a view config has a component which is not registered", () => { - const testViewConfigs: ViewConfig[] = [ - { _id: "view:child", component: "Support" }, - ]; - - service.reloadRouting(testViewConfigs); - - expect(mockLoggingService.warn).toHaveBeenCalled(); - }); }); diff --git a/src/app/core/config/dynamic-routing/router.service.ts b/src/app/core/config/dynamic-routing/router.service.ts index d279b85737..f087dfd363 100644 --- a/src/app/core/config/dynamic-routing/router.service.ts +++ b/src/app/core/config/dynamic-routing/router.service.ts @@ -2,16 +2,12 @@ import { inject, Injectable } from "@angular/core"; import { Route, Router } from "@angular/router"; import { ConfigService } from "../config.service"; import { LoggingService } from "../../logging/logging.service"; -import { - PREFIX_VIEW_CONFIG, - RouteData, - ViewConfig, -} from "./view-config.interface"; +import { PREFIX_VIEW_CONFIG, ViewConfig } from "./view-config.interface"; import { UserRoleGuard } from "../../permissions/permission-guard/user-role.guard"; import { NotFoundComponent } from "./not-found/not-found.component"; -import { ComponentRegistry } from "../../../dynamic-components"; import { AuthGuard } from "../../session/auth.guard"; import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; +import { RoutedViewComponent } from "../../ui/routed-view/routed-view.component"; /** * The RouterService dynamically sets up Angular routing from config loaded through the {@link ConfigService}. @@ -27,7 +23,6 @@ export class RouterService { private configService: ConfigService, private router: Router, private loggingService: LoggingService, - private components: ComponentRegistry, ) {} /** @@ -82,14 +77,15 @@ export class RouterService { return this.generateRouteFromConfig(view, route); } else { return this.generateRouteFromConfig(view, { - loadComponent: this.components.get(view.component), path, + component: RoutedViewComponent, + data: { component: view.component }, }); } } private generateRouteFromConfig(view: ViewConfig, route: Route): Route { - const routeData: RouteData = {}; + route.data = route.data ?? {}; route.canActivate = [AuthGuard]; route.canDeactivate = [ () => inject(UnsavedChangesService).checkUnsavedChanges(), @@ -97,14 +93,13 @@ export class RouterService { if (view.permittedUserRoles) { route.canActivate.push(UserRoleGuard); - routeData.permittedUserRoles = view.permittedUserRoles; + route.data.permittedUserRoles = view.permittedUserRoles; } if (view.config) { - routeData.config = view.config; + route.data.config = view.config; } - route.data = routeData; return route; } } diff --git a/src/app/core/config/dynamic-routing/view-config.interface.ts b/src/app/core/config/dynamic-routing/view-config.interface.ts index 6f6f4dd94f..c2d311ba55 100644 --- a/src/app/core/config/dynamic-routing/view-config.interface.ts +++ b/src/app/core/config/dynamic-routing/view-config.interface.ts @@ -44,10 +44,13 @@ export const PREFIX_VIEW_CONFIG = "view:"; * It contains static data which are used to build components and manage permissions. * The generic type defines the interface for the component specific configuration. * - * It can be accessed through the activated route: - * ``` - * constructor(private route: ActivatedRoute) { - * this.route.data.subscribe(routeData: RouteData => { ...what to do with the data })' + * The properties given in the `config` object here are automatically assigned to the component as `@Input()` properties. + * e.g. for an RouteData `{ config: { "entityType: "Child", "filtered": true } }` + * your component `MyViewComponent` will receive the values mapped to its properties: + * ```javascript + * class MyViewComponent { + * @Input() entityType: string; + * @Input() filtered: boolean; * } * ``` */ diff --git a/src/app/core/core-components.ts b/src/app/core/core-components.ts index eb43460952..093bf0b140 100644 --- a/src/app/core/core-components.ts +++ b/src/app/core/core-components.ts @@ -225,4 +225,11 @@ export const coreComponents: ComponentTuple[] = [ "./entity-details/related-time-period-entities/related-time-period-entities.component" ).then((c) => c.RelatedTimePeriodEntitiesComponent), ], + [ + "RelatedEntitiesWithSummary", + () => + import( + "./entity-details/related-entities-with-summary/related-entities-with-summary.component" + ).then((c) => c.RelatedEntitiesWithSummaryComponent), + ], ]; diff --git a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts index 4c5692beb7..27b11bd14b 100644 --- a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts +++ b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts @@ -1,26 +1,13 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { DashboardComponent } from "./dashboard.component"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject } from "rxjs"; -import { RouteData } from "../../config/dynamic-routing/view-config.interface"; -import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface"; describe("DashboardComponent", () => { let component: DashboardComponent; let fixture: ComponentFixture; - let mockRouteData: BehaviorSubject< - RouteData<{ widgets: DynamicComponentConfig[] }> - >; - beforeEach(() => { - mockRouteData = new BehaviorSubject({ config: { widgets: [] } }); - TestBed.configureTestingModule({ imports: [DashboardComponent], - providers: [ - { provide: ActivatedRoute, useValue: { data: mockRouteData } }, - ], }).compileComponents(); }); @@ -33,21 +20,4 @@ describe("DashboardComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); - - it("should init with widget config from activated route", () => { - const testDashboardConfig = { - widgets: [ - { - component: "ProgressDashboard", - }, - { - component: "ProgressDashboard", - }, - ], - }; - - mockRouteData.next({ config: testDashboardConfig }); - - expect(component.widgets).toEqual(testDashboardConfig.widgets); - }); }); diff --git a/src/app/core/dashboard/dashboard/dashboard.component.ts b/src/app/core/dashboard/dashboard/dashboard.component.ts index 0ea0fd0444..e0ca13a494 100644 --- a/src/app/core/dashboard/dashboard/dashboard.component.ts +++ b/src/app/core/dashboard/dashboard/dashboard.component.ts @@ -15,10 +15,8 @@ * along with ndb-core. If not, see . */ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { Component, Input } from "@angular/core"; import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface"; -import { RouteData } from "../../config/dynamic-routing/view-config.interface"; import { RouteTarget } from "../../../app.routing"; import { NgFor } from "@angular/common"; import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive"; @@ -34,16 +32,10 @@ import { DynamicComponentDirective } from "../../config/dynamic-components/dynam imports: [NgFor, DynamicComponentDirective], standalone: true, }) -export class DashboardComponent implements OnInit { - widgets: DynamicComponentConfig[] = []; - - constructor(private activatedRoute: ActivatedRoute) {} +export class DashboardComponent implements DashboardConfig { + @Input() widgets: DynamicComponentConfig[] = []; +} - ngOnInit() { - this.activatedRoute.data.subscribe( - (data: RouteData<{ widgets: DynamicComponentConfig[] }>) => { - this.widgets = data.config.widgets; - }, - ); - } +export interface DashboardConfig { + widgets: DynamicComponentConfig[]; } diff --git a/src/app/core/database/pouch-database.spec.ts b/src/app/core/database/pouch-database.spec.ts index 49f9774b61..7d455c9049 100644 --- a/src/app/core/database/pouch-database.spec.ts +++ b/src/app/core/database/pouch-database.spec.ts @@ -259,7 +259,11 @@ describe("PouchDatabase tests", () => { }, ]; - const results = await database.putAll(dataWithConflicts); + await expectAsync(database.putAll(dataWithConflicts)).toBeRejectedWith([ + conflictError, + jasmine.objectContaining({ id: "4", ok: true }), + jasmine.objectContaining({ id: "5", ok: true }), + ]); expect(resolveConflictSpy.calls.allArgs()).toEqual([ [ { @@ -271,11 +275,6 @@ describe("PouchDatabase tests", () => { jasmine.objectContaining({ status: 409 }), ], ]); - expect(results).toEqual([ - conflictError, - jasmine.objectContaining({ id: "4", ok: true }), - jasmine.objectContaining({ id: "5", ok: true }), - ]); }); it("should correctly determine if database is empty", async () => { diff --git a/src/app/core/database/pouch-database.ts b/src/app/core/database/pouch-database.ts index 8a3ae8e649..90fc67fdd8 100644 --- a/src/app/core/database/pouch-database.ts +++ b/src/app/core/database/pouch-database.ts @@ -211,7 +211,8 @@ export class PouchDatabase extends Database { * Save an array of documents to the database * @param objects the documents to be saved * @param forceOverwrite whether conflicting versions should be overwritten - * @returns array holding `{ ok: true, ... }` or `{ error: true, ... }` depending on whether the document could be saved + * @returns array with the result for each object to be saved, if any item fails to be saved, this returns a rejected Promise. + * The save can partially fail and return a mix of success and error states in the array (e.g. `[{ ok: true, ... }, { error: true, ... }]`) */ async putAll(objects: any[], forceOverwrite = false): Promise { if (forceOverwrite) { @@ -227,11 +228,17 @@ export class PouchDatabase extends Database { if (result.status === 409) { results[i] = await this.resolveConflict( objects.find((obj) => obj._id === result.id), - false, + forceOverwrite, result, - ).catch((e) => e); + ).catch((e) => { + return new DatabaseException(e); + }); } } + + if (results.some((r) => r instanceof Error)) { + return Promise.reject(results); + } return results; } diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html new file mode 100644 index 0000000000..dc0ba188be --- /dev/null +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/app/child-dev-project/attendance/activities-overview/activities-overview.component.scss b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss similarity index 100% rename from src/app/child-dev-project/attendance/activities-overview/activities-overview.component.scss rename to src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts new file mode 100644 index 0000000000..9854810c04 --- /dev/null +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts @@ -0,0 +1,48 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; + +import { EntityActionsMenuComponent } from "./entity-actions-menu.component"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; + +describe("EntityActionsMenuComponent", () => { + let component: EntityActionsMenuComponent; + let fixture: ComponentFixture; + + let mockEntityRemoveService: jasmine.SpyObj; + + beforeEach(() => { + mockEntityRemoveService = jasmine.createSpyObj(["delete"]); + TestBed.configureTestingModule({ + imports: [EntityActionsMenuComponent, MockedTestingModule], + providers: [ + { provide: EntityActionsService, useValue: mockEntityRemoveService }, + ], + }); + fixture = TestBed.createComponent(EntityActionsMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should emit actionTriggered Output upon action", fakeAsync(() => { + mockEntityRemoveService.delete.and.resolveTo(true); + + let actionEvent; + component.actionTriggered.subscribe((x) => (actionEvent = x)); + + component.executeAction( + component.defaultActions.find((x) => x.action === "delete"), + ); + tick(); + + expect(actionEvent).toBe("delete"); + })); +}); diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts new file mode 100644 index 0000000000..625821d4af --- /dev/null +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts @@ -0,0 +1,119 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, +} from "@angular/core"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; +import { Entity } from "../../entity/model/entity"; +import { NgForOf, NgIf } from "@angular/common"; +import { MatButtonModule } from "@angular/material/button"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { MatMenuModule } from "@angular/material/menu"; +import { Angulartics2Module } from "angulartics2"; +import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { EntityAction } from "../../permissions/permission-types"; +import { MatTooltipModule } from "@angular/material/tooltip"; + +export type EntityMenuAction = "archive" | "anonymize" | "delete"; +type EntityMenuActionItem = { + action: EntityMenuAction; + execute: (entity: Entity, navigateOnDelete?: boolean) => Promise; + permission?: EntityAction; + icon: IconProp; + label: string; + tooltip?: string; +}; + +@Component({ + selector: "app-entity-actions-menu", + templateUrl: "./entity-actions-menu.component.html", + styleUrls: ["./entity-actions-menu.component.scss"], + standalone: true, + imports: [ + NgIf, + MatButtonModule, + FontAwesomeModule, + MatMenuModule, + Angulartics2Module, + DisableEntityOperationDirective, + NgForOf, + MatTooltipModule, + ], +}) +export class EntityActionsMenuComponent implements OnChanges { + @Input() entity: Entity; + + /** + * whether the "delete" action will trigger a navigation back to the parent list. + * This is useful when the entity is deleted from a fullscreen detail view but not for an overlay. + */ + @Input() navigateOnDelete: boolean = false; + + @Output() actionTriggered = new EventEmitter(); + + /** + * The actions being displayed as menu items. + */ + actions: EntityMenuActionItem[]; + + readonly defaultActions: EntityMenuActionItem[] = [ + { + action: "archive", + execute: (e) => this.entityRemoveService.archive(e), + permission: "update", + icon: "box-archive", + label: $localize`:entity context menu:Archive`, + tooltip: $localize`:entity context menu tooltip:Mark the record as inactive, hiding it from lists by default while keeping the data.`, + }, + { + action: "anonymize", + execute: (e) => this.entityRemoveService.anonymize(e), + permission: "update", + icon: "user-secret", + label: $localize`:entity context menu:Anonymize`, + tooltip: $localize`:entity context menu tooltip:Remove all personal data and keep an archived basic record for statistical reporting.`, + }, + { + action: "delete", + execute: (e, nav) => this.entityRemoveService.delete(e, nav), + permission: "delete", + icon: "trash", + label: $localize`:entity context menu:Delete`, + tooltip: $localize`:entity context menu tooltip:Remove the record completely from the database.`, + }, + ]; + + constructor(private entityRemoveService: EntityActionsService) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.entity) { + this.filterAvailableActions(); + } + } + + private filterAvailableActions() { + this.actions = this.defaultActions.filter((action) => { + switch (action.action) { + case "archive": + return this.entity?.isActive && !this.entity?.anonymized; + case "anonymize": + return ( + !this.entity?.anonymized && this.entity?.getConstructor().hasPII + ); + default: + return true; + } + }); + } + + async executeAction(action: EntityMenuActionItem) { + const result = await action.execute(this.entity, this.navigateOnDelete); + if (result) { + this.actionTriggered.emit(action.action); + } + } +} diff --git a/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.html b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.html new file mode 100644 index 0000000000..a86f54544a --- /dev/null +++ b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.html @@ -0,0 +1,34 @@ + + +
+ +
+ Archived + Anonymized & Archived +
+ + +

+ This record has been anonymized. Details containing personal information + have been deleted and cannot be restored. For statistical reporting, this + basic record has been kept. +

+ +

+ This record is archived and will be be hidden from lists and select + options by default. +

+
+ + + + +
diff --git a/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.scss b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.scss new file mode 100644 index 0000000000..e6253f7cf6 --- /dev/null +++ b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.scss @@ -0,0 +1,17 @@ +@use "src/styles/variables/colors"; + +.card { + background-color: colors.$grey-transparent; +} + +.card-header { + display: flex; + align-items: center; +} + +.card-icon { + justify-content: center; + align-items: center; + display: flex; + margin: 0; +} diff --git a/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.spec.ts b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.spec.ts new file mode 100644 index 0000000000..9ac50cea43 --- /dev/null +++ b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { EntityArchivedInfoComponent } from "./entity-archived-info.component"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; + +describe("EntityArchivedInfoComponent", () => { + let component: EntityArchivedInfoComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [EntityArchivedInfoComponent], + providers: [{ provide: EntityActionsService, useValue: null }], + }); + fixture = TestBed.createComponent(EntityArchivedInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.ts b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.ts new file mode 100644 index 0000000000..0724b65363 --- /dev/null +++ b/src/app/core/entity-details/entity-archived-info/entity-archived-info.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { Entity } from "../../entity/model/entity"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; + +/** + * Informs users that the entity is inactive (or anonymized) and provides options to change the status. + */ +@Component({ + selector: "app-entity-archived-info", + standalone: true, + imports: [CommonModule, MatCardModule, MatButtonModule, FontAwesomeModule], + templateUrl: "./entity-archived-info.component.html", + styleUrls: ["./entity-archived-info.component.scss"], +}) +export class EntityArchivedInfoComponent { + @Input() entity: Entity; + + constructor(public entityRemoveService: EntityActionsService) {} +} diff --git a/src/app/core/entity-details/entity-archived-info/entity-archived-info.stories.ts b/src/app/core/entity-details/entity-archived-info/entity-archived-info.stories.ts new file mode 100644 index 0000000000..876b0cf72e --- /dev/null +++ b/src/app/core/entity-details/entity-archived-info/entity-archived-info.stories.ts @@ -0,0 +1,31 @@ +import { applicationConfig, Meta, StoryFn } from "@storybook/angular"; +import { StorybookBaseModule } from "../../../utils/storybook-base.module"; +import { importProvidersFrom } from "@angular/core"; +import { EntityArchivedInfoComponent } from "./entity-archived-info.component"; +import { Entity } from "../../entity/model/entity"; + +export default { + title: "Core/> App Layout/Entity Details/Entity Archived Info", + component: EntityArchivedInfoComponent, + decorators: [ + applicationConfig({ + providers: [importProvidersFrom(StorybookBaseModule)], + }), + ], +} as Meta; + +const Template: StoryFn = ( + args: EntityArchivedInfoComponent, +) => ({ + props: args, +}); + +export const Archived = Template.bind({}); +Archived.args = { + entity: Object.assign(new Entity(), { inactive: true }), +}; + +export const Anonymized = Template.bind({}); +Anonymized.args = { + entity: Object.assign(new Entity(), { inactive: true, anonymized: true }), +}; diff --git a/src/app/core/entity-details/entity-details/entity-details.component.html b/src/app/core/entity-details/entity-details/entity-details.component.html index 9c03c4f7fe..3966c2e644 100644 --- a/src/app/core/entity-details/entity-details/entity-details.component.html +++ b/src/app/core/entity-details/entity-details/entity-details.component.html @@ -17,7 +17,7 @@
- {{ entity?.toString() }} + {{ record?.toString() }} - +
- - - + {{ componentConfig.title }} - +
diff --git a/src/app/core/entity-details/entity-details/entity-details.component.spec.ts b/src/app/core/entity-details/entity-details/entity-details.component.spec.ts index 796b31eb03..f324442a7b 100644 --- a/src/app/core/entity-details/entity-details/entity-details.component.spec.ts +++ b/src/app/core/entity-details/entity-details/entity-details.component.spec.ts @@ -6,22 +6,20 @@ import { waitForAsync, } from "@angular/core/testing"; import { EntityDetailsComponent } from "./entity-details.component"; -import { Observable, of, Subscriber } from "rxjs"; -import { ActivatedRoute, Router } from "@angular/router"; +import { Router } from "@angular/router"; import { EntityDetailsConfig, PanelConfig } from "../EntityDetailsConfig"; import { Child } from "../../../child-dev-project/children/model/child"; import { ChildrenService } from "../../../child-dev-project/children/children.service"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; -import { EntityRemoveService } from "../../entity/entity-remove.service"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; import { EntityAbility } from "../../permissions/ability/entity-ability"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; +import { SimpleChange } from "@angular/core"; describe("EntityDetailsComponent", () => { let component: EntityDetailsComponent; let fixture: ComponentFixture; - let routeObserver: Subscriber; - const routeConfig: EntityDetailsConfig = { entity: "Child", panels: [ @@ -44,21 +42,9 @@ describe("EntityDetailsComponent", () => { }, ], }; - const mockedRoute = { - paramMap: new Observable((observer) => { - routeObserver = observer; - observer.next({ get: () => "new" }); - }), - data: of({ config: routeConfig }), - snapshot: { - queryParamMap: { - get: () => "", - }, - }, - }; let mockChildrenService: jasmine.SpyObj; - let mockEntityRemoveService: jasmine.SpyObj; + let mockEntityRemoveService: jasmine.SpyObj; let mockAbility: jasmine.SpyObj; beforeEach(waitForAsync(() => { @@ -74,9 +60,8 @@ describe("EntityDetailsComponent", () => { TestBed.configureTestingModule({ imports: [EntityDetailsComponent, MockedTestingModule.withState()], providers: [ - { provide: ActivatedRoute, useValue: mockedRoute }, { provide: ChildrenService, useValue: mockChildrenService }, - { provide: EntityRemoveService, useValue: mockEntityRemoveService }, + { provide: EntityActionsService, useValue: mockEntityRemoveService }, { provide: EntityAbility, useValue: mockAbility }, ], }).compileComponents(); @@ -85,6 +70,12 @@ describe("EntityDetailsComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(EntityDetailsComponent); component = fixture.componentInstance; + + Object.assign(component, routeConfig); + component.ngOnChanges( + simpleChangesFor(component, ...Object.keys(routeConfig)), + ); + fixture.detectChanges(); }); @@ -97,7 +88,8 @@ describe("EntityDetailsComponent", () => { TestBed.inject(EntityMapperService).save(testChild); tick(); component.creatingNew = false; - routeObserver.next({ get: () => testChild.getId() }); + component.id = testChild.getId(); + component.ngOnChanges(simpleChangesFor(component, "id")); tick(); component.panels.forEach((p) => @@ -117,12 +109,13 @@ describe("EntityDetailsComponent", () => { tick(); spyOn(entityMapper, "load").and.callThrough(); - routeObserver.next({ get: () => testChild.getId() }); + component.id = testChild.getId(); + component.ngOnChanges(simpleChangesFor(component, "id")); expect(component.isLoading).toBeTrue(); tick(); expect(entityMapper.load).toHaveBeenCalledWith(Child, testChild.getId()); - expect(component.entity).toBe(testChild); + expect(component.record).toBe(testChild); expect(component.isLoading).toBeFalse(); })); @@ -130,7 +123,16 @@ describe("EntityDetailsComponent", () => { mockAbility.cannot.and.returnValue(true); const router = fixture.debugElement.injector.get(Router); spyOn(router, "navigate"); - routeObserver.next({ get: () => "new" }); + component.id = "new"; + component.ngOnChanges(simpleChangesFor(component, "id")); expect(router.navigate).toHaveBeenCalled(); }); }); + +function simpleChangesFor(component, ...properties: string[]) { + const changes = {}; + for (const p of properties) { + changes[p] = new SimpleChange(null, component[p], true); + } + return changes; +} diff --git a/src/app/core/entity-details/entity-details/entity-details.component.ts b/src/app/core/entity-details/entity-details/entity-details.component.ts index 462543b54b..bc7f1fe164 100644 --- a/src/app/core/entity-details/entity-details/entity-details.component.ts +++ b/src/app/core/entity-details/entity-details/entity-details.component.ts @@ -1,5 +1,5 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; +import { Router } from "@angular/router"; import { EntityDetailsConfig, Panel, @@ -8,9 +8,7 @@ import { } from "../EntityDetailsConfig"; import { Entity, EntityConstructor } from "../../entity/model/entity"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; -import { RouteData } from "../../config/dynamic-routing/view-config.interface"; import { AnalyticsService } from "../../analytics/analytics.service"; -import { EntityRemoveService } from "../../entity/entity-remove.service"; import { EntityAbility } from "../../permissions/ability/entity-ability"; import { RouteTarget } from "../../../app.routing"; import { EntityRegistry } from "../../entity/database-entity.decorator"; @@ -28,6 +26,11 @@ import { DynamicComponentDirective } from "../../config/dynamic-components/dynam import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive"; import { LoggingService } from "../../logging/logging.service"; import { UnsavedChangesService } from "../form/unsaved-changes.service"; +import { EntityActionsMenuComponent } from "../entity-actions-menu/entity-actions-menu.component"; +import { EntityArchivedInfoComponent } from "../entity-archived-info/entity-archived-info.component"; +import { filter } from "rxjs/operators"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { Subscription } from "rxjs"; /** * This component can be used to display an entity in more detail. @@ -36,6 +39,7 @@ import { UnsavedChangesService } from "../form/unsaved-changes.service"; * The subcomponents will be provided with the Entity object and the creating new status, as well as its static config. */ @RouteTarget("EntityDetails") +@UntilDestroy() @Component({ selector: "app-entity-details", templateUrl: "./entity-details.component.html", @@ -55,67 +59,83 @@ import { UnsavedChangesService } from "../form/unsaved-changes.service"; ViewTitleComponent, DynamicComponentDirective, DisableEntityOperationDirective, + EntityActionsMenuComponent, + EntityArchivedInfoComponent, ], }) -export class EntityDetailsComponent { - entity: Entity; +export class EntityDetailsComponent implements EntityDetailsConfig, OnChanges { creatingNew = false; isLoading = true; + private changesSubscription: Subscription; - panels: Panel[] = []; - config: EntityDetailsConfig; + /** @deprecated use "entityType" instead, this remains for config backwards compatibility */ + @Input() set entity(v: string) { + this.entityType = v; + } + @Input() entityType: string; entityConstructor: EntityConstructor; + @Input() id: string; + record: Entity; + + @Input() panels: Panel[] = []; + constructor( private entityMapperService: EntityMapperService, - private route: ActivatedRoute, private router: Router, private analyticsService: AnalyticsService, - public entityRemoveService: EntityRemoveService, private ability: EntityAbility, private entities: EntityRegistry, private logger: LoggingService, public unsavedChanges: UnsavedChangesService, - ) { - this.route.data.subscribe((data: RouteData) => { - this.config = data.config; - this.entityConstructor = this.entities.get(this.config.entity); - this.setInitialPanelsConfig(); - this.route.paramMap.subscribe((params) => - this.loadEntity(params.get("id")), - ); - }); + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.entity || changes.entityType) { + this.entityConstructor = this.entities.get(this.entityType); + } + if (changes.id) { + this.loadEntity(this.id); + this.subscribeToEntityChanges(); + // `initPanels()` is already called inside `loadEntity()` + } else if (changes.panels) { + this.initPanels(); + } } - private loadEntity(id: string) { + private subscribeToEntityChanges() { + this.changesSubscription?.unsubscribe(); + this.changesSubscription = this.entityMapperService + .receiveUpdates(this.entityConstructor) + .pipe( + filter(({ entity }) => entity.getId() === this.id), + filter(({ type }) => type !== "remove"), + untilDestroyed(this), + ) + .subscribe(({ entity }) => (this.record = entity)); + } + + private async loadEntity(id: string) { if (id === "new") { if (this.ability.cannot("create", this.entityConstructor)) { this.router.navigate([""]); return; } - this.entity = new this.entityConstructor(); + this.record = new this.entityConstructor(); this.creatingNew = true; - this.setFullPanelsConfig(); } else { this.creatingNew = false; - this.entityMapperService - .load(this.entityConstructor, id) - .then((entity) => { - this.entity = entity; - this.setFullPanelsConfig(); - }); + this.record = await this.entityMapperService.load( + this.entityConstructor, + id, + ); } + this.initPanels(); + this.isLoading = false; } - private setInitialPanelsConfig() { - this.panels = this.config.panels.map((p) => ({ - title: p.title, - components: [], - })); - } - - private setFullPanelsConfig() { - this.panels = this.config.panels.map((p) => ({ + private initPanels() { + this.panels = this.panels.map((p) => ({ title: p.title, components: p.components.map((c) => ({ title: c.title, @@ -123,12 +143,11 @@ export class EntityDetailsComponent { config: this.getPanelConfig(c), })), })); - this.isLoading = false; } private getPanelConfig(c: PanelComponent): PanelConfig { let panelConfig: PanelConfig = { - entity: this.entity, + entity: this.record, creatingNew: this.creatingNew, }; if (typeof c.config === "object" && !Array.isArray(c.config)) { @@ -150,8 +169,8 @@ export class EntityDetailsComponent { trackTabChanged(index: number) { this.analyticsService.eventTrack("details_tab_changed", { - category: this.config?.entity, - label: this.config.panels[index].title, + category: this.entityType, + label: this.panels[index].title, }); } } diff --git a/src/app/core/entity-details/entity-details/entity-details.stories.ts b/src/app/core/entity-details/entity-details/entity-details.stories.ts new file mode 100644 index 0000000000..227e799ff1 --- /dev/null +++ b/src/app/core/entity-details/entity-details/entity-details.stories.ts @@ -0,0 +1,57 @@ +import { applicationConfig, Meta, StoryFn } from "@storybook/angular"; +import { StorybookBaseModule } from "../../../utils/storybook-base.module"; +import { importProvidersFrom } from "@angular/core"; +import { EntityDetailsComponent } from "./entity-details.component"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { EntityDetailsConfig } from "../EntityDetailsConfig"; + +const demoEntity = Child.create("John Doe"); +demoEntity._rev = "1"; // make not "isNew" + +const config: EntityDetailsConfig = { + entity: "Child", + panels: [ + { + title: $localize`:Panel title:Basic Information`, + components: [ + { + title: "", + component: "Form", + config: { + cols: [ + ["photo"], + ["name", "projectNumber", "admissionDate"], + ["center", "phone"], + ], + headers: [null, "Personal Information", "Contact Details"], + }, + }, + ], + }, + { title: "Other Details", components: [] }, + ], +}; + +export default { + title: "Core/> App Layout/Entity Details", + component: EntityDetailsComponent, + decorators: [ + applicationConfig({ + providers: [ + importProvidersFrom(StorybookBaseModule.withData([demoEntity])), + ], + }), + ], +} as Meta; + +const Template: StoryFn = ( + args: EntityDetailsComponent, +) => ({ + props: args, +}); + +export const Primary = Template.bind({}); +Primary.args = { + id: demoEntity.getId(false), + ...config, +}; diff --git a/src/app/core/entity-details/form/form.component.html b/src/app/core/entity-details/form/form.component.html index 78e724ab27..8fe88ce3e6 100644 --- a/src/app/core/entity-details/form/form.component.html +++ b/src/app/core/entity-details/form/form.component.html @@ -20,7 +20,7 @@ Cancel - +
+ +
@@ -127,7 +129,11 @@

{{ listName }}

[columnsToDisplay]="columnsToDisplay" [isLoading]="isLoading" [filter]="filterObj" - [defaultSort]="listConfig?.defaultSort" + [defaultSort]="defaultSort" + [(selectedRecords)]="selectedRows" + [selectable]="!!selectedRows" + [showInactive]="showInactive" + (filteredRecordsChange)="filteredData = $event" > @@ -138,9 +144,7 @@

{{ listName }}

(click)="addNew()" angulartics2On="click" angularticsCategory="UserAction" - [angularticsAction]=" - listName.toLowerCase().replace(' ', '_') + '_add_new' - " + [angularticsAction]="title.toLowerCase().replace(' ', '_') + '_add_new'" *appDisabledEntityOperation="{ entity: entityConstructor, operation: 'create' @@ -171,8 +175,26 @@

{{ listName }}

mat-menu-item [appExportData]="allEntities" format="csv" - [exportConfig]="listConfig?.exportConfig" - [filename]="listName.replace(' ', '')" + [exportConfig]="exportConfig" + [filename]="title.replace(' ', '')" + angulartics2On="click" + [angularticsCategory]="entityConstructor?.ENTITY_TYPE" + angularticsAction="list_csv_export" + > + + Download all data (.csv) + + + + + + + +
+
Actions on selected records:
+ +
+ + + +
+
+
diff --git a/src/app/core/entity-list/entity-list/entity-list.component.scss b/src/app/core/entity-list/entity-list/entity-list.component.scss index 8e7586cfb8..2b68a35382 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.scss +++ b/src/app/core/entity-list/entity-list/entity-list.component.scss @@ -1,4 +1,6 @@ @use "../../../../styles/variables/breakpoints"; +@use "src/styles/variables/sizes"; +@use "src/styles/variables/colors"; /** * Aligns the baseline of the filter-field with the baseline @@ -16,3 +18,12 @@ background-color: white !important; height: 100%; } + +.bulk-action-button { + position: fixed; + right: sizes.$large; + z-index: 999; + + padding: sizes.$regular; + background-color: colors.$background; +} diff --git a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts index b81f9b23de..0ccba24893 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts +++ b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts @@ -109,7 +109,13 @@ describe("EntityListComponent", () => { createComponent(); initComponentInputs(); tick(); - expect(component.columns).toEqual(testConfig.columns); + expect(component.columns).toEqual([ + ...testConfig.columns, + "projectNumber", + "name", + "gender", + "religion", + ]); })); it("should create column groups from config and set correct one", fakeAsync(() => { @@ -117,7 +123,7 @@ describe("EntityListComponent", () => { initComponentInputs(); tick(); - expect(component.columnGroups).toEqual(testConfig.columnGroups.groups); + expect(component.groups).toEqual(testConfig.columnGroups.groups); const defaultGroup = testConfig.columnGroups.groups.findIndex( (g) => g.name === testConfig.columnGroups.default, ); @@ -184,23 +190,22 @@ describe("EntityListComponent", () => { it("should automatically initialize values if directly referenced from config", fakeAsync(() => { mockActivatedRoute.component = EntityListComponent; - const config = { - entity: "Child", - title: "Some title", - columns: ["name", "gender"], - }; const entityMapper = TestBed.inject(EntityMapperService); const children = [new Child(), new Child()]; spyOn(entityMapper, "loadType").and.resolveTo(children); createComponent(); - routeData.next({ config }); + component.listConfig = { + entity: "Child", + title: "Some title", + columns: ["name", "gender"], + }; + component.ngOnChanges({ listConfig: undefined }); tick(); expect(component.entityConstructor).toBe(Child); - expect(component.listConfig).toEqual(config); expect(component.allEntities).toEqual(children); - expect(component.listName).toBe("Some title"); + expect(component.title).toBe("Some title"); const navigateSpy = spyOn(TestBed.inject(Router), "navigate"); component.addNew(); diff --git a/src/app/core/entity-list/entity-list/entity-list.component.ts b/src/app/core/entity-list/entity-list/entity-list.component.ts index 6de23057e7..6b8eab4254 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.ts +++ b/src/app/core/entity-list/entity-list/entity-list.component.ts @@ -21,7 +21,6 @@ import { EntitySubrecordComponent } from "../../common-components/entity-subreco import { entityFilterPredicate } from "../../filter/filter-generator/filter-predicate"; import { AnalyticsService } from "../../analytics/analytics.service"; import { RouteTarget } from "../../../app.routing"; -import { RouteData } from "../../config/dynamic-routing/view-config.interface"; import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; import { EntityRegistry } from "../../entity/database-entity.decorator"; import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; @@ -43,6 +42,10 @@ import { TabStateModule } from "../../../utils/tab-state/tab-state.module"; import { ViewTitleComponent } from "../../common-components/view-title/view-title.component"; import { ExportDataDirective } from "../../export/export-data-directive/export-data.directive"; import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive"; +import { DuplicateRecordService } from "../duplicate-records/duplicate-records.service"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { Sort } from "@angular/material/sort"; +import { ExportColumnConfig } from "../../export/data-transformation-service/export-column-config"; /** * This component allows to create a full-blown table with pagination, filtering, searching and grouping. @@ -58,6 +61,7 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di selector: "app-entity-list", templateUrl: "./entity-list.component.html", styleUrls: ["./entity-list.component.scss"], + providers: [DuplicateRecordService], imports: [ NgIf, NgStyle, @@ -78,36 +82,52 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di ExportDataDirective, DisableEntityOperationDirective, RouterLink, + MatTooltipModule, ], standalone: true, }) @UntilDestroy() export class EntityListComponent - implements OnChanges, AfterViewInit + implements EntityListConfig, OnChanges, AfterViewInit { @Input() allEntities: T[]; + + /** @deprecated this is often used when this has a wrapper component (e.g. ChildrenList), preferably use individual @Input properties */ @Input() listConfig: EntityListConfig; + + @Input() entity: string; @Input() entityConstructor: EntityConstructor; + @Input() defaultSort: Sort; + @Input() exportConfig: ExportColumnConfig[]; + @Input() clickMode: "navigate" | "popup" | "none" = "navigate"; + + /** initial / default state whether to include archived records in the list */ + @Input() showInactive: boolean; + @Input() isLoading: boolean; + @Output() elementClick = new EventEmitter(); @Output() addNewClick = new EventEmitter(); + selectedRows: T[]; @ViewChild(EntitySubrecordComponent) entityTable: EntitySubrecordComponent; isDesktop: boolean; - listName = ""; - columns: (FormFieldConfig | string)[] = []; - columnGroups: GroupConfig[] = []; + @Input() title = ""; + @Input() columns: (FormFieldConfig | string)[] = []; + @Input() columnGroups: ColumnGroupsConfig; + groups: GroupConfig[] = []; defaultColumnGroup = ""; mobileColumnGroup = ""; - filtersConfig: FilterConfig[] = []; + @Input() filters: FilterConfig[] = []; columnsToDisplay: string[] = []; filterObj: DataFilter; filterString = ""; + filteredData = []; get selectedColumnGroupIndex(): number { return this.selectedColumnGroupIndex_; @@ -115,7 +135,7 @@ export class EntityListComponent set selectedColumnGroupIndex(newValue: number) { this.selectedColumnGroupIndex_ = newValue; - this.columnsToDisplay = this.columnGroups[newValue].columns; + this.columnsToDisplay = this.groups[newValue].columns; } selectedColumnGroupIndex_: number = 0; @@ -127,7 +147,7 @@ export class EntityListComponent * tabs with zero top-padding in this case */ get offsetFilterStyle(): object { - const bottomMargin = this.columnGroups.length > 1 ? 29 : 14; + const bottomMargin = this.groups.length > 1 ? 29 : 14; return { "margin-bottom": `${bottomMargin}px`, }; @@ -141,14 +161,8 @@ export class EntityListComponent private entityMapperService: EntityMapperService, private entities: EntityRegistry, private dialog: MatDialog, + private duplicateRecord: DuplicateRecordService, ) { - if (this.activatedRoute.component === EntityListComponent) { - // the component is used for a route and not inside a template - this.activatedRoute.data.subscribe((data: RouteData) => - this.buildComponentFromConfig(data.config), - ); - } - this.screenWidthObserver .platform() .pipe(untilDestroyed(this)) @@ -166,12 +180,22 @@ export class EntityListComponent }); } - private async buildComponentFromConfig(newConfig: EntityListConfig) { - this.listConfig = newConfig; + ngOnChanges(changes: SimpleChanges) { + if (changes.hasOwnProperty("listConfig")) { + Object.assign(this, this.listConfig); + } + return this.buildComponentFromConfig(); + } + + ngAfterViewInit() { + this.entityTable.recordsDataSource.filterPredicate = (data, filter) => + entityFilterPredicate(data.record, filter); + } - if (this.listConfig?.entity) { + private async buildComponentFromConfig() { + if (this.entity) { this.entityConstructor = this.entities.get( - this.listConfig.entity, + this.entity, ) as EntityConstructor; } @@ -180,14 +204,10 @@ export class EntityListComponent await this.loadEntities(); } - this.listName = - this.listConfig.title || - this.listName || - this.entityConstructor?.labelPlural; + this.title = this.title || this.entityConstructor?.labelPlural; this.addColumnsFromColumnGroups(); - this.initColumnGroups(this.listConfig.columnGroups); - this.filtersConfig = this.listConfig.filters ?? this.filtersConfig ?? []; + this.initColumnGroups(this.columnGroups); this.displayColumnGroupByName( this.screenWidthObserver.isDesktop() @@ -206,42 +226,41 @@ export class EntityListComponent this.isLoading = false; } - ngAfterViewInit() { - this.entityTable.recordsDataSource.filterPredicate = (data, filter) => - entityFilterPredicate(data.record, filter); - } - - async ngOnChanges(changes: SimpleChanges): Promise { - if (changes.hasOwnProperty("listConfig")) { - await this.buildComponentFromConfig(this.listConfig); - } - } - private addColumnsFromColumnGroups() { - this.columns = this.listConfig.columns || []; - this.listConfig.columnGroups?.groups?.forEach((group) => - group.columns - .filter( - (columnId) => - !this.columns.some((column) => - // Check if the column is already defined as object or string - typeof column === "string" - ? column === columnId - : column.id === columnId, - ), - ) - .forEach((column) => this.columns.push(column)), + const allColumns = [...this.columns]; + const groupColumns = (this.columnGroups?.groups ?? []).reduce( + (accumulatedColumns: string[], currentGroup) => [ + ...accumulatedColumns, + ...currentGroup.columns, + ], + [], ); + for (const column of groupColumns) { + if ( + !allColumns.some((existingColumn) => + // Check if the column is already defined as object or string + typeof existingColumn === "string" + ? existingColumn === column + : existingColumn.id === column, + ) + ) { + allColumns.push(column); + } + } + + if (allColumns.length !== this.columns.length) { + this.columns = [...allColumns]; + } } private initColumnGroups(columnGroup?: ColumnGroupsConfig) { if (columnGroup && columnGroup.groups.length > 0) { - this.columnGroups = columnGroup.groups; + this.groups = columnGroup.groups; this.defaultColumnGroup = columnGroup.default || columnGroup.groups[0].name; this.mobileColumnGroup = columnGroup.mobile || columnGroup.groups[0].name; } else { - this.columnGroups = [ + this.groups = [ { name: "default", columns: this.columns.map((c) => (typeof c === "string" ? c : c.id)), @@ -253,10 +272,13 @@ export class EntityListComponent } applyFilter(filterValue: string) { + // TODO: turn this into one of our filter types, so that all filtering happens the same way (and we avoid accessing internal datasource of sub-component here) filterValue = filterValue.trim(); filterValue = filterValue.toLowerCase(); // MatTableDataSource defaults to lowercase matches this.entityTable.recordsDataSource.filter = filterValue; - + this.filteredData = this.entityTable.recordsDataSource.filteredData.map( + (x) => x.record, + ); this.analyticsService.eventTrack("list_filter_freetext", { category: this.entityConstructor?.ENTITY_TYPE, }); @@ -271,7 +293,7 @@ export class EntityListComponent } private getSelectedColumnIndexByName(columnGroupName: string) { - return this.columnGroups.findIndex((c) => c.name === columnGroupName); + return this.groups.findIndex((c) => c.name === columnGroupName); } /** @@ -280,7 +302,7 @@ export class EntityListComponent openFilterOverlay() { this.dialog.open(FilterOverlayComponent, { data: { - filterConfig: this.filtersConfig, + filterConfig: this.filters, entityType: this.entityConstructor, entities: this.allEntities, useUrlQueryParams: true, @@ -295,4 +317,9 @@ export class EntityListComponent } this.addNewClick.emit(); } + + duplicateRecords() { + this.duplicateRecord.duplicateRecord(this.selectedRows); + this.selectedRows = undefined; + } } diff --git a/src/app/core/entity/default-datatype/default.datatype.ts b/src/app/core/entity/default-datatype/default.datatype.ts index ffd414bf5e..9545df82c2 100644 --- a/src/app/core/entity/default-datatype/default.datatype.ts +++ b/src/app/core/entity/default-datatype/default.datatype.ts @@ -114,4 +114,16 @@ export class DefaultDatatype { importIncompleteAdditionalConfigBadge(col: ColumnMapping): string { return undefined; } + + /** + * (Partially) anonymize to "retain-anonymized" for reporting purposes without personal identifiable information. + * @param value The original value to be anonymized + */ + async anonymize( + value: EntityType, + schemaField: EntitySchemaField, + parent: any, + ): Promise { + return undefined; + } } diff --git a/src/app/core/entity/default-datatype/edit-component.ts b/src/app/core/entity/default-datatype/edit-component.ts index 9f7e4a4c90..77a86a48ad 100644 --- a/src/app/core/entity/default-datatype/edit-component.ts +++ b/src/app/core/entity/default-datatype/edit-component.ts @@ -55,6 +55,9 @@ export abstract class EditComponent implements OnInit { */ additional?: any; + /** indicating that the value is not in its original state, so that components can explain this to the user */ + isPartiallyAnonymized: boolean; + ngOnInit() { if (!this.formFieldConfig?.forTable) { this.label = this.formFieldConfig?.label ?? this.propertySchema?.label; @@ -68,4 +71,11 @@ export abstract class EditComponent implements OnInit { // This type casts are needed as the normal types throw errors in the templates this.parent = this.formControl.parent as FormGroup; } + + ngOnChanges() { + this.isPartiallyAnonymized = + this.entity?.anonymized && + this.entity?.getSchema()?.get(this.formFieldConfig?.id)?.anonymize === + "retain-anonymized"; + } } diff --git a/src/app/core/entity/default-datatype/view.directive.ts b/src/app/core/entity/default-datatype/view.directive.ts index ffded6f6cc..cb155918e3 100644 --- a/src/app/core/entity/default-datatype/view.directive.ts +++ b/src/app/core/entity/default-datatype/view.directive.ts @@ -1,11 +1,20 @@ import { Entity } from "../model/entity"; -import { Directive, Input } from "@angular/core"; +import { Directive, Input, OnChanges } from "@angular/core"; @Directive() -export abstract class ViewDirective { +export abstract class ViewDirective implements OnChanges { @Input() entity: Entity; @Input() id: string; @Input() tooltip: string; @Input() value: T; @Input() config: C; + + /** indicating that the value is not in its original state, so that components can explain this to the user */ + isPartiallyAnonymized: boolean; + + ngOnChanges() { + this.isPartiallyAnonymized = + this.entity?.anonymized && + this.entity?.getSchema()?.get(this.id)?.anonymize === "retain-anonymized"; + } } diff --git a/src/app/core/entity/entity-actions/cascading-entity-action.spec.ts b/src/app/core/entity/entity-actions/cascading-entity-action.spec.ts new file mode 100644 index 0000000000..f72ccf7fef --- /dev/null +++ b/src/app/core/entity/entity-actions/cascading-entity-action.spec.ts @@ -0,0 +1,197 @@ +import { DatabaseEntity } from "../database-entity.decorator"; +import { Entity } from "../model/entity"; +import { DatabaseField } from "../database-field.decorator"; +import { + comparableEntityData, + expectEntitiesToMatch, +} from "../../../utils/expect-entity-data.spec"; +import { MockEntityMapperService } from "../entity-mapper/mock-entity-mapper-service"; + +/* + Deleting/Anonymizing referenced & related entities + also see doc/compodoc_sources/concepts/entity-anonymization.md + + we distinguish different roles / relations between entities: + ♢ "aggregate" (has-a): both entities have meaning independently + ♦ "composite" (is-part-of): the entity holding the reference is only meaningful in the context of the referenced + */ +@DatabaseEntity("EntityWithAnonRelations") +export class EntityWithAnonRelations extends Entity { + static override hasPII = true; + + @DatabaseField() name: string; + + @DatabaseField({ + dataType: "entity-array", + additional: "EntityWithAnonRelations", + anonymize: "retain", + entityReferenceRole: "aggregate", + }) + refAggregate: string[]; + + @DatabaseField({ + dataType: "entity-array", + additional: "EntityWithAnonRelations", + anonymize: "retain", + entityReferenceRole: "composite", + }) + refComposite: string[]; + + static create(name: string, properties?: Partial) { + return Object.assign(new EntityWithAnonRelations(), { + name: name, + ...properties, + }); + } +} + +export function expectAllUnchangedExcept( + changedEntities: EntityWithAnonRelations[], + entityMapper: MockEntityMapperService, +) { + const isExpectedUnchanged = (entity: EntityWithAnonRelations) => { + !changedEntities.some((c) => entity.getId() === c.getId()); + }; + + const actualEntitiesAfter = + entityMapper.getAllData() as EntityWithAnonRelations[]; + + expectEntitiesToMatch( + actualEntitiesAfter.filter(isExpectedUnchanged), + allEntities.filter(isExpectedUnchanged), + true, + ); +} + +export function expectDeleted( + deletedEntities: Entity[], + entityMapper: MockEntityMapperService, +) { + const actualEntitiesAfter = entityMapper.getAllData(); + + for (const deletedEntity of deletedEntities) { + expect(actualEntitiesAfter).not.toContain(deletedEntity); + } +} + +export function expectUpdated( + updatedEntities: EntityWithAnonRelations[], + entityMapper: MockEntityMapperService, +) { + const actualEntitiesAfter = entityMapper.getAllData(); + + for (const updatedEntity of updatedEntities) { + const actualEntity = actualEntitiesAfter.find( + (e) => e.getId() === updatedEntity.getId(), + ); + expect(comparableEntityData(actualEntity)).toEqual( + comparableEntityData(updatedEntity), + ); + } +} + +const WithoutRelations = EntityWithAnonRelations.create( + "entity without relations", +); + +const ReferencedAsComposite = EntityWithAnonRelations.create( + "entity referenced as composite", +); +const ReferencingSingleComposite = EntityWithAnonRelations.create( + "entity having a composite reference", + { + refComposite: [ReferencedAsComposite.getId()], + }, +); + +const ReferencedAsOneOfMultipleComposites1 = EntityWithAnonRelations.create( + "entity referenced as one composite (1)", +); +const ReferencedAsOneOfMultipleComposites2 = EntityWithAnonRelations.create( + "entity referenced as one composite (2)", +); +const ReferencingTwoComposites = EntityWithAnonRelations.create( + "entity referencing two entities as composites", + { + refComposite: [ + ReferencedAsOneOfMultipleComposites1.getId(), + ReferencedAsOneOfMultipleComposites2.getId(), + ], + }, +); + +const ReferencingCompositeAndAggregate_refComposite = + EntityWithAnonRelations.create( + "referenced as composite from composite+aggregate referencing entity", + ); +const ReferencingCompositeAndAggregate_refAggregate = + EntityWithAnonRelations.create( + "referenced as aggregate from composite+aggregate referencing entity", + ); +const ReferencingCompositeAndAggregate = EntityWithAnonRelations.create( + "having both a composite and a aggregate reference", + { + refComposite: [ReferencingCompositeAndAggregate_refComposite.getId()], + refAggregate: [ReferencingCompositeAndAggregate_refAggregate.getId()], + }, +); + +const ReferencingAggregate_ref = EntityWithAnonRelations.create( + "entity referenced as aggregate", +); +const ReferencingAggregate = EntityWithAnonRelations.create( + "entity having an aggregate reference", + { + refAggregate: [ReferencingAggregate_ref.getId()], + }, +); + +const ReferencingTwoAggregates_ref1 = EntityWithAnonRelations.create( + "entity referenced as one aggregate (1)", +); +const ReferencingTwoAggregates_ref2 = EntityWithAnonRelations.create( + "entity referenced as one aggregate (2)", +); +const ReferencingTwoAggregates = EntityWithAnonRelations.create( + "entity referencing two entities as aggregates", + { + refAggregate: [ + ReferencingTwoAggregates_ref1.getId(), + ReferencingTwoAggregates_ref2.getId(), + ], + }, +); + +export const ENTITIES = { + WithoutRelations, + ReferencedAsComposite, + ReferencingSingleComposite, + ReferencedAsOneOfMultipleComposites1, + ReferencedAsOneOfMultipleComposites2, + ReferencingTwoComposites, + ReferencingCompositeAndAggregate_refComposite, + ReferencingCompositeAndAggregate_refAggregate, + ReferencingCompositeAndAggregate, + ReferencingAggregate_ref, + ReferencingAggregate, + ReferencingTwoAggregates_ref1, + ReferencingTwoAggregates_ref2, + ReferencingTwoAggregates, +}; + +export const allEntities: EntityWithAnonRelations[] = [ + ENTITIES.WithoutRelations, + ENTITIES.ReferencedAsComposite, + ENTITIES.ReferencingSingleComposite, + ENTITIES.ReferencedAsOneOfMultipleComposites1, + ENTITIES.ReferencedAsOneOfMultipleComposites2, + ENTITIES.ReferencingTwoComposites, + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ENTITIES.ReferencingCompositeAndAggregate_refAggregate, + ENTITIES.ReferencingCompositeAndAggregate, + ENTITIES.ReferencingAggregate_ref, + ENTITIES.ReferencingAggregate, + ENTITIES.ReferencingTwoAggregates_ref1, + ENTITIES.ReferencingTwoAggregates_ref2, + ENTITIES.ReferencingTwoAggregates, +]; diff --git a/src/app/core/entity/entity-actions/cascading-entity-action.ts b/src/app/core/entity/entity-actions/cascading-entity-action.ts new file mode 100644 index 0000000000..177094c56d --- /dev/null +++ b/src/app/core/entity/entity-actions/cascading-entity-action.ts @@ -0,0 +1,114 @@ +import { Entity } from "../model/entity"; +import { asArray } from "../../../utils/utils"; +import { EntitySchemaService } from "../schema/entity-schema.service"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; + +export class CascadingActionResult { + /** + * entities that have been updated in the process, in their original state + * (can be used for undo action) + */ + originalEntitiesBeforeChange: Entity[]; + + /** + * entities that may still contain PII related to the primary entity that could not be automatically removed + * (may need manual review by the user) + */ + potentiallyRetainingPII: Entity[]; + + constructor(changedEntities?: Entity[], potentiallyRetainingPII?: Entity[]) { + this.originalEntitiesBeforeChange = changedEntities ?? []; + this.potentiallyRetainingPII = potentiallyRetainingPII ?? []; + } + + mergeResults(otherResult: CascadingActionResult) { + this.originalEntitiesBeforeChange = [ + ...this.originalEntitiesBeforeChange, + ...otherResult.originalEntitiesBeforeChange.filter( + (e) => + !this.originalEntitiesBeforeChange.some( + (x) => x.getId() === e.getId(), + ), + ), + ]; + this.potentiallyRetainingPII = [ + ...this.potentiallyRetainingPII, + ...otherResult.potentiallyRetainingPII.filter( + (e) => + !this.potentiallyRetainingPII.some((x) => x.getId() === e.getId()), + ), + ]; + + return this; + } +} + +/** + * extend this class to implement services that perform actions on an entity + * that require recursive actions to related entities as well. + */ +export abstract class CascadingEntityAction { + protected constructor( + protected entityMapper: EntityMapperService, + protected schemaService: EntitySchemaService, + ) {} + + /** + * Recursively call the given actions on all related entities that contain a reference to the given entity. + * + * Returns an array of all affected related entities (excluding the given entity) in their state before the action + * to support an undo action. + * + * @param entity + * @param compositeAction + * @param aggregateAction + * @private + */ + protected async cascadeActionToRelatedEntities( + entity: Entity, + compositeAction: ( + relatedEntity: Entity, + refField?: string, + entity?: Entity, + ) => Promise, + aggregateAction: ( + relatedEntity: Entity, + refField?: string, + entity?: Entity, + ) => Promise, + ): Promise { + const cascadeActionResult = new CascadingActionResult(); + + const entityTypesWithReferences = + this.schemaService.getEntityTypesReferencingType(entity.getType()); + + for (const refType of entityTypesWithReferences) { + const entities = await this.entityMapper.loadType(refType.entityType); + + for (const refField of refType.referencingProperties) { + const affectedEntities = entities.filter( + (e) => + asArray(e[refField]).includes(entity.getId()) || + asArray(e[refField]).includes(entity.getId(true)), + ); + + for (const e of affectedEntities) { + if ( + refType.entityType.schema.get(refField).entityReferenceRole === + "composite" && + asArray(e[refField]).length === 1 + ) { + // is only composite + const result = await compositeAction(e); + cascadeActionResult.mergeResults(result); + } else { + const result = await aggregateAction(e, refField, entity); + cascadeActionResult.mergeResults(result); + } + } + } + } + + return cascadeActionResult; + } +} diff --git a/src/app/core/entity/entity-actions/entity-actions.service.spec.ts b/src/app/core/entity/entity-actions/entity-actions.service.spec.ts new file mode 100644 index 0000000000..9fa08052a8 --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-actions.service.spec.ts @@ -0,0 +1,141 @@ +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { EntityActionsService } from "./entity-actions.service"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { + MatSnackBar, + MatSnackBarRef, + TextOnlySnackBar, +} from "@angular/material/snack-bar"; +import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service"; +import { Entity } from "../model/entity"; +import { NEVER, of, Subject } from "rxjs"; +import { Router } from "@angular/router"; +import { CoreTestingModule } from "../../../utils/core-testing.module"; +import { EntityDeleteService } from "./entity-delete.service"; +import { EntityAnonymizeService } from "./entity-anonymize.service"; +import { CascadingActionResult } from "./cascading-entity-action"; + +describe("EntityActionsService", () => { + let service: EntityActionsService; + let mockedEntityMapper: jasmine.SpyObj; + let snackBarSpy: jasmine.SpyObj; + let mockSnackBarRef: jasmine.SpyObj>; + let mockConfirmationDialog: jasmine.SpyObj; + let mockRouter; + let mockedEntityDeleteService: jasmine.SpyObj; + + let primaryEntity: Entity; + + beforeEach(() => { + primaryEntity = new Entity(); + + mockedEntityDeleteService = jasmine.createSpyObj(["deleteEntity"]); + mockedEntityDeleteService.deleteEntity.and.resolveTo( + new CascadingActionResult([primaryEntity]), + ); + mockedEntityMapper = jasmine.createSpyObj(["save", "saveAll"]); + + snackBarSpy = jasmine.createSpyObj(["open"]); + mockSnackBarRef = jasmine.createSpyObj(["onAction", "afterDismissed"]); + mockSnackBarRef.onAction.and.returnValue(of()); + snackBarSpy.open.and.returnValue(mockSnackBarRef); + + mockConfirmationDialog = jasmine.createSpyObj([ + "getConfirmation", + "showProgressDialog", + ]); + mockConfirmationDialog.getConfirmation.and.resolveTo(true); + mockConfirmationDialog.showProgressDialog.and.returnValue( + jasmine.createSpyObj(["close"]), + ); + + TestBed.configureTestingModule({ + imports: [CoreTestingModule], + providers: [ + EntityActionsService, + { provide: EntityDeleteService, useValue: mockedEntityDeleteService }, + { provide: EntityAnonymizeService, useValue: null }, + { provide: EntityMapperService, useValue: mockedEntityMapper }, + { provide: MatSnackBar, useValue: snackBarSpy }, + Router, + { + provide: ConfirmationDialogService, + useValue: mockConfirmationDialog, + }, + ], + }); + mockRouter = TestBed.inject(Router); + spyOn(mockRouter, "navigate"); + + service = TestBed.inject(EntityActionsService); + }); + + it("should return false when user cancels confirmation", async () => { + mockConfirmationDialog.getConfirmation.and.resolveTo(false); + + const result = await service.delete(new Entity()); + + expect(result).toBe(false); + expect(snackBarSpy.open).not.toHaveBeenCalled(); + expect(mockedEntityDeleteService.deleteEntity).not.toHaveBeenCalled(); + }); + + it("should delete entity, show snackbar confirmation and navigate back", async () => { + // onAction is never called + mockSnackBarRef.onAction.and.returnValues(NEVER); + mockSnackBarRef.afterDismissed.and.returnValue(of(undefined)); + + const result = await service.delete(new Entity(), true); + + expect(result).toBe(true); + expect(snackBarSpy.open).toHaveBeenCalled(); + expect(mockedEntityDeleteService.deleteEntity).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalled(); + }); + + it("should re-save all affected entities and navigate back to entity on undo", fakeAsync(() => { + const anotherAffectedEntity = new Entity(); + mockedEntityDeleteService.deleteEntity.and.resolveTo( + new CascadingActionResult([primaryEntity, anotherAffectedEntity]), + ); + + // Mock a snackbar where 'undo' is pressed + const onSnackbarAction = new Subject(); + mockSnackBarRef.onAction.and.returnValue(onSnackbarAction.asObservable()); + + mockedEntityMapper.save.and.resolveTo(); + + service.delete(primaryEntity, true); + tick(); + + mockRouter.navigate.calls.reset(); + onSnackbarAction.next(); + onSnackbarAction.complete(); + tick(); + + expect(mockedEntityDeleteService.deleteEntity).toHaveBeenCalled(); + expect(mockedEntityMapper.saveAll).toHaveBeenCalledWith( + [primaryEntity, anotherAffectedEntity], + true, + ); + expect(mockRouter.navigate).toHaveBeenCalled(); + })); + + it("should archive and save entity", async () => { + await service.archive(primaryEntity); + + expect(primaryEntity.isActive).toBeFalse(); + expect(mockedEntityMapper.save).toHaveBeenCalledWith(primaryEntity); + }); + + it("should archiveUndo and save entity", async () => { + await service.archive(primaryEntity); + expect(primaryEntity.isActive).toBeFalse(); + mockedEntityMapper.save.calls.reset(); + + await service.undoArchive(primaryEntity); + + expect(primaryEntity.isActive).toBeTrue(); + expect(mockedEntityMapper.save).toHaveBeenCalledWith(primaryEntity); + }); +}); diff --git a/src/app/core/entity/entity-actions/entity-actions.service.ts b/src/app/core/entity/entity-actions/entity-actions.service.ts new file mode 100644 index 0000000000..ad44a37fcf --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-actions.service.ts @@ -0,0 +1,201 @@ +import { Injectable } from "@angular/core"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { Entity } from "../model/entity"; +import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Router } from "@angular/router"; +import { getUrlWithoutParams } from "../../../utils/utils"; +import { EntityDeleteService } from "./entity-delete.service"; +import { EntityAnonymizeService } from "./entity-anonymize.service"; +import { OkButton } from "../../common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component"; + +/** + * A service that can triggers a user flow for entity actions (e.g. to safely remove or anonymize an entity), + * including a confirmation dialog. + */ +@Injectable({ + providedIn: "root", +}) +export class EntityActionsService { + constructor( + private confirmationDialog: ConfirmationDialogService, + private snackBar: MatSnackBar, + private router: Router, + private entityMapper: EntityMapperService, + private entityDelete: EntityDeleteService, + private entityAnonymize: EntityAnonymizeService, + ) {} + + private showSnackbarConfirmation( + entity: Entity, + action: string, + previousEntitiesForUndo: Entity[], + navigateBackToUrl?: string, + ) { + const snackBarTitle = $localize`:Entity action confirmation message:${ + entity.getConstructor().label + } "${entity.toString()}" ${action}`; + + const snackBarRef = this.snackBar.open( + snackBarTitle, + $localize`:Undo an entity action:Undo`, + { + duration: 8000, + }, + ); + + // Undo Action + snackBarRef.onAction().subscribe(async () => { + const undoProgressRef = this.confirmationDialog.showProgressDialog( + $localize`:Undo entity action progress dialog: Reverting changes ...`, + ); + await this.entityMapper.saveAll(previousEntitiesForUndo, true); + undoProgressRef.close(); + + if (navigateBackToUrl) { + await this.router.navigate([navigateBackToUrl]); + } + }); + } + + /** + * Shows a confirmation dialog to the user + * and removes the entity if the user confirms. + * + * This also triggers a toast message, enabling the user to undo the action. + * + * @param entity The entity to remove + * @param navigate whether upon delete the app will navigate back + */ + async delete( + entity: E, + navigate: boolean = false, + ): Promise { + if ( + !(await this.confirmationDialog.getConfirmation( + $localize`:Delete confirmation title:Delete?`, + $localize`:Delete confirmation dialog: + This will remove the data permanently as if it never existed. This cannot be undone. Statistical reports (also for past time periods) will change and not include this record anymore.\n + If you have not just created this record accidentally, deleting this is probably not what you want to do. If the record represents something that actually happened in your work, consider to use "anonymize" or just "archive" instead, so that you will not lose your documentation for reports.\n + Are you sure you want to delete this ${ + entity.getConstructor().label + } record?`, + )) + ) { + return false; + } + + const progressDialogRef = this.confirmationDialog.showProgressDialog( + $localize`:Entity action progress dialog:Processing ...`, + ); + const result = await this.entityDelete.deleteEntity(entity); + progressDialogRef.close(); + + if (result.potentiallyRetainingPII.length > 0) { + await this.confirmationDialog.getConfirmation( + $localize`:post-delete related PII warning title:Related records may still contain personal data`, + $localize`:post-delete related PII warning dialog:Some related records (e.g. notes) may still contain personal data in their text. We have automatically deleted all records that are linked to ONLY this ${ + entity.getConstructor().label + }. + However, there are some records that are linked to multiple records. We have not deleted these, so that you will not lose relevant data. Please review them manually to ensure all sensitive information is removed, if required (e.g. by looking through the linked notes and editing a note's text).`, + OkButton, + ); + } + + let currentUrl: string; + if (navigate) { + currentUrl = getUrlWithoutParams(this.router); + const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf("/")); + await this.router.navigate([parentUrl]); + } + + this.showSnackbarConfirmation( + result.originalEntitiesBeforeChange[0], + $localize`:Entity action confirmation message verb:Deleted`, + result.originalEntitiesBeforeChange, + currentUrl, + ); + return true; + } + + /** + * Anonymize the given entity, + * removing properties that are not explicitly configured in the schema to be retained. + * + * This triggers UX interactions like confirmation request dialog and snackbar message as well. + * + * @param entity + */ + async anonymize(entity: E) { + if ( + !(await this.confirmationDialog.getConfirmation( + $localize`:Anonymize confirmation dialog:Anonymize?`, + $localize`:Anonymize confirmation dialog: + This will remove all personal information (PII) permanently and keep only a basic record for statistical reports. Details that are removed during anonymization cannot be recovered.\n + If this ${ + entity.getConstructor().label + } has only become inactive and you want to keep all details about the record, consider to use "archive" instead.\n + Are you sure you want to anonymize this record?`, + )) + ) { + return false; + } + + const progressDialogRef = this.confirmationDialog.showProgressDialog( + $localize`:Entity action progress dialog:Processing ...`, + ); + const result = await this.entityAnonymize.anonymizeEntity(entity); + progressDialogRef.close(); + + if (result.potentiallyRetainingPII.length > 0) { + await this.confirmationDialog.getConfirmation( + $localize`:post-anonymize related PII warning title:Related records may still contain personal data`, + $localize`:post-anonymize related PII warning dialog:Some related records (e.g. notes) may still contain personal data in their text. We have automatically anonymized all records that are linked to ONLY this ${ + entity.getConstructor().label + }. + However, there are some records that are linked to multiple records. We have not anonymized these, so that you will not lose relevant data. Please review them manually to ensure all sensitive information is removed (e.g. by looking through the linked notes and editing a note's text).`, + OkButton, + ); + } + + this.showSnackbarConfirmation( + result.originalEntitiesBeforeChange[0], + $localize`:Entity action confirmation message verb:Anonymized`, + result.originalEntitiesBeforeChange, + ); + return true; + } + + /** + * Mark the given entity as inactive. + * @param entity + */ + async archive(entity: E) { + const originalEntity = entity.copy(); + entity.inactive = true; + await this.entityMapper.save(entity); + + this.showSnackbarConfirmation( + originalEntity, + $localize`:Entity action confirmation message verb:Archived`, + [originalEntity], + ); + return true; + } + /** + * Undo the archive action on the given entity. + * @param entity + */ + async undoArchive(entity: E) { + const originalEntity = entity.copy(); + entity.inactive = false; + await this.entityMapper.save(entity); + + this.showSnackbarConfirmation( + originalEntity, + $localize`:Entity action confirmation message verb:Reactivated`, + [originalEntity], + ); + return true; + } +} diff --git a/src/app/core/entity/entity-actions/entity-anonymize.service.spec.ts b/src/app/core/entity/entity-actions/entity-anonymize.service.spec.ts new file mode 100644 index 0000000000..1befc74733 --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-anonymize.service.spec.ts @@ -0,0 +1,315 @@ +import { TestBed } from "@angular/core/testing"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { Entity } from "../model/entity"; +import { of } from "rxjs"; +import { DatabaseEntity } from "../database-entity.decorator"; +import { DatabaseField } from "../database-field.decorator"; +import { + mockEntityMapper, + MockEntityMapperService, +} from "../entity-mapper/mock-entity-mapper-service"; +import { + comparableEntityData, + expectEntitiesToMatch, +} from "../../../utils/expect-entity-data.spec"; +import { UpdateMetadata } from "../model/update-metadata"; +import { FileService } from "../../../features/file/file.service"; +import { CoreTestingModule } from "../../../utils/core-testing.module"; +import { DefaultDatatype } from "../default-datatype/default.datatype"; +import { FileDatatype } from "../../../features/file/file.datatype"; +import moment from "moment"; +import { + allEntities, + ENTITIES, + EntityWithAnonRelations, + expectAllUnchangedExcept, +} from "./cascading-entity-action.spec"; +import { EntityAnonymizeService } from "./entity-anonymize.service"; + +describe("EntityAnonymizeService", () => { + let service: EntityAnonymizeService; + let entityMapper: MockEntityMapperService; + let mockFileService: jasmine.SpyObj; + + beforeEach(() => { + entityMapper = mockEntityMapper(allEntities.map((e) => e.copy())); + + mockFileService = jasmine.createSpyObj(["removeFile"]); + mockFileService.removeFile.and.returnValue(of(null)); + + TestBed.configureTestingModule({ + imports: [CoreTestingModule], + providers: [ + EntityAnonymizeService, + { provide: EntityMapperService, useValue: entityMapper }, + { provide: FileService, useValue: mockFileService }, + { provide: DefaultDatatype, useClass: FileDatatype, multi: true }, + ], + }); + + service = TestBed.inject(EntityAnonymizeService); + }); + + /* + * ANONYMIZATION + */ + @DatabaseEntity("AnonymizableEntity") + class AnonymizableEntity extends Entity { + static override hasPII = true; + + @DatabaseField() defaultField: string; + + @DatabaseField({ anonymize: "retain" }) + retainedField: string; + + @DatabaseField({ + anonymize: "retain-anonymized", + dataType: "array", + innerDataType: "date-only", + }) + retainAnonymizedDates: Date[]; + + @DatabaseField({ dataType: "file" }) file: string; + + @DatabaseField({ anonymize: "retain-anonymized", dataType: "entity-array" }) + referencesToRetainAnonymized: string[]; + + static create(properties: Partial) { + return Object.assign(new AnonymizableEntity(), properties); + } + + static expectAnonymized( + entityId: string, + expectedEntity: AnonymizableEntity, + checkAllBaseProperties = false, + ) { + const actualResult = entityMapper + .get(expectedEntity.getType(), entityId) + .copy(); + + if (!checkAllBaseProperties) { + delete actualResult.inactive; + delete actualResult.anonymized; + } + + expect(comparableEntityData(actualResult, true)).toEqual( + comparableEntityData(expectedEntity, true), + ); + } + } + + it("should anonymize and only keep properties marked to be retained", async () => { + const entity = new AnonymizableEntity(); + entity.defaultField = "test"; + entity.retainedField = "test"; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({ retainedField: "test" }), + ); + }); + + it("should anonymize and keep empty record without any fields", async () => { + const entity = new AnonymizableEntity(); + entity.defaultField = "test"; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({}), + ); + }); + + it("should anonymize and retain created and updated", async () => { + const entityProperties = { + created: new UpdateMetadata("CREATOR", new Date("2020-01-01")), + updated: new UpdateMetadata("UPDATER", new Date("2020-01-02")), + }; + const entity = AnonymizableEntity.create({ + defaultField: "test", + ...entityProperties, + }); + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({ + inactive: true, + anonymized: true, + ...entityProperties, + }), + true, + ); + }); + + it("should mark anonymized entities as inactive", async () => { + const entity = new AnonymizableEntity(); + entity.defaultField = "test"; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({ inactive: true, anonymized: true }), + true, + ); + }); + + it("should anonymize array values recursively and use datatype implementation for 'retain-anonymized", async () => { + const entity = new AnonymizableEntity(); + entity.retainAnonymizedDates = [ + moment("2023-09-25").toDate(), + moment("2023-10-04").toDate(), + ]; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({ + retainAnonymizedDates: [ + moment("2023-07-01").toDate(), + moment("2023-07-01").toDate(), + ], + }), + ); + }); + + it("should anonymize file values, actively deleting file attachments", async () => { + const entity = new AnonymizableEntity(); + entity.file = "test-file.txt"; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({}), + ); + expect(mockFileService.removeFile).toHaveBeenCalled(); + }); + + it("should not anonymize fields if Entity type is set to not have PII", async () => { + AnonymizableEntity.hasPII = false; + const entity = new AnonymizableEntity(); + // make sure the original entity is available initially (we expect it to remain unchanged) + entityMapper.add(entity); + entity.defaultField = "test"; + + await service.anonymizeEntity(entity); + + AnonymizableEntity.expectAnonymized( + entity.getId(), + AnonymizableEntity.create({ defaultField: "test" }), + true, + ); + + // reset actual state + AnonymizableEntity.hasPII = true; + }); + + /* + CASCADING ANONYMIZATION + */ + function expectAnonymized( + expectedToGetAnonymized: EntityWithAnonRelations[], + entityMapper: MockEntityMapperService, + ) { + const actualEntitiesAfter = entityMapper.getAllData(); + + for (const anonEntity of expectedToGetAnonymized) { + const actualEntity = actualEntitiesAfter.find( + (e) => e.getId() === anonEntity.getId(), + ); + + const expectedAnonymizedEntity = new EntityWithAnonRelations( + anonEntity.getId(), + ); + // copy over properties that are marked as `anonymize: "retain"` + expectedAnonymizedEntity.refAggregate = anonEntity.refAggregate; + expectedAnonymizedEntity.refComposite = anonEntity.refComposite; + expectedAnonymizedEntity.inactive = true; + expectedAnonymizedEntity.anonymized = true; + + expect(comparableEntityData(actualEntity)).toEqual( + comparableEntityData(expectedAnonymizedEntity), + ); + } + + expectAllUnchangedExcept(expectedToGetAnonymized, entityMapper); + } + + it("should not cascade anonymize the related entity if the entity holding the reference is anonymized", async () => { + // for direct references (e.g. x.referencesToRetainAnonymized --> recursively calls anonymize on referenced entities) + // see EntityDatatype & EntityArrayDatatype for unit tests + + await service.anonymizeEntity(ENTITIES.ReferencingSingleComposite); + + expectAnonymized([ENTITIES.ReferencingSingleComposite], entityMapper); + }); + + it("should cascade anonymize the 'composite'-type entity that references the entity user acts on", async () => { + await service.anonymizeEntity(ENTITIES.ReferencedAsComposite); + + expectAnonymized( + [ENTITIES.ReferencedAsComposite, ENTITIES.ReferencingSingleComposite], + entityMapper, + ); + }); + + it("should not cascade anonymize the 'composite'-type entity that still references additional other entities but ask user", async () => { + const result = await service.anonymizeEntity( + ENTITIES.ReferencedAsOneOfMultipleComposites1, + ); + + expectAnonymized( + [ENTITIES.ReferencedAsOneOfMultipleComposites1], + entityMapper, + ); + // warn user that there may be personal details in referencing entity which have not been deleted + expectEntitiesToMatch(result.potentiallyRetainingPII, [ + ENTITIES.ReferencingTwoComposites, + ]); + }); + + it("should cascade anonymize the 'composite'-type entity that references the entity user acts on even when another property holds other id (e.g. ChildSchoolRelation)", async () => { + await service.anonymizeEntity( + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ); + + expectAnonymized( + [ + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ENTITIES.ReferencingCompositeAndAggregate, + ], + entityMapper, + ); + }); + + it("should not cascade anonymize the 'aggregate'-type entity that only references the entity user acts on but ask user", async () => { + const result = await service.anonymizeEntity( + ENTITIES.ReferencingAggregate_ref, + ); + + expectAnonymized([ENTITIES.ReferencingAggregate_ref], entityMapper); + // warn user that there may be personal details in referencing entity which have not been deleted + expectEntitiesToMatch(result.potentiallyRetainingPII, [ + ENTITIES.ReferencingAggregate, + ]); + }); + + it("should not cascade anonymize the 'aggregate'-type entity that still references additional other entities but ask user", async () => { + const result = await service.anonymizeEntity( + ENTITIES.ReferencingTwoAggregates_ref1, + ); + + expectAnonymized([ENTITIES.ReferencingTwoAggregates_ref1], entityMapper); + // warn user that there may be personal details in referencing entity which have not been deleted + expectEntitiesToMatch(result.potentiallyRetainingPII, [ + ENTITIES.ReferencingTwoAggregates, + ]); + }); +}); diff --git a/src/app/core/entity/entity-actions/entity-anonymize.service.ts b/src/app/core/entity/entity-actions/entity-anonymize.service.ts new file mode 100644 index 0000000000..731e5757e6 --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-anonymize.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from "@angular/core"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { EntitySchemaService } from "../schema/entity-schema.service"; +import { + CascadingActionResult, + CascadingEntityAction, +} from "./cascading-entity-action"; +import { firstValueFrom } from "rxjs"; +import { FileDatatype } from "../../../features/file/file.datatype"; +import { FileService } from "../../../features/file/file.service"; +import { Entity } from "../model/entity"; + +/** + * Anonymize an entity including handling references with related entities. + * This service is usually used in combination with the `EntityActionsService`, which provides user confirmation processes around this. + */ +@Injectable({ + providedIn: "root", +}) +export class EntityAnonymizeService extends CascadingEntityAction { + constructor( + protected entityMapper: EntityMapperService, + protected schemaService: EntitySchemaService, + private fileService: FileService, + ) { + super(entityMapper, schemaService); + } + + /** + * The actual anonymize action without user interactions. + * @param entity + * @private + */ + async anonymizeEntity(entity: Entity): Promise { + if (!entity.getConstructor().hasPII) { + // entity types that are generally without PII by default retain all fields + // this should only be called through a cascade action anyway + return new CascadingActionResult(); + } + + const originalEntity = entity.copy(); + + for (const [key, schema] of entity.getSchema().entries()) { + if (entity[key] === undefined) { + continue; + } + + switch (schema.anonymize) { + case "retain": + break; + case "retain-anonymized": + await this.anonymizeProperty(entity, key); + break; + default: + await this.removeProperty(entity, key); + } + } + + entity.anonymized = true; + entity.inactive = true; + + await this.entityMapper.save(entity); + + const cascadeResult = await this.cascadeActionToRelatedEntities( + entity, + (e) => this.anonymizeEntity(e), + (e) => this.keepEntityUnchanged(e), + ); + + return new CascadingActionResult([originalEntity]).mergeResults( + cascadeResult, + ); + } + + private async anonymizeProperty(entity: Entity, key: string) { + const dataType = this.schemaService.getDatatypeOrDefault( + entity.getSchema().get(key).dataType, + ); + + entity[key] = await dataType.anonymize( + entity[key], + entity.getSchema().get(key), + entity, + ); + } + + private async removeProperty(entity: Entity, key: string) { + if ( + entity.getSchema().get(key).dataType === FileDatatype.dataType && + entity[key] + ) { + await firstValueFrom(this.fileService.removeFile(entity, key)); + } + + delete entity[key]; + } + + private async keepEntityUnchanged(e: Entity): Promise { + return new CascadingActionResult([], e.getConstructor().hasPII ? [e] : []); + } +} diff --git a/src/app/core/entity/entity-actions/entity-delete.service.spec.ts b/src/app/core/entity/entity-actions/entity-delete.service.spec.ts new file mode 100644 index 0000000000..182f37cf43 --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-delete.service.spec.ts @@ -0,0 +1,205 @@ +import { TestBed } from "@angular/core/testing"; +import { CoreTestingModule } from "../../../utils/core-testing.module"; +import { EntityDeleteService } from "./entity-delete.service"; +import { + mockEntityMapper, + MockEntityMapperService, +} from "../entity-mapper/mock-entity-mapper-service"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { + allEntities, + ENTITIES, + EntityWithAnonRelations, + expectAllUnchangedExcept, + expectDeleted, + expectUpdated, +} from "./cascading-entity-action.spec"; +import { expectEntitiesToMatch } from "../../../utils/expect-entity-data.spec"; +import { Note } from "../../../child-dev-project/notes/model/note"; +import { Child } from "../../../child-dev-project/children/model/child"; + +describe("EntityDeleteService", () => { + let service: EntityDeleteService; + let entityMapper: MockEntityMapperService; + + beforeEach(() => { + entityMapper = mockEntityMapper(allEntities.map((e) => e.copy())); + + TestBed.configureTestingModule({ + imports: [CoreTestingModule], + providers: [ + EntityDeleteService, + { provide: EntityMapperService, useValue: entityMapper }, + ], + }); + + service = TestBed.inject(EntityDeleteService); + }); + + function removeReference( + entity: EntityWithAnonRelations, + property: "refAggregate" | "refComposite", + referencedEntity: EntityWithAnonRelations, + ) { + const result = entity.copy(); + result[property] = result[property].filter( + (id) => + id !== referencedEntity.getId() && id !== referencedEntity.getId(true), + ); + return result; + } + + it("should not cascade delete the related entity if the entity holding the reference is deleted", async () => { + // for direct references (e.g. x.referencesToRetainAnonymized --> recursively calls anonymize on referenced entities) + // see EntityDatatype & EntityArrayDatatype for unit tests + + await service.deleteEntity(ENTITIES.ReferencingSingleComposite); + + expectDeleted([ENTITIES.ReferencingSingleComposite], entityMapper); + expectAllUnchangedExcept( + [ENTITIES.ReferencingSingleComposite], + entityMapper, + ); + }); + + it("should cascade delete the 'composite'-type entity that references the entity user acts on", async () => { + await service.deleteEntity(ENTITIES.ReferencedAsComposite); + + expectDeleted( + [ENTITIES.ReferencedAsComposite, ENTITIES.ReferencingSingleComposite], + entityMapper, + ); + expectAllUnchangedExcept( + [ENTITIES.ReferencedAsComposite, ENTITIES.ReferencingSingleComposite], + entityMapper, + ); + }); + + it("should not cascade delete the 'composite'-type entity that still references additional other entities but remove id", async () => { + const result = await service.deleteEntity( + ENTITIES.ReferencedAsOneOfMultipleComposites1, + ); + + const expectedUpdatedRelEntity = removeReference( + ENTITIES.ReferencingTwoComposites, + "refComposite", + ENTITIES.ReferencedAsOneOfMultipleComposites1, + ); + expectDeleted( + [ENTITIES.ReferencedAsOneOfMultipleComposites1], + entityMapper, + ); + expectUpdated([expectedUpdatedRelEntity], entityMapper); + expectAllUnchangedExcept( + [ + ENTITIES.ReferencedAsOneOfMultipleComposites1, + ENTITIES.ReferencingTwoComposites, + ], + entityMapper, + ); + // warn user that there may be personal details in referencing entity which have not been deleted + expectEntitiesToMatch(result.potentiallyRetainingPII, [ + expectedUpdatedRelEntity, + ]); + }); + + it("should cascade delete the 'composite'-type entity that references the entity user acts on even when another property holds other id (e.g. ChildSchoolRelation)", async () => { + await service.deleteEntity( + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ); + + expectDeleted( + [ + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ENTITIES.ReferencingCompositeAndAggregate, + ], + entityMapper, + ); + expectAllUnchangedExcept( + [ + ENTITIES.ReferencingCompositeAndAggregate_refComposite, + ENTITIES.ReferencingCompositeAndAggregate, + ], + entityMapper, + ); + }); + + it("should not cascade delete the 'aggregate'-type entity that only references the entity user acts on but remove id", async () => { + const result = await service.deleteEntity( + ENTITIES.ReferencingAggregate_ref, + ); + + const expectedUpdatedRelEntity = removeReference( + ENTITIES.ReferencingAggregate, + "refAggregate", + ENTITIES.ReferencingAggregate_ref, + ); + expectDeleted([ENTITIES.ReferencingAggregate_ref], entityMapper); + expectUpdated([expectedUpdatedRelEntity], entityMapper); + expectAllUnchangedExcept( + [ENTITIES.ReferencingAggregate_ref, ENTITIES.ReferencingAggregate], + entityMapper, + ); + // warn user that there may be personal details in referencing entity which have not been deleted + expectEntitiesToMatch(result.potentiallyRetainingPII, [ + expectedUpdatedRelEntity, + ]); + }); + + it("should not cascade delete the 'aggregate'-type entity that still references additional other entities but remove id", async () => { + await service.deleteEntity(ENTITIES.ReferencingTwoAggregates_ref1); + + expectDeleted([ENTITIES.ReferencingTwoAggregates_ref1], entityMapper); + expectUpdated( + [ + removeReference( + ENTITIES.ReferencingTwoAggregates, + "refAggregate", + ENTITIES.ReferencingTwoAggregates_ref1, + ), + ], + entityMapper, + ); + expectAllUnchangedExcept( + [ + ENTITIES.ReferencingTwoAggregates_ref1, + ENTITIES.ReferencingTwoAggregates, + ], + entityMapper, + ); + }); + + it("should remove multiple ref ids from related note", async () => { + const schemaField = Note.schema.get("relatedEntities"); + const originalSchemaAdditional = schemaField.additional; + schemaField.additional = [Child.ENTITY_TYPE]; + + const primary = new Child(); + const note = new Note(); + note.subject = "test"; + note.children = [primary.getId(), "some-other"]; + note.relatedEntities = [primary.getId(true)]; + const originalNote = note.copy(); + await entityMapper.save(primary); + await entityMapper.save(note); + + const result = await service.deleteEntity(primary); + + const actualNote = entityMapper.get( + Note.ENTITY_TYPE, + note.getId(true), + ) as Note; + + expect(actualNote.relatedEntities).toEqual([]); + expect(actualNote.children).toEqual(["some-other"]); + + expect(result.originalEntitiesBeforeChange.length).toBe(2); + expectEntitiesToMatch(result.originalEntitiesBeforeChange, [ + primary, + originalNote, + ]); + + // restore original schema + schemaField.additional = originalSchemaAdditional; + }); +}); diff --git a/src/app/core/entity/entity-actions/entity-delete.service.ts b/src/app/core/entity/entity-actions/entity-delete.service.ts new file mode 100644 index 0000000000..6f28fb8280 --- /dev/null +++ b/src/app/core/entity/entity-actions/entity-delete.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from "@angular/core"; +import { EntityMapperService } from "../entity-mapper/entity-mapper.service"; +import { Entity } from "../model/entity"; +import { EntitySchemaService } from "../schema/entity-schema.service"; +import { + CascadingActionResult, + CascadingEntityAction, +} from "./cascading-entity-action"; + +/** + * Safely delete an entity including handling references with related entities. + * This service is usually used in combination with the `EntityActionsService`, which provides user confirmation processes around this. + */ +@Injectable({ + providedIn: "root", +}) +export class EntityDeleteService extends CascadingEntityAction { + constructor( + protected entityMapper: EntityMapperService, + protected schemaService: EntitySchemaService, + ) { + super(entityMapper, schemaService); + } + + /** + * The actual delete action without user interactions. + * + * Returns an array of all affected entities (including the given entity) in their state before the action + * to support an undo action. + * + * @param entity + * @private + */ + async deleteEntity(entity: Entity): Promise { + const cascadeResult = await this.cascadeActionToRelatedEntities( + entity, + (e) => this.deleteEntity(e), + (e, refField, entity) => + this.removeReferenceFromEntity(e, refField, entity), + ); + + const originalEntity = entity.copy(); + await this.entityMapper.remove(entity); + + return new CascadingActionResult([originalEntity]).mergeResults( + cascadeResult, + ); + } + + /** + * Change and save the entity, removing referenced ids of the given referenced entity. + * + * Returns an array of the affected entities (which here is only the given entity) in the state before the action + * to support an undo action. + * + * @param relatedEntityWithReference + * @param refField + * @param referencedEntity + * @private + */ + private async removeReferenceFromEntity( + relatedEntityWithReference: Entity, + refField: string, + referencedEntity: Entity, + ): Promise { + const originalEntity = relatedEntityWithReference.copy(); + + if (Array.isArray(relatedEntityWithReference[refField])) { + relatedEntityWithReference[refField] = relatedEntityWithReference[ + refField + ].filter( + (id) => + id !== referencedEntity.getId() && + id !== referencedEntity.getId(true), + ); + } else { + delete relatedEntityWithReference[refField]; + } + + await this.entityMapper.save(relatedEntityWithReference); + + return new CascadingActionResult( + [originalEntity], + relatedEntityWithReference.getConstructor().hasPII + ? [relatedEntityWithReference] + : [], + ); + } +} diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts index e7a54c18b9..b33bbd307d 100644 --- a/src/app/core/entity/entity-config.service.ts +++ b/src/app/core/entity/entity-config.service.ts @@ -1,9 +1,5 @@ import { Injectable } from "@angular/core"; -import { - Entity, - ENTITY_CONFIG_PREFIX, - EntityConstructor, -} from "./model/entity"; +import { Entity, EntityConstructor } from "./model/entity"; import { ConfigService } from "../config/config.service"; import { EntitySchemaField } from "./schema/entity-schema-field"; import { addPropertySchema } from "./database-field.decorator"; @@ -35,8 +31,10 @@ export class EntityConfigService { setupEntitiesFromConfig() { for (const config of this.configService.getAllConfigs< EntityConfig & { _id: string } - >(ENTITY_CONFIG_PREFIX)) { - const id = config._id.substring(ENTITY_CONFIG_PREFIX.length); + >(EntityConfigService.PREFIX_ENTITY_CONFIG)) { + const id = config._id.substring( + EntityConfigService.PREFIX_ENTITY_CONFIG.length, + ); if (!this.entities.has(id)) { this.createNewEntity(id, config.extends); } @@ -78,6 +76,7 @@ export class EntityConfigService { ), ); } + // TODO: shall we just assign all properties that are present in the config object? entityType.toStringAttributes = entityConfig.toStringAttributes ?? entityType.toStringAttributes; entityType.label = entityConfig.label ?? entityType.label; @@ -85,6 +84,7 @@ export class EntityConfigService { entityType.icon = (entityConfig.icon as IconName) ?? entityType.icon; entityType.color = entityConfig.color ?? entityType.color; entityType.route = entityConfig.route ?? entityType.route; + entityType.hasPII = entityConfig.hasPII ?? entityType.hasPII; entityType._isCustomizedType = true; } @@ -158,4 +158,9 @@ export interface EntityConfig { * when a new entity is created, all properties from this class will also be available */ extends?: string; + + /** + * whether the type can contain personally identifiable information (PII) + */ + hasPII?: boolean; } diff --git a/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts b/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts index bcc9bf06e1..f8489be4c6 100644 --- a/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts +++ b/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts @@ -19,18 +19,14 @@ import { EntityMapperService } from "./entity-mapper.service"; import { Entity } from "../model/entity"; import { TestBed, waitForAsync } from "@angular/core/testing"; import { PouchDatabase } from "../../database/pouch-database"; -import { - DatabaseEntity, - entityRegistry, - EntityRegistry, -} from "../database-entity.decorator"; +import { DatabaseEntity } from "../database-entity.decorator"; import { Child } from "../../../child-dev-project/children/model/child"; import { SessionService } from "../../session/session-service/session.service"; -import { CoreModule } from "../../core.module"; import { Database } from "../../database/database"; -import { ComponentRegistry } from "../../../dynamic-components"; import { TEST_USER } from "../../../utils/mock-local-session"; +import { CoreTestingModule } from "../../../utils/core-testing.module"; + describe("EntityMapperService", () => { let entityMapper: EntityMapperService; let testDatabase: PouchDatabase; @@ -53,10 +49,8 @@ describe("EntityMapperService", () => { mockSessionService = jasmine.createSpyObj(["getCurrentUser"]); TestBed.configureTestingModule({ - imports: [CoreModule], + imports: [CoreTestingModule], providers: [ - ComponentRegistry, - { provide: EntityRegistry, useValue: entityRegistry }, { provide: Database, useValue: testDatabase }, { provide: SessionService, useValue: mockSessionService }, EntityMapperService, diff --git a/src/app/core/entity/entity-mapper/entity-mapper.service.ts b/src/app/core/entity/entity-mapper/entity-mapper.service.ts index b7c800b417..29c0e54102 100644 --- a/src/app/core/entity/entity-mapper/entity-mapper.service.ts +++ b/src/app/core/entity/entity-mapper/entity-mapper.service.ts @@ -155,13 +155,18 @@ export class EntityMapperService { * This method should be chosen whenever a bigger number of entities needs to be * saved * @param entities The entities to save + * @param forceUpdate Optional flag whether any conflicting version in the database will be quietly overwritten. + * if a conflict occurs without the forceUpdate flag being set, the save will fail, rejecting the returned promise. */ - public async saveAll(entities: Entity[]): Promise { + public async saveAll( + entities: Entity[], + forceUpdate: boolean = false, + ): Promise { entities.forEach((e) => this.setEntityMetadata(e)); const rawData = entities.map((e) => this.entitySchemaService.transformEntityToDatabaseFormat(e), ); - const results = await this._db.putAll(rawData); + const results = await this._db.putAll(rawData, forceUpdate); results.forEach((res, idx) => { if (res.ok) { const entity = entities[idx]; diff --git a/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts b/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts index 5823c46ba6..389339c208 100644 --- a/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts +++ b/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts @@ -157,4 +157,18 @@ export class MockEntityMapperService extends EntityMapperService { } return this.observables.get(name); } + + /** + * Get a flat array of all entities in the database overall + * for testing and debugging. + */ + public getAllData() { + const allData: Entity[] = []; + for (const type of this.data.values()) { + for (const entity of type.values()) { + allData.push(entity); + } + } + return allData; + } } diff --git a/src/app/core/entity/entity-remove.service.spec.ts b/src/app/core/entity/entity-remove.service.spec.ts deleted file mode 100644 index 570cc4a1ad..0000000000 --- a/src/app/core/entity/entity-remove.service.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { fakeAsync, TestBed, tick } from "@angular/core/testing"; -import { EntityRemoveService } from "./entity-remove.service"; -import { EntityMapperService } from "./entity-mapper/entity-mapper.service"; -import { - MatSnackBar, - MatSnackBarRef, - TextOnlySnackBar, -} from "@angular/material/snack-bar"; -import { ConfirmationDialogService } from "../common-components/confirmation-dialog/confirmation-dialog.service"; -import { Entity } from "./model/entity"; -import { NEVER, Subject } from "rxjs"; -import { Router } from "@angular/router"; - -describe("EntityRemoveService", () => { - let service: EntityRemoveService; - let mockEntityMapper: jasmine.SpyObj; - let snackBarSpy: jasmine.SpyObj; - let mockSnackBarRef: jasmine.SpyObj>; - let mockConfirmationDialog: jasmine.SpyObj; - let mockRouter; - - beforeEach(() => { - mockEntityMapper = jasmine.createSpyObj(["remove", "save"]); - snackBarSpy = jasmine.createSpyObj(["open"]); - mockSnackBarRef = jasmine.createSpyObj(["onAction"]); - mockConfirmationDialog = jasmine.createSpyObj(["getConfirmation"]); - mockConfirmationDialog.getConfirmation.and.resolveTo(true); - snackBarSpy.open.and.returnValue(mockSnackBarRef); - mockEntityMapper.remove.and.resolveTo(); - TestBed.configureTestingModule({ - providers: [ - { provide: EntityMapperService, useValue: mockEntityMapper }, - { provide: MatSnackBar, useValue: snackBarSpy }, - Router, - { - provide: ConfirmationDialogService, - useValue: mockConfirmationDialog, - }, - ], - }); - mockRouter = TestBed.inject(Router); - spyOn(mockRouter, "navigate"); - - service = TestBed.inject(EntityRemoveService); - }); - - it("should return false when user cancels confirmation", async () => { - mockConfirmationDialog.getConfirmation.and.resolveTo(false); - - const result = await service.remove(new Entity()); - - expect(result).toBe(false); - expect(snackBarSpy.open).not.toHaveBeenCalled(); - expect(mockEntityMapper.remove).not.toHaveBeenCalled(); - }); - - it("should delete entity, show snackbar confirmation and navigate back", async () => { - // onAction is never called - mockSnackBarRef.onAction.and.returnValues(NEVER); - - const result = await service.remove(new Entity(), true); - - expect(result).toBe(true); - expect(snackBarSpy.open).toHaveBeenCalled(); - expect(mockEntityMapper.remove).toHaveBeenCalled(); - expect(mockRouter.navigate).toHaveBeenCalled(); - }); - - it("should re-save entity and navigate back to entity on undo", fakeAsync(() => { - const entity = new Entity(); - - // Mock a snackbar where 'undo' is immediately pressed - const onSnackbarAction = new Subject(); - mockSnackBarRef.onAction.and.returnValue(onSnackbarAction.asObservable()); - - mockEntityMapper.save.and.resolveTo(); - - service.remove(entity, true); - tick(); - - mockRouter.navigate.calls.reset(); - onSnackbarAction.next(); - onSnackbarAction.complete(); - tick(); - - expect(mockEntityMapper.remove).toHaveBeenCalled(); - expect(mockEntityMapper.save).toHaveBeenCalledWith(entity, true); - expect(mockRouter.navigate).toHaveBeenCalled(); - })); -}); diff --git a/src/app/core/entity/entity-remove.service.ts b/src/app/core/entity/entity-remove.service.ts deleted file mode 100644 index 0a785df89d..0000000000 --- a/src/app/core/entity/entity-remove.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable } from "@angular/core"; -import { ConfirmationDialogService } from "../common-components/confirmation-dialog/confirmation-dialog.service"; -import { EntityMapperService } from "./entity-mapper/entity-mapper.service"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { Entity } from "./model/entity"; -import { getUrlWithoutParams } from "../../utils/utils"; -import { Router } from "@angular/router"; - -/** - * Additional options that can be (partly) specified - * for the several titles - */ -export interface RemoveEntityTextOptions { - dialogTitle?: string; - dialogText?: string; - deletedEntityInformation?: string; -} - -/** - * A service that can triggers a user flow to safely remove an entity, - * including a confirmation dialog. - */ -@Injectable({ - providedIn: "root", -}) -export class EntityRemoveService { - constructor( - private confirmationDialog: ConfirmationDialogService, - private entityMapper: EntityMapperService, - private snackBar: MatSnackBar, - private router: Router, - ) {} - - /** - * Shows a confirmation dialog to the user - * and removes the entity if the user confirms. - * - * This also triggers a toast message, enabling the user to undo the action. - * - * @param entity The entity to remove - * @param textOptions Options that you can specify to override the default options. - * You can only specify some of the options, the options that you don't specify will then be - * the default ones. - * @param navigate whether upon delete the app will navigate back - */ - async remove( - entity: E, - navigate: boolean = false, - textOptions?: RemoveEntityTextOptions, - ): Promise { - const confirmation = await this.showDeleteConfirmationDialog( - textOptions, - entity, - ); - if (!confirmation) { - return false; - } - - await this.entityMapper.remove(entity); - - let currentUrl: string; - if (navigate) { - currentUrl = getUrlWithoutParams(this.router); - const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf("/")); - await this.router.navigate([parentUrl]); - } - - this.showSnackbarConfirmation(textOptions, entity, currentUrl); - - return true; - } - - private async showDeleteConfirmationDialog( - textOptions: RemoveEntityTextOptions, - entity: Entity, - ) { - const dialogTitle = - textOptions?.dialogTitle || $localize`:Delete confirmation title:Delete?`; - const dialogText = - textOptions?.dialogText || - $localize`:Delete confirmation text:Are you sure you want to delete this ${entity.getType()} record?`; - - return this.confirmationDialog.getConfirmation(dialogTitle, dialogText); - } - - private showSnackbarConfirmation( - textOptions: RemoveEntityTextOptions, - entity: Entity, - currentUrl?: string, - ) { - const snackBarTitle = - textOptions?.deletedEntityInformation || - $localize`:Deleted Entity information:Deleted Entity ${entity.toString()}`; - - const snackBarRef = this.snackBar.open( - snackBarTitle, - $localize`:Undo deleting an entity:Undo`, - { - duration: 8000, - }, - ); - snackBarRef.onAction().subscribe(async () => { - await this.entityMapper.save(entity, true); - if (currentUrl) { - await this.router.navigate([currentUrl]); - } - }); - } -} diff --git a/src/app/core/entity/model/entity-update.ts b/src/app/core/entity/model/entity-update.ts index a3f9ec6c97..551548f666 100644 --- a/src/app/core/entity/model/entity-update.ts +++ b/src/app/core/entity/model/entity-update.ts @@ -28,23 +28,34 @@ export interface UpdatedEntity { * @param next An entity that should be updated as well as the type of update. This, as well as the entity * may be undefined or null. In this event, the entities-array is returned as is. * @param entities The entities to update, must be defined + * @param addIfMissing (Optional) whether to add an entity that comes through an update event but is not part of the array yet (default is to ignore) * @return An array of the given entities with the update applied */ export function applyUpdate( entities: T[], next: UpdatedEntity, + addIfMissing: boolean = false, ): T[] { if (!next || !next.entity || !entities) { return entities; } - switch (next.type) { - case "new": - return [next.entity].concat(entities); - case "update": - return entities.map((e) => - e.getId() === next.entity.getId() ? next.entity : e, - ); - case "remove": - return entities.filter((e) => e.getId() !== next.entity.getId()); + + if ( + next.type === "new" || + (addIfMissing && + next.type === "update" && + !entities.find((e) => e.getId() === next.entity.getId())) + ) { + return [next.entity].concat(entities); + } + + if (next.type === "update") { + return entities.map((e) => + e.getId() === next.entity.getId() ? next.entity : e, + ); + } + + if (next.type === "remove") { + return entities.filter((e) => e.getId() !== next.entity.getId()); } } diff --git a/src/app/core/entity/model/entity.spec.ts b/src/app/core/entity/model/entity.spec.ts index 3b5b97dd90..b3480503e4 100644 --- a/src/app/core/entity/model/entity.spec.ts +++ b/src/app/core/entity/model/entity.spec.ts @@ -117,6 +117,27 @@ describe("Entity", () => { entity._rev = "123"; expect(entity.isNew).toBeFalse(); }); + + it("should convert toString using toStringAttributes config or special [anonymized] label", () => { + @DatabaseEntity("TestEntityForToString") + class TestEntity extends Entity { + static toStringAttributes = ["firstname", "lastname"]; + static label = "TestEntity"; + firstname = "John"; + lastname = "Doe"; + } + + const testEntity = new TestEntity(); + + expect(testEntity.toString()).toBe("John Doe"); + + const anonymizedEntity = new TestEntity(); + anonymizedEntity.firstname = undefined; + delete anonymizedEntity.lastname; + anonymizedEntity.anonymized = true; + + expect(anonymizedEntity.toString()).toBe("[anonymized TestEntity]"); + }); }); /** diff --git a/src/app/core/entity/model/entity.ts b/src/app/core/entity/model/entity.ts index 1f9f7724a1..1bf954d818 100644 --- a/src/app/core/entity/model/entity.ts +++ b/src/app/core/entity/model/entity.ts @@ -36,8 +36,6 @@ export type EntityConstructor = (new ( ) => T) & typeof Entity; -export const ENTITY_CONFIG_PREFIX = "entity:"; - /** * "Entity" is a base class for all domain model classes. * It implements the basic general properties and methods that are required for all Entity types @@ -168,34 +166,46 @@ export class Entity { return; } + /** + * whether this entity type can contain "personally identifiable information" (PII) + * and therefore should follow strict data protection requirements + * and offer a function to anonymize records. + */ + static hasPII: boolean = false; + /** * Internal database id. * This is usually combined from the ENTITY_TYPE as a prefix with the entityId field `EntityType:entityId` * @example "Entity:123" */ - @DatabaseField() private _id: string; + @DatabaseField({ anonymize: "retain" }) private _id: string; /** internal database doc revision, used to detect conflicts by PouchDB/CouchDB */ - @DatabaseField() _rev: string; + @DatabaseField({ anonymize: "retain" }) _rev: string; @DatabaseField({ dataType: "schema-embed", additional: UpdateMetadata, + anonymize: "retain", }) created: UpdateMetadata; @DatabaseField({ dataType: "schema-embed", additional: UpdateMetadata, + anonymize: "retain", }) updated: UpdateMetadata; - @DatabaseField({ - label: $localize`:Label of checkbox:Inactive`, - description: $localize`:Description of checkbox:Ticking this box will archive the record. No data will be lost but the record will be hidden.`, - }) + @DatabaseField({ anonymize: "retain" }) inactive: boolean; + /** + * Whether this entity has been anonymized and therefore cannot be re-activated. + */ + @DatabaseField({ anonymize: "retain" }) + anonymized: boolean; + /** whether this entity object is newly created and not yet saved to database */ get isNew(): boolean { return !this._rev; @@ -215,18 +225,21 @@ export class Entity { } /** - * Check, if this entity is considered active. - * This is either taken from the property "inactive" (configured) or "active" (not configured). + * Check, if this entity is considered active or archived. + * + * This is taken from the property "inactive". * If the property doesn't exist, the default is `true`. - * Subclasses may overwrite this functionality. + * + * Some subclasses overwrite this functionality, but this logic is considered deprecated (!) now + * and implementations have to make sure that "inactive" property takes precedence! */ get isActive(): boolean { - if (this["active"] !== undefined) { - return this["active"]; - } if (this.inactive !== undefined) { return !this.inactive; } + if (this["active"] !== undefined) { + return this["active"]; + } return true; } @@ -294,6 +307,17 @@ export class Entity { * @returns {string} the instance's string representation. */ public toString(): string { + if ( + this.anonymized && + this.getConstructor().toStringAttributes.every( + (attr) => this[attr] === undefined, + ) + ) { + return $localize`:Entity.toString fallback for anonymized record:[anonymized ${ + this.getConstructor().label + }]`; + } + return this.getConstructor() .toStringAttributes.map((attr) => this[attr]) .join(" "); diff --git a/src/app/core/entity/schema/entity-schema-field.ts b/src/app/core/entity/schema/entity-schema-field.ts index f8c83694d2..c6c5f210b0 100644 --- a/src/app/core/entity/schema/entity-schema-field.ts +++ b/src/app/core/entity/schema/entity-schema-field.ts @@ -16,6 +16,7 @@ */ import { FormValidatorConfig } from "../../common-components/entity-form/dynamic-form-validators/form-validator-config"; +import { EntityReferenceRole } from "../../basic-datatypes/entity/entity-reference-role"; /** * Interface for additional configuration about a DatabaseField schema. @@ -66,6 +67,17 @@ export interface EntitySchemaField { */ additional?: any; + /** + * (Optional) If the dataType of this field references another entity, + * define the role of this relationship for the entity containing this field. + * + * i.e. how "important" is the entity this field is referencing? + * Does this the entity containing this field (not the referenced entity) still have meaning after the referenced entity has been deleted? + * + * see options of the `EntityReferenceRole` type + */ + entityReferenceRole?: EntityReferenceRole; + /** * (Optional) Define using which component this property should be displayed in lists and forms. * @@ -104,6 +116,15 @@ export interface EntitySchemaField { /** whether to show this field in the default details view */ showInDetailsView?: boolean; + + /** + * whether the field will be retained when the entity is "anonymized". + * + * By default, fields are removed (data minimization by default). + * + * "retain-anonymized" triggers a special dataType action to retain the data partially in a special, anonymized form. + */ + anonymize?: "retain" | "retain-anonymized"; } /** diff --git a/src/app/core/entity/schema/entity-schema.service.spec.ts b/src/app/core/entity/schema/entity-schema.service.spec.ts index 2b72caac62..05bede894e 100644 --- a/src/app/core/entity/schema/entity-schema.service.spec.ts +++ b/src/app/core/entity/schema/entity-schema.service.spec.ts @@ -26,6 +26,7 @@ import { EntitySchemaField } from "./entity-schema-field"; import { ConfigurableEnumDatatype } from "../../basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { EntityDatatype } from "../../basic-datatypes/entity/entity.datatype"; +import { DatabaseEntity, EntityRegistry } from "../database-entity.decorator"; describe("EntitySchemaService", () => { let service: EntitySchemaService; @@ -133,6 +134,52 @@ describe("EntitySchemaService", () => { EntityDatatype, ); }); + + it("should getEntityTypesReferencingType with all entity types having schema fields referencing the given type", () => { + @DatabaseEntity("ReferencingEntity") + class ReferencingEntity extends Entity { + @DatabaseField({ + dataType: "entity-array", + additional: "Child", + }) + refChildren: string[]; + + @DatabaseField({ + dataType: "entity", + additional: "Child", + }) + refChild: string; + + @DatabaseField({ + dataType: "entity", + additional: "School", + }) + refSchool: string; + + @DatabaseField({ + dataType: "entity-array", + additional: ["Child", "School"], + }) + multiTypeRef: string[]; + } + + const entities = new EntityRegistry(); + entities.addAll([ + [ReferencingEntity.ENTITY_TYPE, ReferencingEntity], + [Entity.ENTITY_TYPE, Entity], + ]); + const injector = TestBed.inject(Injector); + spyOn(injector, "get").and.returnValue(entities); + + const result = service.getEntityTypesReferencingType("Child"); + + expect(result).toEqual([ + { + entityType: ReferencingEntity, + referencingProperties: ["refChildren", "refChild", "multiTypeRef"], + }, + ]); + }); }); export function testDatatype( diff --git a/src/app/core/entity/schema/entity-schema.service.ts b/src/app/core/entity/schema/entity-schema.service.ts index ca89c52a94..4fe5075420 100644 --- a/src/app/core/entity/schema/entity-schema.service.ts +++ b/src/app/core/entity/schema/entity-schema.service.ts @@ -15,11 +15,13 @@ * along with ndb-core. If not, see . */ -import { Entity } from "../model/entity"; +import { Entity, EntityConstructor } from "../model/entity"; import { Injectable, Injector } from "@angular/core"; import { EntitySchema } from "./entity-schema"; import { EntitySchemaField } from "./entity-schema-field"; import { DefaultDatatype } from "../default-datatype/default.datatype"; +import { EntityRegistry } from "../database-entity.decorator"; +import { asArray } from "../../../utils/utils"; /** * Transform between entity instances and database objects @@ -236,4 +238,31 @@ export class EntitySchemaService { schemaField.dataType, ).transformToObjectFormat(value, schemaField, dataObject); } + + /** + * Get all entity types whose schema includes fields referencing the given type. + * + * e.g. given Child -> [Note, ChildSchoolRelation, ...] + * @param type + */ + getEntityTypesReferencingType(type: string): { + entityType: EntityConstructor; + referencingProperties: string[]; + }[] { + const referencingTypes = []; + for (const t of this.injector.get(EntityRegistry).values()) { + for (const [key, field] of t.schema.entries()) { + if (asArray(field.additional).includes(type)) { + let refType = referencingTypes.find((e) => e.entityType === t); + if (!refType) { + refType = { entityType: t, referencingProperties: [] }; + referencingTypes.push(refType); + } + + refType.referencingProperties.push(key); + } + } + } + return referencingTypes; + } } diff --git a/src/app/core/export/download-service/download.service.spec.ts b/src/app/core/export/download-service/download.service.spec.ts index 9ae04ea0ce..c45574a155 100644 --- a/src/app/core/export/download-service/download.service.spec.ts +++ b/src/app/core/export/download-service/download.service.spec.ts @@ -49,6 +49,7 @@ describe("DownloadService", () => { // reset createElement otherwise results in: 'an Error was thrown after all' document.createElement = oldCreateElement; }); + it("should contain a column for every property", async () => { const docs = [ { _id: "Test:1", test: 1 }, @@ -77,11 +78,12 @@ describe("DownloadService", () => { '"_id","_rev","propOne","propTwo"' + DownloadService.SEPARATOR_ROW + '"TestForCsvEntity:1","2","first","second"'; + spyOn(service, "exportFile").and.returnValue(expected); const result = await service.createCsv([test]); expect(result).toEqual(expected); }); - it("should transform object properties to their label for export", async () => { + it("should transform object values to their label for export when available (e.g. configurable-enum)", async () => { const testEnumValue: ConfigurableEnumValue = { id: "ID VALUE", label: "label value", @@ -90,9 +92,10 @@ describe("DownloadService", () => { @DatabaseEntity("TestEntity") class TestEntity extends Entity { - @DatabaseField() enumProperty: ConfigurableEnumValue; - @DatabaseField() dateProperty: Date; - @DatabaseField() boolProperty: boolean; + @DatabaseField({ label: "test enum" }) + enumProperty: ConfigurableEnumValue; + @DatabaseField({ label: "test date" }) dateProperty: Date; + @DatabaseField({ label: "test boolean" }) boolProperty: boolean; } const testEntity = new TestEntity(); @@ -105,12 +108,65 @@ describe("DownloadService", () => { const rows = csvExport.split(DownloadService.SEPARATOR_ROW); expect(rows).toHaveSize(1 + 1); // includes 1 header line const columnValues = rows[1].split(DownloadService.SEPARATOR_COL); - expect(columnValues).toHaveSize(3 + 1); // Properties + _id + expect(columnValues).toHaveSize(3); // Properties (_id is filter out by default) expect(columnValues).toContain('"' + testEnumValue.label + '"'); expect(columnValues).toContain('"' + testDate + '"'); expect(columnValues).toContain('"true"'); }); + it("should export all properties using object keys as headers, if no schema is available", async () => { + const docs = [ + { _id: "Test:1", name: "Child 1" }, + { _id: "Test:2", name: "Child 2" }, + ]; + + const csvExport = await service.createCsv(docs); + + const rows = csvExport.split(DownloadService.SEPARATOR_ROW); + expect(rows).toHaveSize(2 + 1); // includes 1 header line + const columnHeaders = rows[0].split(DownloadService.SEPARATOR_COL); + const columnValues = rows[1].split(DownloadService.SEPARATOR_COL); + + expect(columnValues).toHaveSize(2); + expect(columnHeaders).toHaveSize(2); + expect(columnHeaders).toContain('"_id"'); + }); + + it("should only export columns that have labels defined in entity schema and use the schema labels as export headers", async () => { + const testString: string = "Test 1"; + + @DatabaseEntity("LabelTestEntity") + class LabelTestEntity extends Entity { + @DatabaseField({ label: "test string" }) stringProperty: string; + @DatabaseField({ label: "test date" }) otherProperty: string; + @DatabaseField() boolProperty: boolean; + } + + const labelTestEntity = new LabelTestEntity(); + labelTestEntity.stringProperty = testString; + labelTestEntity.otherProperty = "x"; + labelTestEntity.boolProperty = true; + + const incompleteTestEntity = new LabelTestEntity(); + incompleteTestEntity.otherProperty = "second row"; + + const csvExport = await service.createCsv([ + labelTestEntity, + incompleteTestEntity, + ]); + + const rows = csvExport.split(DownloadService.SEPARATOR_ROW); + expect(rows).toHaveSize(1 + 2); // includes 1 header line + + const columnHeaders = rows[0].split(DownloadService.SEPARATOR_COL); + expect(columnHeaders).toHaveSize(2); + expect(columnHeaders).toContain('"test string"'); + expect(columnHeaders).toContain('"test date"'); + + const entity2Values = rows.find((r) => r.includes("second row")); + expect(entity2Values).toEqual(',"second row"'); // first column empty! + }); + it("should export a date as YYYY-MM-dd only", async () => { const dateString = "2021-01-01"; const dateObject = moment(dateString).toDate(); diff --git a/src/app/core/export/download-service/download.service.ts b/src/app/core/export/download-service/download.service.ts index 147df776db..57fe6180fc 100644 --- a/src/app/core/export/download-service/download.service.ts +++ b/src/app/core/export/download-service/download.service.ts @@ -5,6 +5,7 @@ import { LoggingService } from "../../logging/logging.service"; import { DataTransformationService } from "../data-transformation-service/data-transformation.service"; import { transformToReadableFormat } from "../../common-components/entity-subrecord/entity-subrecord/value-accessor"; import { Papa } from "ngx-papaparse"; +import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field"; /** * This service allows to start a download process from the browser. @@ -90,17 +91,62 @@ export class DownloadService { * @returns string a valid CSV string of the input data */ async createCsv(data: any[]): Promise { - // Collect all properties because papa only uses the properties of the first object + let entityConstructor: any; + + if (data.length > 0 && typeof data[0]?.getConstructor === "function") { + entityConstructor = data[0].getConstructor(); + } const keys = new Set(); data.forEach((row) => Object.keys(row).forEach((key) => keys.add(key))); data = data.map(transformToReadableFormat); - return this.papa.unparse(data, { - quotes: true, - header: true, - newline: DownloadService.SEPARATOR_ROW, - columns: [...keys], + if (!entityConstructor) { + return this.papa.unparse(data, { + quotes: true, + header: true, + newline: DownloadService.SEPARATOR_ROW, + columns: [...keys], + }); + } + + const result = this.exportFile(data, entityConstructor); + return result; + } + + exportFile(data: any[], entityConstructor: { schema: any }) { + const entitySchema = entityConstructor.schema; + const columnLabels = new Map(); + + entitySchema.forEach((value: { label: EntitySchemaField }, key: string) => { + if (value.label) columnLabels.set(key, value.label); }); + + const exportEntities = data.map((item) => { + let newItem = {}; + for (const key in item) { + if (columnLabels.has(key)) { + newItem[key] = item[key]; + } + } + return newItem; + }); + + const columnKeys: string[] = Array.from(columnLabels.keys()); + const labels: any[] = Array.from(columnLabels.values()); + const orderedData: any[] = exportEntities.map((item) => + columnKeys.map((key) => item[key]), + ); + + return this.papa.unparse( + { + fields: labels, + data: orderedData, + }, + { + quotes: true, + newline: DownloadService.SEPARATOR_ROW, + }, + ); } } diff --git a/src/app/core/export/query.service.spec.ts b/src/app/core/export/query.service.spec.ts index 1b346dfa12..f70d399a05 100644 --- a/src/app/core/export/query.service.spec.ts +++ b/src/app/core/export/query.service.spec.ts @@ -497,9 +497,8 @@ describe("QueryService", () => { const attendanceArrayQuery = `${EventNote.ENTITY_TYPE}:toArray:getAttendanceArray(true)`; - const attendanceResult: AttendanceInfo = await queryData( - attendanceArrayQuery, - ); + const attendanceResult: AttendanceInfo = + await queryData(attendanceArrayQuery); expect(attendanceResult).toContain({ participant: presentTwiceWithSchool.getId(), @@ -636,6 +635,34 @@ describe("QueryService", () => { expect(res).toBe(3); }); + it("should calculate the average of values", () => { + const data = [ + { a: 5, b: 2 }, + { b: "11" }, + { b: -1 }, + { b: 100 }, + { a: "invalid" }, + ]; + + let res = service.queryData("a:avg", undefined, undefined, data); + expect(res).toBe("5"); + + // Returns valid number if no values are available + res = service.queryData("a:avg", undefined, undefined, [{ b: 1 }]); + expect(res).toBe("0"); + + // Numbers are fixed to provided decimals + res = service.queryData("a:avg(2)", undefined, undefined, [ + { a: 3 }, + { a: 2 }, + ]); + expect(res).toBe("2.50"); + + // Handles 0 correctly + res = service.queryData("a:avg", undefined, undefined, [{ a: 0 }]); + expect(res).toBe("0"); + }); + function queryData(query: string, from?: Date, to?: Date, data?: any) { return service .cacheRequiredData(query, from, to) diff --git a/src/app/core/export/query.service.ts b/src/app/core/export/query.service.ts index a3379768cf..52c2b3ca2c 100644 --- a/src/app/core/export/query.service.ts +++ b/src/app/core/export/query.service.ts @@ -98,6 +98,7 @@ export class QueryService { unique: this.unique, count: this.count, sum: this.sum, + avg: this.avg, addPrefix: this.addPrefix, toEntities: this.toEntities.bind(this), getRelated: this.getRelated.bind(this), @@ -238,7 +239,7 @@ export class QueryService { /** * Returns the (integer) sum of the provided array. * It can also handle integers in strings, e.g. "3" - * @param data and integer array + * @param data an integer array * @private */ private sum(data: any[]): number { @@ -248,6 +249,25 @@ export class QueryService { }, 0); } + /** + * Returns the avg of the provided array as string. + * It can also handle integers in strings, e.g. "3". + * The average is only calculated if the value exists and is a valid number. + * @param data an integer array + * @param decimals the amount of decimals for the result, default 0 + * @private + */ + private avg(data: any[], decimals = 0): string { + const numbers = data + .map((d) => Number.parseInt(d)) + .filter((i) => !Number.isNaN(i)); + const result = + numbers.length === 0 + ? 0 + : numbers.reduce((i, sum) => sum + i, 0) / numbers.length; + return result.toFixed(decimals); + } + /** * Turns a list of ids (with the entity prefix) into a list of entities * @param ids the array of ids with entity prefix diff --git a/src/app/core/filter/filter-generator/filter-generator.service.ts b/src/app/core/filter/filter-generator/filter-generator.service.ts index 60c9fd0094..a07e83e514 100644 --- a/src/app/core/filter/filter-generator/filter-generator.service.ts +++ b/src/app/core/filter/filter-generator/filter-generator.service.ts @@ -90,9 +90,8 @@ export class FilterGeneratorService { this.entities.has(schema.additional) ) { const entityType = filterConfig.type || schema.additional; - const filterEntities = await this.entityMapperService.loadType( - entityType, - ); + const filterEntities = + await this.entityMapperService.loadType(entityType); filter = new EntityFilter( filterConfig.id, filterConfig.label || schema.label, diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts index 8f58997b93..2c90c2db40 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -68,10 +68,10 @@ export class FilterService { [key, value] = this.transformNestedKey(key, value); } const property = schema.get(key); - if (property.dataType === "configurable-enum") { + if (property?.dataType === "configurable-enum") { value = this.parseConfigurableEnumValue(property, value); } - if (property.dataType.includes("date")) { + if (property?.dataType.includes("date")) { value = moment(value).toDate(); } newEntity[key] = value; diff --git a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html index dfca7af7cc..19d8cabcae 100644 --- a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html +++ b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html @@ -33,32 +33,6 @@ Go to details - - - + - + diff --git a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.spec.ts b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.spec.ts index b25d9903a8..796192eac0 100644 --- a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.spec.ts +++ b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.spec.ts @@ -15,7 +15,6 @@ import { Child } from "../../../child-dev-project/children/model/child"; import { Router } from "@angular/router"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { FormGroup } from "@angular/forms"; -import { EntityRemoveService } from "../../entity/entity-remove.service"; import { firstValueFrom, Subject } from "rxjs"; import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; @@ -95,16 +94,11 @@ describe("DialogButtonsComponent", () => { }); it("should close the dialog if a entity is deleted", async () => { - const removeSpy = spyOn(TestBed.inject(EntityRemoveService), "remove"); + component.onAction("some other action"); + expect(dialogRef.close).not.toHaveBeenCalled(); - removeSpy.and.resolveTo(true); - await component.delete(); + component.onAction("delete"); expect(dialogRef.close).toHaveBeenCalled(); - - dialogRef.close.calls.reset(); - removeSpy.and.resolveTo(false); - await component.delete(); - expect(dialogRef.close).not.toHaveBeenCalled(); }); it("should only close the dialog if user confirms to discard changes", fakeAsync(() => { diff --git a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.ts b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.ts index 8006508e4e..b7567487aa 100644 --- a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.ts +++ b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.ts @@ -7,15 +7,14 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di import { Entity } from "../../entity/model/entity"; import { FormGroup } from "@angular/forms"; import { InvalidFormFieldError } from "../../common-components/entity-form/invalid-form-field.error"; -import { EntityRemoveService } from "../../entity/entity-remove.service"; import { EntityFormService } from "../../common-components/entity-form/entity-form.service"; import { AlertService } from "../../alerts/alert.service"; import { MatMenuModule } from "@angular/material/menu"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { Router, RouterLink } from "@angular/router"; import { EntityAbility } from "../../permissions/ability/entity-ability"; -import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service"; import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; +import { EntityActionsMenuComponent } from "../../entity-details/entity-actions-menu/entity-actions-menu.component"; @Component({ selector: "app-dialog-buttons", @@ -29,6 +28,7 @@ import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes MatMenuModule, FontAwesomeModule, RouterLink, + EntityActionsMenuComponent, ], templateUrl: "./dialog-buttons.component.html", styleUrls: ["./dialog-buttons.component.scss"], @@ -42,10 +42,8 @@ export class DialogButtonsComponent implements OnInit { private entityFormService: EntityFormService, private dialog: MatDialogRef, private alertService: AlertService, - private entityRemoveService: EntityRemoveService, private router: Router, private ability: EntityAbility, - private confirmation: ConfirmationDialogService, private unsavedChanges: UnsavedChangesService, ) { this.dialog.disableClose = true; @@ -97,9 +95,8 @@ export class DialogButtonsComponent implements OnInit { }); } - async delete() { - const result = await this.entityRemoveService.remove(this.entity); - if (result) { + onAction(action: string) { + if (action === "delete") { this.dialog.close(); } } diff --git a/src/app/core/import/import.service.spec.ts b/src/app/core/import/import.service.spec.ts index 6af1df0f2b..34b196179a 100644 --- a/src/app/core/import/import.service.spec.ts +++ b/src/app/core/import/import.service.spec.ts @@ -2,10 +2,6 @@ import { TestBed } from "@angular/core/testing"; import { ImportService } from "./import.service"; import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service"; -import { - EntityRegistry, - entityRegistry, -} from "../entity/database-entity.decorator"; import { Entity } from "../entity/model/entity"; import { ImportMetadata, ImportSettings } from "./import-metadata"; import { ColumnMapping } from "./column-mapping"; @@ -13,32 +9,25 @@ import { expectEntitiesToBeInDatabase, expectEntitiesToMatch, } from "../../utils/expect-entity-data.spec"; -import { HealthCheck } from "../../child-dev-project/children/health-checkup/model/health-check"; import moment from "moment"; import { Child } from "../../child-dev-project/children/model/child"; import { RecurringActivity } from "../../child-dev-project/attendance/model/recurring-activity"; import { ChildSchoolRelation } from "../../child-dev-project/children/model/childSchoolRelation"; -import { mockEntityMapper } from "../entity/entity-mapper/mock-entity-mapper-service"; -import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; -import { CoreModule } from "../core.module"; -import { ComponentRegistry } from "../../dynamic-components"; import { EntityArrayDatatype } from "../basic-datatypes/entity-array/entity-array.datatype"; +import { mockEntityMapper } from "../entity/entity-mapper/mock-entity-mapper-service"; +import { CoreTestingModule } from "../../utils/core-testing.module"; +import { EntityRegistry } from "../entity/database-entity.decorator"; +import { DatabaseField } from "../entity/database-field.decorator"; describe("ImportService", () => { let service: ImportService; beforeEach(async () => { TestBed.configureTestingModule({ - imports: [CoreModule], + imports: [CoreTestingModule], providers: [ ImportService, - { provide: EntityRegistry, useValue: entityRegistry }, { provide: EntityMapperService, useValue: mockEntityMapper() }, - { - provide: ConfigurableEnumService, - useValue: new ConfigurableEnumService(mockEntityMapper(), null), - }, - ComponentRegistry, ], }); service = TestBed.inject(ImportService); @@ -67,49 +56,61 @@ describe("ImportService", () => { }); it("should transform raw data to mapped entities", async () => { - HealthCheck.schema.set("entityArray", { - dataType: EntityArrayDatatype.dataType, - additional: "Child", - }); + class ImportTestTarget extends Entity { + @DatabaseField() name: string; + @DatabaseField() counter: number; + @DatabaseField() date: Date; + @DatabaseField({ + dataType: EntityArrayDatatype.dataType, + additional: "Child", + }) + entityRefs: string[]; + } + spyOn(TestBed.inject(EntityRegistry), "get").and.callFake( + (entityType: string) => + entityType === "ImportTestTarget" ? ImportTestTarget : Child, + ); + const child = Child.create("Child Name"); await TestBed.inject(EntityMapperService).save(child); + const rawData: any[] = [ - { x: "John", y: "111" }, - { x: "Jane" }, - { x: "broken date", y: "foo" }, // date column; ("y") ignored - { x: "with broken mapping column", brokenMapping: "foo" }, // column mapped to non-existing property ignored - { x: "", onlyUnmappedColumn: "1" }, // only empty or unmapped columns => row skipped - { x: "with zero", y: "0" }, // 0 value mapped - { x: "custom mapping fn", z: "30.01.2023" }, - { x: "entity array", childName: child.name }, + { rawName: "John", rawCounter: "111" }, + { rawName: "Jane" }, + { rawName: "broken date", rawCounter: "foo" }, // number column; ("rawCounter") ignored + { rawName: "with broken mapping column", brokenMapping: "foo" }, // column mapped to non-existing property ignored + { rawName: "", onlyUnmappedColumn: "1" }, // only empty or unmapped columns => row skipped + { rawName: "with zero", rawCounter: "0" }, // 0 value mapped + { rawName: "custom mapping fn", rawDate: "30.01.2023" }, + { rawName: "entity array", rawRefName: child.name }, ]; const columnMapping: ColumnMapping[] = [ - { column: "x", propertyName: "child" }, - { column: "y", propertyName: "height" }, - { column: "z", propertyName: "date", additional: "DD.MM.YYYY" }, + { column: "rawName", propertyName: "name" }, + { column: "rawCounter", propertyName: "counter" }, + { column: "rawDate", propertyName: "date", additional: "DD.MM.YYYY" }, { column: "brokenMapping", propertyName: "brokenMapping" }, - { column: "childName", propertyName: "entityArray", additional: "name" }, + { column: "rawRefName", propertyName: "entityRefs", additional: "name" }, ]; const parsedEntities = await service.transformRawDataToEntities( rawData, - HealthCheck.ENTITY_TYPE, + "ImportTestTarget", columnMapping, ); let expectedEntities: any[] = [ - { child: "John", height: 111 }, - { child: "Jane" }, - { child: "broken date" }, - { child: "with broken mapping column" }, - { child: "with zero", height: 0 }, - { child: "custom mapping fn", date: moment("2023-01-30").toDate() }, - { child: "entity array", entityArray: [child.getId()] }, + { name: "John", counter: 111 }, + { name: "Jane" }, + { name: "broken date" }, + { name: "with broken mapping column" }, + { name: "with zero", counter: 0 }, + { name: "custom mapping fn", date: moment("2023-01-30").toDate() }, + { name: "entity array", entityRefs: [child.getId()] }, ]; expectEntitiesToMatch( parsedEntities, - expectedEntities.map((e) => Object.assign(new HealthCheck(), e)), + expectedEntities.map((e) => Object.assign(new ImportTestTarget(), e)), true, ); }); diff --git a/src/app/core/site-settings/site-settings.service.spec.ts b/src/app/core/site-settings/site-settings.service.spec.ts index d9aaf3875c..a0dba5c378 100644 --- a/src/app/core/site-settings/site-settings.service.spec.ts +++ b/src/app/core/site-settings/site-settings.service.spec.ts @@ -11,14 +11,13 @@ 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"; +import { CoreTestingModule } from "../../utils/core-testing.module"; describe("SiteSettingsService", () => { let service: SiteSettingsService; @@ -30,9 +29,8 @@ describe("SiteSettingsService", () => { entityMapper = mockEntityMapper(); mockFileService = jasmine.createSpyObj(["loadFile"]); TestBed.configureTestingModule({ - imports: [CoreModule, ConfigurableEnumModule, FileModule], + imports: [CoreTestingModule, ConfigurableEnumModule, FileModule], providers: [ - ComponentRegistry, { provide: FileService, useValue: mockFileService }, { provide: EntityMapperService, useValue: entityMapper }, EntityAbility, diff --git a/src/app/core/support/support/support.component.spec.ts b/src/app/core/support/support/support.component.spec.ts index c1e14b2221..c94c26f7cc 100644 --- a/src/app/core/support/support/support.component.spec.ts +++ b/src/app/core/support/support/support.component.spec.ts @@ -24,6 +24,13 @@ import { DownloadService } from "../../export/download-service/download.service" import { AuthService } from "../../session/auth/auth.service"; import { TEST_USER } from "../../../utils/mock-local-session"; +class MockDeleteRequest { + onsuccess: () => {}; + constructor() { + setTimeout(() => this.onsuccess()); + } +} + describe("SupportComponent", () => { let component: SupportComponent; let fixture: ComponentFixture; @@ -36,6 +43,12 @@ describe("SupportComponent", () => { userAgent: "mock user agent", serviceWorker: { getRegistrations: () => [], ready: Promise.resolve() }, }, + indexedDB: { + databases: jasmine.createSpy(), + deleteDatabase: jasmine + .createSpy() + .and.callFake(() => new MockDeleteRequest()), + }, }; let mockLocation: any; @@ -112,13 +125,18 @@ describe("SupportComponent", () => { mockWindow.navigator.serviceWorker.getRegistrations = () => [ { unregister: unregisterSpy }, ]; + mockWindow.indexedDB.databases.and.resolveTo([ + { name: "db1" }, + { name: "db2" }, + ]); await component.resetApplication(); - expect(mockDB.destroy).toHaveBeenCalled(); expect(unregisterSpy).toHaveBeenCalled(); expect(localStorage.getItem("someItem")).toBeNull(); expect(mockLocation.pathname).toBe(""); + expect(mockWindow.indexedDB.deleteDatabase).toHaveBeenCalledWith("db1"); + expect(mockWindow.indexedDB.deleteDatabase).toHaveBeenCalledWith("db2"); }); it("should display the service worker logs after they are available", fakeAsync(() => { diff --git a/src/app/core/support/support/support.component.ts b/src/app/core/support/support/support.component.ts index 8bb95d6f15..0f5743f03d 100644 --- a/src/app/core/support/support/support.component.ts +++ b/src/app/core/support/support/support.component.ts @@ -108,6 +108,11 @@ export class SupportComponent implements OnInit { } private initDbInfo() { + if (!this.database || !this.database.getPouchDB()) { + this.dbInfo = "db not initialized"; + return; + } + return this.database .getPouchDB() .info() @@ -154,7 +159,9 @@ export class SupportComponent implements OnInit { return; } - await this.database.destroy(); + const dbs = await this.window.indexedDB.databases(); + await Promise.all(dbs.map(({ name }) => this.destroyDatabase(name))); + const registrations = await this.window.navigator.serviceWorker.getRegistrations(); const unregisterPromises = registrations.map((reg) => reg.unregister()); @@ -171,4 +178,12 @@ export class SupportComponent implements OnInit { "aamdigital_data_" + new Date().toISOString(), ); } + + private destroyDatabase(name: string) { + return new Promise((resolve, reject) => { + const del = this.window.indexedDB.deleteDatabase(name); + del.onsuccess = resolve; + del.onerror = reject; + }); + } } diff --git a/src/app/core/ui/routed-view/routed-view.component.spec.ts b/src/app/core/ui/routed-view/routed-view.component.spec.ts new file mode 100644 index 0000000000..a4a9eab81d --- /dev/null +++ b/src/app/core/ui/routed-view/routed-view.component.spec.ts @@ -0,0 +1,76 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; + +import { RoutedViewComponent } from "./routed-view.component"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject } from "rxjs"; +import { ComponentRegistry } from "../../../dynamic-components"; +import { Component } from "@angular/core"; + +@Component({ + template: ``, +}) +class MockComponent {} + +describe("RoutedViewComponent", () => { + let component: RoutedViewComponent; + let fixture: ComponentFixture; + + let mockActivatedRoute; + + function mockParamMap(params: { [key: string]: any }) { + return { keys: Object.keys(params), get: (key: string) => params[key] }; + } + + beforeEach(() => { + mockActivatedRoute = { + data: new BehaviorSubject({ + component: "InitialComponent", + config: { testFlag: true }, + }), + paramMap: new BehaviorSubject(mockParamMap({})), + }; + + TestBed.configureTestingModule({ + imports: [RoutedViewComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: mockActivatedRoute as unknown as ActivatedRoute, + }, + { + provide: ComponentRegistry, + useValue: { get: () => async () => MockComponent }, + }, + ], + }); + fixture = TestBed.createComponent(RoutedViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should take component from route data and use it for dynamic component directive", fakeAsync(() => { + mockActivatedRoute.data.next({ component: "TestComponent" }); + tick(); + + expect(component.component).toEqual("TestComponent"); + })); + + it("should pass config route data on as config", fakeAsync(() => { + mockActivatedRoute.data.next({ config: { testDetail: "test" } }); + tick(); + + expect(component.config).toEqual({ testDetail: "test" }); + })); + + it("should add route param '/:id' to config", fakeAsync(() => { + mockActivatedRoute.paramMap.next(mockParamMap({ id: "123" })); + tick(); + + expect(component.config.id).toEqual("123"); + })); +}); diff --git a/src/app/core/ui/routed-view/routed-view.component.ts b/src/app/core/ui/routed-view/routed-view.component.ts new file mode 100644 index 0000000000..20b5ceb3e2 --- /dev/null +++ b/src/app/core/ui/routed-view/routed-view.component.ts @@ -0,0 +1,45 @@ +import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouteTarget } from "../../../app.routing"; +import { ActivatedRoute } from "@angular/router"; +import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive"; +import { ViewConfig } from "../../config/dynamic-routing/view-config.interface"; + +/** + * Wrapper component for a primary, full page view + * that takes parameters from the route and passes these on to normal @Input properties. + * + * This allows to develop functional feature components in a way to easily reuse them for display + * as a full page view or in a modal dialog. + */ +@RouteTarget("RoutedView") +@Component({ + selector: "app-routed-view", + standalone: true, + imports: [CommonModule, DynamicComponentDirective], + template: ``, +}) +export class RoutedViewComponent { + component: string; + config: T; + + constructor(route: ActivatedRoute) { + route.data.subscribe((data: { component: string } & ViewConfig) => { + this.component = data.component; + // pass all other config properties to the component as config + this.config = Object.assign({}, data.config); + + // merge updated config properties from route params + route.paramMap.subscribe((params) => { + const config = this.config; + for (const key of params.keys) { + config[key] = params.get(key); + } + this.config = { ...config }; + }); + }); + } +} diff --git a/src/app/core/ui/ui-config.ts b/src/app/core/ui/ui-config.ts deleted file mode 100644 index 3ae7f1b1d3..0000000000 --- a/src/app/core/ui/ui-config.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Interface for the general configuration of the application. - * This is independent of the routes. - */ -export interface UiConfig { - /** - * The path to a logo icon inside the `assets` folder. - * This will be displayed on top of the navigation items. - */ - logo_path?: string; - - /** - * Toggle whether the language select component should be displayed. - * This should only be used if configurations for multiple languages are available. - */ - displayLanguageSelect?: boolean; - - /** - * The default language of the application which is used after login if the user doesn't select something else. - */ - default_language?: string; - - /** - * The title which is shown at the top of the application. - */ - site_name?: string; -} diff --git a/src/app/core/user/user.ts b/src/app/core/user/user.ts index 3fba9038d6..4c390db113 100644 --- a/src/app/core/user/user.ts +++ b/src/app/core/user/user.ts @@ -32,6 +32,7 @@ export class User extends Entity { static icon: IconName = "user"; static label = $localize`:label for entity:User`; static labelPlural = $localize`:label (plural) for entity:Users`; + static override hasPII = true; /** username used for login and identification */ @DatabaseField({ diff --git a/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.component.spec.ts b/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.component.spec.ts index 388186b7a7..f058325831 100644 --- a/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.component.spec.ts +++ b/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.component.spec.ts @@ -11,7 +11,6 @@ import { Child } from "../../../../child-dev-project/children/model/child"; import moment from "moment"; import { ConfigService } from "../../../../core/config/config.service"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { Entity } from "../../../../core/entity/model/entity"; import { DatabaseField } from "../../../../core/entity/database-field.decorator"; import { @@ -19,6 +18,7 @@ import { MockEntityMapperService, } from "../../../../core/entity/entity-mapper/mock-entity-mapper-service"; import { DatabaseEntity } from "../../../../core/entity/database-entity.decorator"; +import { DateWithAge } from "../../../../core/basic-datatypes/date-with-age/dateWithAge"; describe("BirthdayDashboardComponent", () => { let component: BirthdayDashboardComponent; diff --git a/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.stories.ts b/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.stories.ts index 44fb95f390..ecb3afdeed 100644 --- a/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.stories.ts +++ b/src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.stories.ts @@ -3,8 +3,8 @@ import { BirthdayDashboardComponent } from "./birthday-dashboard.component"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { Child } from "../../../../child-dev-project/children/model/child"; import moment from "moment"; -import { DateWithAge } from "../../../../child-dev-project/children/model/dateWithAge"; import { importProvidersFrom } from "@angular/core"; +import { DateWithAge } from "app/core/basic-datatypes/date-with-age/dateWithAge"; const child1 = Child.create("First Child"); child1.dateOfBirth = new DateWithAge( diff --git a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard-widget.module.ts b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard-widget.module.ts index c015dcc7b0..a05f2445e7 100644 --- a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard-widget.module.ts +++ b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard-widget.module.ts @@ -1,6 +1,6 @@ -import {NgModule} from "@angular/core"; -import {ComponentRegistry} from "../../../dynamic-components"; -import {ProgressDashboardConfig} from "./progress-dashboard/progress-dashboard-config"; +import { NgModule } from "@angular/core"; +import { ComponentRegistry } from "../../../dynamic-components"; +import { ProgressDashboardConfig } from "./progress-dashboard/progress-dashboard-config"; @NgModule({}) export class ProgressDashboardWidgetModule { diff --git a/src/app/features/file/display-img/display-img.component.html b/src/app/features/file/display-img/display-img.component.html index baee6d3910..2096edb1b3 100644 --- a/src/app/features/file/display-img/display-img.component.html +++ b/src/app/features/file/display-img/display-img.component.html @@ -1,3 +1,6 @@ - - - \ No newline at end of file + + + diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index 20ec726de9..7844374fc1 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -78,6 +78,7 @@ export class EditFileComponent extends EditComponent implements OnInit { // directly reset input so subsequent selections with the same name also trigger the change event this.fileUploadInput.nativeElement.value = ""; this.selectedFile = file; + this.formControl.markAsDirty(); this.formControl.setValue(file.name); } @@ -104,7 +105,7 @@ export class EditFileComponent extends EditComponent implements OnInit { } formClicked(isInputElement?: boolean) { - if (this.formControl.disabled) { + if (this.initialValue && this.formControl.value === this.initialValue) { this.showFile(); } else if (isInputElement) { this.fileUploadInput.nativeElement.click(); @@ -118,6 +119,7 @@ export class EditFileComponent extends EditComponent implements OnInit { } delete() { + this.formControl.markAsDirty(); this.formControl.setValue(null); this.selectedFile = undefined; // remove is only necessary if an initial value was set diff --git a/src/app/features/file/edit-photo/edit-photo.component.html b/src/app/features/file/edit-photo/edit-photo.component.html index fd84cae2eb..d956f66411 100644 --- a/src/app/features/file/edit-photo/edit-photo.component.html +++ b/src/app/features/file/edit-photo/edit-photo.component.html @@ -13,8 +13,14 @@ > - { + // accessing the id of the entity property seems difficult here + // file anonymization requires the FileService to actively delete - not supporting partial anonymization for now + // --> see EntityRemoveService for full anonymization, removing files + throw new Error( + "'retain-anonymized' is not implemented for 'file' datatype", + ); + } } diff --git a/src/app/features/historical-data/demo-historical-data-generator.ts b/src/app/features/historical-data/demo-historical-data-generator.ts index 43d398bb97..38fb1341a9 100644 --- a/src/app/features/historical-data/demo-historical-data-generator.ts +++ b/src/app/features/historical-data/demo-historical-data-generator.ts @@ -3,9 +3,9 @@ import { HistoricalEntityData } from "./model/historical-entity-data"; import { Injectable } from "@angular/core"; import { DemoChildGenerator } from "../../child-dev-project/children/demo-data-generators/demo-child-generator.service"; import { faker } from "../../core/demo-data/faker"; -import { ENTITY_CONFIG_PREFIX } from "../../core/entity/model/entity"; import { DemoConfigGeneratorService } from "../../core/config/demo-config-generator.service"; import { ratingAnswers } from "./model/rating-answers"; +import { EntityConfigService } from "../../core/entity/entity-config.service"; export class DemoHistoricalDataConfig { minCountAttributes: number; @@ -35,7 +35,8 @@ export class DemoHistoricalDataGenerator extends DemoDataGenerator attr.name); const entities: HistoricalEntityData[] = []; for (const child of this.childrenGenerator.entities) { diff --git a/src/app/features/historical-data/model/historical-entity-data.ts b/src/app/features/historical-data/model/historical-entity-data.ts index 82f669510e..6ed0d39c83 100644 --- a/src/app/features/historical-data/model/historical-entity-data.ts +++ b/src/app/features/historical-data/model/historical-entity-data.ts @@ -2,6 +2,7 @@ import { Entity } from "../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../core/entity/database-field.decorator"; import { PLACEHOLDERS } from "../../../core/entity/schema/entity-schema-field"; +import { Child } from "../../../child-dev-project/children/model/child"; /** * A general class that represents data that is collected for a entity over time. @@ -9,10 +10,20 @@ import { PLACEHOLDERS } from "../../../core/entity/schema/entity-schema-field"; */ @DatabaseEntity("HistoricalEntityData") export class HistoricalEntityData extends Entity { + static override hasPII = true; + @DatabaseField({ label: $localize`:Label for date of historical data:Date`, defaultValue: PLACEHOLDERS.NOW, + anonymize: "retain-anonymized", }) date: Date; - @DatabaseField() relatedEntity: string; + + @DatabaseField({ + dataType: "entity", + additional: Child.ENTITY_TYPE, + entityReferenceRole: "composite", + anonymize: "retain", + }) + relatedEntity: string; } diff --git a/src/app/features/location/edit-location/edit-location.component.spec.ts b/src/app/features/location/edit-location/edit-location.component.spec.ts index 0399e50ff2..06f8e245d1 100644 --- a/src/app/features/location/edit-location/edit-location.component.spec.ts +++ b/src/app/features/location/edit-location/edit-location.component.spec.ts @@ -111,7 +111,7 @@ describe("EditLocationComponent", () => { await clearButton.click(); - expect(component.formControl.value).toBeNull(); + expect(component.formControl.value).toBeUndefined(); await expectAsync(input.getValue()).toBeResolvedTo(""); }); diff --git a/src/app/features/location/edit-location/edit-location.component.ts b/src/app/features/location/edit-location/edit-location.component.ts index 7a33627904..f8cb91307e 100644 --- a/src/app/features/location/edit-location/edit-location.component.ts +++ b/src/app/features/location/edit-location/edit-location.component.ts @@ -81,7 +81,8 @@ export class EditLocationComponent ); } - selectLocation(selected: GeoResult) { + selectLocation(selected?: GeoResult) { + this.formControl.markAsDirty(); this.formControl.setValue(selected); this.filteredOptions.next([]); } @@ -92,7 +93,7 @@ export class EditLocationComponent } clearInput() { - this.formControl.setValue(null); + this.selectLocation(); } private getGeoLookupResult(searchTerm) { @@ -122,7 +123,7 @@ export class EditLocationComponent ref .afterClosed() .pipe(concatMap(() => this.lookupCoordinates(marked.value[0]))) - .subscribe((res) => this.formControl.setValue(res)); + .subscribe((res) => this.selectLocation(res)); } } diff --git a/src/app/features/markdown-page/markdown-page/markdown-page.component.ts b/src/app/features/markdown-page/markdown-page/markdown-page.component.ts index 0e37e69ea0..aeba29b681 100644 --- a/src/app/features/markdown-page/markdown-page/markdown-page.component.ts +++ b/src/app/features/markdown-page/markdown-page/markdown-page.component.ts @@ -15,10 +15,7 @@ * along with ndb-core. If not, see . */ -import { Component, Input, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { MarkdownPageConfig } from "../MarkdownPageConfig"; -import { RouteData } from "../../../core/config/dynamic-routing/view-config.interface"; +import { Component, Input } from "@angular/core"; import { RouteTarget } from "../../../app.routing"; import { MarkdownPageModule } from "../markdown-page.module"; @@ -32,16 +29,7 @@ import { MarkdownPageModule } from "../markdown-page.module"; imports: [MarkdownPageModule], standalone: true, }) -export class MarkdownPageComponent implements OnInit { +export class MarkdownPageComponent { /** filepath to be loaded as markdown */ @Input() markdownFile: string; - - constructor(private route: ActivatedRoute) {} - - ngOnInit() { - this.route.data.subscribe( - (data: RouteData) => - (this.markdownFile = data.config.markdownFile), - ); - } } diff --git a/src/app/features/reporting/reporting/reporting.component.html b/src/app/features/reporting/reporting/reporting.component.html index c9252b01fb..291c0d1941 100644 --- a/src/app/features/reporting/reporting/reporting.component.html +++ b/src/app/features/reporting/reporting/reporting.component.html @@ -9,7 +9,7 @@ ) => { - this.availableReports = data.config?.reports; - }, - ); - } - async calculateResults( selectedReport: ReportConfig, fromDate: Date, diff --git a/src/app/features/reporting/reporting/select-report/select-report.component.html b/src/app/features/reporting/reporting/select-report/select-report.component.html index fa2f4f8dbd..d086d2ec20 100644 --- a/src/app/features/reporting/reporting/select-report/select-report.component.html +++ b/src/app/features/reporting/reporting/select-report/select-report.component.html @@ -46,6 +46,7 @@
+