Compare commits

...
Author SHA1 Message Date
Michal Kinas 1a7a59cea7 [MNT-25896] Introduce page rotation button in pdf viewer (#12241)
* [MNT-25896] Introduce page rotation button in pdf viewer

* [MNT-25896] CR fixes
2026-09-16 11:16:57 +02:00
Shivangi Shree 762eba8490 [ACS-10264] Add aria-labelledby so that placeholder is announced only… (#12222)
* [ACS-10264] Add aria-labelledby so that placeholder is announced only once

* [ACS-10264] Add unit test

* [ACS-10264] CR fixes
2026-09-15 17:46:55 +05:30
Domenico Sibilio 62d7d9084d AAE-51800 Update Supply Chain Review GH AW to latest (#12238)
[skip ci]
2026-09-15 10:58:27 +02:00
Bartosz Sekula 6f414ae98e AAE-51538 Xmlxdom library bump (#12239)
* AAE-51538 Xmlxdom library bump

* update
2026-09-15 10:22:46 +02:00
Alex Molodyh c7c2ba292e AAE-49688 Revalidate form after rule-driven visibility changes (#12224) 2026-09-14 18:15:36 -07:00
dependabot[bot] 84e710191d build(deps): bump crowdin/github-action from 2.17.1 to 3.0.2 (#12220)
Bumps [crowdin/github-action](https://github.com/crowdin/github-action) from 2.17.1 to 3.0.2.
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/8f01d54f70f1713ee3f09d82c2bbb2daeac28689...0d5670f539973aea2f01abce61a8989934df0025)

---
updated-dependencies:
- dependency-name: crowdin/github-action
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 14:55:22 +02:00
Domenico Sibilio a776cd02c0 AAE-51793 Log attempt number around npm pack retries (#12236) 2026-09-14 12:12:05 +00:00
Domenico Sibilio 2c1405d053 AAE-51793 Restore verbose npm pack logs (#12235) 2026-09-14 11:40:29 +00:00
Domenico Sibilio 0bc823beea AAE-51793 Retry npm pack directly, drop redundant npm view pre check (#12234) 2026-09-14 13:01:22 +02:00
Domenico Sibilio 89053a1b9d AAE-51793 Widen npm-check-bundle retry window and fix set -e short circuit (#12233) 2026-09-14 10:27:38 +00:00
Domenico Sibilio 68d382a401 AAE-51793 Add retry with backoff to npm view in npm-check-bundle (#12232) 2026-09-14 09:45:22 +00:00
dependabot[bot] ddfabc0e44 build(deps-dev): bump semver from 7.6.3 to 7.8.5 (#12189)
Bumps [semver](https://github.com/npm/node-semver) from 7.6.3 to 7.8.5.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.6.3...v7.8.5)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:56:56 +02:00
dependabot[bot] f524b365f6 build(deps-dev): bump the typescript-eslint group with 4 updates (#12229)
Bumps the typescript-eslint group with 4 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser), [@typescript-eslint/typescript-estree](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-estree) and [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils).


Updates `@typescript-eslint/eslint-plugin` from 8.69.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.69.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/parser)

Updates `@typescript-eslint/typescript-estree` from 8.69.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-estree/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/typescript-estree)

Updates `@typescript-eslint/utils` from 8.69.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/utils)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/typescript-estree"
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:56:25 +02:00
dependabot[bot] 4c15ab2395 build(deps): bump the github-actions group across 2 directories with 9 updates (#12231)
Bumps the github-actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [Alfresco/alfresco-build-tools/.github/actions/send-teams-notification](https://github.com/alfresco/alfresco-build-tools) | `18.27.0` | `18.28.0` |
| [Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml](https://github.com/alfresco/alfresco-build-tools) | `18.27.0` | `18.28.0` |
| [Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment](https://github.com/alfresco/alfresco-build-tools) | `18.27.0` | `18.28.0` |
| [github/gh-aw-actions/setup](https://github.com/github/gh-aw-actions) | `0.88.4` | `0.89.7` |

Bumps the github-actions group with 2 updates in the /.github/actions/setup directory: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [Alfresco/alfresco-build-tools/.github/actions/git-latest-tag](https://github.com/alfresco/alfresco-build-tools).


Updates `github/codeql-action/init` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/autobuild` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `Alfresco/alfresco-build-tools/.github/actions/send-teams-notification` from 18.27.0 to 18.28.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/567a9393acca033008ce34eafcb200469e4ea2f5...a297ff6c5bf20c667047db658dd23f60241b270a)

Updates `Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml` from 18.27.0 to 18.28.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/567a9393acca033008ce34eafcb200469e4ea2f5...a297ff6c5bf20c667047db658dd23f60241b270a)

Updates `Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment` from 18.27.0 to 18.28.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/567a9393acca033008ce34eafcb200469e4ea2f5...a297ff6c5bf20c667047db658dd23f60241b270a)

Updates `github/gh-aw-actions/setup` from 0.88.4 to 0.89.7
- [Release notes](https://github.com/github/gh-aw-actions/releases)
- [Changelog](https://github.com/github/gh-aw-actions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/gh-aw-actions/compare/be41add0f204de343293628bb99a72a3894cfd7a...c9a91649fa6263a455ef783f7c06a96940052fec)

Updates `pnpm/action-setup` from 6.0.10 to 6.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0977fd99725f1db4007ccb2928dbb4e90d06cc86...ea17c68df8912ef543352723c149a84f56e3d413)

Updates `Alfresco/alfresco-build-tools/.github/actions/git-latest-tag` from 18.27.0 to 18.28.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/567a9393acca033008ce34eafcb200469e4ea2f5...a297ff6c5bf20c667047db658dd23f60241b270a)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification
  dependency-version: 18.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml
  dependency-version: 18.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment
  dependency-version: 18.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/gh-aw-actions/setup
  dependency-version: 0.89.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag
  dependency-version: 18.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:18:35 +02:00
Michal Kinas 9480cc39bd [MNT-25235] Add folder searching capability in autocomplete filter (#12223)
* [MNT-25235] Add folder searching capability in autocomplete filter

* [MNT-25235] CR fixes

* [MNT-25235] CR fixes

* [MNT-25235] CR fixes
2026-09-11 07:13:18 +02:00
Dominik Iwanek e28267621a [ACS-12582] [ADW] Document list container is not accessible via keyboard navigation for file selection (#12210) 2026-09-09 14:20:24 +02:00
Bartosz Sekula 27c700165c AAE-50901 Allow provideMockFeatureFlags to receive observables (#12221)
* AAE-50901 Allow provideMockFeatureFlags to receive observables

* update

* update

* cr

* cr
2026-09-07 14:21:03 +02:00
dependabot[bot] 2824543bf0 build(deps-dev): bump the typescript-eslint group with 4 updates (#12218)
Bumps the typescript-eslint group with 4 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser), [@typescript-eslint/typescript-estree](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-estree) and [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils).


Updates `@typescript-eslint/eslint-plugin` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/parser)

Updates `@typescript-eslint/typescript-estree` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-estree/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/typescript-estree)

Updates `@typescript-eslint/utils` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/utils)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/typescript-estree"
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: typescript-eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 08:20:17 +00:00
44 changed files with 1562 additions and 370 deletions
+19 -1
View File
@@ -26,6 +26,24 @@ runs:
echo "TAG_NPM not set, aborting"
exit 1
fi
ADF_VERSION=$(npm view @alfresco/adf-core@${TAG_NPM} version)
# Retry to absorb npm registry propagation delay right after publish.
NPM_VIEW_RETRIES=24
NPM_VIEW_RETRY_DELAY=20
for attempt in $(seq 1 "$NPM_VIEW_RETRIES"); do
ADF_VERSION=$(npm view @alfresco/adf-core@${TAG_NPM} version 2>/dev/null) || true
if [[ -n $ADF_VERSION ]]; then
break
fi
if [[ $attempt -lt $NPM_VIEW_RETRIES ]]; then
sleep "$NPM_VIEW_RETRY_DELAY"
fi
done
if [[ -z $ADF_VERSION ]]; then
echo "Could not resolve @alfresco/adf-core@${TAG_NPM} version, aborting" >&2
exit 1
fi
echo "check bundle on TAG_NPM='${TAG_NPM}' and ADF_VERSION='${ADF_VERSION}'"
./scripts/github/build/npm-check-bundles.sh -v ${ADF_VERSION}
+2 -2
View File
@@ -26,7 +26,7 @@ runs:
shell: bash
run: echo "nx-github-actions" | sudo tee /etc/machine-id > /dev/null
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -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@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@a297ff6c5bf20c667047db658dd23f60241b270a # v18.28.0
- name: load "NPM TAG"
if: ${{ inputs.full-setup == 'true' }}
id: set-npm-tag
+30 -5
View File
@@ -1,9 +1,9 @@
{
"entries": {
"github/gh-aw-actions/setup@v0.87.10": {
"github/gh-aw-actions/setup@v0.88.7": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.87.10",
"sha": "bc8c008a419c5b7a29df6f5641edd35fd1c6ea85"
"version": "v0.88.7",
"sha": "5e508589e03a7757a7e05b26e834292f5445bfb6"
}
},
"containers": {
@@ -12,20 +12,45 @@
"digest": "sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"
},
"ghcr.io/github/gh-aw-firewall/agent:0.28.14": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14",
"digest": "sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44",
"digest": "sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14",
"digest": "sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"
},
"ghcr.io/github/gh-aw-firewall/squid:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.27.44",
"digest": "sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"
},
"ghcr.io/github/gh-aw-firewall/squid:0.28.14": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14",
"digest": "sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.18": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.18",
"digest": "sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.18@sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53"
},
"ghcr.io/github/gh-aw-node": {
"image": "ghcr.io/github/gh-aw-node",
"digest": "sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196",
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"
"digest": "sha256:87366cb93b06d7a4e3db08a705875efc027b6c394da336119e7a067abacbb39b",
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:87366cb93b06d7a4e3db08a705875efc027b6c394da336119e7a067abacbb39b"
},
"ghcr.io/github/github-mcp-server:v1.11.0": {
"image": "ghcr.io/github/github-mcp-server:v1.11.0",
"digest": "sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"
}
}
}
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # 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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # 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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v3.29.5
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v3.29.5
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
- name: Send Teams notification
if: steps.check_label_timing.outputs.should_notify == 'true'
uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0
uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@a297ff6c5bf20c667047db658dd23f60241b270a # v18.28.0
with:
webhook-url: ${{ secrets.TEAMS_NOTIFICATION_ADF_BDU_WEBHOOK }}
skip_checkout: true
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
ref: develop
token: ${{ steps.app-token.outputs.token }}
- name: Pull translations from Crowdin
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2
with:
skip_ref_checkout: true
upload_sources: false
+2 -2
View File
@@ -125,7 +125,7 @@ jobs:
npm-check-bundle:
needs: [release-npm]
timeout-minutes: 15
timeout-minutes: 25
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
@@ -147,7 +147,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Push Source Files to Crowdin
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2
with:
upload_sources: true
upload_sources_args: --delete-obsolete
+1 -1
View File
@@ -11,4 +11,4 @@ permissions:
jobs:
stale-pr-cleanup:
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@a297ff6c5bf20c667047db658dd23f60241b270a # v18.28.0
@@ -16,7 +16,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@567a9393acca033008ce34eafcb200469e4ea2f5 # v18.27.0
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@a297ff6c5bf20c667047db658dd23f60241b270a # v18.28.0
with:
comment-identifier: supply-chain-review-instructions
comment-body: |
File diff suppressed because one or more lines are too long
+45 -16
View File
@@ -23,6 +23,8 @@ network:
- api.osv.dev
- api.scorecard.dev
- search.maven.org
- api.github.com
- github.com
safe-outputs:
add-comment:
@@ -33,8 +35,11 @@ safe-outputs:
remove-labels:
allowed: [security:low, security:medium, security:high]
submit-pull-request-review:
allowed-events: [COMMENT, REQUEST_CHANGES]
supersede-older-reviews: true
dismiss-pull-request-review:
source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@e35840d877477896b1f0aa05d05371cb3b31ce9f
source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@599eebd2a1b84e76d540e41036520df3a64c7cbd
---
# Supply Chain Review
@@ -57,7 +62,7 @@ For each changed dependency extract:
- Old version (or mark as `NEW DEPENDENCY` if newly added)
- New version
If no dependency files were changed, post a brief PR comment stating that no dependency changes were detected and no review is needed, then stop.
If no dependency files were changed, post a brief PR comment stating that no dependency changes were detected, then go directly to Step 6 — treating this as LOW risk — to remove any stale `security:*` labels and submit the required pull request review, then stop.
## Step 1b — Filter Internal Dependencies
@@ -74,7 +79,7 @@ For each internal dependency found:
2. Record the package name (with `@` replaced by `(at)` for GitHub comment compatibility), ecosystem, old version, and new version in a separate "Internal Dependencies (Skipped)" list.
3. Continue with Step 2 only for the remaining external/public dependencies.
If ALL changed dependencies are internal, skip Steps 2-4 and proceed directly to Step 5, posting a report that lists the internal dependencies and notes that no external supply chain analysis was performed.
If ALL changed dependencies are internal, skip Steps 2-4 and proceed directly to Step 5 — treating this as LOW risk for Step 6 — posting a report that lists the internal dependencies and notes that no external supply chain analysis was performed.
## Step 2 — Collect Data for Each Dependency
@@ -268,7 +273,7 @@ Verify that the source repository URL in registry metadata points to the canonic
Assign a risk score (0-100) to each dependency using these guidelines:
| Priority | Signal | Typical Impact |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
| Highest | Known CRITICAL/HIGH CVEs in new version, confirmed typosquatting, malicious code in diff, build provenance mismatch (tag points to different code than published artifact), tag mimicry on fork | 60+ points |
| High | Maintainer takeover pattern (publisher changed + old maintainers removed), dangerous install scripts, known compromised package, moved/recreated tag with different commit, provenance attestations removed from package that previously had them | 20-40 points |
| Medium | Low OpenSSF Scorecard (< 3), publisher changed (without full takeover), new install scripts, very recent publish (< 48h), obfuscated code in diff, unsigned lightweight tags on security-critical packages, absence of provenance on high-profile packages | 10-20 points |
@@ -304,9 +309,9 @@ GitHub enforces a maximum of 10 mentions per comment. Package names containing `
### Internal Dependencies (Skipped)
| Package | Ecosystem | Old Version | New Version | Reason |
|-------------------|-----------|-------------|-------------|---------------------------------|
| (at)hyland/core | npm | 3.1.0 | 3.2.0 | Internal ((at)hyland/* scope) |
| Package | Ecosystem | Old Version | New Version | Reason |
|-----------------|-----------|-------------|-------------|-------------------------------|
| (at)hyland/core | npm | 3.1.0 | 3.2.0 | Internal ((at)hyland/* scope) |
_These dependencies are internal packages not available on public registries. External API checks were skipped._
@@ -342,18 +347,42 @@ No suspicious patterns detected. Routine upgrade.
## Step 6 — Apply Label and Review Status
- First, remove any `security:low`, `security:medium`, or `security:high` labels already present on the PR from a previous review — this PR may have been reviewed before (e.g., after a new commit), and stale risk labels must not remain alongside the new one.
- Then apply a label to the PR based on the highest risk level found:
- `security:low` for LOW risk
- `security:medium` for MEDIUM risk
- `security:high` for HIGH or CRITICAL risk
- If the highest risk level is HIGH or CRITICAL, submit a pull request review requesting changes, with a summary of the critical findings.
- If the risk is MEDIUM, submit a pull request review as a comment, noting that human review is recommended.
- If the risk is LOW, do not submit a review — the PR comment is sufficient.
### 6a. Labels — order-independent update
Determine the single target label for the highest risk level found: `security:low`, `security:medium`, or `security:high`.
- Remove only the OTHER `security:*` labels (the ones that do NOT match the target) if present on the PR — this clears stale risk labels left by a previous review (e.g., after a new commit changed the risk level).
- Add the target label if it is not already present.
- **Never remove the target label itself.** Because the remove and add operations act on disjoint labels, the final state is correct regardless of which of the two safe-output calls (`add_labels` / `remove_labels`) happens to be processed first — do NOT rely on emitting them in a particular order, since that ordering is not guaranteed. (Do not, for example, remove all three `security:*` labels and then add the target back — if the removal is processed after the add, the target label would be stripped again, leaving the PR with no risk label at all.)
### 6b. Dismiss stale reviews from this workflow
Every review this workflow posts (see 6c) MUST start its body with the exact literal marker line `**Supply Chain Review**` as the first line, so future runs can recognize their own prior reviews.
Before posting the new review:
1. Fetch the PR's existing reviews (GitHub MCP `pull_requests` toolset).
2. Identify any review that is authored by this workflow's actor AND whose body starts with the `**Supply Chain Review**` marker AND is still in the `CHANGES_REQUESTED` state — that is a stale review from an earlier run of this same workflow (e.g., posted before the flagged dependency was fixed, downgraded, or removed).
3. For each such review, call `dismiss_pull_request_review` with its explicit numeric `review_id` (do NOT use `'auto'` — this repository may run other agentic workflows that also post as the same actor, and `'auto'` would dismiss their reviews too) and a justification of at least 20 characters (e.g., "Superseded by a newer Supply Chain Review run.").
Do this even though `submit-pull-request-review` is also configured with `supersede-older-reviews: true` — that setting is best-effort and may not always recognize the prior review, so the explicit dismissal above is the reliable mechanism and must always be attempted.
### 6c. Submit the review
**Always submit a pull request review — in every invocation, with no exceptions.** This is not conditional on risk level. Submit a review even when there are no dependency changes, when all dependencies are internal, or when risk is LOW — skipping it would mean a stale `REQUEST_CHANGES` review from an earlier run is never replaced or dismissed.
The review body must start with the `**Supply Chain Review**` marker line (see 6b), followed by the assessment:
- If the highest risk level is HIGH or CRITICAL, submit the review as **request changes**, with a summary of the critical findings.
- If the risk is MEDIUM, submit the review as a **comment**, noting that human review is recommended.
- If the risk is LOW (including when there are no dependency changes, or all changed dependencies are internal), submit the review as a **comment**, summarizing that no concerns were found and the PR comment has the full detail.
- **Never submit the review as an approval, under any circumstance** — this workflow only ever comments or requests changes; a human always makes the merge decision.
## Important Guidelines
- **Never approve or merge the PR** — all actions are advisory or blocking only. A human always makes the merge decision.
- **Never approve or merge the PR** — all actions are advisory or blocking only. A human always makes the merge decision. Every review this workflow submits must use the comment or request-changes event — never the approve event.
- **Always submit exactly one pull request review per invocation, regardless of outcome**, and always prefix its body with the `**Supply Chain Review**` marker — this is required so that a later run of this same workflow can find and dismiss it via `dismiss_pull_request_review` once it becomes stale (see Step 6b). Do not rely on `supersede-older-reviews` alone; it is best-effort.
- **Never remove the `security:*` label matching the current risk level** when clearing stale labels — only remove the other ones, so the final label state is correct no matter which safe-output call is processed first (see Step 6a).
- Be specific in findings — cite exact data (vulnerability ID, maintainer name, script content, file path, API response) rather than vague warnings.
- For Maven packages, adapt npm-specific checks appropriately (e.g., install scripts become build plugin analysis, maintainer metadata may be limited).
- When a package is a NEW dependency (no old version), pay extra attention to project health, name legitimacy, and install scripts since there is no historical baseline to compare against.
@@ -2,7 +2,7 @@
Title: Search Chip Autocomplete Input component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-13
Last reviewed: 2026-09-09
---
# [Search Chip Autocomplete Input component](../../../lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts "Defined in search-chip-autocomplete-input.component.ts")
@@ -31,6 +31,7 @@ Represents an input with autocomplete options.
| preselectedOptions | `AutocompleteOption[]` | [] | Options which are selected from start |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| loading | [`Observable`](https://rxjs.dev/guide/observable)`<boolean>` | `of(false)` | Stream driving a loading spinner shown inside the autocomplete panel while a new batch of options is being fetched. While it emits `true`, the options list is replaced by the spinner. |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
@@ -2,7 +2,7 @@
Title: Search Filter Autocomplete Chips component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-13
Last reviewed: 2026-09-09
---
# [Search Filter Autocomplete Chips component](../../../lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts "Defined in search-filter-autocomplete-chips.component.ts")
@@ -39,18 +39,61 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
### Settings
| Name | Type | Description |
| ---- |----------|--------------------------------------------------------------------------------------------------------------------|
| field | `string` | Field to apply the query to. Required value |
| autocompleteOptions | `AutocompleteOption[]` | Predefined options for autocomplete |
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
| Name | Type | Description |
| ------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| field | `string` | Field to apply the query to. Required value. See [Supported fields](#supported-fields) for the values that enable dynamically fetched options |
| label | `string` | Label displayed for the autocomplete input |
| autocompleteOptions | `AutocompleteOption[]` | Predefined options for autocomplete |
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
### Supported fields
Besides using static `autocompleteOptions`, the `field` value can be set to one of the following to fetch options dynamically from the repository as the user types:
| `field` value | Source of options |
| --------------- | ----------------------------------------------------------- |
| `TAG` | Existing tags |
| `cm:categories` | Existing categories |
| `SITE` | Available sites (plus any predefined `autocompleteOptions`) |
| `ANCESTOR` | Folders matching the typed name (parent folder search) |
While a batch of options is being fetched for any of these fields, a loading spinner is shown inside the autocomplete panel until the results arrive. Only the results of the latest request are applied, so quickly changing the input does not display stale options.
## Details
This component allows the user to choose filter options for the search query.
See the [Search Chip Autocomplete Input component](search-chip-autocomplete-input.component.md) for more details.
### Parent folder search
Set `field` to `ANCESTOR` to let the user search for and select a parent folder to scope the search to. The following example also uses `label` to set the input label:
```json
{
"search": {
"categories": [
{
"id": "parentFolder",
"name": "Parent folder",
"enabled": true,
"component": {
"selector": "autocomplete-chips",
"settings": {
"allowUpdateOnChange": false,
"hideDefaultAction": true,
"allowOnlyPredefinedValues": false,
"field": "ANCESTOR",
"label": "Parent folder"
}
}
}
]
}
}
```
## See also
- [Search Configuration Guide](../../user-guide/search-configuration-guide.md)
+8 -7
View File
@@ -13,26 +13,27 @@ Accesses the Content Services Search API.
### Methods
- **getNodeQueryResults**(term: `string`, options?: [`SearchOptions`](../../../lib/content-services/src/lib/search/services/search.service.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodePaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/NodePaging.md)`>`<br/>
- **getNodeQueryResults**(term: `string`, options?: [`SearchOptions`](../../../lib/content-services/src/lib/search/services/search.service.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodePaging`](../../../lib/js-api/src/api/content-rest-api/model/nodePaging.ts)`>`<br/>
Gets a list of nodes that match the given search criteria.
- _term:_ `string` - Term to search for
- _options:_ [`SearchOptions`](../../../lib/content-services/src/lib/search/services/search.service.ts) - (Optional) Options for delivery of the search results
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodePaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/NodePaging.md)`>` - List of nodes resulting from the search
- **search**(searchTerm: `string`, maxResults: `number`, skipCount: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodePaging`](../../../lib/js-api/src/api/content-rest-api/model/nodePaging.ts)`>` - List of nodes resulting from the search
- **search**(searchTerm: `string`, maxResults: `number`, skipCount: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](../../../lib/js-api/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
Performs a search.
- _searchTerm:_ `string` - Term to search for
- _maxResults:_ `number` - Maximum number of items in the list of results
- _skipCount:_ `number` - Number of higher-ranked items to skip over in the list
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>` - List of search results
- **searchByQueryBody**(queryBody: `SearchRequest`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](../../../lib/js-api/src/api/search-rest-api/docs/ResultSetPaging.md)`>` - List of search results
- **searchByQueryBody**(queryBody: `SearchRequest`, shouldEmit: `boolean` = `true`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](../../../lib/js-api/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
Performs a search with its parameters supplied by a SearchRequest object.
- _queryBody:_ `SearchRequest` - Object containing the search parameters
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>` - List of search results
- _shouldEmit:_ `boolean` - Whether the `dataLoaded` event should be emitted with the results. Set to `false` for auxiliary searches (for example populating autocomplete options) that should not notify the main results subscribers. Defaults to `true`
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](../../../lib/js-api/src/api/search-rest-api/docs/ResultSetPaging.md)`>` - List of search results
## Details
See the
[search method](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-search-rest-api/docs/SearchApi.md#search)
[search method](../../../lib/js-api/src/api/search-rest-api/docs/SearchApi.md#search)
in the Alfresco JS-API for the format of the query and returned data.
The [Search Configuration service](../services/search-configuration.service.md)
has a method to generate the QueryBody object used by `searchByQueryBody`. The properties of the
@@ -412,6 +412,8 @@ export interface SearchWidgetSettings {
allowOnlyPredefinedValues?: boolean;
/* allow the user to predefine autocomplete options */
autocompleteOptions?: AutocompleteOption[];
/* label that will be displayed for autocomplete input */
label?: string;
[indexer: string]: any;
}
@@ -1,22 +1,23 @@
<mat-form-field class="adf-chip-list">
<mat-label>{{ placeholder | translate }}</mat-label>
<mat-chip-grid #chipList [attr.aria-label]="'SEARCH.FILTER.ARIA-LABEL.OPTIONS-SELECTION' | translate">
<mat-chip-row
class="adf-option-chips adf-autocomplete-added-option-chips"
*ngFor="let option of selectedOptions"
(removed)="remove(option)">
<span [title]="option.fullPath ? ('SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath}) : undefined">
{{ option.value }}
</span>
<button
matChipRemove
class="adf-option-chips-delete-button adf-autocomplete-added-option-chips-delete-button"
[title]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value"
[attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value"
>
<mat-icon class="adf-option-chips-delete-icon adf-autocomplete-added-option-chips-delete-icon" adf-icon="close" />
</button>
</mat-chip-row>
@for (option of selectedOptions; track $index) {
<mat-chip-row
class="adf-option-chips adf-autocomplete-added-option-chips"
(removed)="remove(option)">
<span [title]="option.fullPath ? ('SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath}) : undefined">
{{ option.value }}
</span>
<button
matChipRemove
class="adf-option-chips-delete-button adf-autocomplete-added-option-chips-delete-button"
[title]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value"
[attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value"
>
<mat-icon class="adf-option-chips-delete-icon adf-autocomplete-added-option-chips-delete-icon" adf-icon="close" />
</button>
</mat-chip-row>
}
<input
placeholder="{{ placeholder | translate }}"
aria-controls="adf-search-chip-autocomplete"
@@ -33,18 +34,33 @@
data-automation-id="adf-search-chip-autocomplete-input">
</mat-chip-grid>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
<mat-option
*ngFor="let option of filteredOptions"
[value]="option"
(mousedown)=$event.preventDefault()
[disabled]="isOptionSelected(option)"
[attr.data-automation-id]="'option-' + option.value"
[title]="option.fullPath ? ('SEARCH.RESULTS.WILL_CONTAIN' | translate : { searchTerm: option.fullPath || option.value }) : undefined"
class="adf-search-chip-autocomplete-added-option"
[ngClass]="isOptionSelected(option) && 'adf-autocomplete-added-option'"
>
{{ option.fullPath || option.value }}
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false" hideSingleSelectionIndicator>
@if (loading$ | async) {
<mat-option class="adf-search-chip-autocomplete-loading" disabled data-automation-id="adf-search-chip-autocomplete-loading">
<mat-progress-spinner mode="indeterminate" [diameter]="24" />
</mat-option>
} @else {
@for (option of filteredOptions; track $index) {
<mat-option
[value]="option"
(mousedown)=$event.preventDefault()
[disabled]="isOptionSelected(option)"
[attr.data-automation-id]="'option-' + option.value"
[attr.aria-label]="option.fullPath ? ('SEARCH.RESULTS.WILL_CONTAIN' | translate : { searchTerm: option.fullPath || option.value }) : undefined"
class="adf-search-chip-autocomplete-added-option"
[ngClass]="isOptionSelected(option) && 'adf-autocomplete-added-option'"
>
<div class="adf-search-chip-autocomplete-added-option-container">
<span>{{ option.value }}</span>
@if (option.fullPath) {
<mat-icon
class="adf-info-icon"
[matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate : { searchTerm: option.fullPath }"
adf-icon="info" />
}
</div>
</mat-option>
}
}
</mat-autocomplete>
</mat-form-field>
@@ -43,7 +43,32 @@ adf-search-chip-autocomplete-input {
}
}
.adf-search-chip-autocomplete-added-option {
#{ms.$mat-list-item-primary-text} {
width: 100%;
}
&-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.adf-info-icon#{ms.$mat-icon} {
margin-right: 0;
}
}
}
.adf-search-chip-autocomplete-added-option.adf-autocomplete-added-option {
background: var(--mat-sys-surface-variant);
color: var(--mat-sys-primary);
}
.adf-search-chip-autocomplete-loading {
#{ms.$mat-list-item-primary-text} {
display: flex;
justify-content: center;
width: 100%;
}
}
@@ -18,19 +18,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatChipRemove } from '@angular/material/chips';
import { By } from '@angular/platform-browser';
import { Subject } from 'rxjs';
import { of, Subject } from 'rxjs';
import { SearchChipAutocompleteInputComponent } from './search-chip-autocomplete-input.component';
import { DebugElement, SimpleChanges } from '@angular/core';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness, MatChipGridHarness } from '@angular/material/chips/testing';
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatOptionHarness } from '@angular/material/core/testing';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { UnitTestingUtils } from '@alfresco/adf-core';
describe('SearchChipAutocompleteInputComponent', () => {
let component: SearchChipAutocompleteInputComponent;
let fixture: ComponentFixture<SearchChipAutocompleteInputComponent>;
let loader: HarnessLoader;
let testingUtils: UnitTestingUtils;
const onResetSubject = new Subject<void>();
beforeEach(() => {
@@ -40,6 +44,7 @@ describe('SearchChipAutocompleteInputComponent', () => {
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
testingUtils = new UnitTestingUtils(fixture.debugElement, loader);
component = fixture.componentInstance;
component.onReset$ = onResetSubject.asObservable();
component.autocompleteOptions = [{ value: 'option1' }, { value: 'option2' }];
@@ -121,6 +126,15 @@ describe('SearchChipAutocompleteInputComponent', () => {
return fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option'));
}
/**
* Force the autocomplete panel to open regardless of the current options
*/
function openAutocompletePanel() {
const trigger = testingUtils.getByDirective(MatAutocompleteTrigger).injector.get(MatAutocompleteTrigger);
trigger.openPanel();
fixture.detectChanges();
}
it('should assign preselected values to selected options on init', () => {
component.preselectedOptions = [{ value: 'option1' }];
component.ngOnInit();
@@ -293,13 +307,24 @@ describe('SearchChipAutocompleteInputComponent', () => {
expect((await getChipList()).length).toEqual(1);
});
it('should show full category path when fullPath provided', async () => {
it('should display the option value and render the info icon when fullPath is provided', () => {
component.filteredOptions = [{ id: 'test-id', value: 'test-value', fullPath: 'test-full-path' }];
enterNewInputValue('test-value');
const matOption = fixture.debugElement.query(By.css('.adf-search-chip-autocomplete-added-option')).nativeElement;
expect(matOption.textContent).toEqual(' test-full-path ');
const matOption = testingUtils.getByCSS('.adf-search-chip-autocomplete-added-option').nativeElement;
expect(matOption.textContent).toContain('test-value');
expect(testingUtils.getByCSS('.adf-search-chip-autocomplete-added-option .adf-info-icon')).toBeTruthy();
});
it('should not render the info icon when fullPath is not provided', () => {
component.filteredOptions = [{ id: 'test-id', value: 'test-value' }];
enterNewInputValue('test-value');
const matOption = testingUtils.getByCSS('.adf-search-chip-autocomplete-added-option').nativeElement;
expect(matOption.textContent).toContain('test-value');
expect(testingUtils.getAllByCSS('.adf-search-chip-autocomplete-added-option .adf-info-icon').length).toBe(0);
});
it('should emit input value when input changed', async () => {
@@ -316,6 +341,38 @@ describe('SearchChipAutocompleteInputComponent', () => {
expect(inputChangedSpy).not.toHaveBeenCalled();
});
describe('loading', () => {
it('should show a loading spinner in the autocomplete panel when loading emits true', async () => {
component.loading$ = of(true);
fixture.detectChanges();
openAutocompletePanel();
await fixture.whenStable();
expect(testingUtils.getAllByDirective(MatProgressSpinner).length).toBe(1);
});
it('should not render selectable options while loading emits true', async () => {
component.filteredOptions = [{ value: 'option1' }, { value: 'option2' }];
component.loading$ = of(true);
fixture.detectChanges();
openAutocompletePanel();
await fixture.whenStable();
expect(testingUtils.getAllByCSS('.adf-search-chip-autocomplete-added-option').length).toBe(0);
});
it('should render options and no spinner when loading emits false', async () => {
component.filteredOptions = [{ value: 'option1' }, { value: 'option2' }];
component.loading$ = of(false);
fixture.detectChanges();
openAutocompletePanel();
await fixture.whenStable();
expect((await getOptionElements()).length).toBe(2);
expect(testingUtils.getAllByCSS('.adf-search-chip-autocomplete-loading').length).toBe(0);
});
});
describe('isOptionSelected', () => {
beforeEach(() => {
component.autocompleteOptions = [{ value: 'option1' }, { value: 'option2' }];
@@ -33,7 +33,7 @@ import { ENTER } from '@angular/cdk/keycodes';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
import { Observable, timer } from 'rxjs';
import { Observable, of, timer } from 'rxjs';
import { debounce, startWith, tap } from 'rxjs/operators';
import { AutocompleteOption } from '../../models/autocomplete-option.interface';
import { CommonModule } from '@angular/common';
@@ -41,10 +41,22 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { TranslatePipe } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { IconModule } from '@alfresco/adf-core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@Component({
selector: 'adf-search-chip-autocomplete-input',
imports: [CommonModule, MatFormFieldModule, MatChipsModule, TranslatePipe, IconModule, ReactiveFormsModule, MatAutocompleteModule],
imports: [
CommonModule,
MatFormFieldModule,
MatChipsModule,
TranslatePipe,
IconModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatTooltipModule,
MatProgressSpinnerModule
],
templateUrl: './search-chip-autocomplete-input.component.html',
styleUrls: ['./search-chip-autocomplete-input.component.scss'],
encapsulation: ViewEncapsulation.None
@@ -68,6 +80,9 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnChanges {
@Input()
placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION';
@Input()
loading$ = of(false);
@Input()
compareOption?: (option1: AutocompleteOption, option2: AutocompleteOption) => boolean;
@@ -5,13 +5,17 @@
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
(inputChanged)="onInputChange($event)"
[compareOption]="optionComparator"
[placeholder]="settings?.label ?? 'SEARCH.FILTER.ACTIONS.ADD_OPTION'"
[loading$]="loading$"
(optionsChanged)="onOptionsChange($event)" />
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
<button mat-button data-automation-id="adf-search-chip-autocomplete-btn-clear" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>
<button mat-button data-automation-id="adf-search-chip-autocomplete-btn-apply" (click)="submitValues()">
{{ 'SEARCH.FILTER.ACTIONS.APPLY' | translate }}
</button>
</div>
@if (!settings?.hideDefaultAction) {
<div class="adf-facet-buttons">
<button mat-button data-automation-id="adf-search-chip-autocomplete-btn-clear" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>
<button mat-button data-automation-id="adf-search-chip-autocomplete-btn-apply" (click)="submitValues()">
{{ 'SEARCH.FILTER.ACTIONS.APPLY' | translate }}
</button>
</div>
}
@@ -18,18 +18,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
import { of, ReplaySubject } from 'rxjs';
import { of, ReplaySubject, Subject, throwError } from 'rxjs';
import { AutocompleteField, AutocompleteOption } from '../../models/autocomplete-option.interface';
import { TagService } from '../../../tag/services/tag.service';
import { SitesService } from '../../../common/services/sites.service';
import { SitePaging } from '@alfresco/js-api';
import { ResultSetPaging, SitePaging } from '@alfresco/js-api';
import { CategoryService } from '../../../category';
import { SearchService } from '../../services/search.service';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchConfiguration } from '../../models/search-configuration.interface';
describe('SearchFilterAutocompleteChipsComponent', () => {
let component: SearchFilterAutocompleteChipsComponent;
let fixture: ComponentFixture<SearchFilterAutocompleteChipsComponent>;
let tagService: TagService;
let sitesService: SitesService;
let categoryService: CategoryService;
let searchService: SearchService;
let appConfig: AppConfigService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -40,6 +46,9 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
component = fixture.componentInstance;
tagService = TestBed.inject(TagService);
sitesService = TestBed.inject(SitesService);
categoryService = TestBed.inject(CategoryService);
searchService = TestBed.inject(SearchService);
appConfig = TestBed.inject(AppConfigService);
component.id = 'test-id';
component.context = {
queryFragments: {
@@ -47,7 +56,10 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
execute: jasmine.createSpy('execute')
execute: jasmine.createSpy('execute'),
get wildcardsEnabled(): boolean {
return appConfig.get<boolean>('search-wildcards-enabled', true);
}
} as any;
component.settings = {
field: 'test',
@@ -240,6 +252,169 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
expect(searchSpy).toHaveBeenCalledWith('tag', { orderBy: 'tag', direction: 'asc' }, false, 0, 15);
});
describe('loading state', () => {
function categoriesResult(name: string): ResultSetPaging {
return { list: { pagination: {}, entries: [{ entry: { id: `${name}-id`, name, path: { name: '/a/b' } } }] } } as ResultSetPaging;
}
it('should be false initially', (done) => {
component.loading$.subscribe((loading) => {
expect(loading).toBeFalse();
done();
});
});
it('should toggle true while fetching and back to false once results arrive', () => {
component.settings.field = AutocompleteField.CATEGORIES;
const response$ = new Subject<ResultSetPaging>();
spyOn(categoryService, 'searchCategories').and.returnValue(response$.asObservable());
const emitted: boolean[] = [];
component.loading$.subscribe((loading) => emitted.push(loading));
component.onInputChange('mark');
expect(emitted).toEqual([false, true]);
response$.next(categoriesResult('Marketing'));
response$.complete();
expect(emitted).toEqual([false, true, false]);
});
it('should clear loading and emit empty options when the fetch fails', () => {
component.settings.field = AutocompleteField.CATEGORIES;
spyOn(categoryService, 'searchCategories').and.returnValue(throwError(() => new Error('failure')));
const loadingStates: boolean[] = [];
const optionResults: AutocompleteOption[][] = [];
component.loading$.subscribe((loading) => loadingStates.push(loading));
component.autocompleteOptions$.subscribe((options) => optionResults.push(options));
component.onInputChange('mark');
expect(loadingStates).toEqual([false, true, false]);
expect(optionResults[optionResults.length - 1]).toEqual([]);
});
it('should keep the stream alive after a failed fetch', () => {
component.settings.field = AutocompleteField.CATEGORIES;
const searchSpy = spyOn(categoryService, 'searchCategories').and.returnValues(
throwError(() => new Error('failure')),
of(categoriesResult('Marketing'))
);
const optionResults: AutocompleteOption[][] = [];
component.autocompleteOptions$.subscribe((options) => optionResults.push(options));
component.onInputChange('mark');
component.onInputChange('mark');
expect(searchSpy).toHaveBeenCalledTimes(2);
expect(optionResults[optionResults.length - 1]).toEqual([{ id: 'Marketing-id', value: 'Marketing', fullPath: 'Marketing' }]);
});
it('should ignore results from a superseded request', () => {
component.settings.field = AutocompleteField.CATEGORIES;
const firstResponse$ = new Subject<ResultSetPaging>();
const secondResponse$ = new Subject<ResultSetPaging>();
spyOn(categoryService, 'searchCategories').and.returnValues(firstResponse$.asObservable(), secondResponse$.asObservable());
const optionResults: AutocompleteOption[][] = [];
component.autocompleteOptions$.subscribe((options) => optionResults.push(options));
component.onInputChange('ma');
component.onInputChange('mark');
secondResponse$.next(categoriesResult('Fresh'));
secondResponse$.complete();
firstResponse$.next(categoriesResult('Stale'));
firstResponse$.complete();
expect(optionResults[optionResults.length - 1]).toEqual([{ id: 'Fresh-id', value: 'Fresh', fullPath: 'Fresh' }]);
expect(optionResults.some((result) => result.some((option) => option.value === 'Stale'))).toBeFalse();
});
it('should not trigger a fetch for a non-async field', () => {
component.settings.field = 'test';
const searchSpy = spyOn(categoryService, 'searchCategories');
component.onInputChange('mark');
expect(searchSpy).not.toHaveBeenCalled();
});
});
describe('PARENT_FOLDER field', () => {
const folderPaging: ResultSetPaging = {
list: {
pagination: {},
entries: [{ entry: { id: 'folder1', name: 'Documents', path: { name: '/Company Home/Sites/ws/folderA' } } }]
}
} as ResultSetPaging;
function mockWildcardsEnabled(enabled: boolean) {
spyOn(appConfig, 'get').and.callFake((key: string, defaultValue?: any) => (key === 'search-wildcards-enabled' ? enabled : defaultValue));
}
beforeEach(() => {
component.settings.field = AutocompleteField.PARENT_FOLDER;
component.context.config = { filterQueries: [{ query: 'existing' }] } as SearchConfiguration;
});
it('should search folders and map results into options with full paths', (done) => {
spyOn(searchService, 'searchByQueryBody').and.returnValue(of(folderPaging));
component.onInputChange('doc');
component.autocompleteOptions$.subscribe((result) => {
expect(result).toEqual([{ id: 'folder1', value: 'Documents', fullPath: '/Company Home/Sites/ws/folderA/Documents' }]);
done();
});
});
it('should fall back to the folder name when the path name is empty', (done) => {
const folderWithoutPath: ResultSetPaging = {
list: {
pagination: {},
entries: [{ entry: { id: 'folder2', name: 'Documents', path: { name: '' } } }]
}
} as ResultSetPaging;
spyOn(searchService, 'searchByQueryBody').and.returnValue(of(folderWithoutPath));
component.onInputChange('doc');
component.autocompleteOptions$.subscribe((result) => {
expect(result).toEqual([{ id: 'folder2', value: 'Documents', fullPath: 'Documents' }]);
done();
});
});
it('should build a folder-scoped query without emitting the dataLoaded event', () => {
mockWildcardsEnabled(true);
const searchSpy = spyOn(searchService, 'searchByQueryBody').and.returnValue(of(folderPaging));
component.onInputChange('doc');
const [queryBody, shouldEmit] = searchSpy.calls.mostRecent().args;
expect(shouldEmit).toBeFalse();
expect(queryBody.query.query).toBe(`cm:name:"*doc*"`);
expect(queryBody.include).toEqual(['path']);
expect(queryBody.filterQueries).toEqual([{ query: 'existing' }, { query: "TYPE:'cm:folder'" }]);
});
it('should not mutate the shared context filter queries', () => {
spyOn(searchService, 'searchByQueryBody').and.returnValue(of(folderPaging));
component.onInputChange('doc');
component.onInputChange('docs');
expect(component.context.config.filterQueries).toEqual([{ query: 'existing' }]);
});
it('should not wrap the search term with wildcards when wildcards are disabled', () => {
mockWildcardsEnabled(false);
component.context.config = {} as SearchConfiguration;
const searchSpy = spyOn(searchService, 'searchByQueryBody').and.returnValue(of(folderPaging));
component.onInputChange('doc');
const [queryBody] = searchSpy.calls.mostRecent().args;
expect(queryBody.query.query).toBe(`cm:name:"doc"`);
expect(queryBody.filterQueries).toEqual([{ query: "TYPE:'cm:folder'" }]);
});
it('should compose the query fragment using the node reference', () => {
component.selectedOptions = [{ id: 'folder1', value: 'Documents' }];
component.submitValues();
expect(component.context.queryFragments[component.id]).toBe('ANCESTOR:"workspace://SpacesStore/folder1"');
});
});
describe('optionComparator', () => {
it('should return false if either option is undefined', () => {
expect(component.optionComparator(undefined, { value: 'A' })).toBe(false);
@@ -16,8 +16,8 @@
*/
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
@@ -31,6 +31,13 @@ import { TranslatePipe } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { SitesService } from '../../../common/services/sites.service';
import { SearchService } from '../../services/search.service';
import { SearchRequest } from '@alfresco/js-api';
interface AutocompleteFetchState {
options: AutocompleteOption[] | null;
loading: boolean;
}
@Component({
selector: 'adf-search-filter-autocomplete-chips',
@@ -42,6 +49,18 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
private readonly tagService = inject(TagService);
private readonly categoryService = inject(CategoryService);
private readonly sitesService = inject(SitesService);
private readonly searchService = inject(SearchService);
private readonly loadingSubject$ = new BehaviorSubject<boolean>(false);
private readonly inputChange$ = new Subject<string>();
private readonly asyncFields: string[] = [
AutocompleteField.CATEGORIES,
AutocompleteField.TAG,
AutocompleteField.LOCATION,
AutocompleteField.PARENT_FOLDER
];
private readonly resetSubject$ = new Subject<void>();
private readonly autocompleteOptionsSubject$ = new BehaviorSubject<AutocompleteOption[]>([]);
private readonly destroyRef = inject(DestroyRef);
id: string;
settings?: SearchWidgetSettings;
@@ -51,13 +70,9 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
displayValue$ = new ReplaySubject<string>(1);
selectedOptions: AutocompleteOption[] = [];
enableChangeUpdate: boolean;
private readonly resetSubject$ = new Subject<void>();
reset$: Observable<void> = this.resetSubject$.asObservable();
private readonly autocompleteOptionsSubject$ = new BehaviorSubject<AutocompleteOption[]>([]);
autocompleteOptions$: Observable<AutocompleteOption[]> = this.autocompleteOptionsSubject$.asObservable();
private readonly destroyRef = inject(DestroyRef);
loading$ = this.loadingSubject$.asObservable();
constructor() {
this.options = new SearchFilterList<AutocompleteOption[]>();
@@ -71,6 +86,23 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
}
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
}
this.inputChange$
.pipe(
switchMap((value) =>
this.fetchOptions(value).pipe(
map((options): AutocompleteFetchState => ({ options, loading: false })),
catchError((): Observable<AutocompleteFetchState> => of({ options: [], loading: false })),
startWith<AutocompleteFetchState>({ options: null, loading: true })
)
),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(({ options, loading }) => {
this.loadingSubject$.next(loading);
if (options) {
this.autocompleteOptionsSubject$.next(options);
}
});
this.context.populateFilters
.asObservable()
.pipe(
@@ -121,12 +153,9 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
}
onInputChange(value: string) {
if (this.settings.field === AutocompleteField.CATEGORIES) {
this.searchForExistingCategories(value);
} else if (this.settings.field === AutocompleteField.TAG) {
this.searchForExistingTags(value);
} else if (this.settings.field === AutocompleteField.LOCATION) {
this.populateSitesOptions();
const field = this.settings?.field;
if (field && this.asyncFields.includes(field)) {
this.inputChange$.next(value);
}
}
@@ -148,6 +177,7 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
let queryFragments;
switch (this.settings.field) {
case AutocompleteField.CATEGORIES:
case AutocompleteField.PARENT_FOLDER:
queryFragments = this.selectedOptions.map((val) => `${this.settings.field}:"workspace://SpacesStore/${val.id}"`);
break;
case AutocompleteField.LOCATION:
@@ -165,14 +195,11 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
}
private setOptions() {
switch (this.settings.field) {
switch (this.settings?.field) {
case AutocompleteField.TAG:
this.autocompleteOptionsSubject$.next([]);
break;
case AutocompleteField.CATEGORIES:
this.autocompleteOptionsSubject$.next([]);
break;
case AutocompleteField.LOCATION:
case AutocompleteField.PARENT_FOLDER:
this.autocompleteOptionsSubject$.next([]);
break;
default:
@@ -180,44 +207,80 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
}
}
private searchForExistingCategories(searchTerm: string) {
this.categoryService.searchCategories(searchTerm, 0, 15).subscribe((existingCategoriesResult) => {
this.autocompleteOptionsSubject$.next(
private fetchOptions(searchTerm: string): Observable<AutocompleteOption[]> {
switch (this.settings?.field) {
case AutocompleteField.CATEGORIES:
return this.searchForExistingCategories(searchTerm);
case AutocompleteField.TAG:
return this.searchForExistingTags(searchTerm);
case AutocompleteField.LOCATION:
return this.getSitesOptions();
case AutocompleteField.PARENT_FOLDER:
return this.searchFolders(searchTerm);
default:
return of([]);
}
}
private searchForExistingCategories(searchTerm: string): Observable<AutocompleteOption[]> {
return this.categoryService.searchCategories(searchTerm, 0, 15).pipe(
map((existingCategoriesResult) =>
existingCategoriesResult.list.entries.map((rowEntry) => {
const path = rowEntry.entry.path.name.split('/').splice(3).join('/');
const fullPath = path ? `${path}/${rowEntry.entry.name}` : rowEntry.entry.name;
return { id: rowEntry.entry.id, value: rowEntry.entry.name, fullPath };
})
);
});
)
);
}
private searchForExistingTags(searchTerm: string) {
this.tagService.searchTags(searchTerm, { orderBy: 'tag', direction: 'asc' }, false, 0, 15).subscribe((existingTagsResult) => {
this.autocompleteOptionsSubject$.next(
private searchForExistingTags(searchTerm: string): Observable<AutocompleteOption[]> {
return this.tagService.searchTags(searchTerm, { orderBy: 'tag', direction: 'asc' }, false, 0, 15).pipe(
map((existingTagsResult) =>
existingTagsResult.list.entries.map((tag) => ({
id: tag.entry.id,
value: tag.entry.tag
}))
);
});
)
);
}
private populateSitesOptions(): void {
this.sitesService
.getSites()
.pipe(
map((sites) => {
const predefinedOptions = this.settings?.autocompleteOptions || [];
const sitesOptions = sites.list.entries
.filter((siteEntry) => siteEntry.entry.visibility === 'public' || siteEntry.entry?.role)
.map<AutocompleteOption>((siteEntry) => ({
id: siteEntry.entry.id,
value: siteEntry.entry.title
}));
return [...sitesOptions, ...predefinedOptions];
private getSitesOptions(): Observable<AutocompleteOption[]> {
return this.sitesService.getSites().pipe(
map((sites) => {
const predefinedOptions = this.settings?.autocompleteOptions || [];
const sitesOptions = sites.list.entries
.filter((siteEntry) => siteEntry.entry.visibility === 'public' || siteEntry.entry?.role)
.map<AutocompleteOption>((siteEntry) => ({
id: siteEntry.entry.id,
value: siteEntry.entry.title
}));
return [...sitesOptions, ...predefinedOptions];
})
);
}
private searchFolders(searchTerm: string): Observable<AutocompleteOption[]> {
const wildcard = this.context?.wildcardsEnabled ? '*' : '';
const filterQueries = [...(this.context?.config.filterQueries ?? []), { query: "TYPE:'cm:folder'" }];
const queryBody: SearchRequest = {
query: {
language: 'afts',
query: `cm:name:"${wildcard}${searchTerm}${wildcard}"`
},
include: ['path'],
filterQueries
};
return this.searchService.searchByQueryBody(queryBody, false).pipe(
map((folders) =>
folders.list.entries.map((folderEntry) => {
const fullPath = folderEntry.entry.path.name
? `${folderEntry.entry.path.name}/${folderEntry.entry.name}`
: folderEntry.entry.name;
return { id: folderEntry.entry.id, value: folderEntry.entry.name, fullPath };
})
)
.subscribe((options) => this.autocompleteOptionsSubject$.next(options));
);
}
}
@@ -1,9 +1,9 @@
<div class="adf-search-logical-filter-container">
<div *ngFor="let field of fields" class="adf-search-input">
<mat-label data-automation-id="adf-search-input-label">{{('SEARCH.LOGICAL_SEARCH.' + field + '_LABEL') | translate}}</mat-label>
<mat-label [id]="`adf-search-input-label-${field}`" data-automation-id="adf-search-input-label">{{('SEARCH.LOGICAL_SEARCH.' + field + '_LABEL') | translate}}</mat-label>
<input type="text"
[(ngModel)]="searchCondition[LogicalSearchFields[field]]"
placeholder="{{ ('SEARCH.LOGICAL_SEARCH.' + field + '_HINT') | translate }}"
[attr.aria-label]="('SEARCH.LOGICAL_SEARCH.' + field + '_HINT') | translate"/>
[attr.aria-labelledby]="`adf-search-input-label-${field}`"/>
</div>
</div>
@@ -19,10 +19,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { LogicalSearchCondition, LogicalSearchFields, SearchLogicalFilterComponent } from './search-logical-filter.component';
import { ReplaySubject } from 'rxjs';
import { UnitTestingUtils } from '@alfresco/adf-core';
describe('SearchLogicalFilterComponent', () => {
let component: SearchLogicalFilterComponent;
let fixture: ComponentFixture<SearchLogicalFilterComponent>;
let unitTestingUtils: UnitTestingUtils;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -41,6 +43,7 @@ describe('SearchLogicalFilterComponent', () => {
execute: jasmine.createSpy('execute')
} as any;
component.settings = { field: 'field1,field2', allowUpdateOnChange: true, hideDefaultAction: false };
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
fixture.detectChanges();
});
@@ -204,4 +207,17 @@ describe('SearchLogicalFilterComponent', () => {
expect(component.searchCondition).toEqual({ matchAll: 'test', matchAny: 'test2', matchExact: '', exclude: '' });
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
describe('Accessibility', () => {
it('should use aria-labelledby attribute for input fields', () => {
const inputs = getInputs();
const fieldset = unitTestingUtils.getAllByDataAutomationId('adf-search-input-label');
inputs.forEach((input, index) => {
const fieldsetId = fieldset[index].nativeElement.getAttribute('id');
expect(input.getAttribute('aria-labelledby')).toBe(fieldsetId);
expect(input.hasAttribute('aria-label')).toBe(false);
});
});
});
});
@@ -25,7 +25,8 @@ export interface AutocompleteOption {
export const AutocompleteField = {
TAG: 'TAG',
CATEGORIES: 'cm:categories',
LOCATION: 'SITE'
LOCATION: 'SITE',
PARENT_FOLDER: 'ANCESTOR'
} as const;
export type AutocompleteField = (typeof AutocompleteField)[keyof typeof AutocompleteField];
@@ -31,6 +31,8 @@ export interface SearchWidgetSettings {
allowOnlyPredefinedValues?: boolean;
/* allow the user to predefine autocomplete options */
autocompleteOptions?: AutocompleteOption[];
/* label that will be displayed for autocomplete input */
label?: string;
[indexer: string]: any;
}
@@ -76,14 +76,21 @@ export class SearchService {
* Performs a search with its parameters supplied by a request object.
*
* @param queryBody Object containing the search parameters
* @param shouldEmit Should emit dataLoaded event
* @returns List of search results
*/
searchByQueryBody(queryBody: SearchRequest): Observable<ResultSetPaging> {
searchByQueryBody(queryBody: SearchRequest, shouldEmit = true): Observable<ResultSetPaging> {
const promise = this.searchApi.search(queryBody);
promise.then((nodePaging) => {
this.dataLoaded.next(nodePaging);
});
if (shouldEmit) {
promise
.then((nodePaging) => {
this.dataLoaded.next(nodePaging);
})
.catch(() => {
/* error is surfaced to subscribers through the returned Observable */
});
}
return from(promise);
}
@@ -32,11 +32,13 @@ export interface QaFeaturesHelperConfig {
helperExposeKeyOnDocument?: string;
}
export interface FlagChangesetValues {
current: any;
previous: any;
}
export interface FlagChangeset {
[key: string]: {
current: any;
previous: any;
};
[key: string]: FlagChangesetValues;
}
export interface WritableFlagChangeset {
@@ -0,0 +1,133 @@
/*!
* @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 { BehaviorSubject, firstValueFrom, of, Subject } from 'rxjs';
import { FeaturesServiceToken, IFeaturesService } from '../interfaces/features.interface';
import { MockFeatureFlags, provideMockFeatureFlags } from './features-service-mock.factory';
describe('provideMockFeatureFlags', () => {
let featureValue$: BehaviorSubject<boolean>;
const feature1 = 'feature1';
const setupFeatureService = (featureFlags: MockFeatureFlags | string | string[]): IFeaturesService => {
TestBed.configureTestingModule({
providers: [provideMockFeatureFlags(featureFlags)]
});
return TestBed.inject(FeaturesServiceToken);
};
beforeEach(() => {
featureValue$ = new BehaviorSubject(false);
});
it('should emit the updated value when an observable feature flag changes', async () => {
const featuresService = setupFeatureService({ [feature1]: featureValue$ });
expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(false);
expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(true);
featureValue$.next(true);
expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(true);
expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(false);
featureValue$.next(false);
expect(await firstValueFrom(featuresService.isOn$(feature1))).toBe(false);
expect(await firstValueFrom(featuresService.isOff$(feature1))).toBe(true);
});
it('should not let a consumer change the feature flag value through the observable returned by isOn$', async () => {
const originalValue$ = new BehaviorSubject(true);
const service = setupFeatureService({ [feature1]: originalValue$ });
const isOn$ = service.isOn$(feature1) as Subject<boolean>;
expect(isOn$).not.toBe(originalValue$);
expect(() => isOn$.next(false)).toThrow();
expect(await firstValueFrom(service.isOn$(feature1))).toBe(true);
expect(await firstValueFrom(originalValue$)).toBe(true);
});
it('should mock a string feature flag as enabled', async () => {
const service = setupFeatureService(feature1);
expect(await firstValueFrom(service.isOn$(feature1))).toBe(true);
expect(await firstValueFrom(service.isOff$(feature1))).toBe(false);
});
it('should mock every feature flag in an array as enabled', async () => {
const feature2 = 'feature2';
const service = setupFeatureService([feature1, feature2]);
expect(await firstValueFrom(service.getFlags$())).toEqual({
[feature1]: { current: true, previous: null },
[feature2]: { current: true, previous: null }
});
});
it('should emit an empty changeset when no feature flags are provided', async () => {
const service = setupFeatureService({});
expect(await firstValueFrom(service.getFlags$())).toEqual({});
expect(await firstValueFrom(service.init())).toEqual({});
});
it('should emit an empty changeset when an empty feature flag array is provided', async () => {
const service = setupFeatureService([]);
expect(await firstValueFrom(service.getFlags$())).toEqual({});
expect(await firstValueFrom(service.init())).toEqual({});
});
it('should return the expected state when the feature flag is true', async () => {
const service = setupFeatureService({ [feature1]: true });
expect(await firstValueFrom(service.init())).toEqual({
[feature1]: { current: true, previous: null }
});
expect(await firstValueFrom(service.isOn$(feature1))).toBe(true);
expect(await firstValueFrom(service.isOff$(feature1))).toBe(false);
});
it('should return the expected state when the feature flag is false', async () => {
const service = setupFeatureService({ [feature1]: false });
expect(await firstValueFrom(service.init())).toEqual({
[feature1]: { current: false, previous: null }
});
expect(await firstValueFrom(service.isOn$(feature1))).toBe(false);
expect(await firstValueFrom(service.isOff$(feature1))).toBe(true);
});
it('should resolve observable values in the complete feature flags result', async () => {
const service = setupFeatureService({ [feature1]: of(true) });
expect(await firstValueFrom(service.getFlags$())).toEqual({
[feature1]: { current: true, previous: null }
});
});
it('should throw when a feature flag has not been mocked', () => {
const featuresService = setupFeatureService({ [feature1]: true });
expect(() => featuresService.isOn$('missing-feature')).toThrowError(/missing-feature.*not mocked/);
expect(() => featuresService.isOff$('missing-feature')).toThrowError(/missing-feature.*not mocked/);
});
});
@@ -15,11 +15,27 @@
* limitations under the License.
*/
import { of } from 'rxjs';
import { FeaturesServiceToken, FlagChangeset, IFeaturesService } from '../interfaces/features.interface';
import { of, Observable, map, combineLatest, take, defer } from 'rxjs';
import { FeaturesServiceToken, FlagChangeset, FlagChangesetValues, IFeaturesService } from '../interfaces/features.interface';
/**
* Feature flags to mock. A boolean sets a fixed value, an observable lets the test change the value over time.
*
* Observable flags must have a current value, use 'BehaviorSubject' or 'of(...)'.
* A bare 'Subject' has no value until it emits, and 'getFlags$()'/'init()' withhold the whole changeset
* until every observable flag has emitted at least once.
*/
export interface MockFeatureFlags {
[key: string]: boolean;
[key: string]: boolean | Observable<boolean>;
}
interface MockFlagChangesetValues extends FlagChangesetValues {
current: boolean | Observable<boolean>;
previous: null;
}
interface MockFlagChangeset extends FlagChangeset {
[key: string]: MockFlagChangesetValues;
}
const assertFeatureFlag = (flagChangeset: FlagChangeset, key: string): void => {
@@ -32,19 +48,72 @@ const assertFeatureFlag = (flagChangeset: FlagChangeset, key: string): void => {
}
};
const mockFeaturesService = (flagChangeset: FlagChangeset): IFeaturesService => ({
init: () => of(flagChangeset),
/**
* Calling 'pipe' on a 'Subject' returns an 'AnonymousSubject' that still writes through to the original 'Subject',
* so piping alone does not stop a consumer from pushing values into the mocked flag.
* 'defer' breaks that chain and gives back a plain, read only observable.
*
* @param value$ Observable feature flag value provided by the test
* @returns Observable that cannot be used to change the mocked value
*/
const toReadOnly = (value$: Observable<boolean>): Observable<boolean> => defer(() => value$);
const mockFeaturesService = (flagChangeset: MockFlagChangeset): IFeaturesService => ({
init: () => resolveFeatureFlagValues(flagChangeset).pipe(take(1)),
isOn$: (key) => {
assertFeatureFlag(flagChangeset, key);
return of(flagChangeset[key].current);
const featureFlagValue = flagChangeset[key].current;
// In case of an observable, we do not want to return the original observable, so a consumer cannot 'next', 'error' or 'complete' it
return typeof featureFlagValue === 'boolean' ? of(featureFlagValue) : toReadOnly(featureFlagValue).pipe(map(Boolean));
},
isOff$: (key) => {
assertFeatureFlag(flagChangeset, key);
return of(!flagChangeset[key].current);
const featureFlagValue = flagChangeset[key].current;
return typeof featureFlagValue === 'boolean' ? of(!featureFlagValue) : toReadOnly(featureFlagValue).pipe(map((value) => !value));
},
getFlags$: () => of(flagChangeset)
getFlags$: () => resolveFeatureFlagValues(flagChangeset)
});
/**
* 'provideMockFeatureFlags' can receive observables, therefore we need to resolve these values
*
* @param mockFlagChangeset Mocked flag changeset
* @returns FlagChangeset
*/
const resolveFeatureFlagValues = (mockFlagChangeset: MockFlagChangeset): Observable<FlagChangeset> => {
// No FF provided, just return empty object
if (Object.keys(mockFlagChangeset).length === 0) {
return of({});
}
const resolveFeatureFlagValues$ = Object.entries(mockFlagChangeset).map(([featureKey, values]) => {
if (typeof values.current === 'boolean') {
return of([featureKey, { ...values }] as const);
}
// Value is observable, we need to resolve it
const observableValue$ = values.current;
const resolveFlagValue$ = observableValue$.pipe(map((resolvedValue) => [featureKey, { current: resolvedValue, previous: null }] as const));
return resolveFlagValue$;
});
return combineLatest(resolveFeatureFlagValues$).pipe(
map((resolvedFlags) => {
const resolvedFeatureFlag: FlagChangeset = {};
resolvedFlags.forEach(([key, values]) => {
resolvedFeatureFlag[key] = values;
});
return resolvedFeatureFlag;
})
);
};
const arrayToFlagChangeset = (featureFlags: string[]): FlagChangeset => {
const flagChangeset: FlagChangeset = {};
featureFlags.forEach((featureFlag) => {
@@ -62,6 +131,26 @@ const mockFeatureFlagsToFlagChangeset = (mockFeatureFlags: MockFeatureFlags) =>
return flagChangeset;
};
/**
* Mock the FeaturesService with the provided feature flags.
* A string or string[] sets every listed feature to true, a MockFeatureFlags object sets each value explicitly.
* Every flag the code under test asks for has to be mocked, otherwise 'isOn$'/'isOff$' throw.
*
* Use a 'BehaviorSubject' for a flag that changes during the test. A bare 'Subject' has no current value,
* so the flag stays silent and 'getFlags$()'/'init()' withhold the whole changeset, until it emits.
*
* @example
*
* const featureA$ = new BehaviorSubject(false);
*
* providers: [provideMockFeatureFlags('featureA')]
* providers: [provideMockFeatureFlags(['featureA', 'featureB'])]
* providers: [provideMockFeatureFlags({ featureA: true, featureB: false })]
* providers: [provideMockFeatureFlags({ featureA: featureA$ })]
*
* @param featureFlag The feature flag(s) to mock. Can be a single feature flag string, an array of feature flag strings, or a MockFeatureFlags object.
* @returns A provider object for the FeaturesServiceToken with the mocked feature flags.
*/
export const provideMockFeatureFlags = (featureFlag: MockFeatureFlags | string | string[]) => {
if (typeof featureFlag === 'string') {
featureFlag = [featureFlag];
@@ -87,12 +87,26 @@ describe('DataTableRowComponent', () => {
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBeNull();
});
it('should not set tabindex when row is disabled', () => {
it('should set tabindex as focusable only with the arrow keys when row is enabled but not active', () => {
component.disabled = false;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe('-1');
});
it('should set tabindex as focusable with the Tab key when row is enabled and active', () => {
component.disabled = false;
component.active = true;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBe('0');
});
it('should not set tabindex when row is disabled even if it is active', () => {
component.disabled = true;
component.active = true;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('tabindex')).toBeNull();
});
it('should focus element', () => {
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false);
component.disabled = false;
@@ -35,6 +35,8 @@ export class DataTableRowComponent implements FocusableOption {
@Input() disabled = true;
@Input() active = false;
@Output()
select: EventEmitter<any> = new EventEmitter<any>();
@@ -65,7 +67,10 @@ export class DataTableRowComponent implements FocusableOption {
@HostBinding('attr.tabindex')
get tabindex(): number | null {
return this.disabled ? null : 0;
if (this.disabled) {
return null;
}
return this.active ? 0 : -1;
}
@HostListener('keydown.space', ['$event'])
@@ -262,7 +262,9 @@
[class.adf-datatable-row__dragging]="isDraggingRow"
[attr.data-automation-id]="'datatable-row-' + idx"
(contextmenu)="markRowAsContextMenuSource(row)"
[disabled]="!(enableDragRows || multiselect)"
(focus)="onRowFocus(idx)"
[disabled]="!isRowKeyboardNavigable()"
[active]="isRowActive(idx)"
[attr.aria-description]="
(multiselect ? ('ADF-DATATABLE.ACCESSIBILITY.ROW_SELECTION' | translate) : '') +
(multiselect && enableDragRows ? '. ' : '') +
@@ -1881,10 +1881,10 @@ describe('Accessibility', () => {
const getBodyRows = (): DebugElement[] => testingUtils.getAllByCSS(rowSelector);
const expectRowsTabindex = (expected: string | null): void => {
const expectRowsTabindex = (expected: (string | null)[]): void => {
const rowElements = getBodyRows();
expect(rowElements.length).toBeGreaterThan(0);
expect(rowElements.every((row) => row.nativeElement.getAttribute('tabindex') === expected)).toBeTrue();
expect(rowElements.length).toBe(expected.length);
expect(rowElements.map((row) => row.nativeElement.getAttribute('tabindex'))).toEqual(expected);
};
const activateRow = (rowIndex: number): void => {
@@ -1907,24 +1907,42 @@ describe('Accessibility', () => {
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
});
it('should set tabindex to null (disabled === true) on datatable-body rows when neither multiselect nor enableDragRows is enabled', () => {
it('should set tabindex to null (disabled === true) on datatable-body rows when rows cannot be selected nor dragged', () => {
dataTable.selectionMode = 'none';
setRows();
expectRowsTabindex(null);
expectRowsTabindex([null, null]);
});
it('should set tabindex to 0 (disabled === false) on datatable-body rows when multiselect is enabled', () => {
it('should make only the first row reachable with the Tab key when rows are selectable', () => {
setRows();
expectRowsTabindex(['0', '-1']);
});
it('should make only the first row reachable with the Tab key when multiselect is enabled', () => {
dataTable.multiselect = true;
setRows();
expectRowsTabindex('0');
expectRowsTabindex(['0', '-1']);
});
it('should set tabindex to 0 (disabled === false) on datatable-body rows when enableDragRows is enabled', () => {
it('should make only the first row reachable with the Tab key when enableDragRows is enabled', () => {
dataTable.enableDragRows = true;
setRows();
expectRowsTabindex('0');
expectRowsTabindex(['0', '-1']);
});
it('should move the tabindex to the active row', () => {
setRows();
dataTable.ngAfterViewInit();
activateRow(1);
testingUtils.setDebugElement(fixture.debugElement);
fixture.detectChanges();
expectRowsTabindex(['-1', '0']);
});
it('should focus next row on ArrowDown event', () => {
@@ -1954,6 +1972,17 @@ describe('Accessibility', () => {
expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0');
});
it('should navigate between rows with the arrow keys in single selection mode', () => {
dataTable.selectionMode = 'single';
setRows();
dataTable.ngAfterViewInit();
activateRow(1);
dispatchKeyUp(event);
expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0');
});
});
describe('Row cells focus management', () => {
@@ -347,6 +347,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
private readonly destroyRef = inject(DestroyRef);
private get rowsOffset(): number {
return this.isHeaderVisible() ? 1 : 0;
}
@HostListener('keyup', ['$event'])
onKeydown(event: KeyboardEvent): void {
if (event.shiftKey && this.enableDragRows) {
@@ -640,7 +644,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
}
if (row) {
const rowIndex = this.data.getRows().indexOf(row) + (this.isHeaderVisible() ? 1 : 0);
const rowIndex = this.data.getRows().indexOf(row) + this.rowsOffset;
this.keyManager.setActiveItem(rowIndex);
const dataRowEvent = new DataRowEvent(row, mouseEvent, this);
@@ -923,6 +927,19 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
return this.selectionMode && this.selectionMode.toLowerCase() === 'multiple';
}
protected isRowKeyboardNavigable(): boolean {
return this.enableDragRows || this.multiselect || this.isSingleSelectionMode() || this.isMultiSelectionMode();
}
protected isRowActive(rowIndex: number): boolean {
const activeItemIndex = this.keyManager?.activeItemIndex ?? -1;
return activeItemIndex < this.rowsOffset ? rowIndex === 0 : activeItemIndex === rowIndex + this.rowsOffset;
}
protected onRowFocus(rowIndex: number): void {
this.keyManager?.updateActiveItem(rowIndex + this.rowsOffset);
}
getRowStyle(row: DataRow): string {
row.cssClass = row.cssClass ? row.cssClass : '';
this.rowStyleClass = this.rowStyleClass ? this.rowStyleClass : '';
@@ -17,7 +17,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnitTestingUtils } from '../../testing';
import { FormRulesManager } from '../models/form-rules.model';
import { FORM_RULES_MANAGER, FormRulesManager } from '../models/form-rules.model';
import { FormRulesEvent } from '../events/form-rules.event';
import { FormRenderingService } from '../services/form-rendering.service';
import { FormService } from '../services/form.service';
import { FormRendererComponent } from './form-renderer.component';
@@ -52,6 +53,7 @@ import {
import { FormFieldModel, FormModel, TextWidgetComponent } from './widgets';
import { MatDialog } from '@angular/material/dialog';
import { of } from 'rxjs';
import { filter } from 'rxjs/operators';
import { WidgetVisibilityService } from '../services/widget-visibility.service';
const typeIntoInput = (testingUtils: UnitTestingUtils, selector: string, message: string) => {
@@ -137,6 +139,38 @@ describe('Form Renderer Component', () => {
});
describe('visibility refresh on form rules', () => {
const buildFormWithRequiredField = (startVisible: boolean = true): FormModel => {
const form = new FormModel(
{
id: 'required-field-form',
fields: [
{
id: 'container',
type: 'container',
numberOfColumns: 1,
fields: { 1: [{ id: 'requiredText', type: 'text', name: 'Required text', required: true }] }
}
]
},
undefined,
false,
formService
);
if (!startVisible) {
form.changeFieldVisibility('requiredText', false);
form.validateForm();
}
return form;
};
const hideFieldOnFieldValueChanged = (form: FormModel, fieldId: string, visible: boolean): void => {
formService.formRulesEvent
.pipe(filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === form.id))
.subscribe(() => form.changeFieldVisibility(fieldId, visible));
};
it('should refresh visibility when a fieldValueChanged rule event fires for the same form', () => {
const form = formService.parseForm(textWidgetVisibility.formRepresentation);
formRendererComponent.formDefinition = form;
@@ -176,6 +210,80 @@ describe('Form Renderer Component', () => {
expect(refreshVisibilitySpy).not.toHaveBeenCalled();
});
it('should revalidate the form when a rule hides an empty required field', () => {
const form = buildFormWithRequiredField();
formRendererComponent.formDefinition = form;
hideFieldOnFieldValueChanged(form, 'requiredText', false);
fixture.detectChanges();
expect(form.isValid).toBe(false, 'form should start invalid with a visible empty required field');
formService.formRulesEvent.next({ type: 'fieldValueChanged', form } as any);
expect(form.isValid).toBe(true, 'hiding the required field should re-enable the outcome');
});
it('should revalidate the form when a rule reveals an empty required field', () => {
const form = buildFormWithRequiredField(false);
formRendererComponent.formDefinition = form;
hideFieldOnFieldValueChanged(form, 'requiredText', true);
fixture.detectChanges();
expect(form.isValid).toBe(true, 'form should start valid while the required field is hidden');
formService.formRulesEvent.next({ type: 'fieldValueChanged', form } as any);
expect(form.isValid).toBe(false, 'revealing the empty required field should disable the outcome');
});
it('should revalidate the form when a rule hides a group containing an empty required field', () => {
const form = new FormModel(
{
id: 'required-group-form',
fields: [
{
id: 'requiredGroup',
type: 'group',
params: {},
numberOfColumns: 1,
fields: { 1: [{ id: 'textInGroup', type: 'text', name: 'Text', required: true }] }
}
]
},
undefined,
false,
formService
);
form.enableParentVisibilityCheck = true;
form.getFormFields().forEach((field) => (field.checkParentVisibilityForValidation = true));
form.validateForm();
formRendererComponent.formDefinition = form;
hideFieldOnFieldValueChanged(form, 'requiredGroup', false);
fixture.detectChanges();
expect(form.isValid).toBe(false, 'form should start invalid while the group is visible');
formService.formRulesEvent.next({ type: 'fieldValueChanged', form } as any);
expect(form.isValid).toBe(true, 'hiding the group should exempt its required child');
});
it('should revalidate the form for rule events other than fieldValueChanged', () => {
const form = buildFormWithRequiredField();
formRendererComponent.formDefinition = form;
formService.formRulesEvent
.pipe(filter((event) => event?.type === 'onRowCountChanged' && event.form?.id === form.id))
.subscribe(() => form.changeFieldVisibility('requiredText', false));
fixture.detectChanges();
expect(form.isValid).toBe(false, 'form should start invalid with a visible empty required field');
formService.formRulesEvent.next({ type: 'onRowCountChanged', form } as any);
expect(form.isValid).toBe(true, 'row-count driven rules should also recompute validity');
});
});
describe('Display Date Widget ', () => {
@@ -1225,3 +1333,67 @@ describe('Form Renderer Component', () => {
});
});
});
class HideOnFieldValueChangedRulesManager extends FormRulesManager<{ targetFieldId: string }> {
protected getRules(): { targetFieldId: string } {
return { targetFieldId: 'requiredText' };
}
protected handleRuleEvent(event: FormRulesEvent, rules: { targetFieldId: string }): void {
if (event.type === 'fieldValueChanged') {
this.formModel.changeFieldVisibility(rules.targetFieldId, false);
}
}
}
describe('Form Renderer Component form rule ordering', () => {
let fixture: ComponentFixture<FormRendererComponent<any>>;
let formService: FormService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormRendererComponent],
providers: [
{
provide: FORM_RULES_MANAGER,
useFactory: (service: FormService) => new HideOnFieldValueChangedRulesManager(service),
deps: [FormService]
}
]
});
fixture = TestBed.createComponent(FormRendererComponent<any>);
formService = TestBed.inject(FormService);
});
afterEach(() => {
fixture.destroy();
});
it('should revalidate after a real rules manager hides an empty required field', () => {
const form = new FormModel(
{
id: 'rule-ordering-form',
fields: [
{
id: 'container',
type: 'container',
numberOfColumns: 1,
fields: { 1: [{ id: 'requiredText', type: 'text', name: 'Required text', required: true }] }
}
]
},
undefined,
false,
formService
);
fixture.componentInstance.formDefinition = form;
fixture.detectChanges();
expect(form.isValid).toBe(false, 'form should start invalid with a visible empty required field');
formService.formRulesEvent.next({ type: 'fieldValueChanged', form } as any);
expect(form.isValid).toBe(true, 'the rule-driven hide must be reflected in form validity');
});
});
@@ -156,10 +156,15 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
this.formService.formRulesEvent
.pipe(
filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.formDefinition?.id),
filter((event) => !!event?.type && event.form?.id === this.formDefinition?.id),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => this.visibilityService.refreshVisibility(this.formDefinition));
.subscribe((event) => {
if (event.type === 'fieldValueChanged') {
this.visibilityService.refreshVisibility(this.formDefinition);
}
this.formDefinition?.validateForm();
});
}
ngOnDestroy() {
@@ -106,6 +106,14 @@
<mat-icon adf-icon="zoom_out" />
</button>
<button id="viewer-rotate-page-button"
title="{{ 'ADF_VIEWER.ARIA.ROTATE' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ROTATE' | translate }}"
mat-icon-button
(click)="rotatePage()">
<mat-icon adf-icon="rotate_left" />
</button>
<button id="viewer-scale-page-button"
role="button" aria-pressed="true"
title="{{ 'ADF_VIEWER.ARIA.FIT_PAGE' | translate }}"
@@ -114,6 +122,5 @@
(click)="pageFit()">
<mat-icon adf-icon="zoom_out_map" />
</button>
</adf-toolbar>
</div>
@@ -461,8 +461,10 @@ describe('Test PdfViewer - User interaction', () => {
let component: PdfViewerComponent;
let testingUtils: UnitTestingUtils;
let pdfViewerSpy: jasmine.Spy;
let pageViewMock: { width: number; height: number; scale: number; rotation: number; update: jasmine.Spy };
beforeEach(fakeAsync(() => {
pageViewMock = { width: 100, height: 100, scale: 1, rotation: 0, update: jasmine.createSpy() };
pdfViewerSpy = jasmine.createSpy('PDFViewer').and.returnValue({
setDocument: jasmine.createSpy().and.returnValue({
loadingTask: () => ({
@@ -479,7 +481,7 @@ describe('Test PdfViewer - User interaction', () => {
update: jasmine.createSpy(),
currentScaleValue: 1,
_currentPageNumber: 1,
_pages: [{ width: 100, height: 100, scale: 1 }]
_pages: [pageViewMock]
});
TestBed.configureTestingModule({
@@ -617,6 +619,30 @@ describe('Test PdfViewer - User interaction', () => {
}), 300);
});
describe('Rotation', () => {
it('should rotate only the current page counter-clockwise by 90 degrees', () => {
testingUtils.clickByCSS('#viewer-rotate-page-button');
expect(pageViewMock.update).toHaveBeenCalledWith({ rotation: 270 });
});
it('should wrap rotation back to 0 after a full turn', () => {
pageViewMock.rotation = 90;
testingUtils.clickByCSS('#viewer-rotate-page-button');
expect(pageViewMock.update).toHaveBeenCalledWith({ rotation: 0 });
});
it('should recompute the document overflow after rotation', () => {
spyOn(component, 'setDocumentOverflow');
testingUtils.clickByCSS('#viewer-rotate-page-button');
expect(component.setDocumentOverflow).toHaveBeenCalled();
});
});
describe('Resize interaction', () => {
it('should resize event trigger setScaleUpdatePages', () => {
spyOn(component, 'onResize');
@@ -577,6 +577,24 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
this.setScaleUpdatePages(newScale);
}
/**
* Rotate the currently displayed page 90 degrees counter-clockwise.
*
* Only the current page is affected; every other page keeps its own rotation.
* The angle wraps around, so four consecutive calls return the page to its
* original orientation (0 → 270 → 180 → 90 → 0). The document overflow state
* is recomputed afterwards, since rotation swaps the page width and height.
*/
rotatePage() {
if (this.pdfViewer) {
const pageView = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1];
if (pageView) {
pageView.update({ rotation: (pageView.rotation + 270) % 360 });
this.setDocumentOverflow();
}
}
}
/**
* load the previous page
*/
+5 -5
View File
@@ -98,10 +98,10 @@
"@types/jasminewd2": "2.0.13",
"@types/node": "26.1.1",
"@types/sinon": "22.0.0",
"@typescript-eslint/eslint-plugin": "8.68.0",
"@typescript-eslint/parser": "8.68.0",
"@typescript-eslint/typescript-estree": "8.68.0",
"@typescript-eslint/utils": "8.68.0",
"@typescript-eslint/eslint-plugin": "8.70.0",
"@typescript-eslint/parser": "8.70.0",
"@typescript-eslint/typescript-estree": "8.70.0",
"@typescript-eslint/utils": "8.70.0",
"ajv": "8.20.0",
"dotenv": "16.4.7",
"eslint": "10.8.0",
@@ -131,7 +131,7 @@
"react-dom": "19.2.6",
"rimraf": "6.1.3",
"sass-loader": "16.0.8",
"semver": "7.6.3",
"semver": "7.8.5",
"sinon": "22.1.0",
"spdx-license-list": "6.11.0",
"storybook": "10.4.0",
+208 -119
View File
@@ -25,6 +25,7 @@ overrides:
webpack-dev-server: '>=6.0.0'
uuid: '>=11.1.1'
undici: 8.9.0
'@xmldom/xmldom@<0.8.15': 0.8.15
importers:
@@ -32,7 +33,7 @@ importers:
dependencies:
'@angular-eslint/utils':
specifier: 20.7.0
version: 20.7.0(@typescript-eslint/utils@8.68.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.70.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.27
version: 20.3.27(@angular/core@20.3.27(@angular/compiler@20.3.27)(rxjs@7.8.2)(zone.js@0.15.0))
@@ -147,10 +148,10 @@ 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.68.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.70.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.68.0)(@typescript-eslint/utils@8.68.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.70.0)(@typescript-eslint/utils@8.70.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)
@@ -171,7 +172,7 @@ importers:
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.68.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.70.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)
@@ -203,17 +204,17 @@ importers:
specifier: 22.0.0
version: 22.0.0
'@typescript-eslint/eslint-plugin':
specifier: 8.68.0
version: 8.68.0(@typescript-eslint/parser@8.68.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.70.0
version: 8.70.0(@typescript-eslint/parser@8.70.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.68.0
version: 8.68.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.70.0
version: 8.70.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.68.0
version: 8.68.0(supports-color@7.2.0)(typescript@5.9.3)
specifier: 8.70.0
version: 8.70.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/utils':
specifier: 8.68.0
version: 8.68.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.70.0
version: 8.70.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
@@ -302,8 +303,8 @@ importers:
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.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
specifier: 7.8.5
version: 7.8.5
sinon:
specifier: 22.1.0
version: 22.1.0
@@ -4337,11 +4338,11 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/eslint-plugin@8.68.0':
resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==}
'@typescript-eslint/eslint-plugin@8.70.0':
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.68.0
'@typescript-eslint/parser': ^8.70.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
@@ -4351,8 +4352,8 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
'@typescript-eslint/parser@8.68.0':
resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==}
'@typescript-eslint/parser@8.70.0':
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -4364,8 +4365,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.68.0':
resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==}
'@typescript-eslint/project-service@8.69.0':
resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.70.0':
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -4378,8 +4385,12 @@ packages:
resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.68.0':
resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==}
'@typescript-eslint/scope-manager@8.69.0':
resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.70.0':
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.65.0':
@@ -4388,8 +4399,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.68.0':
resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==}
'@typescript-eslint/tsconfig-utils@8.69.0':
resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/tsconfig-utils@8.70.0':
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -4401,8 +4418,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.68.0':
resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==}
'@typescript-eslint/type-utils@8.70.0':
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -4416,8 +4433,12 @@ packages:
resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.68.0':
resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==}
'@typescript-eslint/types@8.69.0':
resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.70.0':
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@5.62.0':
@@ -4435,8 +4456,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/typescript-estree@8.68.0':
resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==}
'@typescript-eslint/typescript-estree@8.69.0':
resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/typescript-estree@8.70.0':
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
@@ -4454,8 +4481,15 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.68.0':
resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==}
'@typescript-eslint/utils@8.69.0':
resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==}
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/utils@8.70.0':
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -4469,8 +4503,12 @@ packages:
resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.68.0':
resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==}
'@typescript-eslint/visitor-keys@8.69.0':
resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.70.0':
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.2':
@@ -4558,10 +4596,9 @@ packages:
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
engines: {node: '>=8'}
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
'@xmldom/xmldom@0.8.15':
resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==}
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==}
@@ -6382,8 +6419,8 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
ignore@7.0.6:
resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
ignore@7.0.9:
resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
engines: {node: '>= 4'}
image-size@0.5.5:
@@ -9493,23 +9530,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.68.0)(@typescript-eslint/utils@8.68.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.70.0)(@typescript-eslint/utils@8.70.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.68.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.68.0
'@typescript-eslint/utils': 8.68.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.70.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.70.0
'@typescript-eslint/utils': 8.70.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.68.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.70.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.68.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.68.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.70.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.70.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
@@ -9521,10 +9558,10 @@ snapshots:
eslint-scope: 9.1.2
typescript: 5.9.3
'@angular-eslint/utils@20.7.0(@typescript-eslint/utils@8.68.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.70.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.68.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.70.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
@@ -9661,7 +9698,7 @@ snapshots:
chokidar: 4.0.3
convert-source-map: 1.9.0
reflect-metadata: 0.2.2
semver: 7.6.3
semver: 7.8.5
tslib: 2.8.1
yargs: 18.0.0
optionalDependencies:
@@ -11420,7 +11457,7 @@ snapshots:
'@es-joy/jsdoccomment@0.90.0':
dependencies:
'@types/estree': 1.0.9
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/types': 8.70.0
comment-parser: 1.4.7
esquery: 1.7.0
jsdoc-type-pratt-parser: 7.3.0
@@ -12487,7 +12524,7 @@ snapshots:
picocolors: 1.1.1
picomatch: 4.0.5
rxjs: 7.8.2
semver: 7.6.3
semver: 7.8.5
tslib: 2.8.1
webpack-merge: 5.10.0
optionalDependencies:
@@ -12544,7 +12581,7 @@ snapshots:
'@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)
detect-port: 2.1.0
semver: 7.6.3
semver: 7.8.5
tree-kill: 1.2.2
tslib: 2.8.1
transitivePeerDependencies:
@@ -12566,25 +12603,25 @@ snapshots:
enquirer: 2.3.6
minimatch: 10.2.6
nx: 23.1.1
semver: 7.6.3
semver: 7.8.5
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.68.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.70.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.68.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.69.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
picocolors: 1.1.1
semver: 7.6.3
semver: 7.8.5
tslib: 2.8.1
optionalDependencies:
'@typescript-eslint/parser': 8.68.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.70.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'
@@ -12602,7 +12639,7 @@ snapshots:
'@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)
eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
semver: 7.6.3
semver: 7.8.5
tslib: 2.8.1
typescript: 6.0.3
optionalDependencies:
@@ -12643,7 +12680,7 @@ snapshots:
npm-run-path: 4.0.1
picocolors: 1.1.1
picomatch: 4.0.5
semver: 7.6.3
semver: 7.8.5
source-map-support: 0.5.19
tinyglobby: 0.2.17
tslib: 2.8.1
@@ -12752,7 +12789,7 @@ snapshots:
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.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))
semver: 7.6.3
semver: 7.8.5
source-map-loader: 5.0.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))
style-loader: 3.3.4(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))
ts-checker-rspack-plugin: 1.4.0(@rspack/core@1.6.8)(tslib@2.8.1)(typescript@5.9.3)
@@ -12805,7 +12842,7 @@ snapshots:
'@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)
'@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.3)
semver: 7.6.3
semver: 7.8.5
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:
@@ -12917,7 +12954,7 @@ snapshots:
enquirer: 2.3.6
nx: 23.1.1
picomatch: 4.0.5
semver: 7.6.3
semver: 7.8.5
tslib: 2.8.1
yargs-parser: 21.1.1
transitivePeerDependencies:
@@ -13956,16 +13993,16 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.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.70.0(@typescript-eslint/parser@8.70.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.68.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.68.0
'@typescript-eslint/type-utils': 8.68.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.68.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.68.0
'@typescript-eslint/parser': 8.70.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.70.0
'@typescript-eslint/type-utils': 8.70.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.70.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.70.0
eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
ignore: 7.0.6
ignore: 7.0.9
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
@@ -13980,12 +14017,12 @@ snapshots:
- supports-color
- typescript
'@typescript-eslint/parser@8.68.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.70.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.68.0
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.68.0
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.70.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
@@ -13994,17 +14031,26 @@ snapshots:
'@typescript-eslint/project-service@8.65.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3)
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3)
'@typescript-eslint/types': 8.70.0
debug: 4.4.3(supports-color@7.2.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@5.9.3)':
'@typescript-eslint/project-service@8.69.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3)
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3)
'@typescript-eslint/types': 8.70.0
debug: 4.4.3(supports-color@7.2.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3)
'@typescript-eslint/types': 8.70.0
debug: 4.4.3(supports-color@7.2.0)
typescript: 5.9.3
transitivePeerDependencies:
@@ -14020,16 +14066,25 @@ snapshots:
'@typescript-eslint/types': 8.65.0
'@typescript-eslint/visitor-keys': 8.65.0
'@typescript-eslint/scope-manager@8.68.0':
'@typescript-eslint/scope-manager@8.69.0':
dependencies:
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/visitor-keys': 8.68.0
'@typescript-eslint/types': 8.69.0
'@typescript-eslint/visitor-keys': 8.69.0
'@typescript-eslint/scope-manager@8.70.0':
dependencies:
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/visitor-keys': 8.70.0
'@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.68.0(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.69.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -14045,11 +14100,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/type-utils@8.68.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.70.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.68.0
'@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/utils': 8.68.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.70.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/utils': 8.70.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)
@@ -14061,7 +14116,9 @@ snapshots:
'@typescript-eslint/types@8.65.0': {}
'@typescript-eslint/types@8.68.0': {}
'@typescript-eslint/types@8.69.0': {}
'@typescript-eslint/types@8.70.0': {}
'@typescript-eslint/typescript-estree@5.62.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
@@ -14070,7 +14127,7 @@ snapshots:
debug: 4.4.3(supports-color@7.2.0)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.6.3
semver: 7.8.5
tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
@@ -14092,12 +14149,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.69.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.68.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3)
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/visitor-keys': 8.68.0
'@typescript-eslint/project-service': 8.69.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3)
'@typescript-eslint/types': 8.69.0
'@typescript-eslint/visitor-keys': 8.69.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/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3)
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6
semver: 7.8.5
@@ -14117,7 +14189,7 @@ snapshots:
'@typescript-eslint/typescript-estree': 5.62.0(supports-color@7.2.0)(typescript@5.9.3)
eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0)
eslint-scope: 5.1.1
semver: 7.6.3
semver: 7.8.5
transitivePeerDependencies:
- supports-color
- typescript
@@ -14133,12 +14205,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.68.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.69.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.68.0
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.69.0
'@typescript-eslint/types': 8.69.0
'@typescript-eslint/typescript-estree': 8.69.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/utils@8.70.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.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.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:
@@ -14154,9 +14237,14 @@ snapshots:
'@typescript-eslint/types': 8.65.0
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.68.0':
'@typescript-eslint/visitor-keys@8.69.0':
dependencies:
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/types': 8.69.0
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.70.0':
dependencies:
'@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.2': {}
@@ -14285,7 +14373,7 @@ snapshots:
dependencies:
tslib: 2.8.1
'@xmldom/xmldom@0.8.13': {}
'@xmldom/xmldom@0.8.15': {}
'@xtuc/ieee754@1.2.0': {}
@@ -15101,7 +15189,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.25)
postcss-modules-values: 4.0.0(postcss@8.5.25)
postcss-value-parser: 4.2.0
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -15115,7 +15203,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.25)
postcss-modules-values: 4.0.0(postcss@8.5.25)
postcss-value-parser: 4.2.0
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -15129,7 +15217,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.25)
postcss-modules-values: 4.0.0(postcss@8.5.25)
postcss-value-parser: 4.2.0
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -15143,7 +15231,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.25)
postcss-modules-values: 4.0.0(postcss@8.5.25)
postcss-value-parser: 4.2.0
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -15718,7 +15806,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.68.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.69.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:
@@ -16067,7 +16155,7 @@ snapshots:
minimatch: 3.1.5
node-abort-controller: 3.1.1
schema-utils: 3.3.0
semver: 7.6.3
semver: 7.8.5
tapable: 2.3.3
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)
@@ -16453,7 +16541,7 @@ snapshots:
ignore@7.0.5: {}
ignore@7.0.6: {}
ignore@7.0.9: {}
image-size@0.5.5:
optional: true
@@ -16615,7 +16703,7 @@ snapshots:
'@babel/parser': 7.29.7
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 7.6.3
semver: 7.8.5
transitivePeerDependencies:
- supports-color
@@ -16644,7 +16732,7 @@ snapshots:
jasmine-reporters@2.5.2:
dependencies:
'@xmldom/xmldom': 0.8.13
'@xmldom/xmldom': 0.8.15
mkdirp: 1.0.4
jest-regex-util@30.4.0: {}
@@ -16708,7 +16796,7 @@ snapshots:
acorn: 8.17.0
eslint-visitor-keys: 3.4.3
espree: 9.6.1
semver: 7.6.3
semver: 7.8.5
jsonc-parser@3.3.1: {}
@@ -17028,7 +17116,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
semver: 7.6.3
semver: 7.8.5
make-dir@5.1.0:
optional: true
@@ -17771,7 +17859,7 @@ snapshots:
cosmiconfig: 9.0.2(typescript@5.9.3)
jiti: 1.21.7
postcss: 8.5.25
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -17783,7 +17871,7 @@ snapshots:
cosmiconfig: 9.0.2(typescript@5.9.3)
jiti: 2.7.0
postcss: 8.5.25
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -17795,7 +17883,7 @@ snapshots:
cosmiconfig: 9.0.2(typescript@5.9.3)
jiti: 2.7.0
postcss: 8.5.25
semver: 7.6.3
semver: 7.8.5
optionalDependencies:
'@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)
@@ -18559,7 +18647,8 @@ snapshots:
semver@6.3.1: {}
semver@7.6.3: {}
semver@7.6.3:
optional: true
semver@7.7.2: {}
@@ -19194,7 +19283,7 @@ snapshots:
chalk: 4.1.2
enhanced-resolve: 5.24.5
micromatch: 4.0.8
semver: 7.6.3
semver: 7.8.5
source-map: 0.7.6
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)
+1
View File
@@ -27,6 +27,7 @@ overrides:
webpack-dev-server: '>=6.0.0'
uuid: '>=11.1.1'
undici: 8.9.0
'@xmldom/xmldom@<0.8.15': 0.8.15
allowBuilds:
'@parcel/watcher': true
+34 -8
View File
@@ -34,6 +34,26 @@ error_out() {
# bg: 40 black, 41 red, 44 blue, 45 purple
}
# Retry to absorb npm registry propagation delay right after publish.
NPM_RETRIES=24
NPM_RETRY_DELAY=20
npm_pack_version() {
local spec=$1
local attempt
for attempt in $(seq 1 "$NPM_RETRIES"); do
echo "📦 npm pack attempt $attempt/$NPM_RETRIES: $spec"
if npm pack "$spec"; then
return 0
fi
if [ "$attempt" -lt "$NPM_RETRIES" ]; then
echo "⏳ Attempt $attempt/$NPM_RETRIES failed, retrying in ${NPM_RETRY_DELAY}s..."
sleep "$NPM_RETRY_DELAY"
fi
done
return 1
}
rm -rf temp
mkdir temp
cd temp
@@ -42,12 +62,14 @@ for PACKAGE in ${projects[@]}
do
mkdir $PACKAGE
cd $PACKAGE
PKG_VERSION=""
# Handle js-api differently - increase major version by 1
if [ $PACKAGE == 'js-api' ]; then
if [ $VERSION == 'alpha' ] || [ $VERSION == 'beta' ] || [ $VERSION == 'latest' ]; then
# For tag versions, we need to get the current version and increment
CURRENT_VERSION=$(npm view @alfresco/$PACKAGE@$VERSION version)
FALLBACK_VERSION=$CURRENT_VERSION
MAJOR_VERSION=$(echo $CURRENT_VERSION | cut -d'.' -f1)
NEXT_MAJOR=$((MAJOR_VERSION + 1))
# Keep the rest of the version string
@@ -55,6 +77,7 @@ do
PACKAGE_VERSION="${NEXT_MAJOR}.${REST_VERSION}"
else
# For specific versions, just increment the major number
FALLBACK_VERSION=$VERSION
MAJOR_VERSION=$(echo $VERSION | cut -d'.' -f1)
NEXT_MAJOR=$((MAJOR_VERSION + 1))
REST_VERSION=$(echo $VERSION | cut -d'.' -f2-)
@@ -65,13 +88,17 @@ do
fi
# Try the calculated package version first
PKG_VERSION=$(npm view @alfresco/$PACKAGE@$PACKAGE_VERSION version 2>/dev/null)
# If that fails for js-api, try the original version
if [ -z "$PKG_VERSION" ] && [ $PACKAGE == 'js-api' ]; then
echo "Warning: js-api@$PACKAGE_VERSION not found, trying @$VERSION"
PACKAGE_VERSION=$VERSION
PKG_VERSION=$(npm view @alfresco/$PACKAGE@$PACKAGE_VERSION version 2>/dev/null)
if npm_pack_version '@alfresco/'$PACKAGE@$PACKAGE_VERSION; then
PKG_VERSION=$PACKAGE_VERSION
elif [ $PACKAGE == 'js-api' ]; then
# If that fails, try the original (already resolved) version.
# FALLBACK_VERSION, not $VERSION directly, since npm pack writes the
# tarball under the resolved version, not the tag it was requested with.
echo "Warning: js-api@$PACKAGE_VERSION not found, trying @$FALLBACK_VERSION"
PACKAGE_VERSION=$FALLBACK_VERSION
if [ -n "$PACKAGE_VERSION" ] && npm_pack_version '@alfresco/'$PACKAGE@$PACKAGE_VERSION; then
PKG_VERSION=$PACKAGE_VERSION
fi
fi
# If still no version found, exit with error
@@ -82,7 +109,6 @@ do
echo "Inspecting: $PACKAGE@$PKG_VERSION"
npm pack '@alfresco/'$PACKAGE@$PACKAGE_VERSION
tar zxf 'alfresco-'$PACKAGE-$PKG_VERSION.tgz
if [ $PACKAGE == 'js-api' ]; then