diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 69a6feef8b..00f35a4057 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -35,7 +35,7 @@ runs: - name: get latest tag sha if: ${{ inputs.full-setup == 'true' }} id: tag-sha - uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3 + uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@da99ab845e78301fcb0680d16cbc185a40a1938c # v18.24.1 - name: load "NPM TAG" if: ${{ inputs.full-setup == 'true' }} id: set-npm-tag diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 88c32e79f5..afc0c5eeed 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5 + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v3.29.5 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -39,7 +39,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5 + uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v3.29.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -53,4 +53,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5 + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v3.29.5 diff --git a/.github/workflows/notify-on-an-bdu-label.yml b/.github/workflows/notify-on-an-bdu-label.yml index 7a1586d94e..2dec383118 100644 --- a/.github/workflows/notify-on-an-bdu-label.yml +++ b/.github/workflows/notify-on-an-bdu-label.yml @@ -7,6 +7,7 @@ on: permissions: pull-requests: read + issues: read jobs: notify-bdu: @@ -18,8 +19,41 @@ jobs: github.event.label.name == 'A/N BDU' && github.event.pull_request.state == 'open' steps: + - name: Check if label was added after PR creation (with time threshold) + id: check_label_timing + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const prCreatedAt = new Date('${{ github.event.pull_request.created_at }}'); + + const timeline = await github.rest.issues.listEvents({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const labelEvent = timeline.data.reverse().find(event => + event.event === 'labeled' && + event.label.name === 'A/N BDU' + ); + + if (!labelEvent) { + core.setOutput('should_notify', 'false'); + return; + } + + const labelAddedAt = new Date(labelEvent.created_at); + const timeDiffSeconds = (labelAddedAt - prCreatedAt) / 1000; + + if (timeDiffSeconds > 15) { + core.setOutput('should_notify', 'true'); + } else { + core.setOutput('should_notify', 'false'); + } + - name: Send Teams notification - uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3 + if: steps.check_label_timing.outputs.should_notify == 'true' + uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@da99ab845e78301fcb0680d16cbc185a40a1938c # v18.24.1 with: webhook-url: ${{ secrets.TEAMS_NOTIFICATION_ADF_BDU_WEBHOOK }} skip_checkout: true diff --git a/.github/workflows/pull-from-crowdin.yml b/.github/workflows/pull-from-crowdin.yml index 6e6feb074d..51ff36ad06 100644 --- a/.github/workflows/pull-from-crowdin.yml +++ b/.github/workflows/pull-from-crowdin.yml @@ -29,7 +29,7 @@ jobs: ref: develop token: ${{ steps.app-token.outputs.token }} - name: Pull translations from Crowdin - uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1 with: skip_ref_checkout: true upload_sources: false diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 424b51941b..371fe5e0fa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -254,6 +254,8 @@ jobs: name: "Unit Tests" needs: [setup] uses: ./.github/workflows/unit-test-workflow.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: base_ref: ${{ github.base_ref || 'develop' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3c66cb632..a5afc3eae1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Push Source Files to Crowdin - uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1 with: upload_sources: true upload_sources_args: --delete-obsolete diff --git a/.github/workflows/sonar-develop.yml b/.github/workflows/sonar-develop.yml new file mode 100644 index 0000000000..83bd79b0e7 --- /dev/null +++ b/.github/workflows/sonar-develop.yml @@ -0,0 +1,22 @@ +name: "SonarCloud Full Scan (develop)" + +on: + schedule: + - cron: '0 5 * * *' + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + full-unit-tests-and-sonar-scan: + name: "Full Unit Tests + SonarCloud Scan" + uses: ./.github/workflows/unit-test-workflow.yml + secrets: inherit + with: + full: true diff --git a/.github/workflows/stale-pr-cleanup.yml b/.github/workflows/stale-pr-cleanup.yml index c50211b485..7a970ff8f5 100644 --- a/.github/workflows/stale-pr-cleanup.yml +++ b/.github/workflows/stale-pr-cleanup.yml @@ -11,4 +11,4 @@ permissions: jobs: stale-pr-cleanup: - uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3 + uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@da99ab845e78301fcb0680d16cbc185a40a1938c # v18.24.1 diff --git a/.github/workflows/supply-chain-pr-instructions.yml b/.github/workflows/supply-chain-pr-instructions.yml index 9ebc9bee49..92340b418c 100644 --- a/.github/workflows/supply-chain-pr-instructions.yml +++ b/.github/workflows/supply-chain-pr-instructions.yml @@ -16,7 +16,7 @@ jobs: permissions: pull-requests: write steps: - - uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3 + - uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@da99ab845e78301fcb0680d16cbc185a40a1938c # v18.24.1 with: comment-identifier: supply-chain-review-instructions comment-body: | diff --git a/.github/workflows/supply-chain-review.lock.yml b/.github/workflows/supply-chain-review.lock.yml index 8b1a7db227..05de38a487 100644 --- a/.github/workflows/supply-chain-review.lock.yml +++ b/.github/workflows/supply-chain-review.lock.yml @@ -41,7 +41,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 +# - github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4 @@ -125,7 +125,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -494,7 +494,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1128,7 +1128,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1404,7 +1404,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1653,7 +1653,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1732,7 +1732,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1 + uses: github/gh-aw-actions/setup@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/unit-test-workflow.yml b/.github/workflows/unit-test-workflow.yml index 8759f8b863..9de5b882cd 100644 --- a/.github/workflows/unit-test-workflow.yml +++ b/.github/workflows/unit-test-workflow.yml @@ -2,12 +2,21 @@ name: "Unit Tests Workflow" on: workflow_call: + secrets: + SONAR_TOKEN: + description: 'Token for SonarCloud analysis' + required: false inputs: base_ref: description: 'Base branch for affected calculation' required: false type: string default: 'develop' + full: + description: 'Run the full (non-affected) test suite for every project instead of only affected ones' + required: false + type: boolean + default: false jobs: generate-affected-matrix: @@ -30,9 +39,15 @@ jobs: id: set-matrix env: BASE_REF: ${{ inputs.base_ref }} + FULL_RUN: ${{ inputs.full }} run: | - echo "Base ref is $BASE_REF" - AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular) + if [ "$FULL_RUN" == "true" ]; then + echo "Running full (non-affected) test suite" + AFFECTED_UNIT=$(pnpm nx show projects --target=test --select=projects --plain --exclude=cli,stories,eslint-angular) + else + echo "Base ref is $BASE_REF" + AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular) + fi echo "Affected projects for UNIT: $AFFECTED_UNIT" if [ -z "$AFFECTED_UNIT" ]; then @@ -74,8 +89,56 @@ jobs: NODE_OPTIONS: "--max-old-space-size=5120" run: | xvfb-run --auto-servernum pnpm nx run ${{ matrix.project }}:test + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-${{ matrix.project }} + path: coverage/${{ matrix.project }}/lcov.info + if-no-files-found: ignore + retention-days: 1 - name: Save nx cache if: ${{ success() }} uses: ./.github/actions/save-nx-cache with: cache-suffix: test-${{ matrix.project }} + + sonarcloud: + name: "SonarCloud Scan" + runs-on: ubuntu-latest + needs: [generate-affected-matrix, unit-tests] + if: ${{ needs.generate-affected-matrix.outputs.hasProjects == 'true' && always() && needs.unit-tests.result != 'cancelled' }} + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Download all coverage artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: coverage-* + path: coverage-reports + - name: Merge coverage reports + run: | + mkdir -p coverage + echo "Artifact structure:" + find coverage-reports -type f -name 'lcov.info' 2>/dev/null || true + for dir in coverage-reports/coverage-*/; do + project_name=$(basename "$dir" | sed 's/^coverage-//') + lcov_file=$(find "$dir" -name 'lcov.info' -type f | head -1) + if [ -n "$lcov_file" ]; then + mkdir -p "coverage/${project_name}" + cp "$lcov_file" "coverage/${project_name}/lcov.info" + echo "Copied coverage for ${project_name}" + fi + done + echo "Coverage files found:" + find coverage -name 'lcov.info' -type f + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: https://sonarcloud.io diff --git a/docs/core/models/form-field.model.md b/docs/core/models/form-field.model.md index ed98e18f8d..ab88e0ab5a 100644 --- a/docs/core/models/form-field.model.md +++ b/docs/core/models/form-field.model.md @@ -52,7 +52,8 @@ Contains the value and metadata for a field of a [`Form`](../../../lib/process-s | columns | [`ContainerColumnModel`](../../../lib/core/src/lib/form/components/widgets/core/container-column.model.ts)\[] | \[] | Column definitions for a container field | | rows | [`ContainerRowModel`](../../../lib/core/src/lib/form/components/widgets/core/container-row.model.ts)\[] | \[] | Row definitions for a repeatable section field | | emptyOption | [`FormFieldOption`](../../../lib/core/src/lib/form/components/widgets/core/form-field-option.ts) | | Dropdown menu item to use when no option is chosen | -| validationSummary | string | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) | +| validationSummary | [`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts) | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) | +| validationSummaryChanges$ | Observable<[`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts)> | | Replays the current validation summary to subscribers and emits the completed summary after each validation | ## Details diff --git a/docs/core/services/nodes-api.service.md b/docs/core/services/nodes-api.service.md index bad26f0fbb..3f3a0a2e08 100644 --- a/docs/core/services/nodes-api.service.md +++ b/docs/core/services/nodes-api.service.md @@ -18,6 +18,7 @@ Accesses and manipulates ACS document nodes using their node IDs. - [Getting folder node contents](#getting-folder-node-contents) - [Creating and updating nodes](#creating-and-updating-nodes) - [Deleting and restoring nodes](#deleting-and-restoring-nodes) + - [Checking out nodes](#checking-out-nodes) - [See also](#see-also) ## Class members @@ -103,6 +104,16 @@ Accesses and manipulates ACS document nodes using their node IDs. - _nodeId:_ `string` - ID of the target node - _opts:_ `{ where?: string; includeSource?: boolean;} & NodesIncludeQuery & ContentPagingQuery` - additional options that API can take - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeAssociationPaging`](../../../lib/js-api/src/api/content-rest-api/docs/NodesApi.md#NodeAssociationPaging)`>` - List of node's parents. +- **checkoutNode**(nodeId: `string`, opts?: `NodesIncludeQuery`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>`
+ Checks out a file node for offline editing. Creates a private working copy and locks the original. + - _nodeId:_ `string` - ID of the file node to check out + - _opts:_ `NodesIncludeQuery` - (Optional) Additional query parameters (`include`, `fields`) + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>` - The working copy node +- **cancelCheckoutNode**(nodeId: `string`, opts?: `NodesIncludeQuery`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>`
+ Cancels a checkout. Accepts either the working copy or the original checked-out node. Deletes the working copy and unlocks the original. + - _nodeId:_ `string` - ID of the working copy or original checked-out node + - _opts:_ `NodesIncludeQuery` - (Optional) Additional query parameters (`include`, `fields`) + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodeEntry`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/content-rest-api/docs/NodeEntry.md)`>` - The original (unlocked) node ## Details @@ -160,6 +171,12 @@ and pages in the Alfresco JS API for further details and options. Note that you can also use the [Deleted Nodes Api service](deleted-nodes-api.service.md) get a list of all items currently in the trashcan. +### Checking out nodes + +Use `checkoutNode` to lock a file node for offline editing. This creates a private working copy and locks the original node so other users cannot modify it. On success, the Observable emits the working copy `NodeEntry`. + +Use `cancelCheckoutNode` to discard the working copy and unlock the original. You can pass either the working copy node ID or the original node ID. On success, the Observable emits the original (unlocked) `NodeEntry`. + ## See also - [Deleted nodes api service](deleted-nodes-api.service.md) diff --git a/docs/process-services-cloud/components/process-filters-cloud.component.md b/docs/process-services-cloud/components/process-filters-cloud.component.md index d960153d1d..31b3b4a6fe 100644 --- a/docs/process-services-cloud/components/process-filters-cloud.component.md +++ b/docs/process-services-cloud/components/process-filters-cloud.component.md @@ -27,6 +27,7 @@ Lists all available process filters and allows to select a filter. | appName | `string` | "" | (required) The application name | | filterParam | `UserTaskFilterRepresentation` | | (optional) The filter to be selected by default | | showIcons | `boolean` | false | (optional) Toggles showing an icon by the side of each filter | +| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. | ### Events diff --git a/docs/process-services-cloud/components/task-filters-cloud.component.md b/docs/process-services-cloud/components/task-filters-cloud.component.md index d7580e5dea..acc85e1a68 100644 --- a/docs/process-services-cloud/components/task-filters-cloud.component.md +++ b/docs/process-services-cloud/components/task-filters-cloud.component.md @@ -36,6 +36,7 @@ Shows all available filters. | appName | `string` | "" | Display filters available to the current user for the application with the specified name. | | filterParam | `FilterParamsModel` | | Parameters to use for the task filter cloud. If there is no match then the default filter (the first one in the list) is selected. | | showIcons | `boolean` | false | Toggles display of the filter's icons. | +| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. | ### Events diff --git a/lib/content-services/karma.conf.js b/lib/content-services/karma.conf.js index a37f38f5e1..6880782c7c 100644 --- a/lib/content-services/karma.conf.js +++ b/lib/content-services/karma.conf.js @@ -56,7 +56,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/content-services'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/content-services/package.json b/lib/content-services/package.json index abce4fea81..7e17e7c9ea 100644 --- a/lib/content-services/package.json +++ b/lib/content-services/package.json @@ -12,14 +12,14 @@ }, "peerDependencies": { "@angular/cdk": ">=20.2.14", - "@angular/common": ">=20.3.25", - "@angular/compiler": ">=20.3.25", - "@angular/core": ">=20.3.25", - "@angular/forms": ">=20.3.25", + "@angular/common": ">=20.3.27", + "@angular/compiler": ">=20.3.27", + "@angular/core": ">=20.3.27", + "@angular/forms": ">=20.3.27", "@angular/material": ">=20.2.14", - "@angular/platform-browser": ">=20.3.25", - "@angular/platform-browser-dynamic": ">=20.3.25", - "@angular/router": ">=20.3.25", + "@angular/platform-browser": ">=20.3.27", + "@angular/platform-browser-dynamic": ">=20.3.27", + "@angular/router": ">=20.3.27", "@alfresco/js-api": ">=10.0.0", "@ngx-translate/core": ">=17.0.0", "@alfresco/adf-core": ">=9.0.0" diff --git a/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts b/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts index 3be4e591ed..8da5a9e199 100644 --- a/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts +++ b/lib/content-services/src/lib/common/services/nodes-api.service.spec.ts @@ -18,11 +18,19 @@ import { TestBed } from '@angular/core/testing'; import { RedirectAuthService } from '@alfresco/adf-core'; import { EMPTY, firstValueFrom, of } from 'rxjs'; -import { JobIdBodyEntry, SizeDetails, SizeDetailsEntry } from '@alfresco/js-api'; +import { JobIdBodyEntry, NodeEntry, SizeDetails, SizeDetailsEntry } from '@alfresco/js-api'; import { NodesApiService } from './nodes-api.service'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AlfrescoApiServiceMock } from '../../mock/alfresco-api.service.mock'; +const fakeNodeEntry = { + entry: { + id: 'fake-node-id', + name: 'fake-file.txt', + nodeType: 'cm:content' + } +} as NodeEntry; + const fakeInitiateFolderSizeResponse: JobIdBodyEntry = { entry: { jobId: 'fake-job-id' @@ -75,4 +83,20 @@ describe('NodesApiService', () => { expect(nodesApiService.nodesApi.listParents).toHaveBeenCalledWith('fake-node-id', { include: ['path'], where: 'isPrimary=true' }); }); + + it('should call nodesApi.checkoutNode with the node ID and optional query params', async () => { + const opts = { include: ['path', 'allowableOperations'], fields: ['id', 'name'] }; + spyOn(nodesApiService.nodesApi, 'checkoutNode').and.returnValue(Promise.resolve(fakeNodeEntry)); + await firstValueFrom(nodesApiService.checkoutNode(fakeNodeEntry.entry.id, opts)); + + expect(nodesApiService.nodesApi.checkoutNode).toHaveBeenCalledWith(fakeNodeEntry.entry.id, opts); + }); + + it('should call nodesApi.cancelCheckoutNode with the node ID and optional query params', async () => { + const opts = { include: ['path'], fields: ['id'] }; + spyOn(nodesApiService.nodesApi, 'cancelCheckoutNode').and.returnValue(Promise.resolve(fakeNodeEntry)); + await firstValueFrom(nodesApiService.cancelCheckoutNode(fakeNodeEntry.entry.id, opts)); + + expect(nodesApiService.nodesApi.cancelCheckoutNode).toHaveBeenCalledWith(fakeNodeEntry.entry.id, opts); + }); }); diff --git a/lib/content-services/src/lib/common/services/nodes-api.service.ts b/lib/content-services/src/lib/common/services/nodes-api.service.ts index 475695f5bd..ba20a90f9f 100644 --- a/lib/content-services/src/lib/common/services/nodes-api.service.ts +++ b/lib/content-services/src/lib/common/services/nodes-api.service.ts @@ -293,6 +293,30 @@ export class NodesApiService { return from(this.nodesApi.listParents(nodeId, opts)); } + /** + * Checks out a file node for offline editing. + * Creates a private working copy and locks the original. + * + * @param nodeId ID of the node to check out + * @param opts Optional query parameters (`include`, `fields`) + * @returns Observable emitting the working copy node + */ + checkoutNode(nodeId: string, opts?: NodesIncludeQuery): Observable { + return from(this.nodesApi.checkoutNode(nodeId, opts)); + } + + /** + * Cancels a checkout. Accepts either the working copy or the original node. + * Deletes the working copy and unlocks the original. + * + * @param nodeId ID of the working copy or the original checked-out node + * @param opts Optional query parameters (`include`, `fields`) + * @returns Observable emitting the original (unlocked) node + */ + cancelCheckoutNode(nodeId: string, opts?: NodesIncludeQuery): Observable { + return from(this.nodesApi.cancelCheckoutNode(nodeId, opts)); + } + private randomNodeName(): string { return `node_${Date.now()}`; } diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html index 8167e8b14e..dd4ca6f210 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component.html @@ -1,6 +1,8 @@
- +

+ +

diff --git a/lib/core/src/lib/form/components/widgets/checkbox/checkbox.widget.ts b/lib/core/src/lib/form/components/widgets/checkbox/checkbox.widget.ts index 5c9eda7791..d1a340e48c 100644 --- a/lib/core/src/lib/form/components/widgets/checkbox/checkbox.widget.ts +++ b/lib/core/src/lib/form/components/widgets/checkbox/checkbox.widget.ts @@ -17,7 +17,7 @@ /* eslint-disable @angular-eslint/component-selector */ -import { NgClass, NgIf } from '@angular/common'; +import { NgClass } from '@angular/common'; import { Component, ViewEncapsulation } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -47,7 +47,7 @@ import { WidgetComponent } from '../widget.component'; '(invalid)': 'event($event)', '(select)': 'event($event)' }, - imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent, NgIf], + imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent], encapsulation: ViewEncapsulation.None }) export class CheckboxWidgetComponent extends WidgetComponent {} diff --git a/lib/core/src/lib/form/components/widgets/core/error-message.model.ts b/lib/core/src/lib/form/components/widgets/core/error-message.model.ts index 19a7b4f183..c55236257c 100644 --- a/lib/core/src/lib/form/components/widgets/core/error-message.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/error-message.model.ts @@ -17,24 +17,36 @@ export class ErrorMessageModel { message: string = ''; - attributes: Map = null; + attributes: Map = new Map(); constructor(obj?: any) { this.message = obj?.message || ''; - this.attributes = obj?.attributes || new Map(); + + if (obj?.attributes) { + this.attributes = obj.attributes; + } } isActive(): boolean { return !!this.message; } - getAttributesAsJsonObj() { - const result = {}; + getAttributesAsJsonObj(): Record { + const result: Record = {}; if (this.attributes.size > 0) { this.attributes.forEach((value, key) => { result[key] = typeof value === 'string' ? value : JSON.stringify(value); }); } + return result; } } + +export const getValidationSummaryTranslationParameters = (validationSummary?: ErrorMessageModel): Record => { + if (validationSummary?.isActive()) { + return validationSummary.getAttributesAsJsonObj(); + } + + return {}; +}; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts index 0e4c65e9b4..03bac3ae4b 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts @@ -17,9 +17,10 @@ import { DateFnsUtils } from '../../../../common'; import { FormRulesEvent } from '../../../events/form-rules.event'; -import { firstValueFrom, map, Subject, take, timeout } from 'rxjs'; +import { firstValueFrom, map, skip, Subject, take, timeout } from 'rxjs'; +import { ErrorMessageModel, getValidationSummaryTranslationParameters } from './error-message.model'; import { FormFieldTypes } from './form-field-types'; -import { RequiredFieldValidator } from './form-field-validator'; +import { MinValueFieldValidator, RequiredFieldValidator } from './form-field-validator'; import { FormFieldModel } from './form-field.model'; import { FormModel } from './form.model'; @@ -36,6 +37,58 @@ describe('FormFieldModel', () => { expect(model.json).toBe(json); }); + it('should return an isolated authored value snapshot', () => { + const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] }; + const model = new FormFieldModel(new FormModel(), { id: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue }); + + const snapshot = model.authoredValue as typeof authoredValue; + snapshot.blocks[0].data.text = 'changed'; + + expect((model.authoredValue as typeof authoredValue).blocks[0].data.text).toBe('${field.name}'); + expect(authoredValue.blocks[0].data.text).toBe('${field.name}'); + }); + + it('should not capture authored values for other field types', () => { + const model = new FormFieldModel(new FormModel(), { id: 'json', type: FormFieldTypes.JSON, value: { content: 'value' } }); + + expect(model.authoredValue).toBeUndefined(); + }); + + it('should return undefined for authored values that cannot be cloned', () => { + const circularValue: { self?: unknown } = {}; + circularValue.self = circularValue; + + const circularValueModel = new FormFieldModel(new FormModel(), { + id: 'circular', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: circularValue + }); + const bigintValueModel = new FormFieldModel(new FormModel(), { + id: 'bigint', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: BigInt(1) + }); + + expect(circularValueModel.authoredValue).toBeUndefined(); + expect(bigintValueModel.authoredValue).toBeUndefined(); + }); + + it('should preserve authored value when form data overrides the field value', () => { + const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] }; + const savedValue = { blocks: [{ data: { text: 'John' } }] }; + const form = new FormModel( + { + fields: [{ id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue }] + }, + { richText: savedValue } + ); + const model = form.getFieldById('richText'); + + expect(model.value).toEqual(savedValue); + expect(model.authoredValue).toEqual(authoredValue); + expect(model.authoredValue).not.toBe(authoredValue); + }); + it('should setup with json config', () => { const json = { fieldType: '', @@ -1222,6 +1275,57 @@ describe('FormFieldModel', () => { }); }); + describe('validation summary changes', () => { + const createField = (): FormFieldModel => { + const form = new FormModel(); + form.fieldValidators = [new MinValueFieldValidator()]; + + return new FormFieldModel(form, { + id: 'number-field', + type: FormFieldTypes.NUMBER, + value: 1, + minValue: '10' + }); + }; + + it('should replay an inactive validation summary before the first validation', async () => { + const field = new FormFieldModel(new FormModel()); + + const validationSummary = await firstValueFrom(field.validationSummaryChanges$); + + expect(validationSummary).toEqual(jasmine.any(ErrorMessageModel)); + expect(validationSummary.isActive()).toBe(false); + }); + + it('should replay the completed validation summary when subscribing after validation', async () => { + const field = createField(); + field.validate(); + + const validationSummary = await firstValueFrom(field.validationSummaryChanges$); + + expect(validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN'); + expect(validationSummary.attributes.get('minValue')).toBe('10'); + }); + + it('should emit completed summaries when validation state changes', async () => { + const field = createField(); + const invalidSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1))); + + field.validate(); + const invalidSummary = await invalidSummaryPromise; + + field.value = 10; + const validSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1))); + field.validate(); + const validSummary = await validSummaryPromise; + + expect(invalidSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN'); + expect(invalidSummary.attributes.get('minValue')).toBe('10'); + expect(validSummary.isActive()).toBe(false); + expect(validSummary.attributes.size).toBe(0); + }); + }); + it('should fail validation for readOnly required display-external-property field with null value', () => { const form = new FormModel(); const field = new FormFieldModel(form, { @@ -2056,3 +2160,35 @@ describe('FormFieldTypes', () => { }); }); }); + +describe('ErrorMessageModel', () => { + it('should initialize empty attributes when attributes are omitted', () => { + const errorMessage = new ErrorMessageModel(); + + expect(errorMessage.attributes).toEqual(new Map()); + }); + + it('should retain provided attributes', () => { + const attributes = new Map([['minValue', '10']]); + + const errorMessage = new ErrorMessageModel({ attributes }); + + expect(errorMessage.attributes).toBe(attributes); + }); +}); + +describe('getValidationSummaryTranslationParameters', () => { + it('should return validation attributes when the summary is active', () => { + const validationSummary = new ErrorMessageModel({ + message: 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN', + attributes: new Map([['minValue', '10']]) + }); + + expect(getValidationSummaryTranslationParameters(validationSummary)).toEqual({ minValue: '10' }); + }); + + it('should return empty parameters when the summary is inactive or omitted', () => { + expect(getValidationSummaryTranslationParameters(new ErrorMessageModel())).toEqual({}); + expect(getValidationSummaryTranslationParameters()).toEqual({}); + }); +}); diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index 6241cf8f84..884e23eb41 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -29,6 +29,7 @@ import { VariableConfig } from './form-field-variable-options'; import { DataColumn } from '../../../../datatable/data/data-column.model'; import { DateFnsUtils } from '../../../../common'; import { isValid as isValidDate } from 'date-fns'; +import { Observable, ReplaySubject } from 'rxjs'; import { ContainerRowModel } from './container-row.model'; import { RepeatableSectionModel, ROW_ID_PREFIX, TEMPLATE_ROW_ID } from './repeatable-section.model'; import { formFieldRuleHandler } from './handlers/form-field-rule.handler'; @@ -38,12 +39,35 @@ export type FieldOptionType = 'rest' | 'manual' | 'variable'; export type FieldSelectionType = 'single' | 'multiple'; export type FieldAlignmentType = 'vertical' | 'horizontal'; +interface ValidationSummaryChangesState { + subject: ReplaySubject; + observable: Observable; +} + +const validationSummaryChangesByField = new WeakMap(); + +const isJsonPrimitive = (value: unknown): value is null | string | number | boolean => + value === null || ['string', 'number', 'boolean'].includes(typeof value); + +const cloneJsonCompatibleValue = (value: unknown): unknown => { + if (value === undefined || isJsonPrimitive(value)) { + return value; + } + + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return undefined; + } +}; + // Maps to FormFieldRepresentation export class FormFieldModel extends FormWidgetModel { private _value: string; private _readOnly: boolean = false; private _isValid: boolean = true; private _required: boolean = false; + private readonly _authoredValue: unknown; readonly defaultDateFormat: string = 'D-M-YYYY'; readonly defaultDateTimeFormat: string = 'D-M-YYYY hh:mm A'; @@ -110,7 +134,21 @@ export class FormFieldModel extends FormWidgetModel { // util members emptyOption: FormFieldOption; - validationSummary: ErrorMessageModel; + validationSummary: ErrorMessageModel = new ErrorMessageModel(); + + get validationSummaryChanges$(): Observable { + const existingState = validationSummaryChangesByField.get(this); + if (existingState) { + return existingState.observable; + } + + const subject = new ReplaySubject(1); + const observable = subject.asObservable(); + validationSummaryChangesByField.set(this, { subject, observable }); + subject.next(this.validationSummary); + + return observable; + } get value(): any { return this._value; @@ -123,6 +161,10 @@ export class FormFieldModel extends FormWidgetModel { } } + get authoredValue(): unknown { + return cloneJsonCompatibleValue(this._authoredValue); + } + get readOnly(): boolean { if (this.form?.readOnly) { return true; @@ -173,16 +215,19 @@ export class FormFieldModel extends FormWidgetModel { for (const validator of validators) { if (!validator.validate(this)) { this._isValid = false; + validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary); return this._isValid; } } this._isValid = true; + validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary); return this._isValid; } constructor(form: any, json?: any, parent?: RepeatableSectionModel) { super(form, json); + this._authoredValue = json?.type === FormFieldTypes.DISPLAY_RICH_TEXT ? cloneJsonCompatibleValue(json.value) : undefined; if (json) { this.fieldType = json.fieldType; this.id = this.getId(json.id, parent); @@ -221,7 +266,6 @@ export class FormFieldModel extends FormWidgetModel { this.enableFractions = json.enableFractions; this.currency = json.currency; this.dateDisplayFormat = json.dateDisplayFormat || this.getDefaultDateFormat(json); - this.validationSummary = new ErrorMessageModel(); this.tooltip = json.tooltip || ''; this.selectionType = json.selectionType; this.alignmentType = json.alignmentType; diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html index d1c5a7b33b..1457221133 100644 --- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html +++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html @@ -10,6 +10,7 @@
@if( (field.name || field?.required) && !field.leftLabels) { @@ -38,11 +39,12 @@ [timeInterval]="5" [disabled]="field.readOnly" /> @if (datetimeInputControl.invalid && datetimeInputControl.touched && field.validationSummary?.message) { - + error_outline {{ field.validationSummary.message | translate:translateParameters }} } +
diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.html b/lib/core/src/lib/form/components/widgets/date/date.widget.html index f36c6ae24e..8633b8db24 100644 --- a/lib/core/src/lib/form/components/widgets/date/date.widget.html +++ b/lib/core/src/lib/form/components/widgets/date/date.widget.html @@ -1,7 +1,7 @@
- + @@ -22,11 +22,12 @@ [startAt]="startAt" [disabled]="field.readOnly" /> @if (dateInputControl.invalid && dateInputControl.touched) { - + error_outline @if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html index e2c5be4965..c531e78288 100644 --- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html @@ -9,7 +9,7 @@
- + @if ( (field.name || field?.required) && !field.leftLabels) { {{ field.name | translate }} } @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts index d4ab7fab85..a1fbbab018 100644 --- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.spec.ts @@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing'; import { FormService } from '../../../services/form.service'; import { FormFieldModel, FormFieldTypes, FormModel } from '../core'; import { DecimalWidgetComponent } from './decimal.component'; +import { TranslateService } from '@ngx-translate/core'; describe('DecimalComponent', () => { + const validatorTranslations = { + FORM: { + FIELD: { + VALIDATOR: { + NOT_LESS_THAN: "Can't be less than {{ minValue }}", + NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}", + INVALID_DECIMAL_PRECISION: 'Precision {{ precision }}' + } + } + } + }; let loader: HarnessLoader; let widget: DecimalWidgetComponent; let fixture: ComponentFixture; @@ -107,6 +119,62 @@ describe('DecimalComponent', () => { }); }); + describe('when validation runs without widget interaction', () => { + let field: FormFieldModel; + + beforeEach(() => { + const translateService = TestBed.inject(TranslateService); + translateService.use('en').subscribe(); + translateService.setTranslation('en', validatorTranslations); + field = new FormFieldModel(new FormModel({ taskId: '' }), { + id: 'decimal-id', + type: FormFieldTypes.DECIMAL, + value: 1, + minValue: 10 + }); + field.validate(); + field.form.showAllValidationErrors = true; + fixture.componentRef.setInput('field', field); + fixture.detectChanges(); + }); + + it('should render the minimum value in the message when initial validation fails', async () => { + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain("Can't be less than 10"); + }); + + it('should render the updated maximum value when programmatic revalidation fails', async () => { + field.value = 10; + field.minValue = '1'; + field.maxValue = '5'; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain("Can't be greater than 5"); + }); + + it('should render decimal precision when programmatic revalidation fails', async () => { + field.value = 1.234; + field.minValue = '1'; + field.precision = 2; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Precision 2'); + }); + }); + describe('when form model has left labels', () => { it('should have left labels classes on leftLabels true', async () => { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), { diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts index a88390caf7..a25bbb1b36 100644 --- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts +++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts @@ -16,13 +16,15 @@ */ import { NgIf } from '@angular/common'; -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; import { ErrorStateMatcher } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { TranslatePipe } from '@ngx-translate/core'; +import { getValidationSummaryTranslationParameters } from '../core/error-message.model'; import { WidgetComponent } from '../widget.component'; @Component({ @@ -46,19 +48,21 @@ import { WidgetComponent } from '../widget.component'; export class DecimalWidgetComponent extends WidgetComponent implements OnInit { errorStateMatcher: ErrorStateMatcher; translateParameters: Record = {}; + private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { this.initErrorStateMatcher(); + this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => { + this.translateParameters = getValidationSummaryTranslationParameters(validationSummary); + }); } onBlur(): void { this.markAsTouched(); - this.updateTranslateParameters(); } onDecimalFieldChanged(): void { this.onFieldChanged(this.field); - this.updateTranslateParameters(); } private initErrorStateMatcher(): void { @@ -67,12 +71,4 @@ export class DecimalWidgetComponent extends WidgetComponent implements OnInit { !this.field.isValid && this.isTouched() }; } - - private updateTranslateParameters(): void { - if (this.field.validationSummary?.isActive()) { - this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj(); - } else { - this.translateParameters = {}; - } - } } diff --git a/lib/core/src/lib/form/components/widgets/error/error.component.scss b/lib/core/src/lib/form/components/widgets/error/error.component.scss index 026768b39d..2b57daf87a 100644 --- a/lib/core/src/lib/form/components/widgets/error/error.component.scss +++ b/lib/core/src/lib/form/components/widgets/error/error.component.scss @@ -10,12 +10,16 @@ } } +error-widget { + display: block; +} + .adf-error { display: flex; align-items: center; &-widget-container { - height: auto; + height: 40px; } &-animate { diff --git a/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss b/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss index a5f2b96287..4dedad8795 100644 --- a/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss +++ b/lib/core/src/lib/form/components/widgets/hyperlink/hyperlink.widget.scss @@ -1,6 +1,6 @@ .adf-hyperlink-widget { padding: 0.4375em 0; - border-top: 0.8438em solid transparent; + margin-bottom: 40px; a { color: var(--mat-sys-primary); diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html index cea1673327..73d7d5036a 100644 --- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html +++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html @@ -7,6 +7,7 @@ @@ -31,14 +32,16 @@ > @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + @if (field.maxLength > 0) {{{ field?.value?.length || 0 }}/{{ field.maxLength }}} error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } @else if (field.maxLength > 0) { - {{ field?.value?.length || 0 }}/{{ field.maxLength }} + {{ field?.value?.length || 0 }}/{{ field.maxLength }} + } @else { + diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss index 435d152e6e..242d172457 100644 --- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss +++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss @@ -43,6 +43,6 @@ @include mixins.adf-error-icon; } -.adf-container-widget .adf-multiline-text-widget .adf-form-field-input.adf-has-counter { - margin-bottom: 44px; +.adf-container-widget .adf-multiline-text-widget mat-form-field.adf-form-field-input.adf-has-counter { + margin-bottom: 20px; } diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts index b2398bf024..96ed448e98 100644 --- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts +++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts @@ -25,8 +25,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UnitTestingUtils } from '../../../../testing/unit-testing-utils'; import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token'; import { of, Subject } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; describe('MultilineTextWidgetComponentComponent', () => { + const validatorTranslations = { + FORM: { + FIELD: { + VALIDATOR: { + AT_LEAST_LONG: 'Minimum {{ minLength }}', + NO_LONGER_THAN: 'Maximum {{ maxLength }}' + } + } + } + }; let loader: HarnessLoader; let widget: MultilineTextWidgetComponentComponent; let fixture: ComponentFixture; @@ -109,6 +120,48 @@ describe('MultilineTextWidgetComponentComponent', () => { }); }); + describe('when validation runs without widget interaction', () => { + let field: FormFieldModel; + + beforeEach(() => { + const translateService = TestBed.inject(TranslateService); + translateService.use('en').subscribe(); + translateService.setTranslation('en', validatorTranslations); + field = new FormFieldModel(new FormModel({ taskId: '' }), { + id: 'multiline-text-id', + type: FormFieldTypes.MULTILINE_TEXT, + value: 'text', + minLength: 10 + }); + field.validate(); + field.form.showAllValidationErrors = true; + fixture.componentRef.setInput('field', field); + fixture.detectChanges(); + }); + + it('should render the minimum length in the message when initial validation fails', async () => { + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Minimum 10'); + }); + + it('should render the updated maximum length when programmatic revalidation fails', async () => { + field.value = 'too long'; + field.minLength = 1; + field.maxLength = 5; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Maximum 5'); + }); + }); + describe('when is required', () => { beforeEach(() => { widget.field = new FormFieldModel(new FormModel({ taskId: '' }), { diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts index ecfad39ae3..53fd4c37aa 100644 --- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts +++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts @@ -27,6 +27,7 @@ import { MatInputModule } from '@angular/material/input'; import { TranslatePipe } from '@ngx-translate/core'; import { isObservable } from 'rxjs'; import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token'; +import { getValidationSummaryTranslationParameters } from '../core/error-message.model'; import { WidgetComponent } from '../widget.component'; @Component({ @@ -56,6 +57,9 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple ngOnInit(): void { this.initErrorStateMatcher(); + this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => { + this.translateParameters = getValidationSummaryTranslationParameters(validationSummary); + }); if (this.enableCustomMessage != null) { if (isObservable(this.enableCustomMessage)) { this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => { @@ -73,12 +77,10 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple onBlur(): void { this.markAsTouched(); - this.updateTranslateParameters(); } onMultilineTextFieldChanged(): void { this.onFieldChanged(this.field); - this.updateTranslateParameters(); } private initErrorStateMatcher(): void { @@ -87,12 +89,4 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple !this.field.isValid && this.isTouched() }; } - - private updateTranslateParameters(): void { - if (this.field.validationSummary?.isActive()) { - this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj(); - } else { - this.translateParameters = {}; - } - } } diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.html b/lib/core/src/lib/form/components/widgets/number/number.widget.html index 11999ea2b1..344d5c9abd 100644 --- a/lib/core/src/lib/form/components/widgets/number/number.widget.html +++ b/lib/core/src/lib/form/components/widgets/number/number.widget.html @@ -9,7 +9,7 @@
- + @if( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} @@ -30,12 +30,13 @@ [errorStateMatcher]="errorStateMatcher" (blur)="onBlur()"> @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.spec.ts b/lib/core/src/lib/form/components/widgets/number/number.widget.spec.ts index 3ef03a164e..e853c0c43b 100644 --- a/lib/core/src/lib/form/components/widgets/number/number.widget.spec.ts +++ b/lib/core/src/lib/form/components/widgets/number/number.widget.spec.ts @@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing'; import { FormFieldModel, FormFieldTypes, FormModel } from '../core'; import { NumberWidgetComponent } from './number.widget'; import { DecimalNumberPipe } from '../../../../pipes'; +import { TranslateService } from '@ngx-translate/core'; describe('NumberWidgetComponent', () => { + const validatorTranslations = { + FORM: { + FIELD: { + VALIDATOR: { + NOT_LESS_THAN: "Can't be less than {{ minValue }}", + NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}", + NO_LONGER_THAN: 'Maximum length {{ maxLength }}' + } + } + } + }; let loader: HarnessLoader; let widget: NumberWidgetComponent; let fixture: ComponentFixture; @@ -175,6 +187,60 @@ describe('NumberWidgetComponent', () => { }); }); + describe('when validation runs without widget interaction', () => { + let field: FormFieldModel; + + beforeEach(() => { + const translateService = TestBed.inject(TranslateService); + translateService.use('en').subscribe(); + translateService.setTranslation('en', validatorTranslations); + field = new FormFieldModel(new FormModel({ taskId: '' }), { + id: 'number-id', + type: FormFieldTypes.NUMBER, + value: 1, + minValue: 10 + }); + field.validate(); + fixture.componentRef.setInput('field', field); + fixture.detectChanges(); + }); + + it('should render the minimum value in the message when initial validation fails', async () => { + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain("Can't be less than 10"); + }); + + it('should render the updated maximum value when programmatic revalidation fails', async () => { + field.value = 10; + field.minValue = '1'; + field.maxValue = '5'; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain("Can't be greater than 5"); + }); + + it('should render the maximum length when programmatic revalidation fails', async () => { + field.value = 12345678901; + field.minValue = '1'; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Maximum length 10'); + }); + }); + describe('when form model has left labels', () => { it('should have left labels classes on leftLabels true', async () => { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), { diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.ts b/lib/core/src/lib/form/components/widgets/number/number.widget.ts index 113219a0ef..e7f378ca1d 100644 --- a/lib/core/src/lib/form/components/widgets/number/number.widget.ts +++ b/lib/core/src/lib/form/components/widgets/number/number.widget.ts @@ -18,7 +18,8 @@ /* eslint-disable @angular-eslint/component-selector */ import { NgIf } from '@angular/common'; -import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; import { ErrorStateMatcher } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -26,6 +27,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { TranslatePipe } from '@ngx-translate/core'; import { DecimalNumberPipe } from '../../../../pipes'; +import { getValidationSummaryTranslationParameters } from '../core/error-message.model'; import { WidgetComponent } from '../widget.component'; @Component({ @@ -53,6 +55,7 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit { translateParameters: Record = {}; private readonly decimalNumberPipe = inject(DecimalNumberPipe); + private readonly destroyRef = inject(DestroyRef); ngOnInit() { if (this.field.readOnly) { @@ -61,11 +64,13 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit { this.displayValue = this.field.value; } this.initErrorStateMatcher(); + this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => { + this.translateParameters = getValidationSummaryTranslationParameters(validationSummary); + }); } onBlur(): void { this.markAsTouched(); - this.updateTranslateParameters(); } protected onNumberChange(value: string) { @@ -74,7 +79,6 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit { } this.onFieldChanged(this.field); - this.updateTranslateParameters(); } private initErrorStateMatcher(): void { @@ -83,12 +87,4 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit { !!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched()) }; } - - private updateTranslateParameters(): void { - if (this.field.validationSummary?.isActive()) { - this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj(); - } else { - this.translateParameters = {}; - } - } } diff --git a/lib/core/src/lib/form/components/widgets/repeat/repeat.widget.scss b/lib/core/src/lib/form/components/widgets/repeat/repeat.widget.scss index 33e09becd6..843afbe1cd 100644 --- a/lib/core/src/lib/form/components/widgets/repeat/repeat.widget.scss +++ b/lib/core/src/lib/form/components/widgets/repeat/repeat.widget.scss @@ -9,7 +9,8 @@ } &-row-action { - margin-left: 10px; + margin-inline-start: 10px; + margin-block-end: 35px; } &-row-limit { diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.html b/lib/core/src/lib/form/components/widgets/text/text.widget.html index e1fc6d47e0..e9dee6e3b9 100644 --- a/lib/core/src/lib/form/components/widgets/text/text.widget.html +++ b/lib/core/src/lib/form/components/widgets/text/text.widget.html @@ -8,7 +8,11 @@
- + @if ( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} @@ -30,7 +34,7 @@ (paste)="onPaste($event)" (blur)="onBlur()"> @if (!fieldStatusTemplate && (maxLengthPasteError.isActive() || field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) { - + @if (maxLengthPasteError.isActive()) { @@ -43,6 +47,9 @@ } + @if (!fieldStatusTemplate) { + diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts b/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts index 48e576800a..816ad455a7 100644 --- a/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts +++ b/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts @@ -27,9 +27,20 @@ import { UnitTestingUtils } from '../../../../testing/unit-testing-utils'; import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token'; import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token'; import { of, Subject } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; describe('TextWidgetComponent', () => { const form = new FormModel({ taskId: 'fake-task-id' }); + const validatorTranslations = { + FORM: { + FIELD: { + VALIDATOR: { + AT_LEAST_LONG: 'Minimum {{ minLength }}', + NO_LONGER_THAN: 'Maximum {{ maxLength }}' + } + } + } + }; let loader: HarnessLoader; let widget: TextWidgetComponent; @@ -64,6 +75,47 @@ describe('TextWidgetComponent', () => { }); }); + describe('when validation runs without widget interaction', () => { + let field: FormFieldModel; + + beforeEach(() => { + const translateService = TestBed.inject(TranslateService); + translateService.use('en').subscribe(); + translateService.setTranslation('en', validatorTranslations); + field = new FormFieldModel(form, { + id: 'text-id', + type: FormFieldTypes.TEXT, + value: 'text', + minLength: 10 + }); + field.validate(); + fixture.componentRef.setInput('field', field); + fixture.detectChanges(); + }); + + it('should render the minimum length in the message when initial validation fails', async () => { + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Minimum 10'); + }); + + it('should render the updated maximum length when programmatic revalidation fails', async () => { + field.value = 'too long'; + field.minLength = 1; + field.maxLength = 5; + field.validate(); + fixture.detectChanges(); + + const formField = await testingUtils.formField.get(); + const errors = await formField.getTextErrors(); + + expect(errors.length).toBe(1); + expect(errors[0]).toContain('Maximum 5'); + }); + }); + describe('when template is ready', () => { describe('and no mask is configured on text element', () => { it('should raise ngModelChange event', async () => { diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.ts b/lib/core/src/lib/form/components/widgets/text/text.widget.ts index 665b315fd7..161afcaaef 100644 --- a/lib/core/src/lib/form/components/widgets/text/text.widget.ts +++ b/lib/core/src/lib/form/components/widgets/text/text.widget.ts @@ -19,6 +19,7 @@ import { NgIf, NgTemplateOutlet } from '@angular/common'; import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; import { ErrorStateMatcher } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -26,7 +27,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { TranslatePipe } from '@ngx-translate/core'; import { WidgetComponent } from '../widget.component'; -import { ErrorMessageModel } from '../core/error-message.model'; +import { ErrorMessageModel, getValidationSummaryTranslationParameters } from '../core/error-message.model'; import { FormattableTextWidgetComponent } from '../core/formattable-text.widget'; import { DEFAULT_TEXT_MAX_LENGTH } from '../core/form-field-validator'; import { InputMaskDirective } from './text-mask.component'; @@ -97,6 +98,9 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent { this.isMaskReversed = this.field.params['inputMaskReversed'] ? this.field.params['inputMaskReversed'] : false; } this.initErrorStateMatcher(); + this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => { + this.translateParameters = getValidationSummaryTranslationParameters(validationSummary); + }); } onPaste(event: ClipboardEvent): void { @@ -125,12 +129,10 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent { onBlur(): void { this.markAsTouched(); - this.updateTranslateParameters(); } onTextFieldChanged(): void { this.onFieldChanged(this.field); - this.updateTranslateParameters(); } private initErrorStateMatcher(): void { @@ -143,14 +145,6 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent { }; } - private updateTranslateParameters(): void { - if (this.field.validationSummary?.isActive()) { - this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj(); - } else { - this.translateParameters = {}; - } - } - private getLengthAfterPaste(input: HTMLInputElement, pastedValue: string): number { const value = input.value ?? ''; const selectionStart = input.selectionStart ?? value.length; diff --git a/lib/core/src/lib/form/services/form-validation-service.interface.ts b/lib/core/src/lib/form/services/form-validation-service.interface.ts index 08b78bd6b5..7a5c5d6e7d 100644 --- a/lib/core/src/lib/form/services/form-validation-service.interface.ts +++ b/lib/core/src/lib/form/services/form-validation-service.interface.ts @@ -16,6 +16,7 @@ */ import { Subject } from 'rxjs'; +import { FormEvent } from '../events/form.event'; import { FormFieldEvent } from '../events/form-field.event'; import { FormRulesEvent } from '../events/form-rules.event'; import { ValidateFormFieldEvent } from '../events/validate-form-field.event'; @@ -26,4 +27,5 @@ export interface FormValidationService { validateForm: Subject; validateFormField: Subject; formRulesEvent?: Subject; + formVisibilityRefreshed?: Subject; } diff --git a/lib/core/src/lib/form/services/form.service.ts b/lib/core/src/lib/form/services/form.service.ts index be9091917a..131430c972 100644 --- a/lib/core/src/lib/form/services/form.service.ts +++ b/lib/core/src/lib/form/services/form.service.ts @@ -62,6 +62,12 @@ export class FormService implements FormValidationService { formRulesEvent = new Subject(); + /** + * Emitted after form field/outcome visibility has been re-evaluated via WidgetVisibilityService.refreshVisibility. + * Internal ADF form-rendering event — not part of the FormValidationService contract. + */ + formVisibilityRefreshed = new Subject(); + constructor() { const injectedFieldValidators = inject(FORM_SERVICE_FIELD_VALIDATORS_TOKEN, { optional: true }); diff --git a/lib/core/src/lib/form/services/widget-visibility.service.spec.ts b/lib/core/src/lib/form/services/widget-visibility.service.spec.ts index 796a01afb9..a75f9fa768 100644 --- a/lib/core/src/lib/form/services/widget-visibility.service.spec.ts +++ b/lib/core/src/lib/form/services/widget-visibility.service.spec.ts @@ -19,6 +19,7 @@ import { TestBed } from '@angular/core/testing'; import { ContainerModel, FormFieldModel, FormFieldTypes, FormModel, TabModel } from '../components/widgets/core'; import { WidgetVisibilityModel } from '../models/widget-visibility.model'; import { WidgetVisibilityService } from './widget-visibility.service'; +import { FormService } from './form.service'; import { fakeFormJson, formTest, @@ -50,6 +51,30 @@ describe('WidgetVisibilityService', () => { service = TestBed.inject(WidgetVisibilityService); }); + it('should emit formVisibilityRefreshed when visibility is refreshed', () => { + const formService = TestBed.inject(FormService); + let emittedForm: FormModel | undefined; + + formService.formVisibilityRefreshed.subscribe((event) => { + emittedForm = event.form; + }); + + service.refreshVisibility(stubFormWithFields); + + expect(emittedForm).toBe(stubFormWithFields); + }); + + it('should not emit formVisibilityRefreshed when form is null', () => { + const formService = TestBed.inject(FormService); + let emitCount = 0; + + formService.formVisibilityRefreshed.subscribe(() => emitCount++); + + service.refreshVisibility(null); + + expect(emitCount).toBe(0); + }); + describe('should be able to evaluate next condition operations', () => { it('using == and return true', () => { const resultsArray = evaluateConditions( diff --git a/lib/core/src/lib/form/services/widget-visibility.service.ts b/lib/core/src/lib/form/services/widget-visibility.service.ts index 3e717afa2f..a7d1ea039d 100644 --- a/lib/core/src/lib/form/services/widget-visibility.service.ts +++ b/lib/core/src/lib/form/services/widget-visibility.service.ts @@ -15,16 +15,20 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { FormFieldModel, FormModel, TabModel, ContainerModel, FormOutcomeModel } from '../components/widgets/core'; +import { FormEvent } from '../events/form.event'; import { TaskProcessVariableModel } from '../models/task-process-variable.model'; import { WidgetVisibilityModel, WidgetTypeEnum } from '../models/widget-visibility.model'; import { format, isValid, parse } from 'date-fns'; +import { FormService } from './form.service'; @Injectable({ providedIn: 'root' }) export class WidgetVisibilityService { + private readonly formService = inject(FormService); + private processVarList: TaskProcessVariableModel[]; private form: FormModel; @@ -45,6 +49,8 @@ export class WidgetVisibilityService { } form.getFormFields().map((field) => this.refreshEntityVisibility(field)); + + this.formService.formVisibilityRefreshed.next(new FormEvent(form)); } } diff --git a/lib/extensions/karma.conf.js b/lib/extensions/karma.conf.js index 4bdd4cd6e4..99b34c1729 100644 --- a/lib/extensions/karma.conf.js +++ b/lib/extensions/karma.conf.js @@ -23,7 +23,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/extensions'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/extensions/package.json b/lib/extensions/package.json index 9f30b6074f..804d7593dc 100644 --- a/lib/extensions/package.json +++ b/lib/extensions/package.json @@ -12,8 +12,8 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "peerDependencies": { - "@angular/common": ">=20.3.25", - "@angular/core": ">=20.3.25", + "@angular/common": ">=20.3.27", + "@angular/core": ">=20.3.27", "@alfresco/js-api": ">=10.0.0" }, "keywords": [ diff --git a/lib/insights/karma.conf.js b/lib/insights/karma.conf.js index 059cf7eb8a..8cf50ccf31 100644 --- a/lib/insights/karma.conf.js +++ b/lib/insights/karma.conf.js @@ -44,7 +44,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/insights'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/insights/package.json b/lib/insights/package.json index 6f4f422df3..bf0225cff7 100644 --- a/lib/insights/package.json +++ b/lib/insights/package.json @@ -16,10 +16,10 @@ "raphael": ">=2.3.0" }, "peerDependencies": { - "@angular/common": ">=20.3.25", - "@angular/compiler": ">=20.3.25", - "@angular/core": ">=20.3.25", - "@angular/forms": ">=20.3.25", + "@angular/common": ">=20.3.27", + "@angular/compiler": ">=20.3.27", + "@angular/core": ">=20.3.27", + "@angular/forms": ">=20.3.27", "@angular/material": ">=20.2.14", "@alfresco/adf-core": ">=9.0.0", "@alfresco/adf-content-services": ">=9.0.0", diff --git a/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts b/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts new file mode 100644 index 0000000000..e56e4854bc --- /dev/null +++ b/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts @@ -0,0 +1,148 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Chart } from './chart.model'; + +describe('Chart Model', () => { + describe('constructor', () => { + it('should create with default values when no argument is provided', () => { + const chart = new Chart(); + expect(chart.labels).toEqual([]); + expect(chart.data).toEqual([]); + expect(chart.datasets).toEqual([]); + expect(chart.showDetails).toBe(false); + }); + + it('should populate properties from input object', () => { + const chart = new Chart({ + id: '1', + title: 'Test Chart', + titleKey: 'KEY', + labels: ['a', 'b'], + data: [1, 2], + datasets: [{ data: [1] }], + showDetails: true, + detailsTable: { key: 'value' }, + options: { responsive: true } + }); + + expect(chart.id).toBe('1'); + expect(chart.title).toBe('Test Chart'); + expect(chart.titleKey).toBe('KEY'); + expect(chart.labels).toEqual(['a', 'b']); + expect(chart.data).toEqual([1, 2]); + expect(chart.datasets).toEqual([{ data: [1] }]); + expect(chart.showDetails).toBe(true); + expect(chart.detailsTable).toEqual({ key: 'value' }); + expect(chart.options).toEqual({ responsive: true }); + }); + + it('should convert type and set icon for pieChart', () => { + const chart = new Chart({ type: 'pieChart' }); + expect(chart.type).toBe('pie'); + expect(chart.icon).toBe('pie_chart'); + }); + + it('should convert type and set icon for barChart', () => { + const chart = new Chart({ type: 'barChart' }); + expect(chart.type).toBe('bar'); + expect(chart.icon).toBe('equalizer'); + }); + + it('should convert type and set icon for line', () => { + const chart = new Chart({ type: 'line' }); + expect(chart.type).toBe('line'); + expect(chart.icon).toBe('show_chart'); + }); + + it('should convert type and set icon for table', () => { + const chart = new Chart({ type: 'table' }); + expect(chart.type).toBe('table'); + expect(chart.icon).toBe('web'); + }); + + it('should convert type and set icon for multiBarChart', () => { + const chart = new Chart({ type: 'multiBarChart' }); + expect(chart.type).toBe('multiBar'); + expect(chart.icon).toBe('poll'); + }); + + it('should convert type and set icon for processDefinitionHeatMap', () => { + const chart = new Chart({ type: 'processDefinitionHeatMap' }); + expect(chart.type).toBe('HeatMap'); + expect(chart.icon).toBe('share'); + }); + + it('should convert type and set icon for masterDetailTable', () => { + const chart = new Chart({ type: 'masterDetailTable' }); + expect(chart.type).toBe('masterDetailTable'); + expect(chart.icon).toBe('subtitles'); + }); + + it('should default to table type for unknown types', () => { + const chart = new Chart({ type: 'unknown' }); + expect(chart.type).toBe('table'); + expect(chart.icon).toBe('web'); + }); + }); + + describe('hasData', () => { + it('should return true when data is not empty', () => { + const chart = new Chart({ data: [1, 2, 3] }); + expect(chart.hasData()).toBe(true); + }); + + it('should return false when data is empty', () => { + const chart = new Chart({ data: [] }); + expect(chart.hasData()).toBe(false); + }); + + it('should return false when no data is provided', () => { + const chart = new Chart(); + expect(chart.hasData()).toBe(false); + }); + }); + + describe('hasDatasets', () => { + it('should return true when datasets is not empty', () => { + const chart = new Chart({ datasets: [{ data: [1] }] }); + expect(chart.hasDatasets()).toBe(true); + }); + + it('should return false when datasets is empty', () => { + const chart = new Chart({ datasets: [] }); + expect(chart.hasDatasets()).toBe(false); + }); + }); + + describe('hasZeroValues', () => { + it('should return true when all data values are zero', () => { + const chart = new Chart({ data: [0, 0, 0] }); + expect(chart.hasZeroValues()).toBe(true); + }); + + it('should return false when at least one value is non-zero', () => { + const chart = new Chart({ data: [0, 1, 0] }); + expect(chart.hasZeroValues()).toBe(false); + }); + + it('should return false when data is empty', () => { + const chart = new Chart({ data: [] }); + expect(chart.hasZeroValues()).toBe(false); + }); + }); +}); diff --git a/lib/js-api/src/api/content-rest-api/api/nodes.api.ts b/lib/js-api/src/api/content-rest-api/api/nodes.api.ts index e9882cfa21..c9f740c6be 100644 --- a/lib/js-api/src/api/content-rest-api/api/nodes.api.ts +++ b/lib/js-api/src/api/content-rest-api/api/nodes.api.ts @@ -1033,4 +1033,63 @@ export class NodesApi extends BaseApi { returnType: SizeDetailsEntry }); } + + /** + * Checkout a node + * + * Checks out a file node for offline editing. Creates a private working copy and locks the original. + * + * @param nodeId The identifier of a file node to check out. + * @param opts Optional parameters + * @returns Promise - the working copy node + */ + checkoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise { + throwIfNotDefined(nodeId, 'nodeId'); + + const pathParams = { + nodeId + }; + + const queryParams = { + include: buildCollectionParam(opts?.include, 'csv'), + fields: buildCollectionParam(opts?.fields, 'csv') + }; + + return this.post({ + path: '/nodes/{nodeId}/checkout', + pathParams, + queryParams, + returnType: NodeEntry + }); + } + + /** + * Cancel checkout of a node + * + * Cancels a checkout. Accepts either the working copy or the original checked-out node. + * Deletes the working copy, unlocks the original, and returns the original node. + * + * @param nodeId The identifier of the working copy or the original checked-out node. + * @param opts Optional parameters + * @returns Promise - the original (unlocked) node + */ + cancelCheckoutNode(nodeId: string, opts?: NodesIncludeQuery): Promise { + throwIfNotDefined(nodeId, 'nodeId'); + + const pathParams = { + nodeId + }; + + const queryParams = { + include: buildCollectionParam(opts?.include, 'csv'), + fields: buildCollectionParam(opts?.fields, 'csv') + }; + + return this.post({ + path: '/nodes/{nodeId}/cancel-checkout', + pathParams, + queryParams, + returnType: NodeEntry + }); + } } diff --git a/lib/js-api/src/api/content-rest-api/docs/NodesApi.md b/lib/js-api/src/api/content-rest-api/docs/NodesApi.md index a2be171308..11ca9b0111 100644 --- a/lib/js-api/src/api/content-rest-api/docs/NodesApi.md +++ b/lib/js-api/src/api/content-rest-api/docs/NodesApi.md @@ -25,8 +25,10 @@ All URIs are relative to *https://localhost/alfresco/api/-default-/public/alfres | [unlockNode](#unlockNode) | **POST** /nodes/{nodeId}/unlock | Unlock a node | | [updateNode](#updateNode) | **PUT** /nodes/{nodeId} | Update a node | | [updateNodeContent](#updateNodeContent) | **PUT** /nodes/{nodeId}/content | Update node content | -| [initiateFolderSizeCalculation](#initiateFolderSizeCalculation) | **POST** /nodes/{nodeId}/size-details | Initiate a new request to calculate folder size | +| [initiateFolderSizeCalculation](#initiateFolderSizeCalculation) | **POST** /nodes/{nodeId}/size-details | Initiate a new request to calculate folder size | | [getFolderSizeInfo](#getFolderSizeInfo) | **GET** /nodes/{nodeId}/size-details/{jobId} | Gets the details of a folder | +| [cancelCheckoutNode](#cancelCheckoutNode) | **POST** /nodes/{nodeId}/cancel-checkout | Cancel checkout of a node | +| [checkoutNode](#checkoutNode) | **POST** /nodes/{nodeId}/checkout | Checkout a node | ## copyNode @@ -1260,6 +1262,66 @@ nodesApi.getFolderSizeInfo(``, ``).then((data) => { }); ``` +## checkoutNode + +Checkout a node + +Checks out a file node for offline editing. Creates a private working copy and locks the original. + +**Parameters** + +| Name | Type | Description | +|--------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **nodeId** | string | The identifier of a file node to check out. | +| opts.include | string[] | Returns additional information about the node. The following optional fields can be requested: `allowableOperations`, `association`, `isLink`, `isFavorite`, `isLocked`, `path`, `permissions`, `definition` | +| opts.fields | string[] | A list of field names. You can use this parameter to restrict the fields returned within a response if, for example, you want to save on overall bandwidth. The list applies to a returned individual entity or entries within a collection. If the API method also supports the **include** parameter, then the fields specified in the **include** parameter are returned in addition to those specified in the **fields** parameter. | + +**Return type**: [NodeEntry](NodeEntry.md) + +**Example** + +```javascript +import { AlfrescoApi, NodesApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const nodesApi = new NodesApi(alfrescoApi); +const opts = {}; + +nodesApi.checkoutNode(``, opts).then((data) => { + console.log('API called successfully. Returned data: ' + data); +}); +``` + +## cancelCheckoutNode + +Cancel checkout of a node + +Cancels a checkout. Accepts either the working copy or the original checked-out node. Deletes the working copy, unlocks the original, and returns the original node. + +**Parameters** + +| Name | Type | Description | +|--------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **nodeId** | string | The identifier of the working copy or original checked-out node. | +| opts.include | string[] | Returns additional information about the node. The following optional fields can be requested: `allowableOperations`, `association`, `isLink`, `isFavorite`, `isLocked`, `path`, `permissions`, `definition` | +| opts.fields | string[] | A list of field names. You can use this parameter to restrict the fields returned within a response if, for example, you want to save on overall bandwidth. The list applies to a returned individual entity or entries within a collection. If the API method also supports the **include** parameter, then the fields specified in the **include** parameter are returned in addition to those specified in the **fields** parameter. | + +**Return type**: [NodeEntry](NodeEntry.md) + +**Example** + +```javascript +import { AlfrescoApi, NodesApi } from '@alfresco/js-api'; + +const alfrescoApi = new AlfrescoApi(/*..*/); +const nodesApi = new NodesApi(alfrescoApi); +const opts = {}; + +nodesApi.cancelCheckoutNode(``, opts).then((data) => { + console.log('API called successfully. Returned data: ' + data); +}); +``` + # Models ## NodeBodyUpdate diff --git a/lib/js-api/test/content-services/nodeApi.spec.ts b/lib/js-api/test/content-services/nodeApi.spec.ts index 4b5f6acc65..ec9bde13cb 100644 --- a/lib/js-api/test/content-services/nodeApi.spec.ts +++ b/lib/js-api/test/content-services/nodeApi.spec.ts @@ -141,6 +141,30 @@ describe('Node', () => { }); }); + describe('Checkout', () => { + it('should POST to the checkout endpoint', async () => { + nodeMock.post200CheckoutNode('fake-node-id'); + const result = await nodesApi.checkoutNode('fake-node-id'); + assert.ok(result.entry, 'cancelCheckoutNode should return a NodeEntry'); + }); + + it('should throw if nodeId is not defined', () => { + assert.throws(() => nodesApi.checkoutNode(undefined)); + }); + }); + + describe('Cancel Checkout', () => { + it('should POST to the cancel-checkout endpoint', async () => { + nodeMock.post200CancelCheckoutNode('fake-node-id'); + const result = await nodesApi.cancelCheckoutNode('fake-node-id'); + assert.ok(result.entry, 'checkoutNode should return a NodeEntry'); + }); + + it('should throw if nodeId is not defined', () => { + assert.throws(() => nodesApi.cancelCheckoutNode(undefined)); + }); + }); + describe('FolderInformation', () => { it('should return jobId on initiateFolderSizeCalculation API call if everything is ok', async () => { nodeMock.post200ResponseInitiateFolderSizeCalculation(); diff --git a/lib/js-api/test/mockObjects/content-services/node.mock.ts b/lib/js-api/test/mockObjects/content-services/node.mock.ts index d4e87b691a..5fa5151132 100644 --- a/lib/js-api/test/mockObjects/content-services/node.mock.ts +++ b/lib/js-api/test/mockObjects/content-services/node.mock.ts @@ -16,6 +16,7 @@ */ import { BaseMock } from '../base.mock'; +import { NodeEntry } from '../../../src'; export class NodeMock extends BaseMock { get200ResponseChildren(): void { @@ -284,4 +285,20 @@ export class NodeMock extends BaseMock { } }); } + + post200CheckoutNode(nodeId: string): void { + this.mock() + .post(`/alfresco/api/-default-/public/alfresco/versions/1/nodes/${nodeId}/checkout`) + .reply(200, { + entry: { id: 'test-node-id', name: 'Test Node' } + } as NodeEntry); + } + + post200CancelCheckoutNode(nodeId: string): void { + this.mock() + .post(`/alfresco/api/-default-/public/alfresco/versions/1/nodes/${nodeId}/cancel-checkout`) + .reply(200, { + entry: { id: 'test-node-id', name: 'Test Node' } + } as NodeEntry); + } } diff --git a/lib/process-services-cloud/karma.conf.js b/lib/process-services-cloud/karma.conf.js index a7d6664844..154eff3679 100644 --- a/lib/process-services-cloud/karma.conf.js +++ b/lib/process-services-cloud/karma.conf.js @@ -48,7 +48,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/process-services-cloud'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/process-services-cloud/package.json b/lib/process-services-cloud/package.json index 86f933119c..24193bc443 100644 --- a/lib/process-services-cloud/package.json +++ b/lib/process-services-cloud/package.json @@ -12,14 +12,14 @@ }, "peerDependencies": { "@angular/cdk": ">=20.2.14", - "@angular/common": ">=20.3.25", - "@angular/compiler": ">=20.3.25", - "@angular/core": ">=20.3.25", - "@angular/forms": ">=20.3.25", + "@angular/common": ">=20.3.27", + "@angular/compiler": ">=20.3.27", + "@angular/core": ">=20.3.27", + "@angular/forms": ">=20.3.27", "@angular/material": ">=20.2.14", - "@angular/platform-browser": ">=20.3.25", - "@angular/platform-browser-dynamic": ">=20.3.25", - "@angular/router": ">=20.3.25", + "@angular/platform-browser": ">=20.3.27", + "@angular/platform-browser-dynamic": ">=20.3.27", + "@angular/router": ">=20.3.27", "@alfresco/js-api": ">=10.0.0", "@alfresco/adf-core": ">=9.0.0", "@alfresco/adf-content-services": ">=9.0.0", diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts index ba87073a06..221f4dedb5 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts @@ -17,6 +17,7 @@ import { VersionCompatibilityService, AlfrescoApiService } from '@alfresco/adf-content-services'; import { + ADF_DISPLAY_TEXT_SETTINGS, ContentLinkModel, CoreModule, FormFieldModel, @@ -31,6 +32,8 @@ import { provideTranslations, AuthModule, FormFieldEvent, + FormEvent, + FormRulesEvent, NoopTranslateModule, NoopAuthModule, FORM_FIELD_VALIDATORS @@ -117,7 +120,8 @@ describe('FormCloudComponent', () => { useValue: {} }, { provide: FormRenderingService, useClass: CloudFormRenderingService }, - { provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] } + { provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }, + { provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } } ] }); const apiService = TestBed.inject(AlfrescoApiService); @@ -874,6 +878,39 @@ describe('FormCloudComponent', () => { expect(savedForm).toEqual(formModel); }); + it('should materialize unrendered rich text expressions when saving a task form', () => { + spyOn(formCloudService, 'saveTaskForm').and.returnValue(of(undefined)); + const formModel = new FormModel({ + id: '23', + taskId: '123-223', + fields: [ + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] } + }, + { id: 'name', type: FormFieldTypes.TEXT, value: 'John' } + ] + }); + const originalValues = JSON.parse(JSON.stringify(formModel.values)); + formComponent.form = formModel; + formComponent.taskId = formModel.taskId; + formComponent.appName = 'test-app'; + + formComponent.saveTaskForm(); + + expect(formCloudService.saveTaskForm).toHaveBeenCalledWith( + 'test-app', + formModel.taskId, + undefined, + formModel.id, + jasmine.objectContaining({ + richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] } + }) + ); + expect(formModel.values).toEqual(originalValues); + }); + it('should handle error during form save', () => { const error = 'Error'; spyOn(formCloudService, 'saveTaskForm').and.callFake(() => throwError(error)); @@ -981,6 +1018,39 @@ describe('FormCloudComponent', () => { expect(completedForm).toBe(formComponent.form); }); + it('should materialize unrendered rich text expressions when completing a task form', () => { + spyOn(formCloudService, 'completeTaskForm').and.returnValue(of(undefined)); + const formModel = new FormModel({ + id: '23', + taskId: '123-223', + fields: [ + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] } + }, + { id: 'name', type: FormFieldTypes.TEXT, value: 'John' } + ] + }); + formComponent.form = formModel; + formComponent.taskId = formModel.taskId; + formComponent.appName = 'test-app'; + + formComponent.completeTaskForm('complete'); + + expect(formCloudService.completeTaskForm).toHaveBeenCalledWith( + 'test-app', + formModel.taskId, + undefined, + formModel.id, + jasmine.objectContaining({ + richText: { blocks: [{ type: 'paragraph', data: { text: 'John' } }] } + }), + 'complete', + undefined + ); + }); + it('should open confirmation dialog on complete task', async () => { formComponent.form = new FormModel({ confirmMessage: { @@ -1231,6 +1301,38 @@ describe('FormCloudComponent', () => { expect(formComponent.visibleOutcomes).toEqual([]); }); + it('should recompute visibleOutcomes when form visibility is refreshed', () => { + formComponent.showCompleteButton = true; + const formModel = new FormModel(cloudFormMock); + formComponent.form = formModel; + + expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0); + + formModel.outcomes.forEach((outcome) => { + outcome.isVisible = false; + }); + + TestBed.inject(FormService).formVisibilityRefreshed.next(new FormEvent(formModel)); + + expect(formComponent.visibleOutcomes).toEqual([]); + }); + + it('should recompute visibleOutcomes when fieldValueChanged rule event fires', () => { + formComponent.showCompleteButton = true; + const formModel = new FormModel(cloudFormMock); + formComponent.form = formModel; + + expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0); + + formModel.outcomes.forEach((outcome) => { + outcome.isVisible = false; + }); + + TestBed.inject(FormService).formRulesEvent.next(new FormRulesEvent('fieldValueChanged', new FormEvent(formModel))); + + expect(formComponent.visibleOutcomes).toEqual([]); + }); + it('should raise [executeOutcome] event for formService', async () => { spyOn(formComponent.executeOutcome, 'emit'); diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts index 33f50d4fb8..c2ab42da40 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts @@ -30,14 +30,17 @@ import { SimpleChanges, ViewChild } from '@angular/core'; -import { forkJoin, isObservable, Observable, of, Subscription } from 'rxjs'; +import { forkJoin, isObservable, merge, Observable, of, Subscription } from 'rxjs'; import { filter, map, switchMap } from 'rxjs/operators'; import { ConfirmDialogComponent, ContentLinkModel, + ADF_DISPLAY_TEXT_SETTINGS, + DisplayTextWidgetSettings, FormatSpacePipe, FormBaseComponent, FormEvent, + FormExpressionService, FormFieldModel, FormRulesEvent, FormFieldValidator, @@ -67,6 +70,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { A11yModule } from '@angular/cdk/a11y'; +import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../services/form-cloud-submission-values'; interface FormFieldRuntimeState { value: any; @@ -228,6 +232,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, protected changeDetector = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); + private readonly expressions = inject(FormExpressionService); + private enableExpressionEvaluation = false; private get currentForm(): FormModel | undefined { return super.form; @@ -252,6 +258,9 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, constructor() { const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true }); const tabNavEnabledToken = inject(ADF_FORM_TAB_NAV_ENABLED, { optional: true }); + const displayTextSettings = inject | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, { + optional: true + }); super(); this.loadInjectedFieldValidators(injectedFieldValidators); @@ -270,6 +279,12 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, } } + getExpressionEvaluationEnabled$(displayTextSettings) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((enabled) => { + this.enableExpressionEvaluation = enabled; + }); + this.formService.formContentClicked.pipe(takeUntilDestroyed()).subscribe((content) => { if (content instanceof UploadWidgetContentLinkModel) { this.form.setNodeIdValueForViewersLinkedToUploadWidget(content); @@ -291,11 +306,11 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, } }); - this.formService.formRulesEvent - .pipe( - filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.form?.id), - takeUntilDestroyed() - ) + merge( + this.formService.formVisibilityRefreshed.pipe(filter((event) => event.form?.id === this.form?.id)), + this.formService.formRulesEvent.pipe(filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.form?.id)) + ) + .pipe(takeUntilDestroyed()) .subscribe(() => this.recomputeVisibleOutcomes()); } @@ -482,7 +497,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, saveTaskForm() { if (this.form && this.appName && this.taskId) { this.formCloudService - .saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values) + .saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.getSubmissionValues()) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { @@ -523,7 +538,15 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, private completeForm(outcome?: string, outcomeId?: string) { if (this.form && this.appName && this.taskId) { this.formCloudService - .completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion) + .completeTaskForm( + this.appName, + this.taskId, + this.processInstanceId, + `${this.form.id}`, + this.getSubmissionValues(), + outcome, + this.appVersion + ) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { @@ -536,6 +559,10 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, } } + private getSubmissionValues(): FormValues { + return materializeSubmissionValues(this.form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions); + } + parseForm(formCloudRepresentationJSON?: any): FormModel | null { if (formCloudRepresentationJSON) { const formValues: FormValues = {}; @@ -568,7 +595,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, checkVisibility(field: FormFieldModel) { if (field?.form) { this.visibilityService.refreshVisibility(field.form); - this.recomputeVisibleOutcomes(); } } @@ -586,7 +612,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, this.setCheckParentVisibilityForValidationOnFields(); this.visibilityService.refreshVisibility(this.form); this.form.validateForm(); - this.recomputeVisibleOutcomes(); this.onFormLoaded(this.form); this.formService.formRulesEvent.next(new FormRulesEvent('dataRefreshed', new FormEvent(this.form))); this.onFormDataRefreshed(this.form); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html index 0fea08b342..cb90cebccc 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html @@ -1,21 +1,26 @@
-
-
@@ -34,12 +39,15 @@ (contentModelFileHandler)="contentModelFormFileHandler($event)" (removeAttachFile)="onRemoveAttachFile($event)" /> -
- {{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }} -
- + @if (!hasFile && field.readOnly) { +
+ {{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }} +
+ }
- - +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html index 4bc5484ca1..a973a54176 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html @@ -1,31 +1,23 @@
- +
- + @if (!previewState) { - + - - - - + [required]="dataTableLoadFailed ? ('FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate) : ''" + /> + } @else {
-
+ }
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss index 532599f8e3..8fcb2550ac 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.scss @@ -1,10 +1,12 @@ .adf-data-table-widget-failed-message { - margin: 10px; + display: block; } -.adf-preview-placeholder { - height: 100%; - width: 100%; - min-height: 100px; - margin-bottom: 10px; +.adf-data-table-widget-container { + .adf-preview-placeholder { + height: 100%; + width: 100%; + min-height: 100px; + margin-bottom: 10px; + } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts index 2ec50a2d85..f770440650 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts @@ -283,7 +283,8 @@ describe('DataTableWidgetComponent', () => { const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message')); assertData(mockCountryColumns, []); - expect(failedErrorMsgElement).toBeNull(); + expect(failedErrorMsgElement).toBeTruthy(); + expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(''); }); it('path points to single object with appropriate schema definition', () => { @@ -294,7 +295,8 @@ describe('DataTableWidgetComponent', () => { const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message')); assertData(mockCountryColumns, [mockEuropeCountriesRows[1]]); - expect(failedErrorMsgElement).toBeNull(); + expect(failedErrorMsgElement).toBeTruthy(); + expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(''); }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts index 15034eed76..a49c9ade68 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts @@ -27,7 +27,6 @@ import { NoContentTemplateDirective, EmptyContentComponent } from '@alfresco/adf-core'; -import { NgIf } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { FormCloudService } from '../../../services/form-cloud.service'; import { TaskVariableCloud } from '../../../models/task-variable-cloud.model'; @@ -36,7 +35,7 @@ import { DataTablePathParserHelper } from './helpers/data-table-path-parser.help @Component({ standalone: true, - imports: [NgIf, TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent], + imports: [TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent], selector: 'data-table', templateUrl: './data-table.widget.html', styleUrls: ['./data-table.widget.scss'], diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html index 1cc7d8e98b..bde9f411f9 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html @@ -15,7 +15,11 @@ >
- + @if ( (field.name || field?.required) && !field.leftLabels) { {{field.name | translate }} ({{field.dateDisplayFormat}}) @@ -36,12 +40,13 @@ @if (dateInputControl.invalid && dateInputControl.touched) { - + error_outline @if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html index 39a356ac8d..709d994c44 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html @@ -11,7 +11,7 @@
- + @if( (field.name || field?.required) && !field.leftLabels) { {{ field.name | translate }} } @@ -32,8 +32,9 @@ @if (propertyLoadFailed && !previewState) { - error_outline{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }} + error_outline{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts index 80062e72f7..cc646237b5 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts @@ -200,6 +200,51 @@ describe('DisplayRichTextWidgetComponent', () => { expect(widget.field.value.blocks[0].data.text).toBe('Hello John'); }); + it('should resolve from authored value after saved data rehydrates the field', () => { + const form = new FormModel( + { + fields: [ + { + id: 'richText1', + name: 'richText1', + type: 'display-rich-text', + value: { + blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] + } + }, + { id: 'name', name: 'name', type: 'text', value: 'John' } + ] + }, + { + richText1: { + blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] + }, + name: 'Jane' + } + ); + + widget.field = form.getFieldById('richText1'); + fixture.detectChanges(); + + expect(widget.field.value.blocks[0].data.text).toBe('Hello Jane'); + }); + + it('should preserve the current value when the authored value is unavailable', () => { + const form = new FormModel({ + fields: [{ id: 'richText1', type: 'display-rich-text' }] + }); + const currentValue = { + blocks: [{ type: 'paragraph', data: { text: 'Current value' } }] + }; + const field = form.getFieldById('richText1'); + field.value = currentValue; + widget.field = field; + + fixture.detectChanges(); + + expect(widget.field.value).toBe(currentValue); + }); + it('should resolve expressions in multiple blocks', () => { const form = new FormModel({ fields: [ diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts index 92708cf69c..785059ac9f 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts @@ -22,6 +22,7 @@ import { BaseDisplayTextWidgetComponent } from '@alfresco/adf-core'; import { DomSanitizer } from '@angular/platform-browser'; import { Subscription } from 'rxjs'; import { RichTextParserService } from '../../../services/rich-text-parser.service'; +import { resolveRichTextExpressions } from './rich-text-expression-resolver'; export const RICH_TEXT_PARSER_TOKEN = new InjectionToken('RichTextParserService', { factory: () => new RichTextParserService() @@ -66,7 +67,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone protected storeOriginalValue(): void { if (this.field) { - this.originalFieldValue = JSON.stringify(this.field.value); + const authoredValue = this.field.authoredValue; + if (authoredValue !== undefined) { + this.originalFieldValue = JSON.stringify(authoredValue); + } } } @@ -75,8 +79,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone return; } - const value = JSON.parse(JSON.stringify(this.field.value)); - this.applyExpressionsToBlocks(value); + const authoredValue = this.field.authoredValue; + if (authoredValue !== undefined) { + this.applyExpressionsToBlocks(authoredValue); + } } protected reevaluateExpressions(): void { @@ -89,16 +95,7 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone } private applyExpressionsToBlocks(value: any): void { - for (const block of value.blocks) { - if (block.type === 'list') { - for (const item of block.data.items) { - item.content = this.resolveExpressions(item.content, true); - } - } else { - block.data.text = this.resolveExpressions(block.data.text, true); - } - } - this.field.value = value; + this.field.value = resolveRichTextExpressions(value, (content) => this.resolveExpressions(content, true)); } private parseAndSanitize(): void { diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.spec.ts new file mode 100644 index 0000000000..07c3b3c5da --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.spec.ts @@ -0,0 +1,137 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolveRichTextExpressions } from './rich-text-expression-resolver'; + +describe('resolveRichTextExpressions', () => { + const resolve = (value: string) => value.replaceAll('${field.name}', 'John').replaceAll('${variable.status}', 'Active'); + + it('should resolve supported rich text content without mutating the input', () => { + const value = { + time: 1, + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello ${field.name}', + caption: 'Status: ${variable.status}', + content: [ + ['Cell ${field.name}'], + { + label: '${variable.status}' + } + ] + } + }, + { + type: 'list', + data: { + items: [ + { + content: '${field.name}', + items: [{ content: '${variable.status}' }] + } + ] + } + } + ], + version: '2.30.0' + }; + const originalValue = JSON.parse(JSON.stringify(value)); + + const result = resolveRichTextExpressions(value, resolve); + + expect(result).toEqual({ + time: 1, + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello John', + caption: 'Status: Active', + content: [['Cell John'], { label: 'Active' }] + } + }, + { + type: 'list', + data: { + items: [{ content: 'John', items: [{ content: 'Active' }] }] + } + } + ], + version: '2.30.0' + }); + expect(value).toEqual(originalValue); + expect(result).not.toBe(value); + }); + + it('should resolve a caller-owned clone without cloning it again', () => { + const value = { + blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] + }; + + const result = resolveRichTextExpressions(value, resolve, { cloneValue: false }) as typeof value; + + expect(result).toBe(value); + expect(result.blocks[0].data.text).toBe('Hello John'); + }); + + it('should preserve unknown blocks and properties', () => { + const value = { + blocks: [ + { + type: 'custom', + data: { + label: '${field.name}' + }, + metadata: '${variable.status}' + } + ] + }; + + expect(resolveRichTextExpressions(value, resolve)).toEqual(value); + }); + + it('should not introduce missing content properties', () => { + const result = resolveRichTextExpressions({ blocks: [{ type: 'paragraph', data: {} }] }, resolve) as { + blocks: Array<{ data: Record }>; + }; + + expect(result.blocks[0].data).toEqual({}); + }); + + it('should return malformed values unchanged', () => { + const malformedValues = [null, undefined, 'text', [], {}, { blocks: null }]; + + malformedValues.forEach((value) => { + expect(resolveRichTextExpressions(value, resolve)).toBe(value); + }); + }); + + it('should return non-cloneable values without mutating them', () => { + const value: { + blocks: Array<{ data: { text: string } }>; + self?: unknown; + } = { + blocks: [{ data: { text: '${field.name}' } }] + }; + value.self = value; + + expect(resolveRichTextExpressions(value, resolve)).toBe(value); + expect(value.blocks[0].data.text).toBe('${field.name}'); + }); +}); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.ts new file mode 100644 index 0000000000..9606b31f76 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/rich-text-expression-resolver.ts @@ -0,0 +1,111 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type JsonObject = Record; + +export type RichTextExpressionResolver = (value: string) => string; + +export interface RichTextExpressionResolverOptions { + cloneValue?: boolean; +} + +const isJsonObject = (value: unknown): value is JsonObject => typeof value === 'object' && value !== null && !Array.isArray(value); + +const cloneJsonValue = (value: unknown): unknown => { + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return undefined; + } +}; + +const resolveNestedContent = (value: unknown, resolve: RichTextExpressionResolver): unknown => { + if (typeof value === 'string') { + return resolve(value); + } + + if (Array.isArray(value)) { + return value.map((entry) => resolveNestedContent(entry, resolve)); + } + + if (isJsonObject(value)) { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveNestedContent(entry, resolve)])); + } + + return value; +}; + +const resolveListItems = (items: unknown, resolve: RichTextExpressionResolver): unknown => { + if (!Array.isArray(items)) { + return items; + } + + return items.map((item) => { + if (!isJsonObject(item)) { + return item; + } + + if (Object.hasOwn(item, 'content')) { + item.content = resolveNestedContent(item.content, resolve); + } + + if (Object.hasOwn(item, 'items')) { + item.items = resolveListItems(item.items, resolve); + } + + return item; + }); +}; + +export const resolveRichTextExpressions = ( + value: unknown, + resolve: RichTextExpressionResolver, + options: RichTextExpressionResolverOptions = {} +): unknown => { + if (!isJsonObject(value) || !Array.isArray(value.blocks)) { + return value; + } + + const resolvedValue = options.cloneValue === false ? value : cloneJsonValue(value); + if (!isJsonObject(resolvedValue) || !Array.isArray(resolvedValue.blocks)) { + return value; + } + + resolvedValue.blocks.forEach((block) => { + if (!isJsonObject(block) || !isJsonObject(block.data)) { + return; + } + + if (typeof block.data.text === 'string') { + block.data.text = resolve(block.data.text); + } + + if (typeof block.data.caption === 'string') { + block.data.caption = resolve(block.data.caption); + } + + if (Object.hasOwn(block.data, 'content')) { + block.data.content = resolveNestedContent(block.data.content, resolve); + } + + if (block.type === 'list' && Object.hasOwn(block.data, 'items')) { + block.data.items = resolveListItems(block.data.items, resolve); + } + }); + + return resolvedValue; +}; diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html index eedec326ff..e994d83967 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html @@ -12,7 +12,7 @@ }
- + @if ( (field.name || this.field?.required) && !field.leftLabels) { {{ field.name | translate }} } @@ -49,12 +49,13 @@ } @if ((dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) || (!previewState && !field.readOnly && (isRestApiFailed || variableOptionsFailed))) { - + error_outline @if (dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (isRestApiFailed) {{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}} @else if (variableOptionsFailed) {{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}} } +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.html index 83b2abe75f..e3ed0116de 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.html @@ -4,11 +4,13 @@ [class.adf-readonly]="field.readOnly" [class.adf-left-label-input-container]="field.leftLabels" > -
- -
+ @if (field.leftLabels) { +
+ +
+ }
- -
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.spec.ts index 15ef00254b..6d6afa7d45 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.spec.ts @@ -141,8 +141,9 @@ describe('GroupCloudWidgetComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - expect(element.querySelector('.adf-error-text')).toBeTruthy(); - expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND'); + const errorMessages = element.querySelectorAll('.adf-error-text'); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0].textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND'); }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.ts index f6555a01a7..c4c6a58a85 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/group/group-cloud.widget.ts @@ -16,7 +16,7 @@ */ import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core'; +import { WidgetComponent } from '@alfresco/adf-core'; import { UntypedFormControl } from '@angular/forms'; import { filter } from 'rxjs/operators'; import { ComponentSelectionMode } from '../../../../types'; @@ -31,7 +31,7 @@ import { GroupCloudComponent } from '../../../../group/components/group-cloud.co @Component({ selector: 'group-cloud-widget', - imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, GroupCloudComponent], + imports: [CommonModule, TranslatePipe, GroupCloudComponent], templateUrl: './group-cloud.widget.html', host: { '(click)': 'event($event)', diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html index 399f01aca0..457aad503a 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html @@ -1,10 +1,16 @@ -
-
- -
+
+ @if (field.leftLabels) { +
+ +
+ }
- -
- diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.spec.ts index 981033fdea..eb0d49d3bf 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.spec.ts @@ -171,8 +171,9 @@ describe('PeopleCloudWidgetComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - expect(element.querySelector('.adf-error-text')).toBeTruthy(); - expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND'); + const errorMessages = element.querySelectorAll('.adf-error-text'); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0].textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND'); }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts index 976e4f3c0b..e849ff395e 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts @@ -16,7 +16,7 @@ */ import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core'; +import { WidgetComponent } from '@alfresco/adf-core'; import { UntypedFormControl } from '@angular/forms'; import { filter } from 'rxjs/operators'; import { ComponentSelectionMode } from '../../../../types'; @@ -27,13 +27,12 @@ import { ReactivePreselectionService } from '../reactive-preselection.service'; import { CommonModule } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { PeopleCloudComponent } from '../../../../people/components/people-cloud.component'; -import { MatFormFieldModule } from '@angular/material/form-field'; /* eslint-disable @angular-eslint/component-selector */ @Component({ selector: 'people-cloud-widget', - imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, PeopleCloudComponent, MatFormFieldModule], + imports: [CommonModule, TranslatePipe, PeopleCloudComponent], templateUrl: './people-cloud.widget.html', host: { '(click)': 'event($event)', diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss index 3e2ae3f677..ca6a8fae94 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss +++ b/lib/process-services-cloud/src/lib/form/components/widgets/radio-buttons/radio-buttons-cloud.widget.scss @@ -11,7 +11,6 @@ } &-radio-button-container-horizontal { - margin-bottom: 15px; display: flex; flex-flow: column wrap; align-items: flex-start; @@ -46,8 +45,4 @@ word-break: break-word; } } - - &-radio-group-error-message .adf-error-container { - margin-top: 5px; - } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.html index 6b8cb1a67d..ed73ace101 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.html @@ -4,51 +4,50 @@ >
- - - - {{file.name}} - - - + @if (hasFile) { + + + + {{file.name}} + @if (!field.readOnly) { + + } + + + }
-
- -
- -
{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}
+ @if ((!hasFile || multipleOption) && !field.readOnly) { +
+ +
+ } @if (!hasFile && field.readOnly) { +
{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}
+ }
- - +
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.spec.ts index 61e4537d8d..39f922fcfd 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/upload/upload-cloud.widget.spec.ts @@ -55,4 +55,12 @@ describe('UploadCloudWidgetComponent', () => { expect(eventSpy).toHaveBeenCalledWith(clickEvent); }); }); + + it('should render one reserved form field status area', () => { + widget.field = new FormFieldModel(new FormModel(), {}); + fixture.detectChanges(); + + const statusAreas = fixture.nativeElement.querySelectorAll('error-widget'); + expect(statusAreas.length).toBe(1); + }); }); diff --git a/lib/process-services-cloud/src/lib/form/public-api.ts b/lib/process-services-cloud/src/lib/form/public-api.ts index 758c688f72..10256da84f 100644 --- a/lib/process-services-cloud/src/lib/form/public-api.ts +++ b/lib/process-services-cloud/src/lib/form/public-api.ts @@ -43,5 +43,6 @@ export * from './services/form-cloud.service'; export * from './services/content-cloud-node-selector.service'; export * from './services/process-cloud-content.service'; export * from './services/display-mode.service'; +export * from './services/form-cloud-submission-values'; export * from './form-cloud.module'; diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.spec.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.spec.ts new file mode 100644 index 0000000000..c5df7f6b1e --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.spec.ts @@ -0,0 +1,185 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { FormExpressionService, FormFieldModel, FormFieldTypes, FormModel } from '@alfresco/adf-core'; +import { firstValueFrom, of } from 'rxjs'; +import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from './form-cloud-submission-values'; + +describe('getExpressionEvaluationEnabled$', () => { + it('should return the configured static value', async () => { + const enabled = await firstValueFrom(getExpressionEvaluationEnabled$({ enableExpressionEvaluation: true })); + + expect(enabled).toBe(true); + }); + + it('should return values emitted by observable settings', async () => { + const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(of({ enableExpressionEvaluation: true }))); + + expect(enabled).toBe(true); + }); + + it('should return false when settings are unavailable', async () => { + const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(undefined)); + + expect(enabled).toBe(false); + }); +}); + +describe('materializeSubmissionValues', () => { + let expressions: FormExpressionService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [FormExpressionService] + }); + expressions = TestBed.inject(FormExpressionService); + }); + + it('should resolve root rich text values from the authored template without mutating the form', () => { + const authoredValue = { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello ${field.name} - ${variable.status} - ${field.missing} - ${field.unsafe}' + } + } + ] + }; + const form = new FormModel({ + fields: [ + { id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue }, + { id: 'name', name: 'name', type: FormFieldTypes.TEXT, value: 'John' }, + { id: 'unsafe', name: 'unsafe', type: FormFieldTypes.TEXT, value: 'John' } + ], + variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }] + }); + const richTextField = form.getFieldById('richText'); + richTextField.value = { blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] }; + const originalValues = JSON.parse(JSON.stringify(form.values)); + const originalDefinition = JSON.parse(JSON.stringify(form.json)); + + const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions); + + expect(values.richText).toEqual({ + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello John - Active - - <b>John</b>' + } + } + ] + }); + expect(form.values).toEqual(originalValues); + expect(form.json).toEqual(originalDefinition); + expect(richTextField.value).toEqual({ blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] }); + }); + + it('should produce stable values across repeated materialization', () => { + const form = new FormModel({ + fields: [ + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] } + }, + { id: 'name', type: FormFieldTypes.TEXT, value: 'John' } + ] + }); + + const firstValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions); + const secondValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions); + + expect(secondValues).toEqual(firstValues); + }); + + it('should return a shallow clone without resolving expressions when evaluation is disabled', () => { + const form = new FormModel({ + fields: [ + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] } + }, + { id: 'name', type: FormFieldTypes.TEXT, value: 'John' } + ] + }); + + const values = materializeSubmissionValues(form, { enableExpressionEvaluation: false }, expressions); + + expect(values).toEqual(form.values); + expect(values).not.toBe(form.values); + expect(values.richText).toBe(form.values.richText); + }); + + it('should isolate materialized repeatable section rows', () => { + const form = new FormModel(); + form.values = { + section: [ + { richText: 'saved row one', untouched: 'one' }, + { richText: 'saved row two', untouched: 'two' } + ], + name: 'John' + }; + const nameField = new FormFieldModel(form, { id: 'name', type: FormFieldTypes.TEXT, value: 'John' }); + const firstField = new FormFieldModel( + form, + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: 'First ${field.name}' } }] } + }, + { id: 'section', uid: 'richText-Row1', fields: {}, rowIndex: 0 } + ); + const secondField = new FormFieldModel( + form, + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: 'Second ${field.name}' } }] } + }, + { id: 'section', uid: 'richText-Row2', fields: {}, rowIndex: 1 } + ); + form.fieldsCache = [nameField, firstField, secondField]; + form.values.section = [ + { richText: 'saved row one', untouched: 'one' }, + { richText: 'saved row two', untouched: 'two' } + ]; + const originalSection = form.values.section; + const originalFirstRow = form.values.section[0]; + const originalSecondRow = form.values.section[1]; + + const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions); + + expect(values.section).toEqual([ + { + richText: { blocks: [{ type: 'paragraph', data: { text: 'First John' } }] }, + untouched: 'one' + }, + { + richText: { blocks: [{ type: 'paragraph', data: { text: 'Second John' } }] }, + untouched: 'two' + } + ]); + expect(values.section).not.toBe(originalSection); + expect(values.section[0]).not.toBe(originalFirstRow); + expect(values.section[1]).not.toBe(originalSecondRow); + expect(form.values.section).toBe(originalSection); + }); +}); diff --git a/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.ts b/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.ts new file mode 100644 index 0000000000..a43709e4e2 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/services/form-cloud-submission-values.ts @@ -0,0 +1,80 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DisplayTextWidgetSettings, FormExpressionService, FormFieldTypes, FormModel, FormValues, ROW_ID_PREFIX } from '@alfresco/adf-core'; +import { isObservable, Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { resolveRichTextExpressions } from '../components/widgets/display-rich-text/rich-text-expression-resolver'; + +type SubmissionRow = Record; + +const isSubmissionRow = (value: unknown): value is SubmissionRow => typeof value === 'object' && value !== null && !Array.isArray(value); + +export interface FormCloudSubmissionValuesOptions { + enableExpressionEvaluation: boolean; +} + +export const getExpressionEvaluationEnabled$ = ( + settings: Observable | DisplayTextWidgetSettings | null | undefined +): Observable => + isObservable(settings) + ? settings.pipe(map((value) => value?.enableExpressionEvaluation ?? false)) + : of(settings?.enableExpressionEvaluation ?? false); + +export const materializeSubmissionValues = ( + form: FormModel, + options: FormCloudSubmissionValuesOptions, + expressions: FormExpressionService +): FormValues => { + const values = { ...form.values }; + + if (!options.enableExpressionEvaluation) { + return values; + } + + for (const field of form.getFormFields([FormFieldTypes.DISPLAY_RICH_TEXT])) { + const { authoredValue, parent } = field; + if (authoredValue === undefined || parent?.isTemplate) { + continue; + } + + const materializedValue = resolveRichTextExpressions(authoredValue, (content) => expressions.resolveExpressions(form, content, true), { + cloneValue: false + }); + + if (!parent) { + values[field.id] = materializedValue; + continue; + } + + const sectionValues = values[parent.id]; + const sectionRow = Array.isArray(sectionValues) ? sectionValues[parent.rowIndex] : undefined; + if (!isSubmissionRow(sectionRow)) { + continue; + } + + const materializedRows = [...sectionValues]; + const fieldId = field.id.split(ROW_ID_PREFIX)[0]; + materializedRows[parent.rowIndex] = { + ...sectionRow, + [fieldId]: materializedValue + }; + values[parent.id] = materializedRows; + } + + return values; +}; diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html index 759941de30..d903177dbb 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html @@ -1,27 +1,37 @@
- - @if (label || required) { {{label}} } + + @if (label || required) { + {{ label }} + } - {{group.name}} - + title="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}" + > + {{ group.name }} + @if (!(group.readonly || readOnly)) { + + } - + - - -
- - {{group.name}} + data-automation-id="adf-cloud-group-autocomplete" + > + @if ((searchGroups$ | async)?.length) { + +
+ + {{ group.name }}
- + } @else { + + } - - {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }} - + @if (searchGroupsControl.hasError('searchTypingError') && !searchLoading) { + + {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }} + + } - -
- - -
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}
-
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}
+ @if (validationLoading) { + + } + @if (hasPreselectError() && !isValidationLoading()) { + + +
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
-
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}
-
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}
-
- - -
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}
-
+ } + @if (searchGroupsControl.hasError('pattern')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }} +
+
+ } + @if (searchGroupsControl.hasError('maxlength')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }} +
+
+ } + @if (searchGroupsControl.hasError('minlength')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }} +
+
+ } + @if ((searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()) { + + +
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}
+
+ } + @if (searchGroupsControl.hasError('searchTypingError') && !this.isFocused) { + + +
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}
+
+ }
diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss index a9cdd5daad..d7e1aa5a21 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss @@ -57,10 +57,13 @@ } } -.adf-error-messages-container .adf-error-icon { - @include mixins.adf-error-icon; -} +.adf-error-messages-container { + .adf-error-icon { + @include mixins.adf-error-icon; + } -.adf-error-messages-container .adf-error { - animation: slide-down-fade-in 300ms cubic-bezier(0.55, 0, 0.55, 0.2); + .adf-error { + padding-top: 3px; + animation: slide-down-fade-in 300ms cubic-bezier(0.55, 0, 0.55, 0.2); + } } diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts index 09d88fe3f2..2c37fff1ca 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts @@ -27,6 +27,8 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatChipHarness } from '@angular/material/chips/testing'; import { MatIconHarness } from '@angular/material/icon/testing'; import { MatInputHarness } from '@angular/material/input/testing'; +import { MatFormField } from '@angular/material/form-field'; +import { MatProgressBar } from '@angular/material/progress-bar'; describe('GroupCloudComponent', () => { let loader: HarnessLoader; @@ -98,6 +100,22 @@ describe('GroupCloudComponent', () => { expect(await inputElement.getPlaceholder()).toEqual(''); }); + it('should use dynamic form field subscript sizing', () => { + fixture.detectChanges(); + + const formField = fixture.debugElement.query(By.directive(MatFormField)).componentInstance as MatFormField; + expect(formField.subscriptSizing).toBe('dynamic'); + }); + + it('should render validation progress inside the reserved status area', () => { + component.validationLoading = true; + fixture.detectChanges(); + + const progressBar = fixture.debugElement.query(By.directive(MatProgressBar)); + + expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue(); + }); + describe('Search group', () => { beforeEach(() => { fixture.detectChanges(); diff --git a/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts new file mode 100644 index 0000000000..5725360d1a --- /dev/null +++ b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts @@ -0,0 +1,55 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FilterCounterEntityType = { + TASK: 'TASK', + PROCESS_INSTANCE: 'PROCESS_INSTANCE' +} as const; + +export type FilterCounterEntityType = (typeof FilterCounterEntityType)[keyof typeof FilterCounterEntityType]; + +export interface FilterCountersQuerySort { + field: string; + direction: string; + isProcessVariable: boolean; +} + +export interface FilterCountersQuery { + requestId: string; + status?: string[]; + assignee?: string[]; + sort?: FilterCountersQuerySort; + [criteria: string]: unknown; +} + +export type FilterCountersRequest = { + [entityType in FilterCounterEntityType]?: FilterCountersQuery[]; +}; + +export interface FilterCounterCandidate { + key?: string | null; + showCounter?: boolean; +} + +export type FilterCounters = { + [entityType in FilterCounterEntityType]?: { [requestId: string]: number }; +}; + +export interface FilterCountersResult { + counters: { [filterKey: string]: number }; + batched: boolean; +} diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html index e71ef489b3..c2b2633fe4 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html @@ -4,10 +4,14 @@ class="adf-people-cloud adf-form-field-input" [class.adf-invalid]="hasError() && isDirty()" > - - {{label}} - - {{ title | translate }} + @if (!title) { + + {{ label }} + + } + @if (title) { + {{ title | translate }} + } {{ user | fullName }} - + @if (!(user.readonly || readOnly)) { + + } - + @if ((searchUsers$ | async)?.length) {
-
- {{ user | fullName : true }} +
+ {{ user | fullName: true }}
-
+ } @else { + + } - - {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: searchedValue } }} - + @if (searchUserCtrl.hasError('searchTypingError') && !searchLoading) { + + {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }} + + } - - -
- - -
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: validateUsersMessage } }}
-
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate : { pattern: getValidationPattern() } }}
-
- - -
- {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate : { requiredLength: getValidationMaxLength() } }} -
-
- - -
- {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate : { requiredLength: getValidationMinLength() } }} -
-
- - -
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}
-
- - -
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: searchedValue } }}
-
+
+ @if (validationLoading) { + + } + @if (showErrors) { + @if (hasPreselectError() && !isValidationLoading()) { + + +
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: validateUsersMessage } }}
+
+ } + @if (searchUserCtrl.hasError('pattern')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }} +
+
+ } + @if (searchUserCtrl.hasError('maxlength')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }} +
+
+ } + @if (searchUserCtrl.hasError('minlength')) { + + +
+ {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }} +
+
+ } + @if ((searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()) { + + +
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}
+
+ } + @if (searchUserCtrl.hasError('searchTypingError') && !this.isFocused) { + + +
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }}
+
+ } + }
diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss index 668683bfc0..a4ed4e3cdb 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss @@ -66,6 +66,10 @@ @include mixins.adf-error-icon; } + .adf-error { + padding-top: 3px; + } + .adf-error-animate { animation: adf-people-cloud-slide-in-down 300ms cubic-bezier(0.55, 0, 0.55, 0.2); } diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts index 3b99609dd0..98381d0c34 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts @@ -27,6 +27,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatChipHarness } from '@angular/material/chips/testing'; import { MatInputHarness } from '@angular/material/input/testing'; import { MatFormFieldHarness } from '@angular/material/form-field/testing'; +import { MatProgressBar } from '@angular/material/progress-bar'; import { IdentityUserService } from '../services/identity-user.service'; describe('PeopleCloudComponent', () => { @@ -99,6 +100,19 @@ describe('PeopleCloudComponent', () => { expect(await inputField.getLabel()).toEqual('TITLE_KEY'); }); + it('should use dynamic form field subscript sizing by default', () => { + expect(component.formFieldSubscriptSizing).toBe('dynamic'); + }); + + it('should render validation progress inside the reserved status area', () => { + component.validationLoading = true; + fixture.detectChanges(); + + const progressBar = fixture.debugElement.query(By.directive(MatProgressBar)); + + expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue(); + }); + describe('Search user', () => { beforeEach(() => { fixture.detectChanges(); diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts index 5a11dd42e7..62a47a8af9 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts @@ -169,7 +169,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, AfterViewInit { * Material form field subscript sizing (fixed / dynamic) */ @Input() - formFieldSubscriptSizing: SubscriptSizing = 'fixed'; + formFieldSubscriptSizing: SubscriptSizing = 'dynamic'; /** * Show errors under the form field diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts index 8c90e5f42a..b3ca4b78f7 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts @@ -16,15 +16,15 @@ */ import { Component, SimpleChange } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { first, of, Subject, throwError } from 'rxjs'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFiltersCloudComponent } from './process-filters-cloud.component'; import { By } from '@angular/platform-browser'; -import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service'; import { mockProcessFilters } from '../../mock/process-filters-cloud.mock'; -import { AppConfigService, AppConfigServiceMock } from '@alfresco/adf-core'; +import { AppConfigService, AppConfigServiceMock, NoopAuthModule } from '@alfresco/adf-core'; import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service'; import { ApolloTestingModule } from 'apollo-angular/testing'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -32,39 +32,39 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatIconHarness } from '@angular/material/icon/testing'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; -import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model'; +import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) class DummyComponent {} const ProcessFilterCloudServiceMock = { getProcessFilters: () => of(mockProcessFilters), - getProcessNotificationSubscription: () => of([]), filterKeyToBeRefreshed$: of(mockProcessFilters[0].key) }; describe('ProcessFiltersCloudComponent', () => { let processFilterService: ProcessFilterCloudService; + let filterCountersService: FilterCountersCloudService; + let processListService: ProcessListCloudService; let component: ProcessFiltersCloudComponent; let fixture: ComponentFixture; let getProcessFiltersSpy: jasmine.Spy; - let getProcessNotificationSubscriptionSpy: jasmine.Spy; + let getFilterCountersSpy: jasmine.Spy; + let refreshFilterCountersSpy: jasmine.Spy; + let getProcessCounterSpy: jasmine.Spy; let loader: HarnessLoader; let router: Router; const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => { TestBed.configureTestingModule({ - imports: [ProcessFiltersCloudComponent, ApolloTestingModule], + imports: [NoopAuthModule, ProcessFiltersCloudComponent, ApolloTestingModule], providers: [ { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, { provide: AppConfigService, useClass: AppConfigServiceMock }, - { - provide: ProcessListCloudService, - useValue: { - getProcessCounter: () => of(10), - getProcessListCount: () => of(10) - } - }, + ProcessListCloudService, { provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock }, provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]), { @@ -88,11 +88,16 @@ describe('ProcessFiltersCloudComponent', () => { component.searchApiMethod = searchApiMethod; processFilterService = TestBed.inject(ProcessFilterCloudService); + filterCountersService = TestBed.inject(FilterCountersCloudService); + processListService = TestBed.inject(ProcessListCloudService); TestBed.inject(ActivatedRoute); router = TestBed.inject(Router); await RouterTestingHarness.create(); - getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); - getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([])); + getProcessFiltersSpy = spyOn(filterCountersService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); + getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({ counters: {}, batched: true })); + refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters'); + getProcessCounterSpy = spyOn(processListService, 'getProcessCounter').and.returnValue(of(10)); + spyOn(processListService, 'getProcessListCount').and.returnValue(of(10)); }; const bindAppName = async (appName = 'my-app-1') => { @@ -463,17 +468,98 @@ describe('ProcessFiltersCloudComponent', () => { expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy(); }); - it('should call fetchProcessFilterCounter only if filter.showCounter is true', () => { - const filterWithCounter = { ...mockProcessFilters[0], showCounter: true }; - const filterWithoutCounter = { ...mockProcessFilters[1], showCounter: false }; - const fetchSpy = spyOn(component, 'fetchProcessFilterCounter').and.returnValue(of(42)); + it('should resolve the counter only of the filters with a counter enabled', () => { + const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true }); + const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false }); + getProcessCounterSpy.calls.reset(); component.filters = [filterWithCounter, filterWithoutCounter]; component.updateFilterCounters(); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter); - expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter); + expect(getProcessCounterSpy).toHaveBeenCalledTimes(1); + expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status); + }); + + describe('Batched counters', () => { + beforeEach(() => { + getProcessFiltersSpy.and.returnValue( + of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }))) + ); + }); + + it('should read the counters of the process filters of the bound app', async () => { + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName('mock-app-name'); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName('mock-app-name'); + + expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses'); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName('mock-app-name'); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + expect(component.counters['FakeRunningProcesses']).toBe(10); + }); + + it('should resolve the counters of the filters the batch left out on their own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + getProcessCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(component.counters['completed-processes']).toBe(0); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName('mock-app-name'); + + component.onFilterClick(mockProcessFilters[1]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name'); + }); }); describe('Notifications config', () => { @@ -507,39 +593,436 @@ describe('ProcessFiltersCloudComponent', () => { expect(component.notificationDebounceTime).toBe(5000); }); - it('should debounce notification subscription using the configured debounce time', fakeAsync(() => { - const notifications$ = new Subject(); - getProcessNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable()); + it('should keep the counters in sync with the counters stream', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); component.appName = 'mock-app-name'; fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); - const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters'); - - notifications$.next([]); - tick(1000); - expect(updateFilterCountersSpy).not.toHaveBeenCalled(); - - tick(2000); - expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1); + counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); + expect(component.counters['FakeRunningProcesses']).toBe(7); flush(); })); }); describe('Highlight Selected Filter', () => { - it('should make subscription', async () => { + const allProcessesFilterKey = mockProcessFilters[0].key; + const allProcessesFilterId = mockProcessFilters[0].id; + + it('should apply active CSS class on filter click', async () => { component.enableNotifications = true; await bindAppName('mock-app-name'); - expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled(); + + let link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.getAttribute('href')).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`); + + link.click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(router.url).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`); + + link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.classList).toContain('adf-active'); }); - it('should not make subscription when notifications are disabled', async () => { - const appConfigService = TestBed.inject(AppConfigService); - spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notifications' ? false : defaultValue)); + it('should add aria-current attribute with value "page" to the active filter', async () => { + component.enableNotifications = true; await bindAppName('mock-app-name'); - expect(getProcessNotificationSubscriptionSpy).not.toHaveBeenCalled(); + const link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement; + expect(link.getAttribute('aria-current')).toBe('page'); + }); + + it('should not have aria-current attribute when filter is not active', async () => { + component.enableNotifications = true; + await bindAppName('mock-app-name'); + + const link = fixture.debugElement.query(By.css(`[data-automation-id="${mockProcessFilters[1].key}_filter"]`)).nativeElement; + expect(link.getAttribute('aria-current')).toBeNull(); + }); + }); + }); + + describe('searchApiMethod set to POST', () => { + beforeEach(async () => { + await configureTestingModule('POST'); + }); + + it('should attach specific icon for each filter if hasIcon is true', async () => { + await bindAppName(); + + component.showIcons = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.filters.length).toBe(3); + const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' })); + expect(filterIcons.length).toBe(3); + expect(await filterIcons[0].getName()).toContain('adjust'); + expect(await filterIcons[1].getName()).toContain('inbox'); + expect(await filterIcons[2].getName()).toContain('done'); + }); + + it('should not attach icons for each filter if hasIcon is false', async () => { + component.showIcons = false; + await bindAppName(); + + const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' })); + expect(filterIcons.length).toBe(0); + }); + + it('should display the filters', async () => { + await bindAppName(); + + component.showIcons = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry')); + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses'); + expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses'); + expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); + + it('should emit success with the filters when filters are loaded', async () => { + const successSpy = spyOn(component.success, 'emit'); + await bindAppName(); + + expect(successSpy).toHaveBeenCalledWith(mockProcessFilters); + expect(component.filters).toBeDefined(); + expect(component.filters[0].name).toEqual('FakeAllProcesses'); + expect(component.filters[1].name).toEqual('FakeRunningProcesses'); + expect(component.filters[2].name).toEqual('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); + + it('should not select any filter as default', async () => { + await bindAppName(); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should filterClicked emit when a filter is clicked from the UI', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + await bindAppName(); + + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`); + filterButton.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.currentFilter).toEqual(mockProcessFilters[0]); + expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); + }); + }); + + describe('API agnostic', () => { + beforeEach(async () => { + await configureTestingModule('GET'); + }); + + it('should emit an error with a bad response', async () => { + getProcessFiltersSpy.and.returnValue(throwError('wrong request')); + let lastValue: any; + component.error.subscribe((err) => (lastValue = err)); + + await bindAppName(); + + expect(lastValue).toBeDefined(); + }); + + it('should not select any process filter if filter input does not exist', async () => { + const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should select the filter based on the input by name param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { name: 'FakeRunningProcesses' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[1]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[1]); + }); + + it('should select the filter based on the input by key param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { key: 'completed-processes' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should select the filter based on the input by index param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { index: 2 }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should select the filter based on the input by id param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { id: '12' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); + + it('should reset the filter when the param is undefined', () => { + const change = new SimpleChange(mockProcessFilters[0], undefined, false); + component.currentFilter = mockProcessFilters[0]; + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(undefined); + }); + + it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const change = new SimpleChange(null, { id: '10' }, true); + + await bindAppName(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBe(mockProcessFilters[0]); + expect(filterClickedSpy).not.toHaveBeenCalled(); + }); + + it('should reload filters by appName on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'my-app-1'; + + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); + }); + + it('should not reload filters by appName null on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = null; + + const change = new SimpleChange(undefined, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).not.toHaveBeenCalledWith(appName); + }); + + it('should reload filters by app name on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'fake-app-name'; + + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); + }); + + it('should return the current filter after one is selected', () => { + const filter = mockProcessFilters[1]; + component.filters = mockProcessFilters; + + expect(component.currentFilter).toBeUndefined(); + component.selectFilter({ id: filter.id }); + expect(component.getCurrentFilter()).toBe(filter); + }); + + it('should remove key from set of updated filters when received refreshed filter key', async () => { + const filterKeyTest = 'filter-key-test'; + component.updatedFiltersSet.add(filterKeyTest); + + expect(component.updatedFiltersSet.size).toBe(1); + processFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); + fixture.detectChanges(); + + expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy(); + }); + + it('should resolve the counter only of the filters with a counter enabled', () => { + const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true }); + const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false }); + getProcessCounterSpy.calls.reset(); + + component.filters = [filterWithCounter, filterWithoutCounter]; + component.updateFilterCounters(); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(1); + expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status); + }); + + describe('Batched counters', () => { + beforeEach(() => { + getProcessFiltersSpy.and.returnValue( + of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }))) + ); + }); + + it('should read the counters of the process filters of the bound app', async () => { + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName('mock-app-name'); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName('mock-app-name'); + + expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses'); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName('mock-app-name'); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + expect(component.counters['FakeRunningProcesses']).toBe(10); + }); + + it('should resolve the counters of the filters the batch left out on their own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true })); + getProcessCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName('mock-app-name'); + + expect(component.counters['FakeRunningProcesses']).toBe(9); + expect(component.counters['completed-processes']).toBe(0); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName('mock-app-name'); + + component.onFilterClick(mockProcessFilters[1]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name'); + }); + }); + + describe('Notifications config', () => { + it('should read enableNotifications and notificationDebounceTime from app config on init', () => { + const appConfigService = TestBed.inject(AppConfigService); + const getSpy = spyOn(appConfigService, 'get').and.callThrough(); + + fixture.detectChanges(); + + expect(getSpy).toHaveBeenCalledWith('notifications', true); + expect(getSpy).toHaveBeenCalledWith('notificationDebounceTime', 3000); + }); + + it('should default notificationDebounceTime to 3000 when not set in app config', () => { + fixture.detectChanges(); + + expect(component.notificationDebounceTime).toBe(3000); + }); + + it('should use notificationDebounceTime from app config', () => { + const appConfigService: AppConfigService = TestBed.inject(AppConfigService); + spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => { + if (key === 'notificationDebounceTime') { + return 5000; + } + return defaultValue; + }); + + fixture.detectChanges(); + + expect(component.notificationDebounceTime).toBe(5000); + }); + + it('should keep the counters in sync with the counters stream', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); + component.appName = 'mock-app-name'; + + fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); + + counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); + + expect(component.counters['FakeRunningProcesses']).toBe(7); + flush(); + })); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', fakeAsync(() => { + const counters$ = new Subject(); + getFilterCountersSpy.and.returnValue(counters$.asObservable()); + component.appName = 'mock-app-name'; + + fixture.detectChanges(); + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); + getProcessCounterSpy.calls.reset(); + + counters$.next({ counters: {}, batched: false }); + + expect(getProcessCounterSpy).toHaveBeenCalledTimes(3); + flush(); + })); + }); + + describe('Highlight Selected Filter', () => { + it('should read the counters of the bound app', async () => { + component.enableNotifications = true; + await bindAppName('mock-app-name'); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false); }); it('should emit filter key when filter counter is set for first time', () => { diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts index 9e7fc7ba5f..d7001220d4 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts @@ -16,14 +16,16 @@ */ import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { EMPTY, Observable } from 'rxjs'; +import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model'; -import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service'; import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { TranslatePipe } from '@ngx-translate/core'; import { AsyncPipe } from '@angular/common'; @@ -43,10 +45,21 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { @Input() appName: string = ''; - /** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count */ + /** + * (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count. + * + */ @Input() searchApiMethod: 'GET' | 'POST' = 'GET'; + /** + * (optional) Resolves the counters of the task and the process filters with a single call to + * `POST /query/v1/count`. Both filter components have to + * ask for it, otherwise the counters are resolved one filter at a time. + */ + @Input() + useBatchedCounters = false; + /** (optional) The filter to be selected by default */ @Input() filterParam: FilterParamsModel; @@ -79,27 +92,31 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { currentFilter?: ProcessFilterCloudModel; filters: ProcessFilterCloudModel[] = []; counters: { [key: string]: number } = {}; - enableNotifications = true; - notificationDebounceTime = 3000; currentFiltersValues: { [key: string]: number } = {}; updatedFiltersSet = new Set(); + enableNotifications = true; + notificationDebounceTime = 3000; private filtersLoadedFor?: string; + private countersSubscription?: Subscription; + private countersFilters$?: Observable; + private batchedCounters = true; private readonly destroyRef = inject(DestroyRef); private readonly processFilterCloudService = inject(ProcessFilterCloudService); private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); private readonly processListCloudService = inject(ProcessListCloudService); + private readonly filterCountersCloudService = inject(FilterCountersCloudService); private readonly activatedRoute = inject(ActivatedRoute); protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId')))); ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000); + if (!this.filtersLoadedFor) { this.getFilters(this.appName); } - this.initProcessNotification(); this.getFilterKeysAfterExternalRefreshing(); } @@ -110,6 +127,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.getFilters(appName.currentValue); } else if (filter && filter.currentValue !== filter.previousValue) { this.selectFilterAndEmit(filter.currentValue); + } else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) { + this.loadFilterCounters(this.filtersLoadedFor); } } @@ -120,8 +139,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { */ getFilters(appName: string): void { this.filtersLoadedFor = appName; - const filters$ = this.processFilterCloudService.getProcessFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true })); - this.filters$ = filters$.pipe(catchError(() => EMPTY)); + const filters$ = this.filterCountersCloudService.getProcessFilters(appName); + this.filters$ = filters$.pipe(catchError(() => of([]))); filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (res) => { @@ -130,19 +149,25 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.initFilterCounters(); this.selectFilterAndEmit(this.filterParam); this.success.emit(res); - this.updateFilterCounters(); }, - error: (err: any) => { + error: (err: unknown) => { this.error.emit(err); } }); + + this.countersFilters$ = filters$; + this.loadFilterCounters(appName); } /** * Initialize counter collection for filters */ - initFilterCounters() { - this.filters.forEach((filter) => (this.counters[filter.key] = 0)); + initFilterCounters(): void { + this.filters.forEach((filter) => { + if (filter.key) { + this.counters[filter.key] = 0; + } + }); } /** @@ -167,20 +192,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { ); // fallback to preserve the previous behavior } - /** - * Check equality of the filter names by translating the given name strings - * - * @param name1 source name - * @param name2 target name - * @returns `true` if filter names are equal, otherwise `false` - */ - private checkFilterNamesEquality(name1: string, name2: string): boolean { - const translatedName1 = this.translationService.instant(name1); - const translatedName2 = this.translationService.instant(name2); - - return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase(); - } - /** * Selects and emits the given filter * @@ -213,7 +224,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { if (filter) { this.selectFilter(filter); this.filterClicked.emit(this.currentFilter); - this.updateFilterCounter(this.currentFilter); + this.refreshFilterCounter(this.currentFilter); this.updatedFiltersSet.delete(filter.key); } else { this.currentFilter = undefined; @@ -247,6 +258,83 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { return this.filters === undefined || (this.filters && this.filters.length === 0); } + isActiveFilter(filter: ProcessFilterCloudModel): boolean { + return this.currentFilter.name === filter.name; + } + + /** + * @deprecated does nothing: the counters keep themselves in sync. Removed in ADF 10.0.0. + */ + initProcessNotification(): void {} + + /** + * Iterate over filters and update counters + * + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. + */ + updateFilterCounters(): void { + this.filters.forEach((filter) => this.updateFilterCounter(filter)); + } + + /** + * Get current value for filter and check if value has changed + * + * @param filter filter + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. + */ + updateFilterCounter(filter: ProcessFilterCloudModel): void { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } + + defer(() => this.fetchProcessFilterCounter(filter)) + .pipe( + catchError(() => EMPTY), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((counter) => { + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; + }); + } + + checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void { + if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { + this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue }; + this.updatedFilter.emit(filterKey); + this.updatedFiltersSet.add(filterKey); + } + } + + /** + * Get filer key when filter was refreshed by external action + * + */ + getFilterKeysAfterExternalRefreshing(): void { + this.processFilterCloudService.filterKeyToBeRefreshed$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((filterKey: string) => this.updatedFiltersSet.delete(filterKey)); + } + + isFilterUpdated(filterName: string): boolean { + return this.updatedFiltersSet.has(filterName); + } + + /** + * Check equality of the filter names by translating the given name strings + * + * @param name1 source name + * @param name2 target name + * @returns `true` if filter names are equal, otherwise `false` + */ + private checkFilterNamesEquality(name1: string, name2: string): boolean { + const translatedName1 = this.translationService.instant(name1); + const translatedName2 = this.translationService.instant(name2); + + return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase(); + } + /** * Reset the filters */ @@ -255,76 +343,53 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.currentFilter = undefined; } - isActiveFilter(filter: ProcessFilterCloudModel): boolean { - return this.currentFilter.name === filter.name; - } - - initProcessNotification(): void { - if (this.appName && this.enableNotifications) { - this.processFilterCloudService - .getProcessNotificationSubscription(this.appName) - .pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.updateFilterCounters(); - }); - } - } - - /** - * Iterate over filters and update counters - */ - updateFilterCounters(): void { - this.filters.forEach((filter: ProcessFilterCloudModel) => { - this.updateFilterCounter(filter); - }); - } - - /** - * Get current value for filter and check if value has changed - * - * @param filter filter - */ - updateFilterCounter(filter: ProcessFilterCloudModel): void { - if (!filter?.showCounter) { + private loadFilterCounters(appName: string): void { + if (!this.countersFilters$) { return; } - this.fetchProcessFilterCounter(filter) - .pipe( - tap((filterCounter) => { - this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); - }) - ) - .subscribe((data) => { - this.counters = { - ...this.counters, - [filter.key]: data - }; + this.countersSubscription?.unsubscribe(); + this.countersSubscription = combineLatest([ + this.countersFilters$.pipe(catchError(() => of([]))), + this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, this.useBatchedCounters) + ]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([, { counters, batched }]) => { + this.batchedCounters = batched; + if (batched) { + this.applyFilterCounters(counters); + } else { + this.updateFilterCounters(); + } }); } - checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void { - if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { - this.currentFiltersValues[filterKey] = filterValue; - this.updatedFilter.emit(filterKey); - this.updatedFiltersSet.add(filterKey); - } - } + private applyFilterCounters(counters: { [filterKey: string]: number }): void { + this.filters.forEach((filter) => { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } - isFilterUpdated(filterName: string): boolean { - return this.updatedFiltersSet.has(filterName); - } + const counter = counters[filterKey]; + if (counter === undefined) { + this.updateFilterCounter(filter); + return; + } - /** - * Get filer key when filter was refreshed by external action - * - */ - getFilterKeysAfterExternalRefreshing(): void { - this.processFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => { - this.updatedFiltersSet.delete(filterKey); + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; }); } + private refreshFilterCounter(filter?: ProcessFilterCloudModel): void { + if (this.batchedCounters) { + this.filterCountersCloudService.refreshFilterCounters(this.appName); + } else if (filter) { + this.updateFilterCounter(filter); + } + } + private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable { return this.searchApiMethod === 'POST' ? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter)) diff --git a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts index 316933cba6..9839470ea2 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts @@ -404,6 +404,12 @@ export class ProcessFilterCloudService { ]; } + /** + * @deprecated use FilterCountersCloudService.getEngineEvents instead. + * + * @param appName Name of the target app + * @returns Process engine events + */ getProcessNotificationSubscription(appName: string): Observable { return this.notificationCloudService .makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY) diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts index 84c5a97cfb..8316bf1f09 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts @@ -100,7 +100,7 @@ export class ProcessListCloudService extends BaseCloudService { ); } - protected buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } { + buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } { const queryData: { [key: string]: any } = { name: requestNode.name, id: requestNode.id, diff --git a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts index 71136e5c5b..0f9559bc03 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.spec.ts @@ -17,8 +17,8 @@ import { DebugElement, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; -import { of, throwError } from 'rxjs'; +import { ADF_DISPLAY_TEXT_SETTINGS, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core'; +import { Subject, of, throwError } from 'rxjs'; import { StartProcessCloudService } from '../services/start-process-cloud.service'; import { FormCloudService } from '../../../form/services/form-cloud.service'; import { FormCloudComponent } from '../../../form/components/form-cloud.component'; @@ -47,7 +47,7 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; import { MatButtonHarness } from '@angular/material/button/testing'; -import { FormCloudDisplayMode } from '../../../services/form-fields.interfaces'; +import { FormCloudDisplayMode, FormContent } from '../../../services/form-fields.interfaces'; import { MatDialogHarness } from '@angular/material/dialog/testing'; import { MatDialog } from '@angular/material/dialog'; import { ReactiveFormsModule } from '@angular/forms'; @@ -92,7 +92,10 @@ describe('StartProcessCloudComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [StartProcessCloudComponent, ReactiveFormsModule, StartProcessScreenCloudComponent], - providers: [provideScreen(screenId, MockedTaskScreenCloudComponent)] + providers: [ + provideScreen(screenId, MockedTaskScreenCloudComponent), + { provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } } + ] }); processService = TestBed.inject(StartProcessCloudService); formCloudService = TestBed.inject(FormCloudService); @@ -417,6 +420,54 @@ describe('StartProcessCloudComponent', () => { expect(startBtn.disabled).toBe(false); }); + it('should keep the start action unavailable while the form definition is loading', async () => { + const formDefinition = new Subject(); + formDefinitionSpy.and.returnValue(formDefinition); + typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form'); + await selectOptionByName('processwithform'); + + const startButton = fixture.nativeElement.querySelector('#button-start'); + expect(startButton).toBeNull(); + expect(startProcessWithFormSpy).not.toHaveBeenCalled(); + }); + + it('should materialize unrendered rich text expressions when starting a process', async () => { + formDefinitionSpy.and.returnValue(of(fakeStartForm)); + component.processDefinitionCurrent = fakeProcessDefinitions[2]; + component.processPayloadCloud.processDefinitionKey = fakeProcessDefinitions[2].key; + component.processInstanceName.setValue('My process'); + fixture.detectChanges(); + await fixture.whenStable(); + + const form = new FormModel({ + fields: [ + { + id: 'richText', + type: FormFieldTypes.DISPLAY_RICH_TEXT, + value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] } + }, + { id: 'name', type: FormFieldTypes.TEXT, value: 'John' } + ] + }); + const formElement = fixture.debugElement.query(By.css('adf-cloud-form')); + const startButton = fixture.debugElement.query(By.css('#button-start')); + + formElement.triggerEventHandler('formLoaded', form); + fixture.detectChanges(); + startButton.triggerEventHandler('click', null); + + expect(startProcessWithFormSpy).toHaveBeenCalledWith( + component.appName, + fakeProcessDefinitions[2].formKey, + fakeProcessDefinitions[2].version, + jasmine.objectContaining({ + values: jasmine.objectContaining({ + richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] } + }) + }) + ); + }); + it('should be able to start a process with form full display mode', async () => { component.displayModeConfigurations = [ { diff --git a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts index c06299d703..40126c5722 100755 --- a/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/start-process/components/start-process-cloud.component.ts @@ -30,10 +30,14 @@ import { ViewEncapsulation } from '@angular/core'; import { + ADF_DISPLAY_TEXT_SETTINGS, ConfirmDialogComponent, ContentLinkModel, + DisplayTextWidgetSettings, + FormExpressionService, FormModel, FormOutcomeEvent, + FormValues, IconModule, InplaceFormInputComponent, LocalizedDatePipe, @@ -65,6 +69,7 @@ import { FormCustomOutcomesComponent } from '../../../form/components/form-cloud import { MatDialog } from '@angular/material/dialog'; import { StartProcessScreenCloudComponent } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component'; import { TaskTypeResolverService } from '../../../services/task-type-resolver/task-type-resolver.service'; +import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../../../form/services/form-cloud-submission-values'; const MAX_NAME_LENGTH: number = 255; const PROCESS_DEFINITION_DEBOUNCE: number = 300; @@ -211,6 +216,11 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { private readonly hasVisibleOutcomesSubject = new BehaviorSubject(false); private readonly dialog = inject(MatDialog); private readonly taskTypeResolverService = inject(TaskTypeResolverService); + private readonly expressions = inject(FormExpressionService); + private readonly displayTextSettings = inject | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, { + optional: true + }); + private enableExpressionEvaluation = false; private screenSubmitPayload: unknown; @@ -218,8 +228,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { showCompleteButton = false; get isProcessFormValid(): boolean { - if (this.hasForm && this.isFormCloudLoaded) { - return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting; + if (this.hasForm) { + if (!this.isFormCloudLoaded || !this.formCloud) { + return false; + } + + return !Object.keys(this.formCloud.values).length || this.formCloud.isValid || this.isProcessStarting; } else if (this.hasScreen) { return true; } else { @@ -268,6 +282,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { constructor() { this.startProcessButtonLabel = this.defaultStartProcessButtonLabel; this.cancelButtonLabel = this.defaultCancelProcessButtonLabel; + + getExpressionEvaluationEnabled$(this.displayTextSettings) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((enabled) => { + this.enableExpressionEvaluation = enabled; + }); } ngOnInit() { @@ -482,6 +502,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { } startProcessWithoutConfirmation() { + let submissionValues = this.screenSubmitPayload; + if (this.hasForm) { + if (!this.formCloud) { + return; + } + submissionValues = this.getFormSubmissionValues(this.formCloud); + } + this.isProcessStarting = true; let action: Observable; @@ -495,7 +523,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { processName: this.processInstanceName.value, processDefinitionKey: this.processPayloadCloud.processDefinitionKey, variables: this.variables ?? {}, - values: this.hasForm ? this.formCloud.values : this.screenSubmitPayload, + values: submissionValues, outcome: this.customOutcomeName }) ); @@ -524,6 +552,10 @@ export class StartProcessCloudComponent implements OnChanges, OnInit { }); } + private getFormSubmissionValues(form: FormModel): FormValues { + return materializeSubmissionValues(form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions); + } + startProcess() { if (!this.formCloud?.confirmMessage?.show) { this.startProcessWithoutConfirmation(); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts new file mode 100644 index 0000000000..f1135da80e --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts @@ -0,0 +1,198 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ComponentRef, OnDestroy } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BaseScreenCloudComponent } from './base-screen-cloud.component'; +import { provideScreen } from '../../../services/provide-screen'; +import { ScreenRenderingService } from '../../../services/screen-rendering.service'; + +@Component({ + selector: 'adf-cloud-test-dynamic-screen', + template: `
dynamic screen
` +}) +class TestDynamicScreenComponent implements OnDestroy { + destroyed = false; + + ngOnDestroy(): void { + this.destroyed = true; + } +} + +@Component({ + selector: 'adf-cloud-test-host-screen', + template: `` +}) +class TestHostScreenComponent extends BaseScreenCloudComponent { + setInputsCalls: ComponentRef[] = []; + subscribeToOutputsCalls: ComponentRef[] = []; + + get dynamicComponentRef(): ComponentRef | undefined { + return this.componentRef; + } + + get dynamicComponentRefSignalValue(): ComponentRef | undefined { + return this.componentRefChanged(); + } + + protected override setInputsForDynamicComponent(componentRef: ComponentRef): void { + this.setInputsCalls.push(componentRef); + } + + protected subscribeToOutputs(componentRef: ComponentRef): void { + this.subscribeToOutputsCalls.push(componentRef); + } +} + +/** Same host component, but without the `#container` anchor in its template. */ +@Component({ + selector: 'adf-cloud-test-host-screen-without-container', + template: `
` +}) +class TestHostScreenWithoutContainerComponent extends TestHostScreenComponent {} + +describe('BaseScreenCloudComponent', () => { + const screenId = 'test-screen'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestHostScreenComponent, TestHostScreenWithoutContainerComponent, TestDynamicScreenComponent], + providers: [provideScreen(screenId, TestDynamicScreenComponent)] + }); + }); + + describe('when a screenId is provided', () => { + let fixture: ComponentFixture; + let component: TestHostScreenComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + fixture.detectChanges(); + }); + + it('should create the dynamic component and expose it through the signal', () => { + expect(component.dynamicComponentRef).toBeDefined(); + expect(component.dynamicComponentRefSignalValue).toBe(component.dynamicComponentRef); + expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeTruthy(); + }); + + it('should wire inputs and outputs once, passing the created component reference', () => { + expect(component.setInputsCalls).toEqual([component.dynamicComponentRef!]); + expect(component.subscribeToOutputsCalls).toEqual([component.dynamicComponentRef!]); + }); + + it('should destroy the dynamic component reference on destroy', () => { + const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy').and.callThrough(); + + fixture.destroy(); + + expect(destroySpy).toHaveBeenCalledTimes(1); + }); + + it('should run the ngOnDestroy hook of the dynamic component on destroy', () => { + const dynamicComponentInstance = component.dynamicComponentRef?.instance; + expect(dynamicComponentInstance?.destroyed).toBeFalse(); + + fixture.destroy(); + + expect(dynamicComponentInstance?.destroyed).toBeTrue(); + }); + + it('should clear the dynamic component reference and the signal on destroy', () => { + fixture.destroy(); + + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + }); + + it('should destroy the dynamic component reference only once when ngOnDestroy runs again', () => { + const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy'); + + component.ngOnDestroy(); + component.ngOnDestroy(); + + expect(destroySpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('when no screenId is provided', () => { + let fixture: ComponentFixture; + let component: TestHostScreenComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should not create any dynamic component nor wire inputs and outputs', () => { + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + expect(component.setInputsCalls).toEqual([]); + expect(component.subscribeToOutputsCalls).toEqual([]); + expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeNull(); + }); + + it('should not throw on destroy', () => { + expect(() => fixture.destroy()).not.toThrow(); + expect(component.dynamicComponentRef).toBeUndefined(); + }); + }); + + describe('when the container anchor is missing', () => { + let fixture: ComponentFixture; + let component: TestHostScreenWithoutContainerComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenWithoutContainerComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + }); + + it('should not throw and should not create any dynamic component', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + + expect(component.container).toBeUndefined(); + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + }); + + it('should not wire inputs and outputs when no dynamic component was created', () => { + fixture.detectChanges(); + + expect(component.setInputsCalls).toEqual([]); + expect(component.subscribeToOutputsCalls).toEqual([]); + }); + + it('should not resolve any component type', () => { + const resolveComponentTypeSpy = spyOn(TestBed.inject(ScreenRenderingService), 'resolveComponentType').and.callThrough(); + + fixture.detectChanges(); + + expect(resolveComponentTypeSpy).not.toHaveBeenCalled(); + }); + + it('should not throw on destroy', () => { + fixture.detectChanges(); + + expect(() => fixture.destroy()).not.toThrow(); + }); + }); +}); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts index b00f85c4da..7da72468dc 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts @@ -15,20 +15,20 @@ * limitations under the License. */ -import { Component, ComponentRef, inject, Input, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ComponentRef, inject, Input, OnDestroy, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core'; import { ScreenRenderingService } from '../../../services/screen-rendering.service'; @Component({ template: '' }) -export abstract class BaseScreenCloudComponent implements OnInit { +export abstract class BaseScreenCloudComponent implements OnInit, OnDestroy { @Input() screenId: string = ''; @ViewChild('container', { read: ViewContainerRef, static: true }) - container: ViewContainerRef; + container: ViewContainerRef | undefined; - protected componentRef: ComponentRef; + protected componentRef: ComponentRef | undefined; private readonly _componentRefChanged = signal | undefined>(undefined); protected readonly componentRefChanged = this._componentRefChanged.asReadonly(); protected readonly screenRenderingService = inject(ScreenRenderingService); @@ -37,17 +37,27 @@ export abstract class BaseScreenCloudComponent imple this.createDynamicComponent(); } - private createDynamicComponent(): void { - if (this.screenId) { - const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); - this.componentRef = this.container.createComponent(componentType); - this._componentRefChanged.set(this.componentRef); - this.setInputsForDynamicComponent(); - this.subscribeToOutputs(); - } + ngOnDestroy(): void { + this.componentRef?.destroy(); + this.componentRef = undefined; + this._componentRefChanged.set(undefined); } - protected setInputsForDynamicComponent(): void {} + private createDynamicComponent(): void { + if (!this.screenId || !this.container) { + return; + } - protected abstract subscribeToOutputs(): void; + const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); + const componentRef: ComponentRef = this.container.createComponent(componentType); + + this.componentRef = componentRef; + this._componentRefChanged.set(componentRef); + this.setInputsForDynamicComponent(componentRef); + this.subscribeToOutputs(componentRef); + } + + protected setInputsForDynamicComponent(_componentRef: ComponentRef): void {} + + protected abstract subscribeToOutputs(componentRef: ComponentRef): void; } diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts index 937d1ad536..da37f04b9e 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts @@ -16,6 +16,7 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, Input, OnDestroy, input, output } from '@angular/core'; import { StartProcessScreenCloudComponent } from './start-process-screen-cloud.component'; import { MockedTaskScreenCloudComponent } from '../../../../testing/start-process-screen-mock.component'; import { provideScreen } from '../../../services/provide-screen'; @@ -66,11 +67,11 @@ describe('StartProcessScreenCloudComponent', () => { it('should set appName', () => { const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; - expect(screenInstance.appName()).toEqual(''); + expect(screenInstance.appName?.()).toEqual(''); const newValue = 'new-app-name'; fixture.componentRef.setInput('appName', newValue); fixture.detectChanges(); - expect(screenInstance.appName()).toEqual(newValue); + expect(screenInstance.appName?.()).toEqual(newValue); }); it('should set process definition id', () => { @@ -84,10 +85,125 @@ describe('StartProcessScreenCloudComponent', () => { it('should set resolvedValues', () => { const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; - expect(screenInstance.resolvedValues()).toBeUndefined(); + expect(screenInstance.resolvedValues?.()).toBeUndefined(); const newValues = [new TaskVariableCloud({ id: 'new-id', name: 'new-name' })]; fixture.componentRef.setInput('resolvedValues', newValues); fixture.detectChanges(); - expect(screenInstance.resolvedValues()).toEqual(newValues); + expect(screenInstance.resolvedValues?.()).toEqual(newValues); + }); +}); + +@Component({ + selector: 'adf-cloud-destroy-tracking-screen', + template: `
screen
` +}) +class DestroyTrackingScreenComponent implements StartProcessScreenCloud, OnDestroy { + readonly appName = input(''); + processDefinitionId = input(''); + readonly resolvedValues = input(); + defaultStartProcessButtonsConfigurationChange = output(); + startProcessPayloadChanged = output(); + + destroyed = false; + + ngOnDestroy(): void { + this.destroyed = true; + } +} + +@Component({ + selector: 'adf-cloud-test-start-process-wrapper', + template: ` + @if (showScreen) { + + } + `, + imports: [StartProcessScreenCloudComponent] +}) +class TestStartProcessWrapperComponent { + @Input() screenId = ''; + showScreen = true; +} + +describe('StartProcessScreenCloudComponent - destroy', () => { + let fixture: ComponentFixture; + let component: TestStartProcessWrapperComponent; + const screenId = 'screen-1234-5678-121212-123456'; + + const getScreenInstance = (): DestroyTrackingScreenComponent => + fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent)).componentInstance; + + const destroyScreen = () => { + component.showScreen = false; + fixture.detectChanges(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestStartProcessWrapperComponent], + providers: [provideScreen(screenId, DestroyTrackingScreenComponent)] + }); + + fixture = TestBed.createComponent(TestStartProcessWrapperComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + fixture.detectChanges(); + }); + + it('should destroy the screen component when the host is destroyed', () => { + const screenInstance = getScreenInstance(); + expect(screenInstance.destroyed).toBeFalse(); + + destroyScreen(); + + expect(screenInstance.destroyed).toBeTrue(); + }); + + it('should remove the screen component from the DOM when the host is destroyed', () => { + expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeTruthy(); + + destroyScreen(); + + expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeNull(); + }); + + it('should create a new screen component instance when the host is re-created', () => { + const firstInstance = getScreenInstance(); + + destroyScreen(); + component.showScreen = true; + fixture.detectChanges(); + + const secondInstance = getScreenInstance(); + expect(secondInstance).not.toBe(firstInstance); + expect(secondInstance.destroyed).toBeFalse(); + expect(secondInstance.processDefinitionId()).toBe('definition-id'); + }); +}); + +describe('StartProcessScreenCloudComponent - without screenId', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [StartProcessScreenCloudComponent] + }); + fixture = TestBed.createComponent(StartProcessScreenCloudComponent); + }); + + it('should not create any screen component and should not throw', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent))).toBeNull(); + }); + + it('should not throw when inputs change or on destroy', () => { + fixture.detectChanges(); + + expect(() => { + fixture.componentRef.setInput('appName', 'new-app-name'); + fixture.componentRef.setInput('resolvedValues', [new TaskVariableCloud({ id: 'id', name: 'name' })]); + fixture.detectChanges(); + }).not.toThrow(); + expect(() => fixture.destroy()).not.toThrow(); }); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts index 0f9c94dc81..ce069b6cdb 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, effect, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ComponentRef, effect, input, output, signal } from '@angular/core'; import { BaseScreenCloudComponent } from '../base-screen/base-screen-cloud.component'; import { MatCardModule } from '@angular/material/card'; import { CommonModule } from '@angular/common'; @@ -43,7 +43,7 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent { const componentRef = this.componentRefChanged(); - if (componentRef.instance && 'appName' in componentRef.instance) { + if (componentRef?.instance && 'appName' in componentRef.instance) { componentRef.setInput('appName', this.appName()); } }); @@ -56,9 +56,9 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent this.screenStartProcessPayloadChange.emit(payload)); - this.componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => { + protected subscribeToOutputs(componentRef: ComponentRef): void { + componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload)); + componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => { this.showStartProcessButtons.set(config.show); this.disableStartProcessButton.emit(config.disable); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts index 3bf74e8816..e271a8a6e6 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts @@ -16,7 +16,7 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ScreenRenderingService } from '../../../services/screen-rendering.service'; import { TaskScreenCloudComponent } from './screen-cloud.component'; @@ -32,18 +32,22 @@ import { TaskScreenCloudComponent } from './screen-cloud.component';
` }) -class TestComponent { +class TestComponent implements OnDestroy { @Input() taskId = ''; @Input() screenId = ''; @Input() rootProcessInstanceId = ''; @Output() taskCompleted = new EventEmitter(); - displayMode: string; + displayMode: string | undefined; + destroyed = false; onComplete() { this.taskCompleted.emit(); } switchToDisplayMode(newDisplayMode?: string) { this.displayMode = newDisplayMode; } + ngOnDestroy(): void { + this.destroyed = true; + } } @Component({ @@ -61,7 +65,7 @@ class TestComponent { }) class TestWrapperComponent { @Input() screenId = ''; - @ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent; + @ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent | undefined; onTaskCompleted() {} switchToDisplayMode(newDisplayMode?: string): void { if (this.adfCloudTaskScreen) { @@ -118,6 +122,118 @@ describe('TaskScreenCloudComponent', () => { component.switchToDisplayMode(); fixture.detectChanges(); - expect(component.adfCloudTaskScreen.switchToDisplayMode).toHaveBeenCalled(); + expect(component.adfCloudTaskScreen?.switchToDisplayMode).toHaveBeenCalled(); + }); +}); + +@Component({ + selector: 'adf-cloud-test-conditional-component', + template: ` + @if (showTaskScreen) { + + } + `, + imports: [TaskScreenCloudComponent] +}) +class TestConditionalWrapperComponent { + showTaskScreen = true; + onTaskCompleted() {} +} + +describe('TaskScreenCloudComponent - destroy', () => { + let fixture: ComponentFixture; + let component: TestConditionalWrapperComponent; + + const getDynamicComponentInstance = (): TestComponent => fixture.debugElement.query(By.directive(TestComponent)).componentInstance; + + const getTaskScreen = (): TaskScreenCloudComponent => fixture.debugElement.query(By.directive(TaskScreenCloudComponent)).componentInstance; + + const destroyTaskScreen = () => { + component.showTaskScreen = false; + fixture.detectChanges(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TaskScreenCloudComponent, TestComponent, TestConditionalWrapperComponent] + }); + TestBed.inject(ScreenRenderingService).register({ ['test']: () => TestComponent }); + + fixture = TestBed.createComponent(TestConditionalWrapperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should destroy the dynamic component when the task screen is destroyed', () => { + const dynamicComponentInstance = getDynamicComponentInstance(); + expect(dynamicComponentInstance.destroyed).toBeFalse(); + + destroyTaskScreen(); + + expect(dynamicComponentInstance.destroyed).toBeTrue(); + }); + + it('should remove the dynamic component from the DOM when the task screen is destroyed', () => { + expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeTruthy(); + + destroyTaskScreen(); + + expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeNull(); + }); + + it('should not emit outputs of the dynamic component after the task screen is destroyed', () => { + const onTaskCompletedSpy = spyOn(component, 'onTaskCompleted'); + const dynamicComponentInstance = getDynamicComponentInstance(); + + destroyTaskScreen(); + dynamicComponentInstance.taskCompleted.emit(); + + expect(onTaskCompletedSpy).not.toHaveBeenCalled(); + }); + + it('should not call the dynamic component when switching display mode after destroy', () => { + const taskScreen = getTaskScreen(); + const switchToDisplayModeSpy = spyOn(getDynamicComponentInstance(), 'switchToDisplayMode'); + + destroyTaskScreen(); + + expect(() => taskScreen.switchToDisplayMode('mode')).not.toThrow(); + expect(switchToDisplayModeSpy).not.toHaveBeenCalled(); + }); + + it('should create a new dynamic component instance when the task screen is re-created', () => { + const firstInstance = getDynamicComponentInstance(); + + destroyTaskScreen(); + component.showTaskScreen = true; + fixture.detectChanges(); + + const secondInstance = getDynamicComponentInstance(); + expect(secondInstance).not.toBe(firstInstance); + expect(secondInstance.destroyed).toBeFalse(); + expect(secondInstance.taskId).toBe('1'); + }); +}); + +describe('TaskScreenCloudComponent - without screenId', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TaskScreenCloudComponent] + }); + fixture = TestBed.createComponent(TaskScreenCloudComponent); + }); + + it('should not create any dynamic component and should not throw', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.debugElement.query(By.directive(TestComponent))).toBeNull(); + }); + + it('should not throw when switching display mode or destroying', () => { + fixture.detectChanges(); + + expect(() => fixture.componentInstance.switchToDisplayMode('mode')).not.toThrow(); + expect(() => fixture.destroy()).not.toThrow(); }); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts index 14ad0b5893..5069d34735 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts @@ -16,7 +16,7 @@ */ import { CommonModule } from '@angular/common'; -import { Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; +import { Component, ComponentRef, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { UserTaskCustomUi } from './screen-cloud.model'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -102,65 +102,65 @@ export class TaskScreenCloudComponent extends BaseScreenCloudComponent): void { + if (this.taskId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskId')) { + componentRef.setInput('taskId', this.taskId); } - if (this.appName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'appName')) { - this.componentRef.setInput('appName', this.appName); + if (this.appName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'appName')) { + componentRef.setInput('appName', this.appName); } - if (this.screenId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'screenId')) { - this.componentRef.setInput('screenId', this.screenId); + if (this.screenId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'screenId')) { + componentRef.setInput('screenId', this.screenId); } - if (this.processInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'processInstanceId')) { - this.componentRef.setInput('processInstanceId', this.processInstanceId); + if (this.processInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'processInstanceId')) { + componentRef.setInput('processInstanceId', this.processInstanceId); } - if (this.taskName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskName')) { - this.componentRef.setInput('taskName', this.taskName); + if (this.taskName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskName')) { + componentRef.setInput('taskName', this.taskName); } - if (this.canClaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canClaimTask')) { - this.componentRef.setInput('canClaimTask', this.canClaimTask); + if (this.canClaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canClaimTask')) { + componentRef.setInput('canClaimTask', this.canClaimTask); } - if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canUnclaimTask')) { - this.componentRef.setInput('canUnclaimTask', this.canUnclaimTask); + if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canUnclaimTask')) { + componentRef.setInput('canUnclaimTask', this.canUnclaimTask); } - if (this.showCancelButton && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showCancelButton')) { - this.componentRef.setInput('showCancelButton', this.showCancelButton); + if (this.showCancelButton && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showCancelButton')) { + componentRef.setInput('showCancelButton', this.showCancelButton); } - if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'rootProcessInstanceId')) { - this.componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId); + if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'rootProcessInstanceId')) { + componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId); } - if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showNextTaskCheckbox')) { - this.componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox); + if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showNextTaskCheckbox')) { + componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox); } - if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'isNextTaskCheckboxChecked')) { - this.componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked); + if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(componentRef.instance, 'isNextTaskCheckboxChecked')) { + componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked); } } - protected override subscribeToOutputs(): void { - if (this.componentRef.instance?.taskSaved) { - this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit()); + protected override subscribeToOutputs(componentRef: ComponentRef): void { + if (componentRef.instance?.taskSaved) { + componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit()); } - if (this.componentRef.instance?.taskCompleted) { - this.componentRef.instance.taskCompleted + if (componentRef.instance?.taskCompleted) { + componentRef.instance.taskCompleted .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((openNextTask) => this.taskCompleted.emit(openNextTask)); } - if (this.componentRef.instance?.error) { - this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data)); + if (componentRef.instance?.error) { + componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data)); } - if (this.componentRef.instance?.claimTask) { - this.componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data)); + if (componentRef.instance?.claimTask) { + componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data)); } - if (this.componentRef.instance?.unclaimTask) { - this.componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data)); + if (componentRef.instance?.unclaimTask) { + componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data)); } - if (this.componentRef.instance?.cancelTask) { - this.componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data)); + if (componentRef.instance?.cancelTask) { + componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data)); } - if (this.componentRef.instance?.nextTaskCheckboxCheckedChanged) { - this.componentRef.instance.nextTaskCheckboxCheckedChanged + if (componentRef.instance?.nextTaskCheckboxCheckedChanged) { + componentRef.instance.nextTaskCheckboxCheckedChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => this.nextTaskCheckboxCheckedChanged.emit(data)); } diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts new file mode 100644 index 0000000000..c0846418f6 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts @@ -0,0 +1,651 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; +import { BehaviorSubject, combineLatest, firstValueFrom, Observable, of, Subject, throwError } from 'rxjs'; +import { ApolloTestingModule } from 'apollo-angular/testing'; +import { FilterCountersCloudService } from './filter-counters-cloud.service'; +import { NotificationCloudService } from './notification-cloud.service'; +import { LocalPreferenceCloudService } from './local-preference-cloud.service'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from './cloud-token.service'; +import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service'; +import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service'; +import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; +import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model'; +import { + FilterCounterEntityType, + FilterCounters, + FilterCountersQuery, + FilterCountersRequest, + FilterCountersResult +} from '../models/filter-counters-cloud.model'; +import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model'; +import { FetchResult } from '@apollo/client/core'; + +type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>; + +interface CountEndpoint { + post: (url: string, request: FilterCountersRequest) => Observable; +} + +describe('FilterCountersCloudService', () => { + let service: FilterCountersCloudService; + let notificationCloudService: NotificationCloudService; + let appConfigService: AppConfigService; + let taskEvents$: Subject; + let processEvents$: Subject; + let makeGQLQuerySpy: jasmine.Spy; + let postSpy: jasmine.Spy; + let getTaskListFiltersSpy: jasmine.Spy; + let getProcessFiltersSpy: jasmine.Spy; + + const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1]; + const countUrl = (): string => postSpy.calls.mostRecent().args[0]; + const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? []; + const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId); + + const countersMock: FilterCounters = { + TASK: { 'my-tasks': 5, 'queued-tasks': 0 }, + PROCESS_INSTANCE: { 'running-processes': 5 } + }; + + const taskFilter = (filter: Partial) => + new TaskFilterCloudModel({ appName: 'mock-app', sort: 'createdDate', order: 'DESC', ...filter }); + const processFilter = (filter: Partial) => + new ProcessFilterCloudModel({ appName: 'mock-app', sort: 'startDate', order: 'DESC', ...filter }); + + const taskFiltersMock = [ + taskFilter({ key: 'my-tasks', status: 'ASSIGNED', assignee: 'mock-user', showCounter: true }), + taskFilter({ key: 'queued-tasks', status: 'CREATED', showCounter: true }), + taskFilter({ key: 'completed-tasks', status: 'COMPLETED', showCounter: false }) + ]; + const processFiltersMock = [ + processFilter({ key: 'running-processes', status: 'RUNNING', showCounter: true }), + processFilter({ key: 'all-processes', status: '', showCounter: false }) + ]; + + const engineEvents = (eventType: string): EngineEventsResult => ({ + data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] } + }); + const emitTaskEvent = (eventType = 'TASK_CREATED') => taskEvents$.next(engineEvents(eventType)); + const emitProcessEvent = (eventType = 'PROCESS_STARTED') => processEvents$.next(engineEvents(eventType)); + + const counters = (entityType: FilterCounterEntityType, appName = 'mock-app') => + firstValueFrom(service.getFilterCounters(appName, entityType, true)); + const taskCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.TASK, appName); + const processCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.PROCESS_INSTANCE, appName); + const bothCounters = (appName = 'mock-app') => + firstValueFrom( + combineLatest([ + service.getFilterCounters(appName, FilterCounterEntityType.TASK, true), + service.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, true) + ]) + ); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAuthModule, ApolloTestingModule], + providers: [ + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService } + ] + }); + + service = TestBed.inject(FilterCountersCloudService); + notificationCloudService = TestBed.inject(NotificationCloudService); + appConfigService = TestBed.inject(AppConfigService); + appConfigService.config.bpmHost = 'https://fake-bpm-host.com'; + + taskEvents$ = new Subject(); + processEvents$ = new Subject(); + makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery'); + makeGQLQuerySpy.and.callFake((_appName: string, query: string) => + (query.includes('TASK_CREATED') ? taskEvents$ : processEvents$).asObservable() + ); + postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); + getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); + getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock)); + }); + + describe('getTaskFilters / getProcessFilters', () => { + it('should load the filters of every entity type', async () => { + expect(await firstValueFrom(service.getTaskFilters('mock-app'))).toEqual(taskFiltersMock); + expect(await firstValueFrom(service.getProcessFilters('mock-app'))).toEqual(processFiltersMock); + }); + + it('should load the filters of an app once for concurrent subscribers', async () => { + await firstValueFrom(combineLatest([service.getTaskFilters('mock-app'), service.getTaskFilters('mock-app')])); + await firstValueFrom(combineLatest([service.getProcessFilters('mock-app'), service.getProcessFilters('mock-app')])); + + expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1); + expect(getProcessFiltersSpy).toHaveBeenCalledTimes(1); + }); + + it('should load the filters of every app', async () => { + await firstValueFrom(service.getTaskFilters('mock-app')); + await firstValueFrom(service.getTaskFilters('other-app')); + + expect(getTaskListFiltersSpy.calls.allArgs()).toEqual([['mock-app'], ['other-app']]); + }); + + it('should share the filters with the batched count request', async () => { + const subscription = service.getTaskFilters('mock-app').subscribe(); + await taskCounters(); + subscription.unsubscribe(); + + expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1); + }); + + it('should propagate the error of the filters that fail to load', async () => { + getTaskListFiltersSpy.and.returnValue(throwError(() => new Error('filters failed'))); + + await expectAsync(firstValueFrom(service.getTaskFilters('mock-app'))).toBeRejectedWithError('filters failed'); + }); + }); + + describe('getFilterCounters', () => { + it('should return EMPTY when appName is not set', () => { + let completed = false; + service.getFilterCounters('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBeTrue(); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should resolve the counters of both entity types with a single request', async () => { + expect(await bothCounters()).toEqual([ + { counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }, + { counters: { 'running-processes': 5 }, batched: true } + ]); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the batched count endpoint of the app', async () => { + await taskCounters(); + + expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count'); + }); + + it('should identify the query of every filter by the key of the filter', async () => { + await bothCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']); + }); + + it('should send the criteria of every filter along with its request id', async () => { + await taskCounters(); + + expect(countQueries(FilterCounterEntityType.TASK)[0]).toEqual({ + requestId: 'my-tasks', + status: ['ASSIGNED'], + assignee: ['mock-user'], + sort: { field: 'createdDate', direction: 'desc', isProcessVariable: false } + }); + }); + + it('should not send the filters without a counter enabled', async () => { + await taskCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).not.toContain('completed-tasks'); + }); + + it('should send the query of a filter targeting every status', async () => { + getProcessFiltersSpy.and.returnValue(of([processFilter({ key: 'all-processes', status: '', showCounter: true })])); + + await processCounters(); + + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['all-processes']); + }); + + it('should omit an entity type without filters with a counter enabled', async () => { + getProcessFiltersSpy.and.returnValue(of([])); + + await bothCounters(); + + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + + it('should leave out a filter the query cannot be built for', async () => { + getTaskListFiltersSpy.and.returnValue( + of([taskFilter({ key: 'broken', status: 'ASSIGNED', showCounter: true, sort: undefined, order: undefined }), taskFiltersMock[1]]) + ); + + await taskCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['queued-tasks']); + }); + + it('should leave out a filter without a key, since it holds no request id', async () => { + getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })])); + + expect(await processCounters()).toEqual({ counters: {}, batched: true }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => { + getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed'))); + + await bothCounters(); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + + it('should resolve no counter when no filter has a counter enabled', async () => { + getTaskListFiltersSpy.and.returnValue(of([])); + getProcessFiltersSpy.and.returnValue(of([])); + + expect(await taskCounters()).toEqual({ counters: {}, batched: true }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + describe('when the batched count endpoint is not available', () => { + it('should report the counters as not batched', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + + expect(await taskCounters()).toEqual({ counters: {}, batched: false }); + }); + + it('should not call the endpoint again for the same app', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + + await taskCounters(); + expect(await processCounters()).toEqual({ counters: {}, batched: false }); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should keep calling the endpoint of the apps that do hold it', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 404 }))); + await taskCounters(); + + postSpy.and.returnValue(of(countersMock)); + expect(await taskCounters('other-app')).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + }); + + it('should keep calling the endpoint after a transient failure', async () => { + postSpy.and.returnValue(throwError(() => ({ status: 500 }))); + expect(await taskCounters()).toEqual({ counters: {}, batched: false }); + + postSpy.and.returnValue(of(countersMock)); + expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + expect(postSpy).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('batched counters opted in by the filter components', () => { + it('should not call the batched count endpoint when it was not asked for', async () => { + const result = await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(result).toEqual({ counters: {}, batched: false }); + expect(postSpy).not.toHaveBeenCalled(); + }); + + it('should not load the filters when the batched count endpoint was not asked for', async () => { + await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(getTaskListFiltersSpy).not.toHaveBeenCalled(); + }); + + it('should call the batched count endpoint when every entity type on screen asked for it', async () => { + await bothCounters(); + + expect(postSpy).toHaveBeenCalledTimes(1); + }); + + it('should not call the batched count endpoint when one entity type on screen did not ask for it', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe(); + tick(0); + + expect(postSpy).not.toHaveBeenCalled(); + expect(results).toEqual([{ counters: {}, batched: false }]); + })); + + it('should call the batched count endpoint once the entity type that opted out leaves the screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + const processSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe(); + tick(0); + + processSubscription.unsubscribe(); + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(1); + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + })); + }); + + describe('counters scoped to the entity types on screen', () => { + it('should send the queries of the entity type on screen alone', async () => { + await taskCounters(); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + }); + + it('should not load the filters of an entity type that is not on screen', async () => { + await taskCounters(); + + expect(getTaskListFiltersSpy).toHaveBeenCalled(); + expect(getProcessFiltersSpy).not.toHaveBeenCalled(); + }); + + it('should send the queries of both entity types when both are on screen', async () => { + await bothCounters(); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); + }); + + it('should resolve the counters again when an entity type joins the ones on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]); + + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(2); + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); + })); + + it('should stop covering an entity type once its counters hold no subscriber', fakeAsync(() => { + const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + taskSubscription.unsubscribe(); + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.PROCESS_INSTANCE]); + })); + }); + + describe('teardown', () => { + it('should close the engine event subscription once the counters hold no subscriber', fakeAsync(() => { + const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + + subscription.unsubscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + })); + + it('should keep the engine event subscription while another subscriber holds the same entity type', fakeAsync(() => { + const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + subscription.unsubscribe(); + emitTaskEvent(); + tick(3000); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledTimes(2); + })); + + it('should release the filters subscription once nothing reads them', fakeAsync(() => { + const filters$ = new BehaviorSubject(taskFiltersMock); + getTaskListFiltersSpy.and.returnValue(filters$.asObservable()); + + const subscriptions = [ + service.getTaskFilters('mock-app').subscribe(), + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe() + ]; + tick(0); + expect(filters$.observed).toBeTrue(); + + subscriptions.forEach((subscription) => subscription.unsubscribe()); + + expect(filters$.observed).toBeFalse(); + })); + + it('should resolve the counters again for a subscriber that comes after a full teardown', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe().unsubscribe(); + tick(0); + postSpy.calls.reset(); + + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + }); + + describe('refreshFilterCounters', () => { + it('should resolve the counters again with a single request', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).toHaveBeenCalledTimes(2); + expect(results.length).toBe(2); + })); + + it('should not resolve the counters of an app without subscribers', fakeAsync(() => { + service.refreshFilterCounters('mock-app'); + tick(0); + + expect(postSpy).not.toHaveBeenCalled(); + })); + }); + + describe('when only one of the two filter families is wired', () => { + const configureTasksOnly = () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [NoopAuthModule, ApolloTestingModule], + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + }); + + const tasksOnlyService = TestBed.inject(FilterCountersCloudService); + TestBed.inject(AppConfigService).config.bpmHost = 'https://fake-bpm-host.com'; + spyOn(TestBed.inject(NotificationCloudService), 'makeGQLQuery').and.returnValue(new Subject().asObservable()); + spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); + postSpy = spyOn(tasksOnlyService as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); + + return tasksOnlyService; + }; + + it('should resolve the counters of the wired family', async () => { + const tasksOnlyService = configureTasksOnly(); + + const result = await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true)); + + expect(result).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + }); + + it('should leave the filters of the family that is not wired out of the request', async () => { + const tasksOnlyService = configureTasksOnly(); + + await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true)); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + }); + + describe('getEngineEvents', () => { + it('should return EMPTY when appName is not set', () => { + let completed = false; + service.getEngineEvents('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBeTrue(); + expect(makeGQLQuerySpy).not.toHaveBeenCalled(); + }); + + it('should subscribe to the events of the task entity type alone', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + + const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args; + expect(appName).toBe('mock-app'); + expect(query).toContain('TASK_CREATED'); + expect(query).not.toContain('PROCESS_STARTED'); + }); + + it('should subscribe to the events of the process entity type alone', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe(); + + const [, query] = makeGQLQuerySpy.calls.mostRecent().args; + expect(query).toContain('PROCESS_STARTED'); + expect(query).not.toContain('TASK_CREATED'); + }); + + it('should open a single subscription for multiple subscribers of the same entity type', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + }); + + it('should open a separate subscription per entity type', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + }); + + it('should open a separate subscription per app', () => { + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(); + service.getEngineEvents('other-app', FilterCounterEntityType.TASK).subscribe(); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2); + }); + + it('should emit the debounced batch of events', fakeAsync(() => { + const batches: TaskCloudEngineEvent[][] = []; + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe((events) => batches.push(events)); + + emitTaskEvent('TASK_CREATED'); + emitTaskEvent('TASK_ASSIGNED'); + tick(3000); + + expect(batches.length).toBe(1); + expect(batches[0][0].eventType).toBe('TASK_ASSIGNED'); + })); + + it('should debounce the events using the configured debounce time', fakeAsync(() => { + spyOnProperty(service, 'notificationDebounceTime', 'get').and.returnValue(5000); + let emitted = false; + service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(() => (emitted = true)); + + emitTaskEvent(); + tick(3000); + expect(emitted).toBeFalse(); + + tick(2000); + expect(emitted).toBeTrue(); + })); + }); + + describe('counters driven by the engine events', () => { + it('should make a single count request for a batch of events of both entity types', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitTaskEvent(); + emitProcessEvent(); + tick(3000); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + + it('should make a single count request for the events of both entity types arriving apart', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitTaskEvent(); + tick(1000); + emitProcessEvent(); + tick(3000); + + expect(postSpy).toHaveBeenCalledTimes(1); + })); + + it('should emit the counters resolved for the batch of events', fakeAsync(() => { + const results: FilterCountersResult[] = []; + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result)); + tick(0); + + postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } })); + emitTaskEvent(); + tick(3000); + + expect(results.length).toBe(2); + expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true }); + })); + + it('should not subscribe to the events of an entity type that is not on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + + expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1); + expect(makeGQLQuerySpy.calls.mostRecent().args[1]).toContain('TASK_CREATED'); + })); + + it('should not resolve the counters again on the events of an entity type that is not on screen', fakeAsync(() => { + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(0); + postSpy.calls.reset(); + + emitProcessEvent(); + tick(3000); + + expect(postSpy).not.toHaveBeenCalled(); + })); + + it('should stop resolving the counters on the events of an entity type that left the screen', fakeAsync(() => { + const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe(); + tick(0); + + taskSubscription.unsubscribe(); + postSpy.calls.reset(); + emitTaskEvent(); + tick(3000); + + expect(postSpy).not.toHaveBeenCalled(); + })); + + it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => { + appConfigService.config.notifications = false; + + service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe(); + tick(3000); + + expect(makeGQLQuerySpy).not.toHaveBeenCalled(); + expect(postSpy).toHaveBeenCalledTimes(1); + })); + }); +}); diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts new file mode 100644 index 0000000000..0d0efb20dc --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts @@ -0,0 +1,375 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject, Injectable, Injector } from '@angular/core'; +import { asapScheduler, combineLatest, defer, EMPTY, merge, Observable, of, Subject, Subscription } from 'rxjs'; +import { catchError, debounceTime, finalize, map, shareReplay, switchMap, take } from 'rxjs/operators'; +import { BaseCloudService } from './base-cloud.service'; +import { NotificationCloudService } from './notification-cloud.service'; +import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model'; +import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service'; +import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service'; +import { TaskListCloudService } from '../task/task-list/services/task-list-cloud.service'; +import { ProcessListCloudService } from '../process/process-list/services/process-list-cloud.service'; +import { TaskFilterCloudAdapter } from '../models/filter-cloud-model'; +import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; +import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model'; +import { ProcessFilterCloudAdapter } from '../process/process-list/models/process-cloud-query-request.model'; +import { + FilterCounterCandidate, + FilterCounterEntityType, + FilterCounters, + FilterCountersQuery, + FilterCountersRequest, + FilterCountersResult +} from '../models/filter-counters-cloud.model'; +const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501]; + +interface FilterCountersFilters { + [FilterCounterEntityType.TASK]: TaskFilterCloudModel[]; + [FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[]; +} + +interface EngineEventsData { + engineEvents?: TaskCloudEngineEvent[]; +} + +const ENGINE_EVENTS_SUBSCRIPTION_QUERIES: Record = { + [FilterCounterEntityType.TASK]: ` + subscription { + engineEvents(eventType: [ + TASK_COMPLETED + TASK_ASSIGNED + TASK_ACTIVATED + TASK_SUSPENDED + TASK_CANCELLED + TASK_CREATED + ]) { + eventType + entity + } + } +`, + [FilterCounterEntityType.PROCESS_INSTANCE]: ` + subscription { + engineEvents(eventType: [ + PROCESS_CANCELLED + PROCESS_COMPLETED + PROCESS_CREATED + PROCESS_RESUMED + PROCESS_SUSPENDED + PROCESS_STARTED + ]) { + eventType + entity + } + } +` +}; + +@Injectable({ providedIn: 'root' }) +export class FilterCountersCloudService extends BaseCloudService { + private readonly notificationCloudService = inject(NotificationCloudService); + private readonly taskListCloudService = inject(TaskListCloudService); + private readonly processListCloudService = inject(ProcessListCloudService); + private readonly injector = inject(Injector); + + private readonly eventsPerEntityType = new Map>(); + private readonly rawEventsPerEntityType = new Map>(); + private readonly recountPerApp = new Map>(); + private readonly eventRecountPerApp = new Map>(); + private readonly activeEntityTypesPerApp = new Map>(); + private readonly subscribersPerEntityType = new Map(); + private readonly batchedCountersPerEntityType = new Map(); + private readonly eventSubscriptionsPerEntityType = new Map(); + private readonly appsWithoutBatchedCounters = new Set(); + private readonly taskFiltersPerApp = new Map>(); + private readonly processFiltersPerApp = new Map>(); + private readonly countersPerApp = new Map>(); + + get notificationDebounceTime(): number { + return this.appConfigService.get('notificationDebounceTime', 3000); + } + + getTaskFilters(appName: string): Observable { + return this.shareFilters(this.taskFiltersPerApp, appName, () => this.injector.get(TaskFilterCloudService).getTaskListFilters(appName)); + } + + getProcessFilters(appName: string): Observable { + return this.shareFilters(this.processFiltersPerApp, appName, () => this.injector.get(ProcessFilterCloudService).getProcessFilters(appName)); + } + + getFilterCounters(appName: string, entityType: FilterCounterEntityType, batchedCounters = false): Observable { + if (!appName) { + return EMPTY; + } + + return defer(() => { + this.activateEntityType(appName, entityType, batchedCounters); + + return this.getCounters(appName); + }).pipe( + map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })), + finalize(() => this.deactivateEntityType(appName, entityType)) + ); + } + + refreshFilterCounters(appName: string): void { + this.recount(appName); + } + + getEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable { + if (!appName) { + return EMPTY; + } + + const key = this.entityTypeKey(appName, entityType); + let events$ = this.eventsPerEntityType.get(key); + if (!events$) { + events$ = this.rawEngineEvents(appName, entityType).pipe( + debounceTime(this.notificationDebounceTime), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.eventsPerEntityType.set(key, events$); + } + + return events$; + } + + private rawEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable { + const key = this.entityTypeKey(appName, entityType); + let events$ = this.rawEventsPerEntityType.get(key); + if (!events$) { + events$ = defer(() => + this.notificationCloudService.makeGQLQuery(appName, ENGINE_EVENTS_SUBSCRIPTION_QUERIES[entityType]) + ).pipe( + map((result) => result.data?.engineEvents ?? []), + catchError(() => EMPTY), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.rawEventsPerEntityType.set(key, events$); + } + + return events$; + } + + private get notificationsEnabled(): boolean { + return this.appConfigService.get('notifications', true); + } + + private activateEntityType(appName: string, entityType: FilterCounterEntityType, batchedCounters: boolean): void { + const key = this.entityTypeKey(appName, entityType); + const subscribers = (this.subscribersPerEntityType.get(key) ?? 0) + 1; + this.subscribersPerEntityType.set(key, subscribers); + + if (subscribers > 1) { + return; + } + + this.batchedCountersPerEntityType.set(key, batchedCounters); + + const activeEntityTypes = this.activeEntityTypes(appName); + const joinsResolvedCounters = activeEntityTypes.size > 0; + activeEntityTypes.add(entityType); + + if (this.notificationsEnabled) { + this.eventSubscriptionsPerEntityType.set( + key, + this.rawEngineEvents(appName, entityType).subscribe(() => this.eventRecountTrigger(appName).next()) + ); + } + + if (joinsResolvedCounters) { + this.recount(appName); + } + } + + private deactivateEntityType(appName: string, entityType: FilterCounterEntityType): void { + const key = this.entityTypeKey(appName, entityType); + const subscribers = (this.subscribersPerEntityType.get(key) ?? 1) - 1; + + if (subscribers > 0) { + this.subscribersPerEntityType.set(key, subscribers); + return; + } + + this.subscribersPerEntityType.delete(key); + this.batchedCountersPerEntityType.delete(key); + this.activeEntityTypes(appName).delete(entityType); + this.eventSubscriptionsPerEntityType.get(key)?.unsubscribe(); + this.eventSubscriptionsPerEntityType.delete(key); + } + + private activeEntityTypes(appName: string): Set { + let activeEntityTypes = this.activeEntityTypesPerApp.get(appName); + if (!activeEntityTypes) { + activeEntityTypes = new Set(); + this.activeEntityTypesPerApp.set(appName, activeEntityTypes); + } + + return activeEntityTypes; + } + + private entityTypeKey(appName: string, entityType: FilterCounterEntityType): string { + return `${appName}|${entityType}`; + } + + private recount(appName: string): void { + this.recountTrigger(appName).next(); + } + + private getFiltersForCounters(appName: string): Observable { + const activeEntityTypes = this.activeEntityTypes(appName); + + return combineLatest({ + [FilterCounterEntityType.TASK]: activeEntityTypes.has(FilterCounterEntityType.TASK) + ? this.getTaskFilters(appName).pipe(catchError(() => of([]))) + : of([]), + [FilterCounterEntityType.PROCESS_INSTANCE]: activeEntityTypes.has(FilterCounterEntityType.PROCESS_INSTANCE) + ? this.getProcessFilters(appName).pipe(catchError(() => of([]))) + : of([]) + }); + } + + private shareFilters(cache: Map>, appName: string, loadFilters: () => Observable): Observable { + let filters$ = cache.get(appName); + if (!filters$) { + filters$ = defer(loadFilters).pipe(shareReplay({ bufferSize: 1, refCount: true })); + cache.set(appName, filters$); + } + + return filters$; + } + + private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> { + let counters$ = this.countersPerApp.get(appName); + if (!counters$) { + counters$ = this.recounts(appName).pipe( + switchMap(() => this.resolveCounters(appName)), + shareReplay({ bufferSize: 1, refCount: true }) + ); + this.countersPerApp.set(appName, counters$); + } + + return counters$; + } + + private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> { + if (!this.batchedCountersEnabled(appName) || this.appsWithoutBatchedCounters.has(appName)) { + return of({ counters: {}, batched: false }); + } + + return this.getFiltersForCounters(appName).pipe( + take(1), + switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))), + map((counters) => ({ counters, batched: true })), + catchError((error) => { + if (BATCHED_COUNTERS_UNAVAILABLE_STATUSES.includes(error?.status)) { + this.appsWithoutBatchedCounters.add(appName); + } + + return of({ counters: {}, batched: false }); + }) + ); + } + + private batchedCountersEnabled(appName: string): boolean { + const activeEntityTypes = [...this.activeEntityTypes(appName)]; + + return ( + activeEntityTypes.length > 0 && + activeEntityTypes.every((entityType) => this.batchedCountersPerEntityType.get(this.entityTypeKey(appName, entityType))) + ); + } + + private recounts(appName: string): Observable { + return merge( + merge(of(undefined), this.recountTrigger(appName)).pipe(debounceTime(0, asapScheduler)), + this.eventRecountTrigger(appName).pipe(debounceTime(this.notificationDebounceTime)) + ); + } + + private recountTrigger(appName: string): Subject { + let recount$ = this.recountPerApp.get(appName); + if (!recount$) { + recount$ = new Subject(); + this.recountPerApp.set(appName, recount$); + } + + return recount$; + } + + private eventRecountTrigger(appName: string): Subject { + let eventRecount$ = this.eventRecountPerApp.get(appName); + if (!eventRecount$) { + eventRecount$ = new Subject(); + this.eventRecountPerApp.set(appName, eventRecount$); + } + + return eventRecount$; + } + + private buildRequest(filters: FilterCountersFilters): FilterCountersRequest { + const request: FilterCountersRequest = {}; + + const taskQueries = this.buildQueries(filters[FilterCounterEntityType.TASK], (filter) => + this.taskListCloudService.buildQueryData(new TaskFilterCloudAdapter(filter)) + ); + if (taskQueries.length) { + request[FilterCounterEntityType.TASK] = taskQueries; + } + + const processQueries = this.buildQueries(filters[FilterCounterEntityType.PROCESS_INSTANCE], (filter) => + this.processListCloudService.buildQueryData(new ProcessFilterCloudAdapter(filter)) + ); + if (processQueries.length) { + request[FilterCounterEntityType.PROCESS_INSTANCE] = processQueries; + } + + return request; + } + + private buildQueries( + filters: T[], + buildQuery: (filter: T) => Omit + ): FilterCountersQuery[] { + return (filters ?? []) + .filter((filter) => filter?.showCounter && this.isCounterBatched(filter)) + .map((filter) => { + try { + return { ...buildQuery(filter), requestId: filter.key as string }; + } catch { + return undefined; + } + }) + .filter((query): query is FilterCountersQuery => !!query); + } + + private fetchFilterCounters(appName: string, request: FilterCountersRequest): Observable { + if (!Object.keys(request).length) { + return of({}); + } + + const queryUrl = `${this.getBasePath(appName)}/query/v1/count`; + + return this.post(queryUrl, request).pipe(map((counters) => counters || {})); + } + + private isCounterBatched(filter: FilterCounterCandidate): boolean { + return !!filter?.key; + } +} diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts index ae0cfe68fc..7b5a46571a 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts @@ -15,8 +15,9 @@ * limitations under the License. */ -import { gql } from '@apollo/client/core'; +import { FetchResult, gql } from '@apollo/client/core'; import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; import { WebSocketService } from './web-socket.service'; @Injectable({ providedIn: 'root' @@ -24,8 +25,8 @@ import { WebSocketService } from './web-socket.service'; export class NotificationCloudService { private readonly webSocketService = inject(WebSocketService); - makeGQLQuery(appName: string, gqlQuery: string) { - return this.webSocketService.getSubscription({ + makeGQLQuery(appName: string, gqlQuery: string): Observable> { + return this.webSocketService.getSubscription({ apolloClientName: appName, wsUrl: `${appName}/notifications`, httpUrl: `${appName}/notifications/v2/ws/graphql`, diff --git a/lib/process-services-cloud/src/lib/services/public-api.ts b/lib/process-services-cloud/src/lib/services/public-api.ts index e3ab7c3b05..e300e35a83 100644 --- a/lib/process-services-cloud/src/lib/services/public-api.ts +++ b/lib/process-services-cloud/src/lib/services/public-api.ts @@ -17,6 +17,7 @@ export * from './base-cloud.service'; export * from './cloud-token.service'; +export * from './filter-counters-cloud.service'; export * from './form-fields.interfaces'; export * from './local-preference-cloud.service'; export * from './notification-cloud.service'; diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts index 5fb1f21daf..6fda86d7a0 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts @@ -17,10 +17,10 @@ import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; import { Component, SimpleChange } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { first, of, Subject, throwError } from 'rxjs'; -import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; +import { first, NEVER, of, Subject, throwError } from 'rxjs'; +import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service'; import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock'; import { TaskFilterCloudService } from '../../services/task-filter-cloud.service'; @@ -35,6 +35,9 @@ import { TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { MatIconHarness } from '@angular/material/icon/testing'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; +import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) class DummyComponent {} @@ -50,7 +53,10 @@ describe('TaskFiltersCloudComponent', () => { let getTaskFilterCounterSpy: jasmine.Spy; let getTaskListFiltersSpy: jasmine.Spy; let getTaskListCountSpy: jasmine.Spy; - let getTaskNotificationSubscriptionSpy: jasmine.Spy; + let getEngineEventsSpy: jasmine.Spy; + let filterCountersService: FilterCountersCloudService; + let getFilterCountersSpy: jasmine.Spy; + let refreshFilterCountersSpy: jasmine.Spy; let router: Router; const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => { @@ -58,6 +64,7 @@ describe('TaskFiltersCloudComponent', () => { imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule], providers: [ { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, provideRouter([{ path: 'task-list-cloud', component: DummyComponent }]), { provide: ActivatedRoute, @@ -76,10 +83,15 @@ describe('TaskFiltersCloudComponent', () => { }); taskFilterService = TestBed.inject(TaskFilterCloudService); taskListService = TestBed.inject(TaskListCloudService); + filterCountersService = TestBed.inject(FilterCountersCloudService); getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11)); getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11)); - getTaskNotificationSubscriptionSpy = spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications)); - getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter)); + getEngineEventsSpy = spyOn(filterCountersService, 'getEngineEvents').and.returnValue(of(taskNotifications)); + getTaskListFiltersSpy = spyOn(filterCountersService, 'getTaskFilters').and.returnValue(of(fakeGlobalFilter)); + getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue( + of({ counters: { 'fake-involved-tasks': 11 }, batched: true }) + ); + refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters'); appConfigService = TestBed.inject(AppConfigService); @@ -261,7 +273,7 @@ describe('TaskFiltersCloudComponent', () => { expect(updatedFilterCounters.length).toBe(0); }); - it('should update filter counter when filter is selected', async () => { + it('should refresh the filter counters when a filter is selected', async () => { component.showIcons = true; await bindAppName(); @@ -269,7 +281,7 @@ describe('TaskFiltersCloudComponent', () => { filterButton.click(); fixture.detectChanges(); - expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); }); describe('Notifications config', () => { @@ -306,30 +318,33 @@ describe('TaskFiltersCloudComponent', () => { }); it('should not subscribe to notifications when appName is missing', () => { - getTaskNotificationSubscriptionSpy.calls.reset(); + getEngineEventsSpy.calls.reset(); component.appName = ''; fixture.detectChanges(); - expect(getTaskNotificationSubscriptionSpy).not.toHaveBeenCalled(); + expect(getEngineEventsSpy).not.toHaveBeenCalled(); }); - it('should debounce notification subscription using the configured debounce time', fakeAsync(() => { - const notifications$ = new Subject(); - getTaskNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable()); + it('should subscribe to the notifications of the bound app', () => { component.appName = 'my-app-1'; fixture.detectChanges(); - const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters'); + expect(getEngineEventsSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK); + }); - notifications$.next(taskNotifications); - tick(1000); - expect(updateFilterCountersSpy).not.toHaveBeenCalled(); + it('should emit the events of the debounced batch', fakeAsync(() => { + const events$ = new Subject(); + getEngineEventsSpy.and.returnValue(events$.asObservable()); + const filterCounterUpdatedSpy = spyOn(component.filterCounterUpdated, 'emit'); + component.appName = 'my-app-1'; - tick(2000); - expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1); + fixture.detectChanges(); + events$.next(taskNotifications); + + expect(filterCounterUpdatedSpy).toHaveBeenCalledWith(taskNotifications); flush(); })); }); @@ -438,7 +453,7 @@ describe('TaskFiltersCloudComponent', () => { expect(updatedFilterCounters.length).toBe(0); }); - it('should update filter counter when filter is selected', async () => { + it('should refresh the filter counters when a filter is selected', async () => { await bindAppName(); const filterButton = await loader.getHarness( @@ -446,6 +461,14 @@ describe('TaskFiltersCloudComponent', () => { ); await filterButton.click(); + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); + }); + + it('should resolve the counters with the POST method when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName(); + expect(getTaskListCountSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0])); }); }); @@ -658,17 +681,137 @@ describe('TaskFiltersCloudComponent', () => { expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true); }); - it('should call fetchTaskFilterCounter only if filter.showCounter is true', () => { + it('should resolve the counter only of the filters with a counter enabled', () => { const filterWithCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true }); const filterWithoutCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false }); - const fetchSpy = spyOn(component, 'fetchTaskFilterCounter').and.returnValue(of(42)); + getTaskFilterCounterSpy.calls.reset(); component.filters = [filterWithCounter, filterWithoutCounter]; component.updateFilterCounters(); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter); - expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter); + expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1); + expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(filterWithCounter); + }); + + describe('Batched counters', () => { + it('should read the counters without waiting for the filters', async () => { + getTaskListFiltersSpy.and.returnValue(NEVER); + + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should hold the counters until the filters they belong to arrive', async () => { + const filters$ = new Subject(); + getTaskListFiltersSpy.and.returnValue(filters$.asObservable()); + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + + await bindAppName(); + expect(component.counters['fake-involved-tasks']).toBeUndefined(); + + filters$.next(fakeGlobalFilter); + fixture.detectChanges(); + + expect(component.counters['fake-involved-tasks']).toBe(9); + }); + + it('should read the counters of the task filters of the bound app', async () => { + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should not ask for the batched count endpoint by default', async () => { + await bindAppName(); + + expect(component.useBatchedCounters).toBeFalse(); + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false); + }); + + it('should ask for the batched count endpoint when the input is set', async () => { + fixture.componentRef.setInput('useBatchedCounters', true); + + await bindAppName(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true); + }); + + it('should read the counters again when the input changes', async () => { + await bindAppName(); + getFilterCountersSpy.calls.reset(); + + fixture.componentRef.setInput('useBatchedCounters', true); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true); + }); + + it('should hold the counters resolved by the batched count request', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + + await bindAppName(); + + expect(component.counters['fake-involved-tasks']).toBe(9); + }); + + it('should emit the filters whose counter changed', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true })); + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + + await bindAppName(); + + expect(updatedFilterSpy).toHaveBeenCalledWith('fake-involved-tasks'); + }); + + it('should resolve the counter of a filter the batch left out on its own', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: true })); + + await bindAppName(); + + expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + expect(component.counters['fake-involved-tasks']).toBe(11); + }); + + it('should keep the counters of the other filters when one counter cannot be resolved', async () => { + getTaskListFiltersSpy.and.returnValue(of([fakeGlobalFilter[0], { ...fakeGlobalFilter[1], showCounter: true }])); + getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 4 }, batched: true })); + getTaskFilterCounterSpy.and.throwError('the query of the filter cannot be built'); + + await bindAppName(); + + expect(component.counters['fake-involved-tasks']).toBe(4); + expect(component.counters['fake-my-task1']).toBe(0); + }); + + it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + + await bindAppName(); + + expect(getTaskFilterCounterSpy).toHaveBeenCalled(); + expect(component.counters['fake-involved-tasks']).toBe(11); + }); + + it('should refresh the counters of every filter when a filter is clicked', async () => { + await bindAppName(); + + component.onFilterClick(fakeGlobalFilter[0]); + + expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1'); + }); + + it('should refresh the counter of the clicked filter alone when the batched endpoint is not available', async () => { + getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false })); + await bindAppName(); + getTaskFilterCounterSpy.calls.reset(); + + component.onFilterClick(fakeGlobalFilter[0]); + + expect(refreshFilterCountersSpy).not.toHaveBeenCalled(); + expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1); + }); }); describe('Highlight Selected Filter', () => { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts index 3c553aa7c2..54f2891877 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.ts @@ -16,16 +16,18 @@ */ import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { EMPTY, Observable } from 'rxjs'; +import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs'; import { TaskFilterCloudService } from '../../services/task-filter-cloud.service'; import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; -import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component'; import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model'; import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service'; import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model'; +import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; +import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; @@ -42,10 +44,21 @@ import { AsyncPipe } from '@angular/common'; export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges { protected readonly TASKS_ROUTE = '/task-list-cloud'; - /** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. */ + /** + * (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. + * + */ @Input() searchApiMethod: 'GET' | 'POST' = 'GET'; + /** + * (optional) Resolves the counters of the task and the process filters with a single call to + * `POST /query/v1/count`. Both filter components have to + * ask for it, otherwise the counters are resolved one filter at a time. + */ + @Input() + useBatchedCounters = false; + /** Emitted when a filter is being selected based on the filterParam input. */ @Output() filterSelected = new EventEmitter(); @@ -69,9 +82,13 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp notificationDebounceTime = 3000; currentFiltersValues: { [key: string]: number } = {}; private filtersLoadedFor?: string; + private countersSubscription?: Subscription; + private countersFilters$?: Observable; + private batchedCounters = true; private readonly taskFilterCloudService = inject(TaskFilterCloudService); private readonly taskListCloudService = inject(TaskListCloudService); + private readonly filterCountersCloudService = inject(FilterCountersCloudService); private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); private readonly activatedRoute = inject(ActivatedRoute); @@ -80,6 +97,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000); + if (!this.filtersLoadedFor) { this.getFilters(this.appName); } @@ -94,6 +112,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp this.getFilters(appName.currentValue); } else if (filter && filter.currentValue !== filter.previousValue) { this.selectFilterAndEmit(filter.currentValue); + } else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) { + this.loadFilterCounters(this.filtersLoadedFor); } } @@ -104,8 +124,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp */ getFilters(appName: string): void { this.filtersLoadedFor = appName; - const filters$ = this.taskFilterCloudService.getTaskListFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true })); - this.filters$ = filters$.pipe(catchError(() => EMPTY)); + const filters$ = this.filterCountersCloudService.getTaskFilters(appName); + this.filters$ = filters$.pipe(catchError(() => of([]))); filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (res) => { @@ -113,13 +133,15 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp this.filters = res || []; this.initFilterCounters(); this.selectFilterAndEmit(this.filterParam); - this.updateFilterCounters(); this.success.emit(res); }, error: (err) => { this.error.emit(err); } }); + + this.countersFilters$ = filters$; + this.loadFilterCounters(appName); } /** @@ -131,55 +153,47 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp /** * Iterate over filters and update counters + * + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. */ updateFilterCounters(): void { - this.filters.forEach((filter: TaskFilterCloudModel) => this.updateFilterCounter(filter)); + this.filters.forEach((filter) => this.updateFilterCounter(filter)); } /** * Get current value for filter and check if value has changed * * @param filter filter + * @deprecated counts one filter at a time. Removed in ADF 10.0.0. */ updateFilterCounter(filter: TaskFilterCloudModel): void { if (!filter?.showCounter) { return; } - this.fetchTaskFilterCounter(filter) + + defer(() => this.fetchTaskFilterCounter(filter)) .pipe( - tap((filterCounter) => { - this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); - }) + catchError(() => EMPTY), + takeUntilDestroyed(this.destroyRef) ) - .subscribe((data) => { - this.counters = { - ...this.counters, - [filter.key]: data - }; + .subscribe((counter) => { + this.checkIfFilterValuesHasBeenUpdated(filter.key, counter); + this.counters = { ...this.counters, [filter.key]: counter }; }); } - private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable { - return this.searchApiMethod === 'POST' - ? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter)) - : this.taskFilterCloudService.getTaskFilterCounter(filter); - } - - initFilterCounterNotifications() { + initFilterCounterNotifications(): void { if (!this.appName) { return; } - if (this.enableNotifications) { - this.taskFilterCloudService - .getTaskNotificationSubscription(this.appName) - .pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef)) - .subscribe((result) => { - result.forEach((taskEvent) => { - this.checkFilterCounter(taskEvent.entity); - }); - this.updateFilterCounters(); - this.filterCounterUpdated.emit(result); + if (this.enableNotifications) { + this.filterCountersCloudService + .getEngineEvents(this.appName, FilterCounterEntityType.TASK) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((events) => { + events.forEach((taskEvent) => this.checkFilterCounter(taskEvent.entity)); + this.filterCounterUpdated.emit(events); }); } else { this.counters = {}; @@ -240,7 +254,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp onFilterClick(filter: FilterParamsModel) { if (filter) { this.selectFilter(filter); - this.updateFilterCounter(this.currentFilter); + this.refreshFilterCounter(this.currentFilter); this.filterClicked.emit(this.currentFilter); this.updatedCountersSet.delete(filter.key); } else { @@ -267,17 +281,9 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp return this.filters === undefined || (this.filters && this.filters.length === 0); } - /** - * Reset the filters properties - */ - private resetFilter() { - this.filters = []; - this.currentFilter = undefined; - } - checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number) { if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) { - this.currentFiltersValues[filterKey] = filterValue; + this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue }; this.updatedFilter.emit(filterKey); this.updatedCountersSet.add(filterKey); } @@ -288,8 +294,69 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp * */ getFilterKeysAfterExternalRefreshing(): void { - this.taskFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => { - this.updatedCountersSet.delete(filterKey); + this.taskFilterCloudService.filterKeyToBeRefreshed$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((filterKey: string) => this.updatedCountersSet.delete(filterKey)); + } + + private loadFilterCounters(appName: string): void { + if (!this.countersFilters$) { + return; + } + + this.countersSubscription?.unsubscribe(); + this.countersSubscription = combineLatest([ + this.countersFilters$.pipe(catchError(() => of([]))), + this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK, this.useBatchedCounters) + ]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([, { counters, batched }]) => { + this.batchedCounters = batched; + if (batched) { + this.applyFilterCounters(counters); + } else { + this.updateFilterCounters(); + } + }); + } + + private applyFilterCounters(counters: { [filterKey: string]: number }): void { + this.filters.forEach((filter) => { + const filterKey = filter?.showCounter ? filter.key : undefined; + if (!filterKey) { + return; + } + + const counter = counters[filterKey]; + if (counter === undefined) { + this.updateFilterCounter(filter); + return; + } + + this.checkIfFilterValuesHasBeenUpdated(filterKey, counter); + this.counters = { ...this.counters, [filterKey]: counter }; }); } + + private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable { + return this.searchApiMethod === 'POST' + ? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter)) + : this.taskFilterCloudService.getTaskFilterCounter(filter); + } + + /** + * Reset the filters properties + */ + private resetFilter() { + this.filters = []; + this.currentFilter = undefined; + } + + private refreshFilterCounter(filter: TaskFilterCloudModel): void { + if (this.batchedCounters) { + this.filterCountersCloudService.refreshFilterCounters(this.appName); + } else { + this.updateFilterCounter(filter); + } + } } diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts index 0018f4231f..e8a38a776b 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts @@ -361,6 +361,11 @@ export class TaskFilterCloudService extends BaseCloudService { ]; } + /** + * @deprecated use FilterCountersCloudService.getEngineEvents instead. + * @param appName Name of the target app + * @returns Task engine events + */ getTaskNotificationSubscription(appName: string): Observable { return this.notificationCloudService .makeGQLQuery(appName, TASK_EVENT_SUBSCRIPTION_QUERY) diff --git a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts index 7d6f6ce04c..c46f7f7ef0 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts @@ -135,7 +135,7 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl return this.post(queryUrl, queryData).pipe(map((response) => response || 0)); } - protected buildQueryData(requestNode: TaskListRequestModel) { + buildQueryData(requestNode: TaskListRequestModel) { const queryData: any = { id: requestNode.id, parentId: requestNode.parentId, diff --git a/lib/process-services-cloud/src/public-api.ts b/lib/process-services-cloud/src/public-api.ts index fa63874f47..b945932abf 100644 --- a/lib/process-services-cloud/src/public-api.ts +++ b/lib/process-services-cloud/src/public-api.ts @@ -33,6 +33,7 @@ export * from './lib/models/application-version.model'; export * from './lib/models/engine-event-cloud.model'; export * from './lib/models/task-cloud.model'; export * from './lib/models/filter-cloud-model'; +export * from './lib/models/filter-counters-cloud.model'; export * from './lib/models/task-list-sorting.model'; export * from './lib/models/process-instance-variable.model'; export * from './lib/models/variable-definition'; diff --git a/lib/process-services/karma.conf.js b/lib/process-services/karma.conf.js index 03f0c6ac0f..27c5bf8de9 100644 --- a/lib/process-services/karma.conf.js +++ b/lib/process-services/karma.conf.js @@ -43,7 +43,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/process-services'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/process-services/package.json b/lib/process-services/package.json index 17fc12b3fe..19125a69b4 100644 --- a/lib/process-services/package.json +++ b/lib/process-services/package.json @@ -12,14 +12,14 @@ }, "peerDependencies": { "@angular/cdk": ">=20.2.14", - "@angular/common": ">=20.3.25", - "@angular/compiler": ">=20.3.25", - "@angular/core": ">=20.3.25", - "@angular/forms": ">=20.3.25", + "@angular/common": ">=20.3.27", + "@angular/compiler": ">=20.3.27", + "@angular/core": ">=20.3.27", + "@angular/forms": ">=20.3.27", "@angular/material": ">=20.2.14", - "@angular/platform-browser": ">=20.3.25", - "@angular/platform-browser-dynamic": ">=20.3.25", - "@angular/router": ">=20.3.25", + "@angular/platform-browser": ">=20.3.27", + "@angular/platform-browser-dynamic": ">=20.3.27", + "@angular/router": ">=20.3.27", "@alfresco/js-api": ">=10.0.0", "@alfresco/adf-core": ">=9.0.0", "@alfresco/adf-content-services": ">=9.0.0", diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html index 55a0f27e0c..744843e374 100644 --- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html +++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html @@ -6,17 +6,18 @@ - + {{opt.name}} {{field.value}} @if (!isReadOnlyField && dropdownControl.touched && (field.validationSummary?.message || dropdownControl.hasError('required'))) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html index c4160a0040..8cea85eaa1 100644 --- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html +++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html @@ -4,7 +4,7 @@ [class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly" id="typehead-div"> - +
@if (field.validationSummary?.message || isInvalidFieldRequired()) { - + error_outline @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}} } +
diff --git a/nx.json b/nx.json index 45492e45a3..30bdd681b6 100644 --- a/nx.json +++ b/nx.json @@ -10,7 +10,8 @@ "cache": true }, "test": { - "cache": true + "cache": true, + "outputs": ["{workspaceRoot}/coverage/{projectName}"] }, "stylelint": { "cache": true diff --git a/package.json b/package.json index f28c8eba5f..17da1c277e 100644 --- a/package.json +++ b/package.json @@ -40,19 +40,19 @@ ], "dependencies": { "@angular-eslint/utils": "20.7.0", - "@angular/animations": "20.3.26", + "@angular/animations": "20.3.27", "@angular/cdk": "20.2.14", "@angular/common": "20.3.27", "@angular/compiler": "20.3.27", "@angular/core": "20.3.27", - "@angular/forms": "20.3.26", + "@angular/forms": "20.3.27", "@angular/material": "20.2.14", "@angular/material-date-fns-adapter": "20.2.14", - "@angular/platform-browser": "20.3.26", - "@angular/platform-browser-dynamic": "20.3.26", - "@angular/router": "20.3.26", + "@angular/platform-browser": "20.3.27", + "@angular/platform-browser-dynamic": "20.3.27", + "@angular/router": "20.3.27", "@apollo/client": "3.13.1", - "@cspell/eslint-plugin": "10.0.0", + "@cspell/eslint-plugin": "10.0.1", "@mat-datetimepicker/core": "16.0.1", "@ngx-translate/core": "17.0.0", "angular-oauth2-oidc": "19.0.0", @@ -83,7 +83,7 @@ "@angular-eslint/eslint-plugin-template": "20.7.0", "@angular-eslint/template-parser": "22.1.0", "@angular/build": "20.3.32", - "@angular/compiler-cli": "20.3.26", + "@angular/compiler-cli": "20.3.27", "@chromatic-com/storybook": "4.1.3", "@eslint/compat": "^2.1.0", "@nx/angular": "23.1.1", @@ -98,10 +98,10 @@ "@types/jasminewd2": "2.0.13", "@types/node": "26.1.1", "@types/sinon": "22.0.0", - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "ajv": "8.20.0", "dotenv": "16.4.7", "eslint": "10.8.0", @@ -115,7 +115,7 @@ "eslint-plugin-unicorn": "61.0.2", "graphql": "16.14.0", "husky": "9.1.7", - "jasmine-core": "5.13.0", + "jasmine-core": "6.3.0", "jasmine-reporters": "2.5.2", "jsonc-eslint-parser": "2.4.2", "karma": "6.4.4", @@ -140,7 +140,7 @@ "ts-node": "10.9.2", "typescript": "5.9.3", "undici": "8.9.0", - "webpack": "5.109.0" + "webpack": "5.109.2" }, "license": "Apache-2.0", "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 089c200cbf..1f5dd58083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,12 +6,13 @@ settings: overrides: fast-uri: 3.1.5 - js-yaml@4.2.0: 4.3.0 + js-yaml@4.2.0: 4.3.1 + brace-expansion@5.0.8: 5.0.9 brace-expansion@<1.1.18: 1.1.18 + socket.io-parser@4.2.6: 4.2.7 svgo: 4.0.2 shell-quote: 1.9.0 adm-zip: 0.6.0 - brace-expansion@5.0.8: 5.0.9 minimatch@10.2.5: 10.2.6 axios: 1.18.0 serialize-javascript: '>=7.0.5' @@ -30,10 +31,10 @@ importers: dependencies: '@angular-eslint/utils': specifier: 20.7.0 - version: 20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular/animations': - specifier: 20.3.26 - version: 20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + specifier: 20.3.27 + version: 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/cdk': specifier: 20.2.14 version: 20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -47,32 +48,32 @@ importers: specifier: 20.3.27 version: 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/forms': - specifier: 20.3.26 - version: 20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + specifier: 20.3.27 + version: 20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@angular/material': specifier: 20.2.14 - version: 20.2.14(455430e5d13335858a88189cadfe3159) + version: 20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7) '@angular/material-date-fns-adapter': specifier: 20.2.14 - version: 20.2.14(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(455430e5d13335858a88189cadfe3159))(date-fns@2.30.0) + version: 20.2.14(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7))(date-fns@2.30.0) '@angular/platform-browser': - specifier: 20.3.26 - version: 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + specifier: 20.3.27 + version: 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/platform-browser-dynamic': - specifier: 20.3.26 - version: 20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))) + specifier: 20.3.27 + version: 20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))) '@angular/router': - specifier: 20.3.26 - version: 20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + specifier: 20.3.27 + version: 20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@apollo/client': specifier: 3.13.1 version: 3.13.1(graphql-ws@6.0.8(graphql@16.14.0)(ws@8.21.0))(graphql@16.14.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@cspell/eslint-plugin': - specifier: 10.0.0 - version: 10.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) + specifier: 10.0.1 + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) '@mat-datetimepicker/core': specifier: 16.0.1 - version: 16.0.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(455430e5d13335858a88189cadfe3159)) + version: 16.0.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7)) '@ngx-translate/core': specifier: 17.0.0 version: 17.0.0(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) @@ -145,19 +146,19 @@ importers: version: 20.3.32(chokidar@4.0.3) '@angular-eslint/eslint-plugin': specifier: 20.7.0 - version: 20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular-eslint/eslint-plugin-template': specifier: 20.7.0 - version: 20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.65.0)(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + version: 20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.67.0)(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular-eslint/template-parser': specifier: 22.1.0 version: 22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) '@angular/build': specifier: 20.3.32 - version: 20.3.32(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) + version: 20.3.32(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) '@angular/compiler-cli': - specifier: 20.3.26 - version: 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + specifier: 20.3.27 + version: 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@chromatic-com/storybook': specifier: 4.1.3 version: 4.1.3(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) @@ -166,16 +167,16 @@ importers: version: 2.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) '@nx/angular': specifier: 23.1.1 - version: 23.1.1(14b0b041890a4d9ad84851ba43aed6f3) + version: 23.1.1(1871ac9779a3c1c87e7a2019dffef8ff) '@nx/eslint-plugin': specifier: 23.1.1 - version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) + version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) '@nx/js': specifier: 23.1.1 version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) '@nx/storybook': specifier: 23.1.1 - version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@nx/web@23.1.1(69165f71226cdd786def4faf955c6def))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3) + version: 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@nx/web@23.1.1(9aad18717af1cde75effe0fe4ceb25af))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3) '@nx/workspace': specifier: 23.1.1 version: 23.1.1 @@ -187,7 +188,7 @@ importers: version: 10.4.0(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) '@storybook/angular': specifier: 10.4.0 - version: 10.4.0(641969a558ebac8287ee9e40d24c89b8) + version: 10.4.0(69b0d8b90b0fe3d6016d76a1b6c3fcbb) '@types/jasmine': specifier: 4.0.3 version: 4.0.3 @@ -201,17 +202,17 @@ importers: specifier: 22.0.0 version: 22.0.0 '@typescript-eslint/eslint-plugin': - specifier: 8.66.0 - version: 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.67.0 + version: 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: 8.66.0 - version: 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.67.0 + version: 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/typescript-estree': - specifier: 8.66.0 - version: 8.66.0(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.67.0 + version: 8.67.0(supports-color@7.2.0)(typescript@5.9.3) '@typescript-eslint/utils': - specifier: 8.66.0 - version: 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + specifier: 8.67.0 + version: 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) ajv: specifier: 8.20.0 version: 8.20.0 @@ -252,8 +253,8 @@ importers: specifier: 9.1.7 version: 9.1.7 jasmine-core: - specifier: 5.13.0 - version: 5.13.0 + specifier: 6.3.0 + version: 6.3.0 jasmine-reporters: specifier: 2.5.2 version: 2.5.2 @@ -274,13 +275,13 @@ importers: version: 5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) karma-jasmine-html-reporter: specifier: 2.2.0 - version: 2.2.0(jasmine-core@5.13.0)(karma-jasmine@5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)))(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) + version: 2.2.0(jasmine-core@6.3.0)(karma-jasmine@5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)))(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) lint-staged: specifier: 17.2.0 version: 17.2.0 ng-packagr: specifier: 20.3.2 - version: 20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + version: 20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) nx: specifier: 23.1.1 version: 23.1.1 @@ -298,7 +299,7 @@ importers: version: 6.1.3 sass-loader: specifier: 16.0.8 - version: 16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + version: 16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) semver: specifier: 7.6.3 version: 7.6.3 @@ -327,8 +328,8 @@ importers: specifier: 8.9.0 version: 8.9.0 webpack: - specifier: 5.109.0 - version: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + specifier: 5.109.2 + version: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) lib/eslint-angular: {} @@ -463,12 +464,12 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '*' - '@angular/animations@20.3.26': - resolution: {integrity: sha512-hfNrX19v8xs/usNkELSqc6q5IwBfS2GGW8sQ4OMpxAmLZDJwaLUxkj48t8VYhUFezsIfzR+sDEi6ZQBOaMIYug==} + '@angular/animations@20.3.27': + resolution: {integrity: sha512-BgGTloDiD3qIFVSxZq8xO6CiyhKn00WbhQQiklZF8WI2hXd3Hmc1OUAAHqSMh2c9uL7X1ZYkg9lSzjiasK2vKg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.' peerDependencies: - '@angular/core': 20.3.26 + '@angular/core': 20.3.27 '@angular/build@20.3.16': resolution: {integrity: sha512-p1W3wwMG1Bs4tkPW7ceXO4woO1KCP28sjfpBJg32dIMW3dYSC+iWNmUkYS/wb4YEkqCV0wd6Apnd98mZjL6rNg==} @@ -576,12 +577,12 @@ packages: '@angular/core': 20.3.27 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@20.3.26': - resolution: {integrity: sha512-3rHtC87ecldvaiFHwQEZ6Wx3QaZ/Q7b0Gb7XORDOjn/M+5CYZ4rQsvbxE5TUjwreg07oQ4Y2h8ADESXTJEUYOQ==} + '@angular/compiler-cli@20.3.27': + resolution: {integrity: sha512-R0j9mFfUdGmmw867V/TfMSOBkLZT6ASxyY5tc1NDNmxQioZDVIDP9pqBOayzhJ0xiuDc9JellQXUTZ+vm+b/Zg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 20.3.26 + '@angular/compiler': 20.3.27 typescript: '>=5.8 <6.0' peerDependenciesMeta: typescript: @@ -604,13 +605,13 @@ packages: zone.js: optional: true - '@angular/forms@20.3.26': - resolution: {integrity: sha512-ia0YaPVjlG2oBFKCfaAgqQ0jGRrhGTAcrbZG3tVeFDpi7LQ6WdP3Syw2H+0D3GPyzpl/5UqU10Fum5Wr1br4QQ==} + '@angular/forms@20.3.27': + resolution: {integrity: sha512-cNG26wi3tr3m8At6puxJpAMVk9mBhEqREA4Jk/klalMwWuYEf8ApTEAw0a5NUfMxDkL3dcJJwDRf8rK6ma6TYQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.3.26 - '@angular/core': 20.3.26 - '@angular/platform-browser': 20.3.26 + '@angular/common': 20.3.27 + '@angular/core': 20.3.27 + '@angular/platform-browser': 20.3.27 rxjs: ^6.5.3 || ^7.4.0 '@angular/material-date-fns-adapter@20.2.14': @@ -630,34 +631,34 @@ packages: '@angular/platform-browser': ^20.0.0 || ^21.0.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/platform-browser-dynamic@20.3.26': - resolution: {integrity: sha512-/9eq0GGmtMoBV5UvcjvhnV4ZDcgZr15h+dzoxkZodUncb2z7fxybSyha7wWD9aTeabZ/q/KYVUSnoYqVyBhbVQ==} + '@angular/platform-browser-dynamic@20.3.27': + resolution: {integrity: sha512-4UUs8vOswgBOWCRoeZrswguarBLrM3j6WGb927Y3GXdN37fVJQYjyKHivNeQwZvwsGgl5Nl6mvpBpZv47n/OrQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} deprecated: '@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.' peerDependencies: - '@angular/common': 20.3.26 - '@angular/compiler': 20.3.26 - '@angular/core': 20.3.26 - '@angular/platform-browser': 20.3.26 + '@angular/common': 20.3.27 + '@angular/compiler': 20.3.27 + '@angular/core': 20.3.27 + '@angular/platform-browser': 20.3.27 - '@angular/platform-browser@20.3.26': - resolution: {integrity: sha512-In4wUiLUUT9LqyV9Rjz78k/dsnKAwec4AtDmwZoX8/ZmeJOSH7g5X1gTM+hxTxmifmZkrapQjXR299IcfIkzrw==} + '@angular/platform-browser@20.3.27': + resolution: {integrity: sha512-IV2zQ4zk6liyw5NE48bQqSk3nOAZ1rmDAQi7W4Kw0N8cs9MYVgK3zulo0zx5UqSv1kuNDAGb0HPR5tUGVOf9kw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/animations': 20.3.26 - '@angular/common': 20.3.26 - '@angular/core': 20.3.26 + '@angular/animations': 20.3.27 + '@angular/common': 20.3.27 + '@angular/core': 20.3.27 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/router@20.3.26': - resolution: {integrity: sha512-q0k0b5uuQx93Trk4qEMYe8LoPOozheRBIjze51q+LUTlLXWik4W0ughXLiTJL346KRudR/vB5ksfZP8b6WlQyA==} + '@angular/router@20.3.27': + resolution: {integrity: sha512-F3hfJQ0GAuD6LdeB7A6fMfaErc4HXCtCQGh3C/8VrbVEbGfIgSveNjQXzGYPNklnOLhj1BmWf5w0WniUAEjLBA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.3.26 - '@angular/core': 20.3.26 - '@angular/platform-browser': 20.3.26 + '@angular/common': 20.3.27 + '@angular/core': 20.3.27 + '@angular/platform-browser': 20.3.27 rxjs: ^6.5.3 || ^7.4.0 '@apollo/client@3.13.1': @@ -1287,28 +1288,28 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cspell/cspell-bundled-dicts@10.0.0': - resolution: {integrity: sha512-ci410HEkng2582oOjlRHQtlGXwh+rUC/mVcN9dObLHpKhvPgzn2S6vT56pARstxxZpcCUG/oLhn3dCqdJlVzmA==} + '@cspell/cspell-bundled-dicts@10.0.1': + resolution: {integrity: sha512-WvkSDNX4Uyyj/ZgbPO6L38iFNMfK1EqsH1FteRiI2qLz6QZMXRFrIt12OqiWIplzZDDaVpBH9FCJOPJll0fjCQ==} engines: {node: '>=22.18.0'} - '@cspell/cspell-performance-monitor@10.0.0': - resolution: {integrity: sha512-2vMh2pLt2dg/ArYvWjMP4v9HCm0pRhONsEJyc8oHdZyOYvX7trixX894I0M39+VBf3yWtPCEgYRh1UDXNIZRig==} + '@cspell/cspell-performance-monitor@10.0.1': + resolution: {integrity: sha512-9tVcHXwRnbazUv4WSG0h3MqV4+LgmLNgSALAQUflPPW0EMxTf7C4Dmv9cgxJyCEQrdnVKCr58nPPaahhz9LJUg==} engines: {node: '>=22.18.0'} - '@cspell/cspell-pipe@10.0.0': - resolution: {integrity: sha512-qcgHhQvtEX8LSwIVsWrdUgiGim52lN3jT+ghlkdp72v+nBcGKsS2frEKTmbGLug+xcqppkzs6Q6VmsFp1MGtfA==} + '@cspell/cspell-pipe@10.0.1': + resolution: {integrity: sha512-HPeXMD9AZ3V/qPkvQaPcak+C7cJ2z7JTHN8smd6J8L2aThLRky2cHc2OyeaHPSHB7WA47b4z2n5u5nawZhv5VQ==} engines: {node: '>=22.18.0'} - '@cspell/cspell-resolver@10.0.0': - resolution: {integrity: sha512-8H+IUDB7SmrpcRugQ5f55qG81ZShk6nQRk+natLz41TEY98D8/LCmjHEkh/vhDPph9pVJmNUp7JcM2E1UHEa2g==} + '@cspell/cspell-resolver@10.0.1': + resolution: {integrity: sha512-PIzkZHD1fGUQx1XteK2d1iQ0Mzq/maYcoB4jkvAiiR6WqP3MWYNKFdI9z+R5pOq5KgMfW+5Ig1q0oSR6h8irlA==} engines: {node: '>=22.18.0'} - '@cspell/cspell-service-bus@10.0.0': - resolution: {integrity: sha512-V7eigqg/TOoKwNK4Q18wr9KGxA8U5SFcoWVS8RyAxv4mQ+yNKHhvHEbRBifjPbQDer66afOrclb2UbqkIy2SOw==} + '@cspell/cspell-service-bus@10.0.1': + resolution: {integrity: sha512-y6NcIGP2IdXaBL4PVH8vxsr7K27wzz3Ech87UtUtrDSXAiVEOvXgAIknEOUVp59rTlUE8Rn4IRURC6f/hgMyfw==} engines: {node: '>=22.18.0'} - '@cspell/cspell-types@10.0.0': - resolution: {integrity: sha512-IQA++Idqb8fZzkCbHq3+T+9yG9WpeaBxomOrG2KcR/Pj0CgnovzuApYKL2cc35UWLePboKinMeqEPiweFpHVug==} + '@cspell/cspell-types@10.0.1': + resolution: {integrity: sha512-kLgLShnWADDVreKC63pBrWkcvxgZzFIfO34Jhx/SWfuOIA3cD8AXT+HjyuLfoGJ7mUb58hv2kUziKzEy4INb1w==} engines: {node: '>=22.18.0'} '@cspell/dict-ada@4.1.1': @@ -1323,11 +1324,11 @@ packages: '@cspell/dict-bash@4.2.3': resolution: {integrity: sha512-ljUZoKHbDqw5Sx0qpL2qTUlmkmr+vhZH/sCNrNaBZKTbdgiswErSnIF1jRbGmEitJNxHRHWsuZyVgnTGfVO1Yw==} - '@cspell/dict-companies@3.2.11': - resolution: {integrity: sha512-0cmafbcz2pTHXLd59eLR1gvDvN6aWAOM0+cIL4LLF9GX9yB2iKDNrKsvs4tJRqutoaTdwNFBbV0FYv+6iCtebQ==} + '@cspell/dict-companies@3.2.12': + resolution: {integrity: sha512-mjiz/N3zWOCsz5VfwMUydSl7uW0OU9H2PnbCNc3RV44Vj6Q59CSp6EYGSGZQxrXU1gpsuZUrwr6QCjNjFOOg5A==} - '@cspell/dict-cpp@7.0.2': - resolution: {integrity: sha512-dfbeERiVNeqmo/npivdR6rDiBCqZi3QtjH2Z0HFcXwpdj6i97dX1xaKyK2GUsO/p4u1TOv63Dmj5Vm48haDpuA==} + '@cspell/dict-cpp@7.1.0': + resolution: {integrity: sha512-rcjycobioQUd9Jm/Y1n8U+sq6ytpZ0iV8AYIo+vyEFfutC5x7g6hL6Fh3bCdh6IporGHRqfEutGK/4sfopD2ZA==} '@cspell/dict-cryptocurrencies@5.0.5': resolution: {integrity: sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==} @@ -1341,8 +1342,8 @@ packages: '@cspell/dict-dart@2.3.2': resolution: {integrity: sha512-sUiLW56t9gfZcu8iR/5EUg+KYyRD83Cjl3yjDEA2ApVuJvK1HhX+vn4e4k4YfjpUQMag8XO2AaRhARE09+/rqw==} - '@cspell/dict-data-science@2.0.14': - resolution: {integrity: sha512-jl6Ds4u5u5JT+yY30pWQpAbdCHfy3lCcNkLbpL/AZKoUaLEoXbaYsps9xQtvD7DyaiXxiLZkdH2yHHXtoFtZyg==} + '@cspell/dict-data-science@2.0.16': + resolution: {integrity: sha512-M72mxv5asuAnORurz4iXRJ+Tw9XBq6eu7D2Ne7biP0Z1RciKGNxXWu9JycA/KlVvK1hAlKj/fANlXhuEWpXKFg==} '@cspell/dict-django@4.1.6': resolution: {integrity: sha512-SdbSFDGy9ulETqNz15oWv2+kpWLlk8DJYd573xhIkeRdcXOjskRuxjSZPKfW7O3NxN/KEf3gm3IevVOiNuFS+w==} @@ -1356,14 +1357,14 @@ packages: '@cspell/dict-elixir@4.0.8': resolution: {integrity: sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==} - '@cspell/dict-en-common-misspellings@2.1.12': - resolution: {integrity: sha512-14Eu6QGqyksqOd4fYPuRb58lK1Va7FQK9XxFsRKnZU8LhL3N+kj7YKDW+7aIaAN/0WGEqslGP6lGbQzNti8Akw==} + '@cspell/dict-en-common-misspellings@2.2.0': + resolution: {integrity: sha512-5PmCHv+AhY0LVNo3bE1FdRKMmw1esKj83GPwLymnVPYNtlI2Jf6y27EjC0azg4zny5keWmku0GQiMf55Fi+qeA==} - '@cspell/dict-en-gb-mit@3.1.24': - resolution: {integrity: sha512-Oowb/Uzkh7OmDRdCcETzMc9imEb4IpLlHJXoYjX8A8DS2X/54gqSjI915JFB8hKtFjBko5OM0BLQ+6cZhFEMmQ==} + '@cspell/dict-en-gb-mit@3.1.25': + resolution: {integrity: sha512-zGODptk24CMrXi49ieG2SUm94CKxEsVF0dYNF+1ZYH0MSsQDZ/PKDlrrbvtBqSupKdPSj0Z9sjOmMNfHHW9ZSg==} - '@cspell/dict-en_us@4.4.35': - resolution: {integrity: sha512-xWpxBCc/FzzMMo/A+0qwARVaIIhR0Ql8yhhv4rvsvg+GfQF+LG9yzg2GwTM5N2rjvzmM3nKuR9zxFZq2I6fJSg==} + '@cspell/dict-en_us@4.4.36': + resolution: {integrity: sha512-2yOhI/+7d1DbfvMljGW4jw8pLqDEsVmnvUXBOCFXtLU2BWgQkrqOJDCNseYjEiEbTp0OtdrWEWWPFSP1TNugQw==} '@cspell/dict-filetypes@3.0.18': resolution: {integrity: sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==} @@ -1386,8 +1387,8 @@ packages: '@cspell/dict-git@3.1.0': resolution: {integrity: sha512-KEt9zGkxqGy2q1nwH4CbyqTSv5nadpn8BAlDnzlRcnL0Xb3LX9xTgSGShKvzb0bw35lHoYyLWN2ZKAqbC4pgGQ==} - '@cspell/dict-golang@6.0.26': - resolution: {integrity: sha512-YKA7Xm5KeOd14v5SQ4ll6afe9VSy3a2DWM7L9uBq4u3lXToRBQ1W5PRa+/Q9udd+DTURyVVnQ+7b9cnOlNxaRg==} + '@cspell/dict-golang@6.0.27': + resolution: {integrity: sha512-rRDb2JL8EefaezHGTEclKXCs4eMOS0q/datk94Lm7KQOhlGlzS9FDVZiR1/guh0o+iJCmejQuf5NR2FQeQSWsg==} '@cspell/dict-google@1.0.9': resolution: {integrity: sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==} @@ -1398,8 +1399,8 @@ packages: '@cspell/dict-html-symbol-entities@4.0.5': resolution: {integrity: sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==} - '@cspell/dict-html@4.0.15': - resolution: {integrity: sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==} + '@cspell/dict-html@4.0.16': + resolution: {integrity: sha512-9B6/Cpb5YVcYgsEAlKudun1yd4ONllYS/ZibKLYea2apPR+l2vQdARnPrP9kTqa7NUNfRKl7m9Fb6k9rjimnow==} '@cspell/dict-java@5.0.12': resolution: {integrity: sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==} @@ -1407,8 +1408,8 @@ packages: '@cspell/dict-julia@1.1.1': resolution: {integrity: sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==} - '@cspell/dict-k8s@1.0.12': - resolution: {integrity: sha512-2LcllTWgaTfYC7DmkMPOn9GsBWsA4DZdlun4po8s2ysTP7CPEnZc1ZfK6pZ2eI4TsZemlUQQ+NZxMe9/QutQxg==} + '@cspell/dict-k8s@1.0.13': + resolution: {integrity: sha512-ELGkS13k7K/NEfVimBSrxVTfqXvOF/Kvxj4I62YxRm8bvHbfoXgrGaOx28lPiNRz+dmu+yYtvuXbnURKtYbC6g==} '@cspell/dict-kotlin@1.1.1': resolution: {integrity: sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==} @@ -1425,22 +1426,22 @@ packages: '@cspell/dict-makefile@1.0.5': resolution: {integrity: sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==} - '@cspell/dict-markdown@2.0.17': - resolution: {integrity: sha512-H8bAxih6U8NOnSPL7R8My+tqjaB4tmnJTjERuz4zYqmf+cH+5xshX3UVgKlwWFcyjsYfv/zEDuRdMctQv1q6HQ==} + '@cspell/dict-markdown@2.0.18': + resolution: {integrity: sha512-KLRSwVrwKDz4n7bl2XQjzvaBZbGgx5QKkP5Ok1b24xaO3TpXsLhAbAn1mItvFlI2tF6Rkt83iYjQcYppywuf5Q==} peerDependencies: '@cspell/dict-css': ^4.1.2 - '@cspell/dict-html': ^4.0.15 + '@cspell/dict-html': ^4.0.16 '@cspell/dict-html-symbol-entities': ^4.0.5 '@cspell/dict-typescript': ^3.2.3 - '@cspell/dict-monkeyc@1.0.12': - resolution: {integrity: sha512-MN7Vs11TdP5mbdNFQP5x2Ac8zOBm97ARg6zM5Sb53YQt/eMvXOMvrep7+/+8NJXs0jkp70bBzjqU4APcqBFNAw==} + '@cspell/dict-monkeyc@1.1.0': + resolution: {integrity: sha512-mc/hgvSy/emOIYtc8kuVEaLUlnaERunfAubfJ5plUXq6s0A69bcukJzUzSt9C0UTCwS28641ILRPv80m3L9QbA==} '@cspell/dict-node@5.0.9': resolution: {integrity: sha512-hO+ga+uYZ/WA4OtiMEyKt5rDUlUyu3nXMf8KVEeqq2msYvAPdldKBGH7lGONg6R/rPhv53Rb+0Y1SLdoK1+7wQ==} - '@cspell/dict-npm@5.2.41': - resolution: {integrity: sha512-To3xsfRmMBYVXtWVEdUgV35M9a/JZ54dSuoY6m6D3uHKKL3I326Wmy4xifZ3PU8MQaWhyEH7zbIcUEtKwTQMcA==} + '@cspell/dict-npm@5.2.46': + resolution: {integrity: sha512-Z5GG59c17UEk6BPZ3lVD/++GvPBI+O05OTGm4sRA/0I/qszbstuYRw+j2//5f8SWzOFXKaGYI8kygwKX9EQZ9Q==} '@cspell/dict-php@4.1.1': resolution: {integrity: sha512-EXelI+4AftmdIGtA8HL8kr4WlUE11OqCSVlnIgZekmTkEGSZdYnkFdiJ5IANSALtlQ1mghKjz+OFqVs6yowgWA==} @@ -1451,14 +1452,14 @@ packages: '@cspell/dict-public-licenses@2.0.16': resolution: {integrity: sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==} - '@cspell/dict-python@4.2.27': - resolution: {integrity: sha512-Rj6xQgYS4X6ienjgAZF+njA0GRY4oSPouJWv0vfikCTn6EWlfk0V6Dy1HP3Migj1O+IC2NmespgVq+BZNSp8OA==} + '@cspell/dict-python@4.4.0': + resolution: {integrity: sha512-49Eju/a97V6ExAeMJDM2Tk4jsYr0pm+kQzqQrKLAZdrRd6VrLIgseI481EQSmgoVkPPdsuwK1xj9ZlpDQIF4qg==} '@cspell/dict-r@2.1.1': resolution: {integrity: sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==} - '@cspell/dict-ruby@5.1.1': - resolution: {integrity: sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==} + '@cspell/dict-ruby@5.1.2': + resolution: {integrity: sha512-f6Slf11N91ittf71AWlfVVG9GZPezVRBfcMPCTNTWhUsY/VEspkBRZYDhPXjV4df3jqHy5YJs8nlsFcFVF/bMA==} '@cspell/dict-rust@4.1.2': resolution: {integrity: sha512-O1FHrumYcO+HZti3dHfBPUdnDFkI+nbYK3pxYmiM1sr+G0ebOd6qchmswS0Wsc6ZdEVNiPYJY/gZQR6jfW3uOg==} @@ -1469,8 +1470,8 @@ packages: '@cspell/dict-shell@1.2.0': resolution: {integrity: sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==} - '@cspell/dict-software-terms@5.2.2': - resolution: {integrity: sha512-0CaYd6TAsKtEoA7tNswm1iptEblTzEe3UG8beG2cpSTHk7afWIVMtJLgXDv0f/Li67Lf3Z1Jf3JeXR7GsJ2TRw==} + '@cspell/dict-software-terms@5.4.1': + resolution: {integrity: sha512-tY88gnOWj2ax6h+i7BjU7dIH1f3fvY4WypS0a+TpjBgXC318AdOMmy0zK1SzxGNOjhWJfCeCZNVoLFM/DuOG4g==} '@cspell/dict-sql@2.2.1': resolution: {integrity: sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==} @@ -1481,8 +1482,8 @@ packages: '@cspell/dict-swift@2.0.6': resolution: {integrity: sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==} - '@cspell/dict-terraform@1.1.3': - resolution: {integrity: sha512-gr6wxCydwSFyyBKhBA2xkENXtVFToheqYYGFvlMZXWjviynXmh+NK/JTvTCk/VHk3+lzbO9EEQKee6VjrAUSbA==} + '@cspell/dict-terraform@1.1.4': + resolution: {integrity: sha512-Ere42ilvMFvQA4GlcN0OKlruMPR6EsvaB+iTHzj2xc+NJGRK64V7yApUcWrOrSgTiM/vhWXPIsK3OMfiAiNdmA==} '@cspell/dict-typescript@3.2.3': resolution: {integrity: sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==} @@ -1493,30 +1494,30 @@ packages: '@cspell/dict-zig@1.0.0': resolution: {integrity: sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==} - '@cspell/dynamic-import@10.0.0': - resolution: {integrity: sha512-fMqu/5Ma1Q5ZCR/Par+Q4pvaTKmx5pKZzQmkwld2hNounVdk2OaIPM9MzpNn6I1mLk5J+wTnIZmfcWNAzNP9aQ==} + '@cspell/dynamic-import@10.0.1': + resolution: {integrity: sha512-mP1gdq00aIcH8HxNMqnH11X6BKxLcneDtFgl/ecjIKnaGKwi44m8AndP5Kr4ODaYdl8UUw9O3dJh7KaQXnLHZQ==} engines: {node: '>=22.18.0'} - '@cspell/eslint-plugin@10.0.0': - resolution: {integrity: sha512-NXXismQgYtAg9w399Oa57TYZpm1u9VLX5LZADjgYz0ZwR/9FVwbOP2yiUisQXODCTAP+fnWGJPmYRiHmSYrW9Q==} + '@cspell/eslint-plugin@10.0.1': + resolution: {integrity: sha512-VgRVWIWyM5bz2d7eC3/F4ON1fhMdrFB0R6slXmJewDDbJMDloSsMsWCSktiFET3a0TX/DTjOGGDzBN9tqGDGPw==} engines: {node: '>=22.18.0'} peerDependencies: eslint: ^8 || ^9 || ^10 - '@cspell/filetypes@10.0.0': - resolution: {integrity: sha512-UP57j9yrDtlCHpFxc/eGho1m8DP5olfu9KRWwd5fiqL9nMSE2rUJtPzQyvqmDwO5bVZt3B+fTVdo4gxuiqw25A==} + '@cspell/filetypes@10.0.1': + resolution: {integrity: sha512-Z5S35giU5IW49fBBq6BksUbE8PC4IYPfaKuwl5Nl9jkf/OkAKiBmCowKX45NzRUQInwK/GSqqIUifrNeI6LdLw==} engines: {node: '>=22.18.0'} - '@cspell/rpc@10.0.0': - resolution: {integrity: sha512-QrpOZMwz2pAjvl6Hky2PauYoMpLCASn3osjn7uKUbgFV70sahyj6tmx4rRgRX7vHu2WQLZev+YsuO4EujiBDOg==} + '@cspell/rpc@10.0.1': + resolution: {integrity: sha512-axSRKv3zEAmBm66iD/FV/MPmE4/Yf7c3PZiwTW894Yd3iEhtn3KPKeTrqQ2/tDrhB1Z2qTsap/Hue0MK4o5WXg==} engines: {node: '>=22.18.0'} - '@cspell/strong-weak-map@10.0.0': - resolution: {integrity: sha512-JRsato0s2IjYdsng+AGL6oAqgZVQgih5aWKdmxs21H6EdhMaoFDmRE5kXm/RT5a6OMdtnzQM9DqeToqBChWIOQ==} + '@cspell/strong-weak-map@10.0.1': + resolution: {integrity: sha512-lenN1DVyPi8nJLSMSJJ670ddTjyiruLueuSZO1qLcxBqUhgxDt/mALu9N/1m6WdOVcg6m/5cLiZVg2KOo2UzRw==} engines: {node: '>=22.18.0'} - '@cspell/url@10.0.0': - resolution: {integrity: sha512-q+0pHQ8DbqjemyaOn/mTtBRbCuKDqhnsVbZ6J9zkTsxPgMpccjy0s5oLXwomfrrxMRBH+UcbERwtUmE+SbnoIQ==} + '@cspell/url@10.0.1': + resolution: {integrity: sha512-abYYgI29wJhWIfWTYrYuzRYDcHQUQ1N5ylnhxYn1NJnIQMqUWGLbDmt12JABtZ+R6h6UNatQrS7rhP86etvJyQ==} engines: {node: '>=22.18.0'} '@cspotcode/source-map-support@0.8.1': @@ -4281,9 +4282,6 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} - '@types/node@26.1.1': resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} @@ -4338,11 +4336,11 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.66.0': - resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} + '@typescript-eslint/eslint-plugin@8.67.0': + resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.66.0 + '@typescript-eslint/parser': ^8.67.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' @@ -4352,8 +4350,8 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@typescript-eslint/parser@8.66.0': - resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} + '@typescript-eslint/parser@8.67.0': + resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4371,6 +4369,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.67.0': + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4383,6 +4387,10 @@ packages: resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.67.0': + resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.65.0': resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4395,6 +4403,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.67.0': + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.65.0': resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4402,8 +4416,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.66.0': - resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} + '@typescript-eslint/type-utils@8.67.0': + resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4421,6 +4435,10 @@ packages: resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.67.0': + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4442,6 +4460,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.67.0': + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4462,6 +4486,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.67.0': + resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4474,6 +4505,10 @@ packages: resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.67.0': + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} @@ -4562,6 +4597,7 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4604,6 +4640,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + address@2.0.3: resolution: {integrity: sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==} engines: {node: '>= 16.0.0'} @@ -4863,6 +4904,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + baseline-browser-mapping@2.11.14: + resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==} + engines: {node: '>=6.0.0'} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -4922,6 +4968,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -4974,6 +5025,9 @@ packages: caniuse-lite@1.0.30001803: resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -5129,8 +5183,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-json@4.6.2: - resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} comment-parser@1.4.7: @@ -5269,36 +5323,36 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cspell-config-lib@10.0.0: - resolution: {integrity: sha512-HWK7SRnJ3N/kOThw/uzmXmQYCzBxu58Jkq2hHyte1voDl118BeNFoaNRWMpYdHbBi3kCj8gaZu8wGtm+Zmdhxw==} + cspell-config-lib@10.0.1: + resolution: {integrity: sha512-hMpo/0j6k7pbiqrLDOLJKD2IGP9XwhjKf2miiM6p84Xeo4nyuFZaxxDCQ68R851HSYFrrdltgpoipMbj1h2Tnw==} engines: {node: '>=22.18.0'} - cspell-dictionary@10.0.0: - resolution: {integrity: sha512-KubSoEAJO+77KPSSWjoLCz0+MIWVNq3joGTSyxucAZrBSJD64Y1O4BHHr1aj6XHIZwXhWWNScQlrQR3OcIulng==} + cspell-dictionary@10.0.1: + resolution: {integrity: sha512-3cZ659vgsZWkzGQJR/sNqGDVt/OnvTSieLKI76V++4t1bHJfochb9ZrrwsuMsb1VPGiyqClUP1/O6WrefF/FVg==} engines: {node: '>=22.18.0'} - cspell-glob@10.0.0: - resolution: {integrity: sha512-bXS35fMcA9X7GEkfnWBfoPd/vTnxxfXW+YHt6tWxu5fejfs00qUbjWp1oLC9FxRaXWxIkfsYp2mi1k1jYl4RVw==} + cspell-glob@10.0.1: + resolution: {integrity: sha512-7bII9J3aSSpZDwhx7w+zfQXbMxHZQ3be0ilUp5bHrsjz6o07v/NqOHMGcwKdPn1sw2dxDz9sv057xE5pqXnSdw==} engines: {node: '>=22.18.0'} - cspell-grammar@10.0.0: - resolution: {integrity: sha512-49udtYzkcCYEIDJbFOb4IwiAJebOYZnYvG6o6Ep19Tq0Xwjk7i4vxUprNiFNDCWFbcbJRPE9cpwQUVwp5WFGLw==} + cspell-grammar@10.0.1: + resolution: {integrity: sha512-xC9AFYmaI9wsO//a7S5tdDGKGJVD5UEEsTg+Up2fi7lPfXIryisYmV6tePNL1SEg0idYss4ja8LUZ3Mib09BjQ==} engines: {node: '>=22.18.0'} hasBin: true - cspell-io@10.0.0: - resolution: {integrity: sha512-NQCAUhx9DwKApxPuFl7EK1K1XSaQEAPld45yjjwv93xF8rJkEGkgzOwjbqafwAD20eKYv1a7oj/9EC0S5jETSw==} + cspell-io@10.0.1: + resolution: {integrity: sha512-8C2ka07faxflnaqEBO3pektS21XViE/SEHT7F5ZD1ou7FyMR5u3xawTBJSczClfsxLt/WYeztBYrpmGAjmjksw==} engines: {node: '>=22.18.0'} - cspell-lib@10.0.0: - resolution: {integrity: sha512-PowW6JEjuv/F2aFEirZvBxpzHdchOnpsUJbeIcFcai0++taLTbHQObROBEBf7e0S8DnHpVD5TZkqrTME5e44wg==} + cspell-lib@10.0.1: + resolution: {integrity: sha512-RpsIPiLzc4/YMW8BMRKpyJ81x439qjYWcqgdKeXnMkbKM88J9PexzutfFf/4v97v96KzfNitEzMpbI0uj8OeUg==} engines: {node: '>=22.18.0'} - cspell-trie-lib@10.0.0: - resolution: {integrity: sha512-R8qrMx10E/bm3Lecslwxn9XYo5NzSRK1rtandEX5n9UmEYHoBXjZELkg5+TOnV8VgrVaJSK57XtcGrbKp/4kSg==} + cspell-trie-lib@10.0.1: + resolution: {integrity: sha512-BFvhalSkRQFjKrZ//FKK7fRGrZFpifnxB5AwCkzsIsBZqicsfafcQ1xP21qpb0QqyV/IomjNgviG+tRJs+0rMw==} engines: {node: '>=22.18.0'} peerDependencies: - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-types': 10.0.1 css-declaration-sorter@7.4.0: resolution: {integrity: sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==} @@ -5617,6 +5671,9 @@ packages: electron-to-chromium@1.5.389: resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + electron-to-chromium@1.5.406: + resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -5649,12 +5706,8 @@ packages: resolution: {integrity: sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.24.0: - resolution: {integrity: sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==} - engines: {node: '>=10.13.0'} - - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} enquirer@2.3.6: @@ -5717,8 +5770,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -5931,8 +5984,8 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-equals@6.0.0: - resolution: {integrity: sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==} + fast-equals@6.0.2: + resolution: {integrity: sha512-sAjhj9ZhOxYCGiNMnZLaucOqf5ZeFnHNoKoAZiD9thhJ0N8RP85qJK759/97C/3L7NzzmGVB5uiX9AUpySZmUQ==} engines: {node: '>=6.0.0'} fast-glob@3.3.3: @@ -6627,8 +6680,8 @@ packages: jasmine-core@4.6.1: resolution: {integrity: sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==} - jasmine-core@5.13.0: - resolution: {integrity: sha512-vsYjfh7lyqvZX5QgqKc4YH8phs7g96Z8bsdIFNEU3VqXhlHaq+vov/Fgn/sr6MiUczdZkyXRC3TX369Ll4Nzbw==} + jasmine-core@6.3.0: + resolution: {integrity: sha512-eMm5qBovNjNoGOcgE/W207+wrcK5zrQv0Rg/rWGboUJUmZp0dFCpHTyjpuDAfCwRCqg7f9U2q2jtv/aUuzdCQg==} jasmine-reporters@2.5.2: resolution: {integrity: sha512-qdewRUuFOSiWhiyWZX8Yx3YNQ9JG51ntBEO4ekLQRpktxFTwUHy24a86zD/Oi2BRTKksEdfWQZcQFqzjqIkPig==} @@ -6664,8 +6717,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsdoc-type-pratt-parser@7.3.0: @@ -7239,6 +7292,10 @@ packages: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + node-schedule@2.1.1: resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} engines: {node: '>=6'} @@ -8403,11 +8460,15 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} + smol-toml@1.8.0: + resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} + engines: {node: '>= 18'} + socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} - socket.io-parser@4.2.6: - resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + socket.io-parser@4.2.7: + resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==} engines: {node: '>=10.0.0'} socket.io@4.8.3: @@ -8704,8 +8765,8 @@ packages: engines: {node: '>=10'} hasBin: true - terser@5.49.0: - resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} + terser@5.50.0: + resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} engines: {node: '>=10'} hasBin: true @@ -8889,9 +8950,6 @@ packages: resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} hasBin: true - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -8941,6 +8999,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -9187,8 +9251,8 @@ packages: webpack-cli: optional: true - webpack@5.109.0: - resolution: {integrity: sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==} + webpack@5.109.2: + resolution: {integrity: sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -9369,14 +9433,14 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@20.3.16(18d336cd389c837facbd563dfff009d7)': + '@angular-devkit/build-angular@20.3.16(c48ca78b9b10b3f91f8dcbb193e451e5)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.16(chokidar@4.0.3) '@angular-devkit/build-webpack': 0.2003.16(chokidar@4.0.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) '@angular-devkit/core': 20.3.16(chokidar@4.0.3) - '@angular/build': 20.3.16(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.4.0)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/build': 20.3.16(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.4.0)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@babel/core': 7.28.3(supports-color@7.2.0) '@babel/generator': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 @@ -9387,11 +9451,11 @@ snapshots: '@babel/preset-env': 7.28.3(@babel/core@7.28.3(supports-color@7.2.0))(supports-color@7.2.0) '@babel/runtime': 7.28.3 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 20.3.16(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@ngtools/webpack': 20.3.16(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) ansi-colors: 4.1.3 autoprefixer: 10.4.21(postcss@8.5.25) babel-loader: 10.0.0(@babel/core@7.28.3(supports-color@7.2.0))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - browserslist: 4.28.5 + browserslist: 4.28.8 copy-webpack-plugin: 13.0.1(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) css-loader: 7.1.2(@rspack/core@1.6.8)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) esbuild-wasm: 0.25.9 @@ -9426,13 +9490,13 @@ snapshots: webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) webpack-dev-server: 6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) optionalDependencies: '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) esbuild: 0.25.9 karma: 6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) - ng-packagr: 20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + ng-packagr: 20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) transitivePeerDependencies: - '@angular/compiler' - '@minify-html/node' @@ -9507,23 +9571,23 @@ snapshots: '@angular-eslint/bundled-angular-compiler@22.1.0': {} - '@angular-eslint/eslint-plugin-template@20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.65.0)(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/eslint-plugin-template@20.7.0(@angular-eslint/template-parser@22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3))(@typescript-eslint/types@8.67.0)(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 '@angular-eslint/template-parser': 22.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) aria-query: 5.3.2 axobject-query: 4.1.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 - '@angular-eslint/eslint-plugin@20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/eslint-plugin@20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 - '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@angular-eslint/utils': 20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -9535,31 +9599,31 @@ snapshots: eslint-scope: 9.1.2 typescript: 5.9.3 - '@angular-eslint/utils@20.7.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': + '@angular-eslint/utils@20.7.0(@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.7.0 - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 - '@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))': + '@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))': dependencies: '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - '@angular/build@20.3.16(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.4.0)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0)': + '@angular/build@20.3.16(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.4.0)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.16(chokidar@4.0.3) '@angular/compiler': 20.3.27 - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@babel/core': 7.28.3(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.14(@types/node@26.1.1) '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.11(@types/node@26.1.1)(jiti@2.7.0)(less@4.4.0)(sass-embedded@1.100.0)(sass@1.90.0)(terser@5.43.1)(yaml@2.9.0)) beasties: 0.3.5 - browserslist: 4.28.5 + browserslist: 4.28.8 esbuild: 0.25.9 https-proxy-agent: 7.0.6(supports-color@7.2.0) istanbul-lib-instrument: 6.0.3(supports-color@7.2.0) @@ -9581,11 +9645,11 @@ snapshots: watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) karma: 6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) less: 4.4.0 lmdb: 3.4.2 - ng-packagr: 20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + ng-packagr: 20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.25 transitivePeerDependencies: - '@types/node' @@ -9600,12 +9664,12 @@ snapshots: - tsx - yaml - '@angular/build@20.3.32(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0)': + '@angular/build@20.3.32(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.32(chokidar@4.0.3) '@angular/compiler': 20.3.27 - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 @@ -9634,11 +9698,11 @@ snapshots: watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) karma: 6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) less: 4.6.6 lmdb: 3.4.2 - ng-packagr: 20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + ng-packagr: 20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.25 transitivePeerDependencies: - '@types/node' @@ -9667,7 +9731,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3)': + '@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@angular/compiler': 20.3.27 '@babel/core': 7.29.7(supports-color@7.2.0) @@ -9695,52 +9759,52 @@ snapshots: '@angular/compiler': 20.3.27 zone.js: 0.15.0 - '@angular/forms@20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': + '@angular/forms@20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/material-date-fns-adapter@20.2.14(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(455430e5d13335858a88189cadfe3159))(date-fns@2.30.0)': + '@angular/material-date-fns-adapter@20.2.14(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7))(date-fns@2.30.0)': dependencies: '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/material': 20.2.14(455430e5d13335858a88189cadfe3159) + '@angular/material': 20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7) date-fns: 2.30.0 tslib: 2.8.1 - '@angular/material@20.2.14(455430e5d13335858a88189cadfe3159)': + '@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7)': dependencies: '@angular/cdk': 20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/forms': 20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/forms': 20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/platform-browser-dynamic@20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))': + '@angular/platform-browser-dynamic@20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))': dependencies: '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/compiler': 20.3.27 '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) tslib: 2.8.1 - '@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))': + '@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))': dependencies: '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 optionalDependencies: - '@angular/animations': 20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/animations': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) - '@angular/router@20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': + '@angular/router@20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) rxjs: 7.8.2 tslib: 2.8.1 @@ -9843,7 +9907,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.5 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -11117,26 +11181,26 @@ snapshots: '@colors/colors@1.5.0': {} - '@cspell/cspell-bundled-dicts@10.0.0': + '@cspell/cspell-bundled-dicts@10.0.1': dependencies: '@cspell/dict-ada': 4.1.1 '@cspell/dict-al': 1.1.1 '@cspell/dict-aws': 4.0.17 '@cspell/dict-bash': 4.2.3 - '@cspell/dict-companies': 3.2.11 - '@cspell/dict-cpp': 7.0.2 + '@cspell/dict-companies': 3.2.12 + '@cspell/dict-cpp': 7.1.0 '@cspell/dict-cryptocurrencies': 5.0.5 '@cspell/dict-csharp': 4.0.8 '@cspell/dict-css': 4.1.2 '@cspell/dict-dart': 2.3.2 - '@cspell/dict-data-science': 2.0.14 + '@cspell/dict-data-science': 2.0.16 '@cspell/dict-django': 4.1.6 '@cspell/dict-docker': 1.1.17 '@cspell/dict-dotnet': 5.0.13 '@cspell/dict-elixir': 4.0.8 - '@cspell/dict-en-common-misspellings': 2.1.12 - '@cspell/dict-en-gb-mit': 3.1.24 - '@cspell/dict-en_us': 4.4.35 + '@cspell/dict-en-common-misspellings': 2.2.0 + '@cspell/dict-en-gb-mit': 3.1.25 + '@cspell/dict-en_us': 4.4.36 '@cspell/dict-filetypes': 3.0.18 '@cspell/dict-flutter': 1.1.1 '@cspell/dict-fonts': 4.0.6 @@ -11144,52 +11208,52 @@ snapshots: '@cspell/dict-fullstack': 3.2.9 '@cspell/dict-gaming-terms': 1.1.2 '@cspell/dict-git': 3.1.0 - '@cspell/dict-golang': 6.0.26 + '@cspell/dict-golang': 6.0.27 '@cspell/dict-google': 1.0.9 '@cspell/dict-haskell': 4.0.6 - '@cspell/dict-html': 4.0.15 + '@cspell/dict-html': 4.0.16 '@cspell/dict-html-symbol-entities': 4.0.5 '@cspell/dict-java': 5.0.12 '@cspell/dict-julia': 1.1.1 - '@cspell/dict-k8s': 1.0.12 + '@cspell/dict-k8s': 1.0.13 '@cspell/dict-kotlin': 1.1.1 '@cspell/dict-latex': 5.1.0 '@cspell/dict-lorem-ipsum': 4.0.5 '@cspell/dict-lua': 4.0.8 '@cspell/dict-makefile': 1.0.5 - '@cspell/dict-markdown': 2.0.17(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3) - '@cspell/dict-monkeyc': 1.0.12 + '@cspell/dict-markdown': 2.0.18(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.16)(@cspell/dict-typescript@3.2.3) + '@cspell/dict-monkeyc': 1.1.0 '@cspell/dict-node': 5.0.9 - '@cspell/dict-npm': 5.2.41 + '@cspell/dict-npm': 5.2.46 '@cspell/dict-php': 4.1.1 '@cspell/dict-powershell': 5.0.15 '@cspell/dict-public-licenses': 2.0.16 - '@cspell/dict-python': 4.2.27 + '@cspell/dict-python': 4.4.0 '@cspell/dict-r': 2.1.1 - '@cspell/dict-ruby': 5.1.1 + '@cspell/dict-ruby': 5.1.2 '@cspell/dict-rust': 4.1.2 '@cspell/dict-scala': 5.0.9 '@cspell/dict-shell': 1.2.0 - '@cspell/dict-software-terms': 5.2.2 + '@cspell/dict-software-terms': 5.4.1 '@cspell/dict-sql': 2.2.1 '@cspell/dict-svelte': 1.0.7 '@cspell/dict-swift': 2.0.6 - '@cspell/dict-terraform': 1.1.3 + '@cspell/dict-terraform': 1.1.4 '@cspell/dict-typescript': 3.2.3 '@cspell/dict-vue': 3.0.5 '@cspell/dict-zig': 1.0.0 - '@cspell/cspell-performance-monitor@10.0.0': {} + '@cspell/cspell-performance-monitor@10.0.1': {} - '@cspell/cspell-pipe@10.0.0': {} + '@cspell/cspell-pipe@10.0.1': {} - '@cspell/cspell-resolver@10.0.0': + '@cspell/cspell-resolver@10.0.1': dependencies: global-directory: 5.0.0 - '@cspell/cspell-service-bus@10.0.0': {} + '@cspell/cspell-service-bus@10.0.1': {} - '@cspell/cspell-types@10.0.0': {} + '@cspell/cspell-types@10.0.1': {} '@cspell/dict-ada@4.1.1': {} @@ -11201,9 +11265,9 @@ snapshots: dependencies: '@cspell/dict-shell': 1.2.0 - '@cspell/dict-companies@3.2.11': {} + '@cspell/dict-companies@3.2.12': {} - '@cspell/dict-cpp@7.0.2': {} + '@cspell/dict-cpp@7.1.0': {} '@cspell/dict-cryptocurrencies@5.0.5': {} @@ -11213,7 +11277,7 @@ snapshots: '@cspell/dict-dart@2.3.2': {} - '@cspell/dict-data-science@2.0.14': {} + '@cspell/dict-data-science@2.0.16': {} '@cspell/dict-django@4.1.6': {} @@ -11223,11 +11287,11 @@ snapshots: '@cspell/dict-elixir@4.0.8': {} - '@cspell/dict-en-common-misspellings@2.1.12': {} + '@cspell/dict-en-common-misspellings@2.2.0': {} - '@cspell/dict-en-gb-mit@3.1.24': {} + '@cspell/dict-en-gb-mit@3.1.25': {} - '@cspell/dict-en_us@4.4.35': {} + '@cspell/dict-en_us@4.4.36': {} '@cspell/dict-filetypes@3.0.18': {} @@ -11243,7 +11307,7 @@ snapshots: '@cspell/dict-git@3.1.0': {} - '@cspell/dict-golang@6.0.26': {} + '@cspell/dict-golang@6.0.27': {} '@cspell/dict-google@1.0.9': {} @@ -11251,13 +11315,13 @@ snapshots: '@cspell/dict-html-symbol-entities@4.0.5': {} - '@cspell/dict-html@4.0.15': {} + '@cspell/dict-html@4.0.16': {} '@cspell/dict-java@5.0.12': {} '@cspell/dict-julia@1.1.1': {} - '@cspell/dict-k8s@1.0.12': {} + '@cspell/dict-k8s@1.0.13': {} '@cspell/dict-kotlin@1.1.1': {} @@ -11269,18 +11333,18 @@ snapshots: '@cspell/dict-makefile@1.0.5': {} - '@cspell/dict-markdown@2.0.17(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3)': + '@cspell/dict-markdown@2.0.18(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.16)(@cspell/dict-typescript@3.2.3)': dependencies: '@cspell/dict-css': 4.1.2 - '@cspell/dict-html': 4.0.15 + '@cspell/dict-html': 4.0.16 '@cspell/dict-html-symbol-entities': 4.0.5 '@cspell/dict-typescript': 3.2.3 - '@cspell/dict-monkeyc@1.0.12': {} + '@cspell/dict-monkeyc@1.1.0': {} '@cspell/dict-node@5.0.9': {} - '@cspell/dict-npm@5.2.41': {} + '@cspell/dict-npm@5.2.46': {} '@cspell/dict-php@4.1.1': {} @@ -11288,13 +11352,13 @@ snapshots: '@cspell/dict-public-licenses@2.0.16': {} - '@cspell/dict-python@4.2.27': + '@cspell/dict-python@4.4.0': dependencies: - '@cspell/dict-data-science': 2.0.14 + '@cspell/dict-data-science': 2.0.16 '@cspell/dict-r@2.1.1': {} - '@cspell/dict-ruby@5.1.1': {} + '@cspell/dict-ruby@5.1.2': {} '@cspell/dict-rust@4.1.2': {} @@ -11302,7 +11366,7 @@ snapshots: '@cspell/dict-shell@1.2.0': {} - '@cspell/dict-software-terms@5.2.2': {} + '@cspell/dict-software-terms@5.4.1': {} '@cspell/dict-sql@2.2.1': {} @@ -11310,7 +11374,7 @@ snapshots: '@cspell/dict-swift@2.0.6': {} - '@cspell/dict-terraform@1.1.3': {} + '@cspell/dict-terraform@1.1.4': {} '@cspell/dict-typescript@3.2.3': {} @@ -11318,26 +11382,26 @@ snapshots: '@cspell/dict-zig@1.0.0': {} - '@cspell/dynamic-import@10.0.0': + '@cspell/dynamic-import@10.0.1': dependencies: - '@cspell/url': 10.0.0 + '@cspell/url': 10.0.1 import-meta-resolve: 4.2.0 - '@cspell/eslint-plugin@10.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': + '@cspell/eslint-plugin@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': dependencies: - '@cspell/cspell-types': 10.0.0 - '@cspell/url': 10.0.0 - cspell-lib: 10.0.0 + '@cspell/cspell-types': 10.0.1 + '@cspell/url': 10.0.1 + cspell-lib: 10.0.1 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) synckit: 0.11.13 - '@cspell/filetypes@10.0.0': {} + '@cspell/filetypes@10.0.1': {} - '@cspell/rpc@10.0.0': {} + '@cspell/rpc@10.0.1': {} - '@cspell/strong-weak-map@10.0.0': {} + '@cspell/strong-weak-map@10.0.1': {} - '@cspell/url@10.0.0': {} + '@cspell/url@10.0.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -11434,7 +11498,7 @@ snapshots: '@es-joy/jsdoccomment@0.90.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/types': 8.67.0 comment-parser: 1.4.7 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.3.0 @@ -12063,12 +12127,12 @@ snapshots: '@lmdb/lmdb-win32-x64@3.4.2': optional: true - '@mat-datetimepicker/core@16.0.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(455430e5d13335858a88189cadfe3159))': + '@mat-datetimepicker/core@16.0.1(@angular/cdk@20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/material@20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7))': dependencies: '@angular/cdk': 20.2.14(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/material': 20.2.14(455430e5d13335858a88189cadfe3159) + '@angular/material': 20.2.14(7fc7dc26a5364b20a95267fc7b4f10c7) tslib: 2.8.1 '@module-federation/bridge-react-webpack-plugin@2.5.1(node-fetch@2.7.0(encoding@0.1.13))': @@ -12113,7 +12177,7 @@ snapshots: - utf-8-validate optional: true - '@module-federation/enhanced@2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': + '@module-federation/enhanced@2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': dependencies: '@module-federation/bridge-react-webpack-plugin': 2.5.1(node-fetch@2.7.0(encoding@0.1.13)) '@module-federation/cli': 2.5.1(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3) @@ -12131,7 +12195,7 @@ snapshots: upath: 2.0.1 optionalDependencies: typescript: 5.9.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) transitivePeerDependencies: - '@rspack/core' - bufferutil @@ -12172,16 +12236,16 @@ snapshots: - vue-tsc optional: true - '@module-federation/node@2.7.44(@rspack/core@1.6.8)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': + '@module-federation/node@2.7.44(@rspack/core@1.6.8)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': dependencies: - '@module-federation/enhanced': 2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@module-federation/enhanced': 2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) '@module-federation/runtime': 2.5.1(node-fetch@2.7.0(encoding@0.1.13)) '@module-federation/sdk': 2.5.1(node-fetch@2.7.0(encoding@0.1.13)) encoding: 0.1.13 node-fetch: 2.7.0(encoding@0.1.13) tapable: 2.3.0 optionalDependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) transitivePeerDependencies: - '@rspack/core' - bufferutil @@ -12455,9 +12519,9 @@ snapshots: '@neoconfetti/react@1.0.0': {} - '@ngtools/webpack@20.3.16(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': + '@ngtools/webpack@20.3.16(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': dependencies: - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) typescript: 5.9.3 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) @@ -12481,17 +12545,17 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nx/angular@23.1.1(14b0b041890a4d9ad84851ba43aed6f3)': + '@nx/angular@23.1.1(1871ac9779a3c1c87e7a2019dffef8ff)': dependencies: '@angular-devkit/core': 20.3.32(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.32(chokidar@4.0.3) '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/eslint': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) - '@nx/module-federation': 23.1.1(159f6bdc10a59ab922b88ca0434a74ba) - '@nx/rspack': 23.1.1(d1f2db652c3431e28213a9ed1561260a) - '@nx/web': 23.1.1(69165f71226cdd786def4faf955c6def) - '@nx/webpack': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@nx/module-federation': 23.1.1(fbabc342ac83f23db68b8d27ddbe599a) + '@nx/rspack': 23.1.1(f7d730e8feeef54478a80edef0d832f6) + '@nx/web': 23.1.1(9aad18717af1cde75effe0fe4ceb25af) + '@nx/webpack': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3) '@schematics/angular': 20.3.32(chokidar@4.0.3) '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) @@ -12505,10 +12569,10 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular-devkit/build-angular': 20.3.16(18d336cd389c837facbd563dfff009d7) - '@angular/build': 20.3.32(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) + '@angular-devkit/build-angular': 20.3.16(c48ca78b9b10b3f91f8dcbb193e451e5) + '@angular/build': 20.3.32(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@26.1.1)(chokidar@4.0.3)(jiti@2.7.0)(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0))(less@4.6.6)(ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.25)(sass-embedded@1.100.0)(supports-color@7.2.0)(terser@5.43.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.9.0) '@nx/cypress': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) - ng-packagr: 20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + ng-packagr: 20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) transitivePeerDependencies: - '@babel/traverse' - '@minify-html/node' @@ -12584,13 +12648,13 @@ snapshots: tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/eslint-plugin@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)': + '@nx/eslint-plugin@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3) '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) confusing-browser-globals: 1.0.11 globals: 17.6.0 jsonc-eslint-parser: 2.4.2 @@ -12598,7 +12662,7 @@ snapshots: semver: 7.6.3 tslib: 2.8.1 optionalDependencies: - '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) transitivePeerDependencies: - '@babel/traverse' @@ -12668,11 +12732,11 @@ snapshots: - nx - supports-color - '@nx/module-federation@23.1.1(159f6bdc10a59ab922b88ca0434a74ba)': + '@nx/module-federation@23.1.1(fbabc342ac83f23db68b8d27ddbe599a)': dependencies: '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) - '@nx/web': 23.1.1(69165f71226cdd786def4faf955c6def) + '@nx/web': 23.1.1(9aad18717af1cde75effe0fe4ceb25af) '@rspack/core': 2.1.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13))) express: 4.22.2(supports-color@7.2.0) http-proxy-middleware: 3.0.7(supports-color@7.2.0) @@ -12680,8 +12744,8 @@ snapshots: tslib: 2.8.1 webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: - '@module-federation/enhanced': 2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - '@module-federation/node': 2.7.44(@rspack/core@1.6.8)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@module-federation/enhanced': 2.5.1(@rspack/core@1.6.8)(node-fetch@2.7.0(encoding@0.1.13))(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@module-federation/node': 2.7.44(@rspack/core@1.6.8)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) transitivePeerDependencies: - '@babel/traverse' - '@minify-html/node' @@ -12742,15 +12806,15 @@ snapshots: '@nx/nx-win32-x64-msvc@23.1.1': optional: true - '@nx/rspack@23.1.1(d1f2db652c3431e28213a9ed1561260a)': + '@nx/rspack@23.1.1(f7d730e8feeef54478a80edef0d832f6)': dependencies: '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) - '@nx/module-federation': 23.1.1(159f6bdc10a59ab922b88ca0434a74ba) - '@nx/web': 23.1.1(69165f71226cdd786def4faf955c6def) + '@nx/module-federation': 23.1.1(fbabc342ac83f23db68b8d27ddbe599a) + '@nx/web': 23.1.1(9aad18717af1cde75effe0fe4ceb25af) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3) autoprefixer: 10.5.0(postcss@8.5.25) - browserslist: 4.28.5 + browserslist: 4.28.8 css-loader: 6.11.0(@rspack/core@1.6.8)(webpack@5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) enquirer: 2.3.6 express: 4.22.2(supports-color@7.2.0) @@ -12775,7 +12839,7 @@ snapshots: webpack-node-externals: 3.0.0 optionalDependencies: '@rspack/core': 1.6.8 - '@rspack/dev-server': 1.2.1(@rspack/core@1.6.8)(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@rspack/dev-server': 1.2.1(@rspack/core@1.6.8)(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) '@rspack/plugin-react-refresh': 1.6.2(react-refresh@0.18.0)(webpack-hot-middleware@2.26.1) transitivePeerDependencies: - '@babel/traverse' @@ -12812,7 +12876,7 @@ snapshots: - verdaccio - webpack-cli - '@nx/storybook@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@nx/web@23.1.1(69165f71226cdd786def4faf955c6def))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3)': + '@nx/storybook@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@nx/web@23.1.1(9aad18717af1cde75effe0fe4ceb25af))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@nx/cypress': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) '@nx/devkit': 23.1.1(nx@23.1.1) @@ -12823,7 +12887,7 @@ snapshots: storybook: 10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) tslib: 2.8.1 optionalDependencies: - '@nx/web': 23.1.1(69165f71226cdd786def4faf955c6def) + '@nx/web': 23.1.1(9aad18717af1cde75effe0fe4ceb25af) transitivePeerDependencies: - '@babel/traverse' - '@nx/jest' @@ -12838,7 +12902,7 @@ snapshots: - typescript - verdaccio - '@nx/web@23.1.1(69165f71226cdd786def4faf955c6def)': + '@nx/web@23.1.1(9aad18717af1cde75effe0fe4ceb25af)': dependencies: '@nx/devkit': 23.1.1(nx@23.1.1) '@nx/js': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) @@ -12849,7 +12913,7 @@ snapshots: optionalDependencies: '@nx/cypress': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3) '@nx/eslint': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@zkochan/js-yaml@0.0.7)(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(nx@23.1.1)(supports-color@7.2.0) - '@nx/webpack': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + '@nx/webpack': 23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -12860,7 +12924,7 @@ snapshots: - supports-color - verdaccio - '@nx/webpack@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': + '@nx/webpack@23.1.1(@babel/traverse@7.29.7(supports-color@7.2.0))(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(nx@23.1.1)(supports-color@7.2.0)(typescript@5.9.3)(webpack-dev-server@6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@nx/devkit': 23.1.1(nx@23.1.1) @@ -12868,36 +12932,36 @@ snapshots: '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3) ajv: 8.20.0 autoprefixer: 10.5.0(postcss@8.5.25) - babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@7.2.0))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - browserslist: 4.28.5 - copy-webpack-plugin: 14.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - css-loader: 6.11.0(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - css-minimizer-webpack-plugin: 8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.9)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@7.2.0))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + browserslist: 4.28.8 + copy-webpack-plugin: 14.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + css-loader: 6.11.0(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + css-minimizer-webpack-plugin: 8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.9)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) less: 4.5.1 - less-loader: 12.3.3(@rspack/core@1.6.8)(less@4.5.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - license-webpack-plugin: 4.0.2(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + less-loader: 12.3.3(@rspack/core@1.6.8)(less@4.5.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + license-webpack-plugin: 4.0.2(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) loader-utils: 2.0.4 - mini-css-extract-plugin: 2.4.7(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + mini-css-extract-plugin: 2.4.7(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) parse5: 4.0.0 picocolors: 1.1.1 postcss: 8.5.25 postcss-import: 14.1.0(postcss@8.5.25) - postcss-loader: 8.2.1(@rspack/core@1.6.8)(postcss@8.5.25)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + postcss-loader: 8.2.1(@rspack/core@1.6.8)(postcss@8.5.25)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) rxjs: 7.8.2 sass: 1.101.0 sass-embedded: 1.100.0 - sass-loader: 16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - source-map-loader: 5.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - style-loader: 3.3.4(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - ts-loader: 9.6.1(loader-utils@2.0.4)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + sass-loader: 16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + source-map-loader: 5.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + style-loader: 3.3.4(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + ts-loader: 9.6.1(loader-utils@2.0.4)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) tsconfig-paths-webpack-plugin: 4.2.0 tslib: 2.8.1 webpack-node-externals: 3.0.0 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) optionalDependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) webpack-dev-server: 6.0.0(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) transitivePeerDependencies: - '@babel/traverse' @@ -13578,7 +13642,7 @@ snapshots: optionalDependencies: '@module-federation/runtime-tools': 2.5.1(node-fetch@2.7.0(encoding@0.1.13)) - '@rspack/dev-server@1.2.1(@rspack/core@1.6.8)(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': + '@rspack/dev-server@1.2.1(@rspack/core@1.6.8)(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(tslib@2.8.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25))': dependencies: '@rspack/core': 1.6.8 '@types/bonjour': 3.5.13 @@ -13607,7 +13671,7 @@ snapshots: serve-index: 1.9.2(supports-color@7.2.0) sockjs: 0.3.24 spdy: 4.0.2(supports-color@7.2.0) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -13663,17 +13727,17 @@ snapshots: storybook: 10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) ts-dedent: 2.3.0 - '@storybook/angular@10.4.0(641969a558ebac8287ee9e40d24c89b8)': + '@storybook/angular@10.4.0(69b0d8b90b0fe3d6016d76a1b6c3fcbb)': dependencies: '@angular-devkit/architect': 0.2003.16(chokidar@4.0.3) - '@angular-devkit/build-angular': 20.3.16(18d336cd389c837facbd563dfff009d7) + '@angular-devkit/build-angular': 20.3.16(c48ca78b9b10b3f91f8dcbb193e451e5) '@angular-devkit/core': 20.3.32(chokidar@4.0.3) '@angular/common': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/compiler': 20.3.27 - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@angular/core': 20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) - '@angular/platform-browser-dynamic': 20.3.26(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.26(@angular/animations@20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))) + '@angular/platform-browser': 20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': 20.3.27(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.27)(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.27(@angular/animations@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))) '@storybook/builder-webpack5': 10.4.0(@rspack/core@1.6.8)(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) '@storybook/global': 5.0.0 rxjs: 7.8.2 @@ -13682,9 +13746,9 @@ snapshots: ts-dedent: 2.3.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: - '@angular/animations': 20.3.26(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/animations': 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0)) zone.js: 0.15.0 transitivePeerDependencies: - '@minify-html/node' @@ -13707,17 +13771,17 @@ snapshots: '@storybook/core-webpack': 10.4.0(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 - css-loader: 7.1.4(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + css-loader: 7.1.4(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) magic-string: 0.30.21 storybook: 10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - style-loader: 4.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + style-loader: 4.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) ts-dedent: 2.3.0 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - webpack-dev-middleware: 6.1.3(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack-dev-middleware: 6.1.3(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -13905,10 +13969,6 @@ snapshots: '@types/node': 26.1.1 optional: true - '@types/node@25.9.1': - dependencies: - undici-types: 7.24.6 - '@types/node@26.1.1': dependencies: undici-types: 8.3.0 @@ -13974,14 +14034,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/parser': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.67.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.6 natural-compare: 1.4.0 @@ -13998,12 +14058,12 @@ snapshots: - supports-color - typescript - '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.67.0 debug: 4.4.3(supports-color@7.2.0) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 5.9.3 @@ -14012,8 +14072,8 @@ snapshots: '@typescript-eslint/project-service@8.65.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3) - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: @@ -14021,8 +14081,17 @@ snapshots: '@typescript-eslint/project-service@8.66.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3) - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.67.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: @@ -14043,6 +14112,11 @@ snapshots: '@typescript-eslint/types': 8.66.0 '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -14051,6 +14125,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.65.0 @@ -14063,11 +14141,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) debug: 4.4.3(supports-color@7.2.0) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -14081,6 +14159,8 @@ snapshots: '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/typescript-estree@5.62.0(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -14125,6 +14205,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@5.62.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) @@ -14162,6 +14257,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.67.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.67.0 + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -14177,6 +14283,11 @@ snapshots: '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.2': {} '@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.11(@types/node@26.1.1)(jiti@2.7.0)(less@4.4.0)(sass-embedded@1.100.0)(sass@1.90.0)(terser@5.43.1)(yaml@2.9.0))': @@ -14325,20 +14436,22 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-phases@1.0.4(acorn@8.17.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn@8.17.0: {} + acorn@8.18.0: {} + address@2.0.3: {} adjust-sourcemap-loader@4.0.0: @@ -14483,8 +14596,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.25): dependencies: - browserslist: 4.28.5 - caniuse-lite: 1.0.30001803 + browserslist: 4.28.8 + caniuse-lite: 1.0.30001809 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -14493,7 +14606,7 @@ snapshots: autoprefixer@10.5.0(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 caniuse-lite: 1.0.30001803 fraction.js: 5.3.4 picocolors: 1.1.1 @@ -14518,12 +14631,12 @@ snapshots: find-up: 5.0.0 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - babel-loader@9.2.1(@babel/core@7.29.7(supports-color@7.2.0))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + babel-loader@9.2.1(@babel/core@7.29.7(supports-color@7.2.0))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) babel-plugin-const-enum@1.2.0(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): dependencies: @@ -14617,6 +14730,8 @@ snapshots: baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.11.14: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -14717,6 +14832,14 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.5) + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.14 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.406 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + buffer-from@1.1.2: {} buffer@5.7.1: @@ -14763,8 +14886,8 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.28.5 - caniuse-lite: 1.0.30001803 + browserslist: 4.28.8 + caniuse-lite: 1.0.30001809 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -14772,6 +14895,8 @@ snapshots: caniuse-lite@1.0.30001803: {} + caniuse-lite@1.0.30001809: {} + case-sensitive-paths-webpack-plugin@2.4.0: {} caseless@0.12.0: {} @@ -14909,7 +15034,7 @@ snapshots: commander@8.3.0: {} - comment-json@4.6.2: + comment-json@5.0.0: dependencies: array-timsort: 1.0.3 esprima: 4.0.1 @@ -14988,18 +15113,18 @@ snapshots: tinyglobby: 0.2.17 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - copy-webpack-plugin@14.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + copy-webpack-plugin@14.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 7.0.6 tinyglobby: 0.2.17 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) core-js-compat@3.49.0: dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 core-util-is@1.0.3: optional: true @@ -15022,7 +15147,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -15032,7 +15157,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -15052,54 +15177,54 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cspell-config-lib@10.0.0: + cspell-config-lib@10.0.1: dependencies: - '@cspell/cspell-types': 10.0.0 - comment-json: 4.6.2 - smol-toml: 1.6.1 + '@cspell/cspell-types': 10.0.1 + comment-json: 5.0.0 + smol-toml: 1.8.0 yaml: 2.9.0 - cspell-dictionary@10.0.0: + cspell-dictionary@10.0.1: dependencies: - '@cspell/cspell-performance-monitor': 10.0.0 - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-types': 10.0.0 - cspell-trie-lib: 10.0.0(@cspell/cspell-types@10.0.0) - fast-equals: 6.0.0 + '@cspell/cspell-performance-monitor': 10.0.1 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-types': 10.0.1 + cspell-trie-lib: 10.0.1(@cspell/cspell-types@10.0.1) + fast-equals: 6.0.2 - cspell-glob@10.0.0: + cspell-glob@10.0.1: dependencies: - '@cspell/url': 10.0.0 + '@cspell/url': 10.0.1 picomatch: 4.0.5 - cspell-grammar@10.0.0: + cspell-grammar@10.0.1: dependencies: - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-types': 10.0.1 - cspell-io@10.0.0: + cspell-io@10.0.1: dependencies: - '@cspell/cspell-service-bus': 10.0.0 - '@cspell/url': 10.0.0 + '@cspell/cspell-service-bus': 10.0.1 + '@cspell/url': 10.0.1 - cspell-lib@10.0.0: + cspell-lib@10.0.1: dependencies: - '@cspell/cspell-bundled-dicts': 10.0.0 - '@cspell/cspell-performance-monitor': 10.0.0 - '@cspell/cspell-pipe': 10.0.0 - '@cspell/cspell-resolver': 10.0.0 - '@cspell/cspell-types': 10.0.0 - '@cspell/dynamic-import': 10.0.0 - '@cspell/filetypes': 10.0.0 - '@cspell/rpc': 10.0.0 - '@cspell/strong-weak-map': 10.0.0 - '@cspell/url': 10.0.0 - cspell-config-lib: 10.0.0 - cspell-dictionary: 10.0.0 - cspell-glob: 10.0.0 - cspell-grammar: 10.0.0 - cspell-io: 10.0.0 - cspell-trie-lib: 10.0.0(@cspell/cspell-types@10.0.0) + '@cspell/cspell-bundled-dicts': 10.0.1 + '@cspell/cspell-performance-monitor': 10.0.1 + '@cspell/cspell-pipe': 10.0.1 + '@cspell/cspell-resolver': 10.0.1 + '@cspell/cspell-types': 10.0.1 + '@cspell/dynamic-import': 10.0.1 + '@cspell/filetypes': 10.0.1 + '@cspell/rpc': 10.0.1 + '@cspell/strong-weak-map': 10.0.1 + '@cspell/url': 10.0.1 + cspell-config-lib: 10.0.1 + cspell-dictionary: 10.0.1 + cspell-glob: 10.0.1 + cspell-grammar: 10.0.1 + cspell-io: 10.0.1 + cspell-trie-lib: 10.0.1(@cspell/cspell-types@10.0.1) env-paths: 4.0.0 gensequence: 8.0.8 import-fresh: 4.0.0 @@ -15108,9 +15233,9 @@ snapshots: vscode-uri: 3.1.0 xdg-basedir: 5.1.0 - cspell-trie-lib@10.0.0(@cspell/cspell-types@10.0.0): + cspell-trie-lib@10.0.1(@cspell/cspell-types@10.0.1): dependencies: - '@cspell/cspell-types': 10.0.0 + '@cspell/cspell-types': 10.0.1 css-declaration-sorter@7.4.0(postcss@8.5.25): dependencies: @@ -15132,7 +15257,7 @@ snapshots: '@rspack/core': 1.6.8 webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - css-loader@6.11.0(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + css-loader@6.11.0(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: icss-utils: 5.1.0(postcss@8.5.25) postcss: 8.5.25 @@ -15144,7 +15269,7 @@ snapshots: semver: 7.6.3 optionalDependencies: '@rspack/core': 1.6.8 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) css-loader@7.1.2(@rspack/core@1.6.8)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: @@ -15160,7 +15285,7 @@ snapshots: '@rspack/core': 1.6.8 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - css-loader@7.1.4(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + css-loader@7.1.4(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: icss-utils: 5.1.0(postcss@8.5.25) postcss: 8.5.25 @@ -15172,9 +15297,9 @@ snapshots: semver: 7.6.3 optionalDependencies: '@rspack/core': 1.6.8 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.9)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + css-minimizer-webpack-plugin@8.0.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.9)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 7.1.9(postcss@8.5.25) @@ -15182,7 +15307,7 @@ snapshots: postcss: 8.5.25 schema-utils: 4.3.3 serialize-javascript: 7.0.6 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 @@ -15232,7 +15357,7 @@ snapshots: cssnano-preset-default@7.0.17(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 css-declaration-sorter: 7.4.0(postcss@8.5.25) cssnano-utils: 5.0.3(postcss@8.5.25) postcss: 8.5.25 @@ -15440,6 +15565,8 @@ snapshots: electron-to-chromium@1.5.389: {} + electron-to-chromium@1.5.406: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -15478,12 +15605,7 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.24.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -15537,7 +15659,7 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@2.3.1: {} es-object-atoms@1.1.1: dependencies: @@ -15751,7 +15873,7 @@ snapshots: eslint-plugin-storybook@10.4.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@7.2.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) storybook: 10.4.0(@testing-library/dom@10.4.1)(prettier@3.9.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: @@ -15835,14 +15957,14 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 espree@9.6.1: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -15953,7 +16075,7 @@ snapshots: fast-diff@1.3.0: {} - fast-equals@6.0.0: {} + fast-equals@6.0.2: {} fast-glob@3.3.3: dependencies: @@ -16088,7 +16210,7 @@ snapshots: optionalDependencies: debug: 4.4.3(supports-color@7.2.0) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -16103,7 +16225,7 @@ snapshots: semver: 7.6.3 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) form-data@4.0.6: dependencies: @@ -16321,11 +16443,11 @@ snapshots: he: 1.2.0 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.49.0 + terser: 5.50.0 html-tags@3.3.1: {} - html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -16334,7 +16456,7 @@ snapshots: tapable: 2.3.3 optionalDependencies: '@rspack/core': 1.6.8 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) htmlparser2@10.1.0: dependencies: @@ -16673,7 +16795,7 @@ snapshots: jasmine-core@4.6.1: {} - jasmine-core@5.13.0: {} + jasmine-core@6.3.0: {} jasmine-reporters@2.5.2: dependencies: @@ -16693,7 +16815,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.9.1 + '@types/node': 26.1.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16714,7 +16836,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -16770,9 +16892,9 @@ snapshots: transitivePeerDependencies: - supports-color - karma-jasmine-html-reporter@2.2.0(jasmine-core@5.13.0)(karma-jasmine@5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)))(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)): + karma-jasmine-html-reporter@2.2.0(jasmine-core@6.3.0)(karma-jasmine@5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)))(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)): dependencies: - jasmine-core: 5.13.0 + jasmine-core: 6.3.0 karma: 6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) karma-jasmine: 5.1.0(karma@6.4.4(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)) @@ -16845,12 +16967,12 @@ snapshots: '@rspack/core': 1.6.8 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - less-loader@12.3.3(@rspack/core@1.6.8)(less@4.5.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + less-loader@12.3.3(@rspack/core@1.6.8)(less@4.5.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: less: 4.5.1 optionalDependencies: '@rspack/core': 1.6.8 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) less-loader@12.3.3(@rspack/core@1.6.8)(less@4.6.6)(webpack@5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: @@ -16917,11 +17039,11 @@ snapshots: optionalDependencies: webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - license-webpack-plugin@4.0.2(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + license-webpack-plugin@4.0.2(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: webpack-sources: 3.5.1 optionalDependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) lilconfig@3.1.3: {} @@ -17144,10 +17266,10 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.4.7(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + mini-css-extract-plugin@2.4.7(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: schema-utils: 4.3.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) mini-css-extract-plugin@2.9.4(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: @@ -17168,13 +17290,13 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + minimizer-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.49.0 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + terser: 5.50.0 + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: clean-css: 5.3.3 cssnano: 7.1.9(postcss@8.5.25) @@ -17239,10 +17361,10 @@ snapshots: neo-async@2.6.2: {} - ng-packagr@20.3.2(@angular/compiler-cli@20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3): + ng-packagr@20.3.2(@angular/compiler-cli@20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3): dependencies: '@ampproject/remapping': 2.3.0 - '@angular/compiler-cli': 20.3.26(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 20.3.27(@angular/compiler@20.3.27)(supports-color@7.2.0)(typescript@5.9.3) '@rollup/plugin-json': 6.1.0(rollup@4.62.2) '@rollup/wasm-node': 4.62.2 ajv: 8.20.0 @@ -17310,6 +17432,8 @@ snapshots: node-releases@2.0.51: {} + node-releases@2.0.53: {} + node-schedule@2.1.1: dependencies: cron-parser: 4.9.0 @@ -17765,14 +17889,14 @@ snapshots: postcss-colormin@7.0.10(postcss@8.5.25): dependencies: '@colordx/core': 5.4.3 - browserslist: 4.28.5 + browserslist: 4.28.8 caniuse-api: 3.0.0 postcss: 8.5.25 postcss-value-parser: 4.2.0 postcss-convert-values@7.0.12(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 postcss: 8.5.25 postcss-value-parser: 4.2.0 @@ -17824,7 +17948,7 @@ snapshots: transitivePeerDependencies: - typescript - postcss-loader@8.2.1(@rspack/core@1.6.8)(postcss@8.5.25)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + postcss-loader@8.2.1(@rspack/core@1.6.8)(postcss@8.5.25)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: cosmiconfig: 9.0.2(typescript@5.9.3) jiti: 2.7.0 @@ -17832,7 +17956,7 @@ snapshots: semver: 7.6.3 optionalDependencies: '@rspack/core': 1.6.8 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) transitivePeerDependencies: - typescript @@ -17846,7 +17970,7 @@ snapshots: postcss-merge-rules@7.0.11(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 caniuse-api: 3.0.0 cssnano-utils: 5.0.3(postcss@8.5.25) postcss: 8.5.25 @@ -17866,14 +17990,14 @@ snapshots: postcss-minify-params@7.0.9(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 cssnano-utils: 5.0.3(postcss@8.5.25) postcss: 8.5.25 postcss-value-parser: 4.2.0 postcss-minify-selectors@7.1.2(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 caniuse-api: 3.0.0 cssesc: 3.0.0 postcss: 8.5.25 @@ -17931,7 +18055,7 @@ snapshots: postcss-normalize-unicode@7.0.9(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 postcss: 8.5.25 postcss-value-parser: 4.2.0 @@ -17953,7 +18077,7 @@ snapshots: postcss-reduce-initial@7.0.9(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 caniuse-api: 3.0.0 postcss: 8.5.25 @@ -18513,14 +18637,14 @@ snapshots: sass-embedded: 1.100.0 webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - sass-loader@16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + sass-loader@16.0.8(@rspack/core@1.6.8)(sass-embedded@1.100.0)(sass@1.101.0)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: neo-async: 2.6.2 optionalDependencies: '@rspack/core': 1.6.8 sass: 1.101.0 sass-embedded: 1.100.0 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) sass@1.100.0: dependencies: @@ -18742,6 +18866,8 @@ snapshots: smol-toml@1.6.1: {} + smol-toml@1.8.0: {} + socket.io-adapter@2.5.8(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -18751,7 +18877,7 @@ snapshots: - supports-color - utf-8-validate - socket.io-parser@4.2.6(supports-color@7.2.0): + socket.io-parser@4.2.7(supports-color@7.2.0): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.4.3(supports-color@7.2.0) @@ -18766,7 +18892,7 @@ snapshots: debug: 4.4.3(supports-color@7.2.0) engine.io: 6.6.9(supports-color@7.2.0) socket.io-adapter: 2.5.8(supports-color@7.2.0) - socket.io-parser: 4.2.6(supports-color@7.2.0) + socket.io-parser: 4.2.7(supports-color@7.2.0) transitivePeerDependencies: - bufferutil - supports-color @@ -18796,11 +18922,11 @@ snapshots: source-map-js: 1.2.1 webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - source-map-loader@5.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + source-map-loader@5.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) source-map-support@0.5.19: dependencies: @@ -18936,17 +19062,17 @@ snapshots: dependencies: webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - style-loader@3.3.4(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + style-loader@3.3.4(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) - style-loader@4.0.0(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + style-loader@4.0.0(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) stylehacks@7.0.11(postcss@8.5.25): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 postcss: 8.5.25 postcss-selector-parser: 7.1.4 @@ -19099,7 +19225,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.49.0 + terser: 5.50.0 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: clean-css: 5.3.3 @@ -19114,7 +19240,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.49.0 + terser: 5.50.0 webpack: 5.105.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: clean-css: 5.3.3 @@ -19124,13 +19250,13 @@ snapshots: html-minifier-terser: 6.1.0 postcss: 8.5.25 - terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.49.0 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + terser: 5.50.0 + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: clean-css: 5.3.3 cssnano: 7.1.9(postcss@8.5.25) @@ -19142,14 +19268,14 @@ snapshots: terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 - terser@5.49.0: + terser@5.50.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -19221,15 +19347,15 @@ snapshots: dependencies: tslib: 2.8.1 - ts-loader@9.6.1(loader-utils@2.0.4)(typescript@5.9.3)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + ts-loader@9.6.1(loader-utils@2.0.4)(typescript@5.9.3)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.5 micromatch: 4.0.8 semver: 7.6.3 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: loader-utils: 2.0.4 @@ -19254,7 +19380,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.0 + enhanced-resolve: 5.24.5 tapable: 2.3.3 tsconfig-paths: 4.2.0 @@ -19311,8 +19437,6 @@ snapshots: ua-parser-js@0.7.41: {} - undici-types@7.24.6: {} - undici-types@8.3.0: {} undici@8.9.0: {} @@ -19353,6 +19477,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -19443,7 +19573,7 @@ snapshots: webidl-conversions@3.0.1: optional: true - webpack-dev-middleware@6.1.3(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + webpack-dev-middleware@6.1.3(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -19451,7 +19581,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) webpack-dev-middleware@7.4.2(tslib@2.8.1)(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: @@ -19466,7 +19596,7 @@ snapshots: transitivePeerDependencies: - tslib - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: colorette: 2.0.20 memfs: 4.57.8(tslib@2.8.1) @@ -19475,7 +19605,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) transitivePeerDependencies: - tslib optional: true @@ -19548,19 +19678,19 @@ snapshots: webpack-sources@3.5.1: {} - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: typed-assert: 1.0.9 webpack: 5.105.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: - html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)))(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)): dependencies: typed-assert: 1.0.9 - webpack: 5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) + webpack: 5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25) optionalDependencies: - html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + html-webpack-plugin: 5.6.7(@rspack/core@1.6.8)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) webpack-virtual-modules@0.6.2: {} @@ -19572,12 +19702,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.5 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.3 - es-module-lexer: 2.1.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -19613,12 +19743,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.5 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.3 - es-module-lexer: 2.1.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -19646,23 +19776,23 @@ snapshots: - postcss - uglify-js - webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25): + webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - browserslist: 4.28.5 + acorn: 8.18.0 + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.3 - es-module-lexer: 2.1.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.0(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) + minimizer-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)(webpack@5.109.2(clean-css@5.3.3)(cssnano@7.1.9(postcss@8.5.25))(csso@5.0.5)(esbuild@0.25.9)(html-minifier-terser@6.1.0)(postcss@8.5.25)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 965dd4910c..3cf22bafd5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,9 +8,10 @@ minimumReleaseAgeStrict: true overrides: # Security fixes fast-uri: 3.1.5 - "js-yaml@4.2.0": "4.3.0" + "js-yaml@4.2.0": "4.3.1" "brace-expansion@5.0.8": "5.0.9" "brace-expansion@<1.1.18": "1.1.18" + "socket.io-parser@4.2.6": 4.2.7 svgo: 4.0.2 shell-quote: 1.9.0 adm-zip: 0.6.0 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..03cc6905f7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.organization=alfresco +sonar.projectKey=Alfresco_alfresco-ng2-components + +sonar.sources=lib +sonar.tests=lib +sonar.test.inclusions=**/*.spec.ts +sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.ts,**/*.mock.ts,**/mock/**,**/mocks/**,**/testing/**,**/stories/** + +sonar.javascript.lcov.reportPaths=coverage/core/lcov.info,coverage/content-services/lcov.info,coverage/extensions/lcov.info,coverage/insights/lcov.info,coverage/process-services/lcov.info,coverage/process-services-cloud/lcov.info + +sonar.sourceEncoding=UTF-8