mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-16 18:13:06 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7a59cea7 | ||
|
|
762eba8490 | ||
|
|
62d7d9084d | ||
|
|
6f414ae98e | ||
|
|
c7c2ba292e | ||
|
|
84e710191d | ||
|
|
a776cd02c0 | ||
|
|
2c1405d053 | ||
|
|
0bc823beea | ||
|
|
89053a1b9d | ||
|
|
68d382a401 | ||
|
|
ddfabc0e44 | ||
|
|
f524b365f6 | ||
|
|
4c15ab2395 | ||
|
|
9480cc39bd | ||
|
|
e28267621a | ||
|
|
27c700165c | ||
|
|
2824543bf0 |
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
+122
-65
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+44
-28
@@ -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>
|
||||
|
||||
+25
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-4
@@ -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' }];
|
||||
|
||||
+17
-2
@@ -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;
|
||||
|
||||
|
||||
+12
-8
@@ -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>
|
||||
}
|
||||
|
||||
+178
-3
@@ -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);
|
||||
|
||||
+105
-42
@@ -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));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+16
@@ -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];
|
||||
|
||||
+15
-1
@@ -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
@@ -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",
|
||||
|
||||
Generated
+208
-119
@@ -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)
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user