Compare commits

..
Author SHA1 Message Date
Michal Kinas 3496363b8e Release 9.0.0 (#12075)
* Release 9.0.0

* Add missing entry
2026-07-20 13:54:51 +02:00
dependabot[bot]andDomenico Sibilio d25038dc59 build(deps): bump the github-actions group across 2 directories with 8 updates (#12072)
Bumps the github-actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.0` | `4.37.1` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.37.0` | `4.37.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.0` | `4.37.1` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `2.16.3` | `2.16.4` |
| [Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml](https://github.com/alfresco/alfresco-build-tools) | `18.18.0` | `18.19.0` |
| [Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment](https://github.com/alfresco/alfresco-build-tools) | `18.18.0` | `18.19.0` |
| [github/gh-aw-actions/setup](https://github.com/github/gh-aw-actions) | `0.82.7` | `0.82.12` |

Bumps the github-actions group with 1 update in the /.github/actions/setup directory: [Alfresco/alfresco-build-tools/.github/actions/git-latest-tag](https://github.com/alfresco/alfresco-build-tools).


Updates `github/codeql-action/init` from 4.37.0 to 4.37.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a)

Updates `github/codeql-action/autobuild` from 4.37.0 to 4.37.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a)

Updates `github/codeql-action/analyze` from 4.37.0 to 4.37.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a)

Updates `crowdin/github-action` from 2.16.3 to 2.16.4
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/52aa776766211d83d975df51f3b9c53c2f8ba35f...e0c8f73cdc0fafde9396e056c5038217000a32d1)

Updates `Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml` from 18.18.0 to 18.19.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/a8bc14631a9ce72275346e536607c4527591112a...1d671f8f10336861c89c67be57c6648d98876103)

Updates `Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment` from 18.18.0 to 18.19.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/a8bc14631a9ce72275346e536607c4527591112a...1d671f8f10336861c89c67be57c6648d98876103)

Updates `github/gh-aw-actions/setup` from 0.82.7 to 0.82.12
- [Release notes](https://github.com/github/gh-aw-actions/releases)
- [Changelog](https://github.com/github/gh-aw-actions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/gh-aw-actions/compare/bf7ba42ce6443bf79fa184c9c6a35de202690bfc...23fd0ef1f0a76a3707df928f9c733abd98e4ec75)

Updates `Alfresco/alfresco-build-tools/.github/actions/git-latest-tag` from 18.18.0 to 18.19.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/a8bc14631a9ce72275346e536607c4527591112a...1d671f8f10336861c89c67be57c6648d98876103)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crowdin/github-action
  dependency-version: 2.16.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml
  dependency-version: 18.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment
  dependency-version: 18.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/gh-aw-actions/setup
  dependency-version: 0.82.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag
  dependency-version: 18.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
2026-07-20 11:20:35 +00:00
dependabot[bot]andDomenico Sibilio c348ea32c0 build(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (#12073)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
2026-07-20 11:19:39 +00:00
dependabot[bot]andDomenico Sibilio 0505b1d5c3 build(deps): bump actions/setup-node from 6.4.0 to 7.0.0 in /.github/actions/setup (#12074)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
2026-07-20 11:19:01 +00:00
Michal KinasandClaude Opus 4.8 0a19be090e [ACS-12286] Pass includeFields through to getRecentFiles for -recent- (#12067)
* [ACS-12286] Pass includeFields through to getRecentFiles for -recent-

loadFolderByNodeId forwarded includeFields to every custom source except
-recent-, and getRecentFiles had no includeFields parameter, hardcoding the
search include set. Thread includeFields through to the recent-files search
request (de-duplicated, defaults preserved) so callers can request extra
fields such as isFavorite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [ACS-12286] Document includeFields parameter for getRecentFiles

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [ACS-12286] Use typed PersonEntry instead of any in getRecentFiles spec

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:03:05 +02:00
AleksanderSklorz b69c3ae1ae [MNT-25230] Displayed modified time for node version (#12066)
* [MNT-25230] Displayed modified time for node version

* [MNT-25230] Addressed copilot comment
2026-07-16 13:11:37 +02:00
ce907417b7 AAE-48166 Compensate for VM/Citrix clock drift in token expiry check (#12024)
* fix: compensate for VM/Citrix clock drift in token expiry check

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* feat(core/auth): add TimeSyncDateTimeProvider for angular-oauth2-oidc clock drift correction

* feat(auth): add periodic clock re-sync to protect against mid-session drift in Citrix/VM environments

- Add startPeriodicSync() and stopPeriodicSync() to TimeSyncService
- Re-sync clock offset every 5 minutes and on document visibility change
- Add security cap (maxAllowedOffsetMs) to reject unreasonably large offsets
- Preserve existing offset on sync failures instead of resetting to 0
- Wire periodic sync start in RedirectAuthService.configureAuth()
- Wire periodic sync stop in RedirectAuthService.logout()
- Add unit tests for periodic sync and offset cap behavior

* feat: replace dedicated time API with passive Date response header interception

- Add `updateClockOffsetFromDateHeader()` to `TimeSyncService` so the clock
  offset can be updated from any HTTP `Date` response header (RFC 7231)
- Make `serverTimeUrl` config optional: `syncClockOffset` is a safe no-op and
  `checkTimeSync` uses the stored `clockOffsetMs` when no URL is configured
- Add `DateHeaderTimeSyncInterceptor` that reads the `Date` header from every
  HTTP response and passively keeps `TimeSyncService.clockOffsetMs` current
- Register the new interceptor in `provideCoreAuth()` / `AuthModule`
- Export `DateHeaderTimeSyncInterceptor` from the public API
- Add unit tests for all new paths

* feat: retry token refresh before checking for clock out-of-sync

- tokenHasExpiredDueToClockOutOfSync$: skip the first "token expired" event
  so the library's automatic refresh has a chance to run before clock drift
  is diagnosed as the root cause
- oauthErrorEventOccurDueToClockOutOfSync$: use scan() to skip the first
  token_refresh_error (allowing one retry) and only check clock sync on the
  second occurrence; all other error types are still checked immediately
- Update test for token expiry: now requires two events before logout
- Replace single token_refresh_error clock-sync test with two tests:
  one verifying no logout on first error, one verifying logout on second

* fix(core): remove duplicate @angular/common/http import in date-header-time-sync interceptor spec

* fix(core): treat empty/whitespace serverTimeUrl as not configured in TimeSyncService

* fix(core): limit DateHeaderTimeSyncInterceptor to IAM API responses only

* refactor: remove tokenHasExpiredDueToClockOutOfSync$ and re-sync clock + refresh token on validation failure

Instead of having a separate observable that detects token expiration due to clock
drift and immediately logs the user out, the oauthErrorEventOccurDueToClockOutOfSync$
now re-syncs the clock offset and attempts a token refresh when clock drift is detected.
Only if the refresh fails after re-syncing does it propagate the error and trigger logout.

* fix(auth): check token validity after clock re-sync before attempting refresh

After re-syncing the clock offset, the code now checks whether the token
is actually still valid using the corrected time (via tokenHasExpired()).
If the token is valid, no refresh is needed — the token only appeared
expired due to clock drift. This avoids the impossible situation of
retrying a token refresh when the triggering error was itself a refresh
error.

* refactor(auth): replace IAM header interceptor with HEAD request to app root for time sync

Instead of passively reading the Date header from IAM responses (which
requires IAM to be reachable and has CORS constraints), the TimeSyncService
now makes a lightweight HEAD request to the application's own root URL
(served by nginx). This is simpler and more reliable because:

- No CORS issues (same origin)
- Nginx always includes a Date header
- HEAD request has minimal payload (no body)
- Works before authentication is established
- No dedicated serverTimeUrl configuration needed

The DateHeaderTimeSyncInterceptor has been removed as it is no longer needed.

* fix(auth): reset tokenRefreshErrorCount on successful token event

The tokenRefreshErrorCount in the oauthErrorEventOccurDueToClockOutOfSync$
stream now resets to 0 when a 'token_received' or 'token_refreshed' event
occurs. This prevents intermittent errors that self-correct from
accumulating toward the clock-drift detection threshold.

* fix(core): provide meaningful error message when API response body is null

When the server returns no response body (e.g., during token expiration,
network interruption, or CORS failures), err.error is null causing
JSON.stringify(null) to produce the unhelpful message "null".

Now we explicitly handle the null case and produce a message like
"401 Unauthorized" instead of "null".

* fix: resync clock before invalidating tokens at startup

At startup, when the first OAuth event fires, the service checks if the
access token is valid. Previously it would immediately clear storage if
the token appeared invalid. This could cause false negatives when the
local clock was out of sync (e.g. VM/Citrix environments).

Now the service first re-syncs the clock offset via TimeSyncService, then
re-evaluates the token validity. Storage is only cleared if the token is
still invalid after the corrected time check.

* Remove maxAllowedOffsetMs from TimeSyncService

* Fix startPeriodicSync call to match updated signature

* WIP

* Add Flag

* AAE-48166 Sync time using server time

* AAE-48166 Fix unit test

* AAE-48166 Fix service

* AAE-48166 Fix service

* AAE-48166 Fix service and add tests

* AAE-48166 Use app root instead of server time url

* AAE-48166 Use oauth2.timeSync config key for time sync gate

* AAE-48166 Keep redirect auth flow unchanged when time sync is disable

* Add oauth2.showDebugInformation and related debug logging for time sync

* AAE-48166 Fix multi-tab OIDC refresh failures by resyncing server time when timeSync enabled

* AAE-48166 Avoid repeated server time calls by caching the result for 2 seconds

* AAE-48166 test(auth): cover timeSync clock sync for implicit flow silent refresh

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Eugenio Romano <eromano@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Diogo Bastos <diogo.bastos@hyland.com>
Co-authored-by: alep85 <amedeo.lepore@hyland.com>
2026-07-16 11:31:15 +02:00
Darren Thornton 4c7765e3fa AAE-46517 Add onRowCountChange form event for Repeatable Sections (#12062)
* AAE-46517 Add onRowCountChange event for Repeatable Sections

* code review fixes for unit test
2026-07-14 18:40:41 -05:00
Alex Molodyh 407a64996e AAE-46248 Format dropdown/radio option labels in display-text expressions (#12045) 2026-07-14 14:54:13 -07:00
dependabot[bot] c0a102f87f build(deps): bump undici from 7.24.7 to 8.7.0 (#12049)
Bumps [undici](https://github.com/nodejs/undici) from 7.24.7 to 8.7.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.24.7...v8.7.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 12:21:55 +01:00
066ef30516 GH auto: Automated Update of Translations from Crowdin (#12044)
* New Crowdin translations by GitHub Action

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: hxpstudiocicduser <hxpstudiocicduser@hyland.com>
Co-authored-by: Denys Vuika <denys.vuika@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-14 12:08:47 +01:00
AleksanderSklorz 01cea60490 [MNT-25230] adw version history does not provide user information (#12050)
* [MNT-25230] Displayed user displayName in version history for specific version

* [MNT-25230] Unit tests

* [MNT-25230] Addressed copilot comment

* [MNT-25230] Addressed copilot comment

* [MNT-25230] Addressed copilot comment

* [MNT-25230] Addressed copilot comment

* [MNT-25230] Addressed copilot comment
2026-07-13 10:38:13 +02:00
dependabot[bot] f438c6ce69 build(deps): bump the github-actions group across 2 directories with 7 updates (#12059)
Bumps the github-actions group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.3` | `4.37.0` |
| [Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml](https://github.com/alfresco/alfresco-build-tools) | `18.15.1` | `18.18.0` |
| [Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment](https://github.com/alfresco/alfresco-build-tools) | `18.15.1` | `18.18.0` |
| [github/gh-aw-actions/setup](https://github.com/github/gh-aw-actions) | `0.82.2` | `0.82.7` |

Bumps the github-actions group with 1 update in the /.github/actions/setup directory: [Alfresco/alfresco-build-tools/.github/actions/git-latest-tag](https://github.com/alfresco/alfresco-build-tools).


Updates `github/codeql-action/init` from 4.36.3 to 4.37.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

Updates `github/codeql-action/autobuild` from 4.36.3 to 4.37.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

Updates `github/codeql-action/analyze` from 4.36.3 to 4.37.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

Updates `Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml` from 18.15.1 to 18.18.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/499eb5d58f3f82a4efa4eaedab26dbecff18328c...a8bc14631a9ce72275346e536607c4527591112a)

Updates `Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment` from 18.15.1 to 18.18.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/499eb5d58f3f82a4efa4eaedab26dbecff18328c...a8bc14631a9ce72275346e536607c4527591112a)

Updates `github/gh-aw-actions/setup` from 0.82.2 to 0.82.7
- [Release notes](https://github.com/github/gh-aw-actions/releases)
- [Changelog](https://github.com/github/gh-aw-actions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/gh-aw-actions/compare/3fac1cfa7a5a375a6a5eb9839178f6dad7adb60a...bf7ba42ce6443bf79fa184c9c6a35de202690bfc)

Updates `Alfresco/alfresco-build-tools/.github/actions/git-latest-tag` from 18.15.1 to 18.18.0
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/499eb5d58f3f82a4efa4eaedab26dbecff18328c...a8bc14631a9ce72275346e536607c4527591112a)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml
  dependency-version: 18.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment
  dependency-version: 18.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/gh-aw-actions/setup
  dependency-version: 0.82.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag
  dependency-version: 18.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 09:07:38 +01:00
Denys Vuika 09ef05038c chore: remove deprecated MaterialModule and its references (#12051)
* chore: remove deprecated MaterialModule and its references

* chore: remove MaterialModule exports from public API
2026-07-13 09:07:00 +01:00
Anamika Dey c7bd56f244 AAE-43974 Fix for alfresco-content-app e2e failure (#12055)
* AAE-43974 Fix for alfresco-content-app e2e failure

* AAE-43974 Refactored based on review comments

* AAE-43974 Removing unit test cases written for isError as it is being tested by passwordErrorStateMatcher

* [ci:force] re-trigger CI
2026-07-10 18:53:53 +00:00
Grzegorz Jaśkowski b86e41515e [ACS-12037] Remove Knowledge Discovery from ADF (#12042)
* [ACS-12037] Remove Knowledge Discovery from ADF

* [ACS-12037] Resolve Copilot CR comments

* [ACS-12037] Complete cleanup, revert i18n changes

* [ACS-12037] Cleanup features and APIs related to hxiconnector

* [ACS-12037] Update docs
2026-07-10 15:37:24 +02:00
David Olson 0b35118dee AAE-47880 UI breaks when pasting very large text into text field (#12043) 2026-07-09 10:11:28 -05:00
Michal Kinas fe2b3d85dd [ACS-12108] Introduce signed commits into ADF workflows (#12040) 2026-07-09 15:05:58 +02:00
Anamika DeyandEhsan Rezaei b0dc85422c Improvement/AAE-43974 Refactoring form fields (#11945)
* AAE-43974 Refactoring form fields with mat-error

* AAE-43974 Removed adf-error redundant class from all available places

* AAE-43974 Followed code flow syntax

* AAE-43974 Refactored based on review comments

* AAE-43974 Added dummy test lable

* AAE-43974 Added dummy error message for amount field

* AAE-43974 Refactored multiline widget based on reviewer's comment

* AAE-43974 Error icon style added by mixins

* AAE-43974 Missing Conditions added

* AAE-43974 Added Missing conditions

* AAE-43974 Consistent conditions added and dummy error message added

* AAE-43974 Added subscriptsizing dynamic for testing

* AAE-43974 Reverted Subscriptsizing

* AAE-43974 Added spacing between form fields in mobile view

* AAE-43974 Dummy error message changed

* AAE-43974 Changed error icon allignment for testing

* AAE-43974 Added dummy error message

* AAE-43974 Added form field gap in mobile view and dummy erro message

* AAE-43974 Refactored based on review coemments and changed the error icon allignment

* AAE-43974 Fixed the mixin path

* AAE-43974 Removed dummy error message for amount widget

* AAE-43974 Added adf-error-text class to mat-error for e2e fix

* AAE-43974 adf-error-text class added inside mat-error

* AAE-43974 Whitespace issue resolved

* AAE-43974 Whiespace issue fix

* AAE-43974 Added context pushing logic in text widget

* AAE-43974 Added margin-bottom for regression fix

* AAE-43974 Trying with different margin-bottom

* AAE-43974 Testing regression issue

* AAE-43974 Testing for regression failure

* AAE-43974 Testing Regression failures

* AAE-43974 Test Regression failure

* AAE-43974 Testing multiline Regression failure

* AAE-43974 Testing fix regarding regression failure related to multiline-text widget

* AAE-43974 Test Regression failure for multiline-text widget

* AAE-43974 Testing regression failure for multiline-text

* AAE-43974 Testing regression failure for multi-line

* AAE-43974 Testing regression failure

* AAE-43974 Testing Regression failure

* AAE-43974 Testing regression failure

* AAE-43974 Removed the commented code

* [ci:force] re-trigger CI

---------

Co-authored-by: Ehsan Rezaei <ehsan.rezaei@hyland.com>
2026-07-08 18:32:46 +00:00
23f0392c12 AAE-47634 Refactor: switch form service getTask to use runtime bundle API (#12020)
* refactor: switch form service getTask to use runtime bundle API instead of query service

* Update form-cloud.service.ts

* Add TaskDetailsCloudModelRuntimeBundle interface

* Update getTaskById to return TaskDetailsCloudModelRuntimeBundle

* Modify return type of getTaskById method

Updated return type of getTaskById to include TaskDetailsCloudModel.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor TaskDetailsCloudModel by removing fields

Removed unused properties from TaskDetailsCloudModel.

* Update task-cloud.service.ts

* Update task-cloud.service.ts

* fix: add 404 fallback to query service in FormCloudService.getTask for completed tasks

* AAE-47634 getTaskById: call Runtime Bundle first, fall back to Query Service on 404

* AAE-47634 Gate Runtime Bundle task fallback behind ADF_TASK_RUNTIME_BUNDLE_FALLBACK_ENABLED token and drop no longer used taskDetailsSource input

* refactor: update task fetching tests to use async/await and firstValueFrom

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Eugenio Romano <eromano@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: alep85 <amedeo.lepore@hyland.com>
2026-07-08 13:48:09 +00:00
Mykyta Maliarchuk 997303e6a7 [ACS-11325] remove chevron button from tab order in adf-tree (#12041)
* [ACS-11325] remove chevron button from tab order in adf-tree

* [ACS-11325] add unit tests
2026-07-08 10:13:39 +02:00
Adam Świderski 45d1818f04 [ACS-12098] firefox headless race condition viewer fix (#12023)
* [ACS-12098] firefox headless race condition viewer fix

* [ACS-12098] review fixes 1

* [ACS-12098] review fixes 2

* [ACS-12098] review fixes 3

* last firefox headless fixes
2026-07-07 13:21:14 +02:00
dependabot[bot]andDomenico Sibilio 2f50af623d build(deps): bump js-yaml from 3.14.2 to 3.15.0 (#12018)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.2 to 3.15.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
2026-07-06 13:59:13 +00:00
dependabot[bot] 9c56712328 build(deps): bump the github-actions group across 2 directories with 7 updates (#12037)
Bumps the github-actions group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` |
| [Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml](https://github.com/alfresco/alfresco-build-tools) | `18.13.0` | `18.15.1` |
| [Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment](https://github.com/alfresco/alfresco-build-tools) | `18.13.0` | `18.15.1` |
| [github/gh-aw-actions/setup](https://github.com/github/gh-aw-actions) | `0.81.6` | `0.82.2` |

Bumps the github-actions group with 1 update in the /.github/actions/setup directory: [Alfresco/alfresco-build-tools/.github/actions/git-latest-tag](https://github.com/alfresco/alfresco-build-tools).


Updates `github/codeql-action/init` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

Updates `github/codeql-action/autobuild` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

Updates `github/codeql-action/analyze` from 4.36.2 to 4.36.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

Updates `Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml` from 18.13.0 to 18.15.1
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/10cea6c0b390c4f8af21d9dc1670d581bd10319f...499eb5d58f3f82a4efa4eaedab26dbecff18328c)

Updates `Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment` from 18.13.0 to 18.15.1
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/10cea6c0b390c4f8af21d9dc1670d581bd10319f...499eb5d58f3f82a4efa4eaedab26dbecff18328c)

Updates `github/gh-aw-actions/setup` from 0.81.6 to 0.82.2
- [Release notes](https://github.com/github/gh-aw-actions/releases)
- [Changelog](https://github.com/github/gh-aw-actions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/gh-aw-actions/compare/ba6380cc6e5be5d21677bebe04d52fb48e3abec7...3fac1cfa7a5a375a6a5eb9839178f6dad7adb60a)

Updates `Alfresco/alfresco-build-tools/.github/actions/git-latest-tag` from 18.13.0 to 18.15.1
- [Release notes](https://github.com/alfresco/alfresco-build-tools/releases)
- [Commits](https://github.com/alfresco/alfresco-build-tools/compare/10cea6c0b390c4f8af21d9dc1670d581bd10319f...499eb5d58f3f82a4efa4eaedab26dbecff18328c)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml
  dependency-version: 18.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment
  dependency-version: 18.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/gh-aw-actions/setup
  dependency-version: 0.82.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag
  dependency-version: 18.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 15:34:53 +02:00
Michal Kinas 448e667c52 [MNT-25681] Content node selector panel filtering fixed (#12032)
* [MNT-25681] Content node selector panel filtering fixed

* [MNT-25681] Small cleanup

* [MNT-25681] CR fix
2026-07-03 16:41:39 +02:00
AleksanderSklorz 18e602aa85 [PRODSEC-13253] Increased peerDependencies versions to allow to increase angular version in other projects (#12031)
* [PRODSEC-13253] Increased peerDependencies versions to allow to increase angular version in other projects

* [PRODSEC-13253] Addressec copilot comments
2026-07-03 14:05:13 +02:00
Alfresco Buildandhxpstudiocicduser 53ef0cfc00 New Crowdin translations by GitHub Action (#12021)
Co-authored-by: hxpstudiocicduser <hxpstudiocicduser@hyland.com>
2026-07-03 10:07:27 +01:00
Michal Kinas 4cd8648a05 Adjust release triggers to use cache-writable token (#12030) 2026-07-03 07:56:50 +02:00
Joshua Cain 7f57b3936b AAE-39528 Session timeout features (#12008)
* feat: session timeout features
2026-07-02 12:52:43 -04:00
Michal Kinas 5a30baad99 Release cache read/write fix (#12029) 2026-07-02 18:30:38 +02:00
Michal Kinas 5050c3d974 [ACS-12041] Add debounceTime in PeopleWidgetComponent to limit API calls on every keystroke (#12027) 2026-07-02 17:46:42 +02:00
Michal Kinas d8b36606e2 [MNT-25681] Search refactoring and unification (#12019)
* [MNT-25681] Search refactoring and unification

* [MNT-25681] CR fixes
2026-07-02 17:46:30 +02:00
Vito Albano 37f8fe47ac AAE-46695 Upgrade to pnpm 11.9 (#12028)
* [AAE-46695] Upgrade to pnpm 11.9

* [ci:force] - Checking

* [ci:force] - Changes to package.json should trigger --all
2026-07-02 16:26:55 +01:00
Domenico Sibilio af0d1e4ead AAE-47093 Update Supply Chain Review GH AW to latest (#12025)
[skip ci]
2026-07-02 11:59:47 +02:00
309 changed files with 7460 additions and 4711 deletions
+7 -2
View File
@@ -12,9 +12,14 @@ runs:
- name: base vars
shell: bash
run: |
if [ -n "${GITHUB_BASE_REF:-}" ]; then
BASE_HASH=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD 2>/dev/null || git rev-parse HEAD)
else
BASE_HASH=$(git rev-parse HEAD)
fi
{
echo "GIT_HASH=$(git rev-parse HEAD)";
echo "BASE_HASH=$(git merge-base origin/${GITHUB_BASE_REF} HEAD)";
echo "BASE_HASH=${BASE_HASH}";
echo "HEAD_HASH=HEAD";
echo "HEAD_COMMIT_HASH=${GH_COMMIT}";
echo "NX_CALCULATION_FLAGS=--all";
@@ -55,7 +60,7 @@ runs:
} >> $GITHUB_ENV
- name: RELEASE on master/develop patch branch
if: ${{ env.BREAK_ACTION == false && github.event.pull_request.merged }}
if: ${{ env.BREAK_ACTION == false && (github.event.pull_request.merged || github.event_name == 'push') }}
shell: bash
env:
REF_NAME: ${{ github.ref_name }}
+2 -2
View File
@@ -23,8 +23,8 @@ runs:
env:
DRY_RUN_FLAG: ${{ inputs.dry-run-flag }}
run: |
if [[ '$DRY_RUN_FLAG' == 'true' ]]; then
echo "dryrun=--dryrun" >> $GITHUB_OUTPUT;
if [[ "$DRY_RUN_FLAG" == 'true' ]]; then
echo "dryrun=--dry-run" >> $GITHUB_OUTPUT;
echo "enabling dryrun"
else
echo "dryrun=" >> $GITHUB_OUTPUT;
+2 -2
View File
@@ -28,14 +28,14 @@ runs:
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: get latest tag sha
if: ${{ inputs.full-setup == 'true' }}
id: tag-sha
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@10cea6c0b390c4f8af21d9dc1670d581bd10319f # v18.13.0
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@1d671f8f10336861c89c67be57c6648d98876103 # v18.19.0
- name: load "NPM TAG"
if: ${{ inputs.full-setup == 'true' }}
id: set-npm-tag
-31
View File
@@ -1,31 +0,0 @@
{
"entries": {
},
"containers": {
"ghcr.io/github/gh-aw-firewall/agent:0.25.58": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.25.58",
"digest": "sha256:a316a2c021accba8a9ea194c75466b0c4a166be6ac783bf8c2d0afd73373dec2",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.58@sha256:a316a2c021accba8a9ea194c75466b0c4a166be6ac783bf8c2d0afd73373dec2"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58",
"digest": "sha256:43a5cdbe4e1156920dcdaab26d6c6761777d5c6bdc572d7d07b11739fb34c749",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58@sha256:43a5cdbe4e1156920dcdaab26d6c6761777d5c6bdc572d7d07b11739fb34c749"
},
"ghcr.io/github/gh-aw-firewall/squid:0.25.58": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.25.58",
"digest": "sha256:558682b7b6313a5443cbb3d702899823bd732f991c8b52db6b5b8066abefe7a1",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.58@sha256:558682b7b6313a5443cbb3d702899823bd732f991c8b52db6b5b8066abefe7a1"
},
"ghcr.io/github/gh-aw-mcpg:v0.3.22": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.3.22",
"digest": "sha256:ce5c6f5461b077af0d8e8eb1763436e85153f8e9531117d58a7bdb23de71f00a",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.3.22@sha256:ce5c6f5461b077af0d8e8eb1763436e85153f8e9531117d58a7bdb23de71f00a"
},
"ghcr.io/github/github-mcp-server:v1.1.0": {
"image": "ghcr.io/github/github-mcp-server:v1.1.0",
"digest": "sha256:71b07d9abecb83b4a2595bcd8ccb35f9a0166361a12335f9e16da1ef07172029",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.1.0@sha256:71b07d9abecb83b4a2595bcd8ccb35f9a0166361a12335f9e16da1ef07172029"
}
}
}
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3.29.5
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # 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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3.29.5
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # 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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3.29.5
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v3.29.5
+10 -1
View File
@@ -8,13 +8,22 @@ jobs:
runs-on: ubuntu-latest
if: github.event.registry_package.package_type == 'npm' && github.event.registry_package.name == 'adf-core'
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_APP_ENGINEERING_CONTRIB_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_ENGINEERING_CONTRIB_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: alfresco-apps
permission-contents: write
- name: Dispatch event to monorepo
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PACKAGE_NAME: ${{ github.event.registry_package.name }}
PACKAGE_VERSION: ${{ github.event.registry_package.package_version.name }}
with:
github-token: ${{ secrets.PAT_WRITE_PKG }}
github-token: ${{ steps.app-token.outputs.token }}
retries: 3
script: |
const payload = {
+12 -4
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
workflow_call:
secrets:
BOT_GITHUB_TOKEN:
GH_APP_ENGINEERING_CONTRIB_PRIVATE_KEY:
required: true
CROWDIN_TRANSLATIONS_TOKEN:
required: true
@@ -15,13 +15,21 @@ jobs:
pull-from-crowdin:
runs-on: ubuntu-latest
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_APP_ENGINEERING_CONTRIB_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_ENGINEERING_CONTRIB_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: develop
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
- name: Pull translations from Crowdin
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
uses: crowdin/github-action@e0c8f73cdc0fafde9396e056c5038217000a32d1 # v2.16.4
with:
skip_ref_checkout: true
upload_sources: false
@@ -34,5 +42,5 @@ jobs:
github_user_email: ${{ vars.HXPS_GIT_EMAIL }}
gpg_private_key: ${{ secrets.HXPS_GIT_COMMIT_SIGNING_PRIVATE_KEY }}
env:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_TOKEN: ${{ secrets.CROWDIN_TRANSLATIONS_TOKEN }}
+14 -26
View File
@@ -1,13 +1,6 @@
name: "release"
on:
workflow_call:
inputs:
dry-run-flag:
description: 'enable dry-run on artifact push'
required: false
type: boolean
default: true
workflow_dispatch:
inputs:
dry-run-flag:
@@ -15,13 +8,10 @@ on:
required: false
type: boolean
default: true
pull_request:
types: [closed]
push:
branches:
- develop
- develop-patch*
push:
branches:
- master
- master-patch-*
@@ -34,8 +24,6 @@ concurrency:
cancel-in-progress: false
env:
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
GH_COMMIT: ${{ github.sha }}
GH_BUILD_NUMBER: ${{ github.run_id }}
LOG_LEVEL: "ERROR"
@@ -44,17 +32,18 @@ env:
jobs:
setup:
timeout-minutes: 20
if: github.event.pull_request.merged == true || github.ref_name == 'master' || github.ref_name == 'master-patch-*' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
name: "Setup"
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- uses: ./.github/actions/setup
with:
enable-node-modules-cache: false
- name: install
run: |
pnpm install --frozen-lockfile
@@ -67,12 +56,13 @@ jobs:
outputs:
release_version: ${{ steps.set-version.outputs.release_version }}
timeout-minutes: 30
if: github.event.pull_request.merged == true || github.ref_name == 'master' || github.ref_name == 'master-patch-*' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
packages: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -80,8 +70,6 @@ jobs:
fetch-depth: 0
- id: setup
uses: ./.github/actions/setup
with:
enable-node-modules-cache: false
- id: set-dryrun
uses: ./.github/actions/enable-dryrun
with:
@@ -102,22 +90,22 @@ jobs:
run: |
pnpm build:libs
pnpm build:schematics
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
name: release libraries GH registry
with:
node-version-file: '.nvmrc'
registry-url: 'https://npm.pkg.github.com'
scope: '@alfresco'
- run: pnpm run publish --tag ${{ steps.setup.outputs.npm-tag }} --provenance=false
- run: pnpm run publish --tag ${{ steps.setup.outputs.npm-tag }} --provenance=false ${{ steps.set-dryrun.outputs.dryrun }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
name: release libraries Npm registry
with:
node-version-file: '.nvmrc'
registry-url: 'https://${{ vars.NPM_REGISTRY_ADDRESS }}'
scope: '@alfresco'
- run: pnpm run publish --tag ${{ steps.setup.outputs.npm-tag }}
- run: pnpm run publish --tag ${{ steps.setup.outputs.npm-tag }} ${{ steps.set-dryrun.outputs.dryrun }}
create-git-tag:
runs-on: ubuntu-latest
@@ -138,7 +126,7 @@ jobs:
npm-check-bundle:
needs: [release-npm]
timeout-minutes: 15
if: github.event.pull_request.merged == true || github.ref_name == 'master' || github.ref_name == 'master-patch-*' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -159,7 +147,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Push Source Files to Crowdin
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
uses: crowdin/github-action@e0c8f73cdc0fafde9396e056c5038217000a32d1 # v2.16.4
with:
upload_sources: true
upload_sources_args: --delete-obsolete
@@ -171,7 +159,7 @@ jobs:
name: Run Crowdin pull pipeline for up-to-date sync
uses: ./.github/workflows/pull-from-crowdin.yml
secrets:
BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
GH_APP_ENGINEERING_CONTRIB_PRIVATE_KEY: ${{ secrets.GH_APP_ENGINEERING_CONTRIB_PRIVATE_KEY }}
CROWDIN_TRANSLATIONS_TOKEN: ${{ secrets.CROWDIN_TRANSLATIONS_TOKEN }}
HXPS_GIT_COMMIT_SIGNING_PRIVATE_KEY: ${{ secrets.HXPS_GIT_COMMIT_SIGNING_PRIVATE_KEY }}
+1 -1
View File
@@ -11,4 +11,4 @@ permissions:
jobs:
stale-pr-cleanup:
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@10cea6c0b390c4f8af21d9dc1670d581bd10319f # v18.13.0
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@1d671f8f10336861c89c67be57c6648d98876103 # v18.19.0
@@ -16,7 +16,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@10cea6c0b390c4f8af21d9dc1670d581bd10319f # v18.13.0
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@1d671f8f10336861c89c67be57c6648d98876103 # v18.19.0
with:
comment-identifier: supply-chain-review-instructions
comment-body: |
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -10,7 +10,7 @@ permissions:
engine:
id: copilot
model: gpt-5.4-nano
model: gpt-5-mini
tools:
github:
@@ -30,9 +30,11 @@ safe-outputs:
hide-older-comments: true
add-labels:
allowed: [security:low, security:medium, security:high]
remove-labels:
allowed: [security:low, security:medium, security:high]
submit-pull-request-review:
source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@52467f0241079de71fe14591f97bdec7555ab545
source: Alfresco/alfresco-build-tools/.github/workflows/supply-chain-review.md@7479d99410acb8b63f78a5b17abb3b8ebec3a66d
---
# Supply Chain Review
@@ -336,7 +338,8 @@ No suspicious patterns detected. Routine upgrade.
## Step 6 — Apply Label and Review Status
- Apply a label to the PR based on the highest risk level found:
- 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
+9 -2
View File
@@ -351,7 +351,6 @@ for more information about installing and using the source code.
| Name | Description | Source link |
| ---- | ----------- | ----------- |
| [Agent Service](content-services/services/agent.service.md) | Manages agents in Content Services. | [Source](../lib/content-services/src/lib/agent/services/agent.service.ts) |
| [Audit Service](content-services/services/audit.service.md) | Manages Audit apps and entries. | [Source](../lib/content-services/src/lib/audit/audit.service.ts) |
| [Card View Content Update Service](content-services/services/card-view-content-update.service.md) | Manages Card View properties in the content services environment. Implements BaseCardViewContentUpdate. | [Source](../lib/content-services/src/lib/common/services/card-view-content-update.service.ts) | |
| [Category tree datasource service](content-services/services/category-tree-datasource.service.md) | Datasource service for category tree. | [Source](../lib/content-services/src/lib/category/services/category-tree-datasource.service.ts) |
@@ -367,7 +366,6 @@ for more information about installing and using the source code.
| [Node Comments Service](content-services/services/node-comments.service.md) | Adds and retrieves comments for nodes in Content Services. | [Source](../lib/content-services/src/lib/node-comments/services/node-comments.service.ts) |
| [Node permission dialog service](content-services/services/node-permission-dialog.service.md) | Displays dialogs to let the user set node permissions. | [Source](../lib/content-services/src/lib/permission-manager/services/node-permission-dialog.service.ts) |
| [Node Permission service](content-services/services/node-permission.service.md) | Manages role permissions for content nodes. | [Source](../lib/content-services/src/lib/permission-manager/services/node-permission.service.ts) |
| [Search Ai Service](content-services/services/search-ai.service.md) | Manages search AI in Content Services. | [Source](../lib/content-services/src/lib/search-ai/services/search-ai.service.ts) |
| [Search filter service](content-services/services/search-filter.service.md) | Registers widgets for use with the Search Filter component. | [Source](../lib/content-services/src/lib/search/services/search-filter.service.ts) |
| [Search Query Builder service](content-services/services/search-query-builder.service.md) | Stores information from all the custom search and faceted search widgets, compiles and runs the final search query. | [Source](../lib/content-services/src/lib/search/services/search-query-builder.service.ts) |
| [Security Controls service](content-services/services/security-controls.service.md) | Manages security groups & marks in Content Services. | [Source](../lib/content-services/src/lib/security/services/security-controls-groups-marks-security.service.ts) |
@@ -567,3 +565,12 @@ Contains all custom rules used by ESLint.
<!--eslint-angular end-->
[(Back to Contents)](#contents)
## Notes
### Knowledge Discovery removal (v9.0.0)
The Knowledge Discovery components, services, and APIs have been removed as of version 9.0.0.
The integrated Knowledge Discovery features have been replaced with a section in the side navigation panel containing a link to the standalone Knowledge Discovery UI application in higher level apps such as ACA and ADW.
If you were using those features, please consider cleaning Local Storage in your browser as *aiReferences* entry may remain.
@@ -46,9 +46,9 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
| field | string | Field to apply the query fragment to. Required value |
| pattern | string | Regular expression pattern to restrict the format of the input text |
| placeholder | string | Text displayed in the widget when the input string is empty |
| searchSuffix | string | Text to append always in the search of a string |
| searchPrefix | string | Text to prepend always in the search of a string |
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true. |
| searchSuffix | string | Text to append in the search of a string. Only applied when wildcard matching is enabled (the `search-wildcards-enabled` app config flag, default `true`). |
| searchPrefix | string | Text to prepend in the search of a string. Only applied when wildcard matching is enabled (the `search-wildcards-enabled` app config flag, default `true`). |
| allowUpdateOnChange | `boolean` | Enable/Disable firing the search update when the text changes. Defaults to `false`; when disabled the search runs only when the user submits the value. |
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false. |
## Details
@@ -115,8 +115,8 @@ that will be used when performing the actual query.
Every query fragment is stored and retrieved using its widget `id`.
It is your responsibility to format the query correctly.
Once your change to the query is finished, update the context and call the `update` method
to inform other components about the change:
Once your change to the query is finished, update the context and call the `execute` method
to rebuild and run the query so the results reflect your change:
```ts
@Component({...})
@@ -126,12 +126,15 @@ export class MyComponent implements SearchWidget, OnInit {
onUIChanged() {
this.context.queryFragments[this.id] = `some query`;
this.context.update();
void this.context.execute();
}
}
```
> **Note:** Earlier versions called `this.context.update()` here. The `update()` method and the
> `updated` stream have been removed; call `this.context.execute()` directly instead.
When executed, your fragment will be injected into the resulting query based on the category order in the application configuration file.
```text
@@ -1,27 +0,0 @@
---
Title: Agent service
Added: v7.0.0-alpha.3
Status: Active
Last reviewed: 2024-07-12
---
# [Agent service](../../../lib/content-services/src/lib/agent/services/agent.service.ts "Defined in agent.service.ts")
Manages agents in Content Services.<br/>
<b>In order to use this service, you need to have the HX Insights Connector (additional ACS module) installed.</b>
## Class members
### Methods
- **getAgents**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AgentPaging`](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md#agentpaging)`>`<br/>
Gets all agents.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AgentPaging`](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md#agentpaging)`>` - AgentPaging object containing the agents.
## Details
See the
[Agents API](../../../lib/js-api/src/api/content-rest-api/docs/AgentsApi.md) for more information about the types returned by [Agent
service](agent.service.md) methods and for the implementation of the REST API the service is
based on.
@@ -23,11 +23,12 @@ Manages Document List information that is specific to a user.
- _node:_ `any` - Node object
- _nodeId:_ `string` - ID of the node object
- **Returns** `string` - ID value
- **getRecentFiles**(personId: `string`, pagination: [`PaginationModel`](../../../lib/core/src/lib/models/pagination.model.ts), filters?: `string[]`): [`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/>
- **getRecentFiles**(personId: `string`, pagination: [`PaginationModel`](../../../lib/core/src/lib/models/pagination.model.ts), filters?: `string[]`, includeFields: `string[]` = `[]`): [`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/>
Gets files recently accessed by a user.
- _personId:_ `string` - ID of the user
- _pagination:_ [`PaginationModel`](../../../lib/core/src/lib/models/pagination.model.ts) - Specifies how to paginate the results
- _filters:_ `string[]` - (Optional) Specifies additional filters to apply (joined with **AND**)
- _includeFields:_ `string[]` - List of data field names to include in the 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 for the recently used files
- **hasCorrespondingNodeIds**(nodeId: `string`): `boolean`<br/>
Does the well-known alias have a corresponding node ID?
@@ -1,43 +0,0 @@
---
Title: Search Ai service
Added: v7.0.0-alpha.3
Status: Active
Last reviewed: 2024-07-12
---
# [Search Ai service](../../../lib/content-services/src/lib/search-ai/services/search-ai.service.ts "Defined in search-ai.service.ts")
Manages search AI in Content Services.
<b>In order to use this service, you need to have the HX Insights Connector (additional ACS module) installed.</b>
## Class members
### Methods
- **updateSearchAiInputState**(state: `SearchAiInputState`): `void`<br/>
Update the state of the search AI input.
- _state:_ `SearchAiInputState` - The new state of the search AI input.
- **ask**(question: [`QuestionRequest`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionrequest)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`QuestionModel`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionmodel)`>`<br/>
Ask a question to the AI.
- _question:_ [`QuestionRequest`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionrequest) - The question to ask.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`QuestionModel`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#questionmodel)`>` - QuestionModel object containing information about questions.
- **getAnswer**(questionId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AiAnswerEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#aianswerentry)`>`<br/>
Get an answer to specific question.
- _questionId:_ `string` - The ID of the question to get an answer for.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`AiAnswerEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#aianswerentry)`>` - AiAnswerEntry object containing the answer.
- **getConfig**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`KnowledgeRetrievalConfigEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#knowledgeretrievalconfigentry)`>`<br/>
Get the knowledge retrieval configuration.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`KnowledgeRetrievalConfigEntry`](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md#knowledgeretrievalconfigentry)`>` - KnowledgeRetrievalConfigEntry object containing the configuration.
- **checkSearchAvailability**(selectedNodesState: `SelectionState`, maxSelectedNodes: `number`): `string`<br/>
Check if using of search is possible (if all conditions are met).
- _selectedNodesState:_ `SelectionState` - information about selected nodes.
- _maxSelectedNodes:_ `number` - max number of selected nodes. Default 100.
- **Returns** `string` - string with error if any condition is not met, empty string otherwise.
## Details
See the
[Search Ai API](../../../lib/js-api/src/api/content-rest-api/docs/SearchAiApi.md) for more information about the types returned by [Search Ai
service](search-ai.service.md) methods and for the implementation of the REST API the service is
based on.
@@ -2,7 +2,7 @@
Title: Search Query Builder service
Added: v2.3.0
Status: Active
Last reviewed: 2019-03-19
Last reviewed: 2026-06-29
---
# [Search Query Builder service](../../../lib/content-services/src/lib/search/services/search-query-builder.service.ts "Defined in search-query-builder.service.ts")
@@ -11,6 +11,17 @@ Stores information from all the custom search and faceted search widgets, compil
## Class members
### Properties
| Name | Type | Description |
| ----------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| userQuery | `string` | The raw query string typed by the user. Setting it stores the value, records it in `filterRawParams` and recomputes `parsedQuery` according to the current `searchMode`. |
| parsedQuery | `string` (read-only) | The query derived from `userQuery`. In `regular` mode the user terms are expanded against the configured fields (see `app:fields`) and optionally wildcarded; in `formula` mode it is identical to `userQuery`. |
| searchMode | `'regular' \| 'formula'` | Controls how `userQuery` is turned into `parsedQuery`. `regular` (default) parses the user input into a field query; `formula` uses the user input verbatim as an AFTS expression. |
| selectedConfigurationId | `string` | Id of the currently active search configuration. Setting it also records the value in `filterRawParams`. |
| encodedQuery | `string` (read-only) | The Base64-encoded `filterRawParams`, produced by `encodeQuery()` and written to the `q` route query parameter. |
| wildcardsEnabled | `boolean` (read-only) | Reads the `search-wildcards-enabled` app config flag (default `true`). When enabled, query terms are suffixed with `*` so partial matches are returned. |
### Methods
- **addFilterQuery**(query: `string`)<br/>
@@ -25,9 +36,10 @@ Stores information from all the custom search and faceted search widgets, compil
- **Returns** `SearchRequest` - The finished query
- **encodeQuery**()<br/>
Encodes query shards stored in `filterRawParams` property.
- **execute**(queryBody?: `SearchRequest`)<br/>
Builds and executes the current query.
- _queryBody:_ `SearchRequest` - (Optional)
- **execute**(updateQueryParams: `boolean` = `true`, queryBody?: `SearchRequest`)<br/>
Builds and executes the current query, then emits the result on the `executed` stream.
- _updateQueryParams:_ `boolean` - (Optional) When `true` (default) the encoded query is written to the `q` route query parameter. Pass `false` to execute without updating the URL.
- _queryBody:_ `SearchRequest` - (Optional) Pre-built query to execute instead of building one from the current state.
- **getDefaultConfiguration**(): [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)`|undefined`<br/>
- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)`|undefined` -
@@ -70,6 +82,11 @@ Stores information from all the custom search and faceted search widgets, compil
- **Returns** `boolean` -
- **isOperator**(input: `string`): `boolean`<br/>
Checks whether the supplied string is a logical `AND` or `OR` operator. Used when parsing a multi-word user query in `regular` search mode.
- _input:_ `string` - String to check
- **Returns** `boolean` - `true` if the trimmed string is `AND` or `OR`, otherwise `false`
- **loadConfiguration**(): [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)<br/>
- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts) -
@@ -86,7 +103,10 @@ Stores information from all the custom search and faceted search widgets, compil
Removes an existing bucket from a field.
- _field:_ [`FacetField`](../../../lib/content-services/src/lib/search/models/facet-field.interface.ts) - The target field
- _bucket:_ [`FacetFieldBucket`](../../../lib/content-services/src/lib/search/models/facet-field-bucket.interface.ts) - Bucket to remove
- **resetToDefaults**()<br/>
- **resetToDefaults**(withNavigate: `boolean` = `false`, resetUserQuery: `boolean` = `true`)<br/>
Resets the query builder back to the default search configuration.
- _withNavigate:_ `boolean` - (Optional) When `true`, clears the `q` route query parameter while resetting. Defaults to `false`.
- _resetUserQuery:_ `boolean` - (Optional) When `true` (default), the `userQuery` and its parsed form are cleared. Pass `false` to keep the current user query while resetting the rest of the options.
- **search**(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/>
@@ -97,14 +117,13 @@ Stores information from all the custom search and faceted search widgets, compil
- _scope:_ `RequestScope` -
- **update**(queryBody?: `SearchRequest`)<br/>
Builds the current query and triggers the `updated` event.
- _queryBody:_ `SearchRequest` - (Optional)
- **updateSearchQueryParams**() <br/>
Encodes the query and navigates to existing search route adding encoded query as a search param.
- **updateSelectedConfiguration**(index: `number`)<br/>
- _index:_ `number` -
- **updateSelectedConfiguration**(id: `string`, resetFilters: `boolean` = `true`, shouldExecute: `boolean` = `true`)<br/>
Switches the active search configuration to the one matching the supplied id (only relevant when multiple configurations are provided).
- _id:_ `string` - Id of the configuration to select
- _resetFilters:_ `boolean` - (Optional) When `true` (default), the current search options are reset before applying the new configuration. Pass `false` to keep them.
- _shouldExecute:_ `boolean` - (Optional) When `true` (default), the query is executed immediately after switching configuration.
## Details
@@ -127,10 +146,6 @@ You can use custom widgets to populate and edit the following parts of the resul
```ts
constructor(queryBuilder: SearchQueryBuilderService) {
queryBuilder.updated.subscribe(query => {
this.queryBuilder.execute();
});
queryBuilder.executed.subscribe(data => {
this.onDataLoaded(data);
});
@@ -138,6 +153,29 @@ constructor(queryBuilder: SearchQueryBuilderService) {
}
```
To run a search, build the query state (for example by setting `userQuery` or by letting a
search widget update `queryFragments`) and then call `execute()`. The result is delivered
through the `executed` stream.
```ts
this.queryBuilder.userQuery = 'invoice';
void this.queryBuilder.execute();
```
> **Note:** Earlier versions exposed an `updated` stream and an `update()` method that built the
> query and emitted it so that a subscriber could call `execute()`. Both have been removed; build
> the query state and call `execute()` directly instead.
### Search modes
The builder supports two search modes, selected through the `searchMode` property:
- **regular** (default) - the text in `userQuery` is treated as user input and parsed into a
field query. The query is split into terms, each term is matched against the fields listed in
the `app:fields` search configuration entry (falling back to `cm:name`), and a `*` wildcard is
appended when wildcards are enabled. Bare `AND`/`OR` words are preserved as operators.
- **formula** - the text in `userQuery` is used verbatim as an [AFTS](https://docs.alfresco.com/content-services/latest/develop/search-api/) expression, allowing callers that already build their own query syntax to bypass parsing.
> **Note:** From ADF 3.0.0, the query contains the `"facetFormat": "V2"` parameter so that all the responses have the same structure whether they come from search queries containing facetFields, facetQueries, grouped facetQueries or facetIntervals.
## Runtime Configuration
+1
View File
@@ -63,3 +63,4 @@ The pages linked below contain the licenses for all third party dependencies of
- [ADF 8.3.1](license-info-8.3.1.md)
- [ADF 8.4.1](license-info-8.4.1.md)
- [ADF 8.5.0](license-info-8.5.0.md)
- [ADF 9.0.0](license-info-9.0.0.md)
+81
View File
@@ -0,0 +1,81 @@
---
Title: License info, alfresco-ng2-components 9.0.0
---
# License information for alfresco-ng2-components 9.0.0
This page lists all third party libraries the project depends on.
## Libraries
| Name | Version | License |
| --- | --- | --- |
| [@angular-eslint/bundled-angular-compiler](https://github.com/angular-eslint/angular-eslint.git) | 20.7.0 | [MIT](https://opensource.org/license/mit/) |
| [@angular-eslint/utils](https://github.com/angular-eslint/angular-eslint.git) | 20.7.0 | [MIT](https://opensource.org/license/mit/) |
| [@angular/animations](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/cdk](https://github.com/angular/components.git) | 20.2.14 | [MIT](https://opensource.org/license/mit/) |
| [@angular/common](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/compiler](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/core](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/forms](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/material](https://github.com/angular/components.git) | 20.2.14 | [MIT](https://opensource.org/license/mit/) |
| [@angular/material-date-fns-adapter](https://github.com/angular/components.git) | 20.2.14 | [MIT](https://opensource.org/license/mit/) |
| [@angular/platform-browser](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@angular/router](git+https://github.com/angular/angular.git) | 20.3.25 | [MIT](https://opensource.org/license/mit/) |
| [@apollo/client](git+https://github.com/apollographql/apollo-client.git) | 3.13.1 | [MIT](https://opensource.org/license/mit/) |
| [@babel/runtime](https://github.com/babel/babel.git) | 7.26.10 | [MIT](https://opensource.org/license/mit/) |
| [@cspell/eslint-plugin](https://github.com/streetsidesoftware/cspell.git) | 10.0.0 | [MIT](https://opensource.org/license/mit/) |
| [@graphql-typed-document-node/core](git@github.com:dotansimha/graphql-typed-document-node.git) | 3.2.0 | [MIT](https://opensource.org/license/mit/) |
| [@mat-datetimepicker/core](https://github.com/kuhnroyal/mat-datetimepicker.git) | 16.0.1 | [MIT](https://opensource.org/license/mit/) |
| [@napi-rs/canvas](git+https://github.com/Brooooooklyn/canvas.git) | 0.1.100 | [MIT](https://opensource.org/license/mit/) |
| [@napi-rs/canvas-darwin-arm64](git+https://github.com/Brooooooklyn/canvas.git) | 0.1.100 | [MIT](https://opensource.org/license/mit/) |
| [@ngx-translate/core](https://github.com/ngx-translate/core) | 17.0.0 | [MIT](https://opensource.org/license/mit/) |
| [@wry/caches](git+https://github.com/benjamn/wryware.git) | 1.0.1 | [MIT](https://opensource.org/license/mit/) |
| [@wry/context](git+https://github.com/benjamn/wryware.git) | 0.7.4 | [MIT](https://opensource.org/license/mit/) |
| [@wry/equality](git+https://github.com/benjamn/wryware.git) | 0.5.7 | [MIT](https://opensource.org/license/mit/) |
| [@wry/trie](git+https://github.com/benjamn/wryware.git) | 0.5.0 | [MIT](https://opensource.org/license/mit/) |
| [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) | 19.0.0 | [MIT](https://opensource.org/license/mit/) |
| [apollo-angular](https://github.com/kamilkisiela/apollo-angular) | 11.0.0 | [MIT](https://opensource.org/license/mit/) |
| [balanced-match](git://github.com/juliangruber/balanced-match.git) | 1.0.2 | [MIT](https://opensource.org/license/mit/) |
| [brace-expansion](git://github.com/juliangruber/brace-expansion.git) | 1.1.15 | [MIT](https://opensource.org/license/mit/) |
| [brace-expansion](git://github.com/juliangruber/brace-expansion.git) | 2.1.1 | [MIT](https://opensource.org/license/mit/) |
| [concat-map](git://github.com/substack/node-concat-map.git) | 0.0.1 | [MIT](https://opensource.org/license/mit/) |
| [cropperjs](git+https://github.com/fengyuanchen/cropperjs.git) | 1.6.2 | [MIT](https://opensource.org/license/mit/) |
| [date-fns](https://github.com/date-fns/date-fns) | 2.30.0 | [MIT](https://opensource.org/license/mit/) |
| dotenv-expand | 5.1.0 | [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) |
| editorjs-html | 4.0.5 | [MIT](https://opensource.org/license/mit/) |
| [eve-raphael](https://github.com/tomasAlabes/eve.git) | 0.5.0 | UNKNOWN |
| [eventemitter3](git://github.com/primus/eventemitter3.git) | 5.0.4 | [MIT](https://opensource.org/license/mit/) |
| [fetch-blob](https://github.com/node-fetch/fetch-blob.git) | 3.2.0 | [MIT](https://opensource.org/license/mit/) |
| [formdata-polyfill](git+https://jimmywarting@github.com/jimmywarting/FormData.git) | 4.0.10 | [MIT](https://opensource.org/license/mit/) |
| [graphql-tag](git+https://github.com/apollographql/graphql-tag.git) | 2.12.7 | [MIT](https://opensource.org/license/mit/) |
| [graphql-ws](git+https://github.com/enisdenjo/graphql-ws.git) | 6.0.8 | [MIT](https://opensource.org/license/mit/) |
| [hoist-non-react-statics](git://github.com/mridgway/hoist-non-react-statics.git) | 3.3.2 | [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) |
| [js-tokens](lydell/js-tokens) | 4.0.0 | [MIT](https://opensource.org/license/mit/) |
| [lodash-es](lodash/lodash) | 4.18.1 | [MIT](https://opensource.org/license/mit/) |
| [loose-envify](git://github.com/zertosh/loose-envify.git) | 1.4.0 | [MIT](https://opensource.org/license/mit/) |
| [material-icons](git+https://github.com/marella/material-icons.git) | 1.13.14 | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) |
| [minimatch](git://github.com/isaacs/minimatch.git) | 5.1.9 | [ISC](https://www.isc.org/licenses/) |
| [minimatch-browser](git://github.com/isaacs/minimatch.git) | 1.0.0 | [ISC](https://www.isc.org/licenses/) |
| [ng2-charts](git+https://github.com/valor-software/ng2-charts.git) | 4.1.1 | [ISC](https://www.isc.org/licenses/) |
| [node-domexception](git+https://github.com/jimmywarting/node-domexception.git) | 1.0.0 | [MIT](https://opensource.org/license/mit/) |
| [node-fetch](https://github.com/node-fetch/node-fetch.git) | 3.3.2 | [MIT](https://opensource.org/license/mit/) |
| [object-assign](sindresorhus/object-assign) | 4.1.1 | [MIT](https://opensource.org/license/mit/) |
| [optimism](git+https://github.com/benjamn/optimism.git) | 0.18.1 | [MIT](https://opensource.org/license/mit/) |
| [pdfjs-dist](git+https://github.com/mozilla/pdf.js.git) | 5.1.91 | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) |
| [prop-types](facebook/prop-types) | 15.8.1 | [MIT](https://opensource.org/license/mit/) |
| [raphael](git://github.com/DmitryBaranovskiy/raphael.git) | 2.3.0 | [MIT](https://opensource.org/license/mit/) |
| [react-is](https://github.com/facebook/react.git) | 16.13.1 | [MIT](https://opensource.org/license/mit/) |
| [regenerator-runtime](https://github.com/facebook/regenerator/tree/main/packages/runtime) | 0.14.1 | [MIT](https://opensource.org/license/mit/) |
| [rehackt](git+https://github.com/phryneas/rehackt.git) | 0.1.0 | [MIT](https://opensource.org/license/mit/) |
| [rxjs](https://github.com/reactivex/rxjs.git) | 7.8.2 | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) |
| [symbol-observable](blesh/symbol-observable) | 4.0.0 | [MIT](https://opensource.org/license/mit/) |
| [ts-invariant](git+https://github.com/apollographql/invariant-packages.git) | 0.10.3 | [MIT](https://opensource.org/license/mit/) |
| [tslib](https://github.com/Microsoft/tslib.git) | 2.8.1 | [0BSD](http://landley.net/toybox/license.html) |
| [uuid](https://github.com/uuidjs/uuid.git) | 14.0.1 | [MIT](https://opensource.org/license/mit/) |
| [web-streams-polyfill](git+https://github.com/MattiasBuelens/web-streams-polyfill.git) | 3.3.3 | [MIT](https://opensource.org/license/mit/) |
| [zen-observable](zenparsing/zen-observable) | 0.8.15 | [MIT](https://opensource.org/license/mit/) |
| [zen-observable-ts](git+https://github.com/apollographql/zen-observable-ts.git) | 1.2.5 | [MIT](https://opensource.org/license/mit/) |
| [zone.js](git://github.com/angular/angular.git) | 0.15.0 | [MIT](https://opensource.org/license/mit/) |
+1
View File
@@ -9,6 +9,7 @@ The first **General Availability** release was v2.0.0.
## General Availability
- [9.0.0](RelNote-9.0.0.md)
- [8.5.0](RelNote-8.5.0.md)
- [8.4.1](RelNote-8.4.1.md)
- [8.3.1](RelNote-8.3.1.md)
+48
View File
@@ -0,0 +1,48 @@
---
Title: Changelog for alfresco-ng2-components v9.0.0
---
# Changelog
- [0a19be090e](git@github.com:Alfresco/alfresco-ng2-components/commit/0a19be090e) [ACS-12286] Pass includeFields through to getRecentFiles for -recent- (#12067)
- [b69c3ae1ae](git@github.com:Alfresco/alfresco-ng2-components/commit/b69c3ae1ae) [MNT-25230] Displayed modified time for node version (#12066)
- [ce907417b7](git@github.com:Alfresco/alfresco-ng2-components/commit/ce907417b7) AAE-48166 Compensate for VM/Citrix clock drift in token expiry check (#12024)
- [4c7765e3fa](git@github.com:Alfresco/alfresco-ng2-components/commit/4c7765e3fa) AAE-46517 Add onRowCountChange form event for Repeatable Sections (#12062)
- [407a64996e](git@github.com:Alfresco/alfresco-ng2-components/commit/407a64996e) AAE-46248 Format dropdown/radio option labels in display-text expressions (#12045)
- [01cea60490](git@github.com:Alfresco/alfresco-ng2-components/commit/01cea60490) [MNT-25230] adw version history does not provide user information (#12050)
- [09ef05038c](git@github.com:Alfresco/alfresco-ng2-components/commit/09ef05038c) chore: remove deprecated MaterialModule and its references (#12051)
- [c7bd56f244](git@github.com:Alfresco/alfresco-ng2-components/commit/c7bd56f244) AAE-43974 Fix for alfresco-content-app e2e failure (#12055)
- [b86e41515e](git@github.com:Alfresco/alfresco-ng2-components/commit/b86e41515e) [ACS-12037] Remove Knowledge Discovery from ADF (#12042)
- [0b35118dee](git@github.com:Alfresco/alfresco-ng2-components/commit/0b35118dee) AAE-47880 UI breaks when pasting very large text into text field (#12043)
- [fe2b3d85dd](git@github.com:Alfresco/alfresco-ng2-components/commit/fe2b3d85dd) [ACS-12108] Introduce signed commits into ADF workflows (#12040)
- [b0dc85422c](git@github.com:Alfresco/alfresco-ng2-components/commit/b0dc85422c) Improvement/AAE-43974 Refactoring form fields (#11945)
- [23f0392c12](git@github.com:Alfresco/alfresco-ng2-components/commit/23f0392c12) AAE-47634 Refactor: switch form service getTask to use runtime bundle API (#12020)
- [997303e6a7](git@github.com:Alfresco/alfresco-ng2-components/commit/997303e6a7) [ACS-11325] remove chevron button from tab order in adf-tree (#12041)
- [45d1818f04](git@github.com:Alfresco/alfresco-ng2-components/commit/45d1818f04) [ACS-12098] firefox headless race condition viewer fix (#12023)
- [448e667c52](git@github.com:Alfresco/alfresco-ng2-components/commit/448e667c52) [MNT-25681] Content node selector panel filtering fixed (#12032)
- [18e602aa85](git@github.com:Alfresco/alfresco-ng2-components/commit/18e602aa85) [PRODSEC-13253] Increased peerDependencies versions to allow to increase angular version in other projects (#12031)
- [4cd8648a05](git@github.com:Alfresco/alfresco-ng2-components/commit/4cd8648a05) Adjust release triggers to use cache-writable token (#12030)
- [7f57b3936b](git@github.com:Alfresco/alfresco-ng2-components/commit/7f57b3936b) AAE-39528 Session timeout features (#12008)
- [5a30baad99](git@github.com:Alfresco/alfresco-ng2-components/commit/5a30baad99) Release cache read/write fix (#12029)
- [5050c3d974](git@github.com:Alfresco/alfresco-ng2-components/commit/5050c3d974) [ACS-12041] Add debounceTime in PeopleWidgetComponent to limit API calls on every keystroke (#12027)
- [d8b36606e2](git@github.com:Alfresco/alfresco-ng2-components/commit/d8b36606e2) [MNT-25681] Search refactoring and unification (#12019)
- [37f8fe47ac](git@github.com:Alfresco/alfresco-ng2-components/commit/37f8fe47ac) AAE-46695 Upgrade to pnpm 11.9 (#12028)
- [af0d1e4ead](git@github.com:Alfresco/alfresco-ng2-components/commit/af0d1e4ead) AAE-47093 Update Supply Chain Review GH AW to latest (#12025)
- [bd7f393db9](git@github.com:Alfresco/alfresco-ng2-components/commit/bd7f393db9) AAE-47634 Fix block task claim checking status on Runtime Bundle (#12009)
- [d2d6696c05](git@github.com:Alfresco/alfresco-ng2-components/commit/d2d6696c05) Migrate to Angular 20, TypeScript 5.9, and Angular Material 20 (#11657)
- [7546aa91a3](git@github.com:Alfresco/alfresco-ng2-components/commit/7546aa91a3) AAE-47577 Form Spinner persists outside of Automate Form (#12005)
- [74d67c39a0](git@github.com:Alfresco/alfresco-ng2-components/commit/74d67c39a0) [ACS-11317] a11y Fix: Tags interactive controls are not manageable via keyboard (#11845)
- [20c20f2749](git@github.com:Alfresco/alfresco-ng2-components/commit/20c20f2749) AAE-46247 Add type-aware form field value adapter service (#11968)
- [b498d5744c](git@github.com:Alfresco/alfresco-ng2-components/commit/b498d5744c) [AAE-47470] - Upgrade Pnpm to 11.8.0 (#12004)
- [80eef35e1d](git@github.com:Alfresco/alfresco-ng2-components/commit/80eef35e1d) [ACS-11325] Add keyboard navigation and a11y support for adf-tree component (#11973)
- [49a3f1e104](git@github.com:Alfresco/alfresco-ng2-components/commit/49a3f1e104) AAE-47412 Update suprocesses and linked processes for process instance model (#11996)
- [0beaa18452](git@github.com:Alfresco/alfresco-ng2-components/commit/0beaa18452) [ACS-11973] Fix: context menu disappears during bulk upload (#11994)
- [76fd461741](git@github.com:Alfresco/alfresco-ng2-components/commit/76fd461741) AAE-47042 Add auto grow support to multiline text widget (#11981)
- [78697ee62a](git@github.com:Alfresco/alfresco-ng2-components/commit/78697ee62a) [AAE-0000] - Fixed js-api error index.js (#11970)
- [43c5bc17fb](git@github.com:Alfresco/alfresco-ng2-components/commit/43c5bc17fb) [ACS-12020] Updated init-aps passwords with secret ones (#11993)
- [cda20e0386](git@github.com:Alfresco/alfresco-ng2-components/commit/cda20e0386) Post-release version bump (#11988)
- [8f5099e822](git@github.com:Alfresco/alfresco-ng2-components/commit/8f5099e822) [ACS-11928] convert ReadStream to Blob before appending to FormData (#11987)
- [5dd709ed58](git@github.com:Alfresco/alfresco-ng2-components/commit/5dd709ed58) AAE-44809 Fix form multi-file attachment - document viewer not updated on file selection (#11980)
- [9d353bf271](git@github.com:Alfresco/alfresco-ng2-components/commit/9d353bf271) [ACS-11974] [ACA] Upload new version button is no longer disabled when clicked once (#11977)
- [633702860f](git@github.com:Alfresco/alfresco-ng2-components/commit/633702860f) Post-release version bump (#11984)
- [bf5b3af285](git@github.com:Alfresco/alfresco-ng2-components/commit/bf5b3af285) AAE-47094 Add Stale PR Cleanup workflow (#11983)
+44
View File
@@ -13,6 +13,50 @@ The authType parameter specifies the authentication method, with BASIC and OAUTH
"authType": "OAUTH"
}
```
## Session Timeout
ADF can track user activity and show a countdown dialog before logging out an idle authenticated session. Register the feature with `provideSessionTimeout` in your application providers:
```ts
import { ApplicationConfig } from '@angular/core';
import { provideSessionTimeout } from '@alfresco/adf-core';
export const appConfig: ApplicationConfig = {
providers: [
provideSessionTimeout({
enabled: true,
idleTimeoutMs: 30 * 60 * 1000,
dialogTimeoutMs: 60 * 1000
})
]
};
```
The same values can be configured in `app.config.json` with the `sessionTimeout` key:
```json
{
"sessionTimeout": {
"enabled": true,
"idleTimeoutMs": 1800000,
"dialogTimeoutMs": 60000
}
}
```
Registering `provideSessionTimeout` enables the feature: `enabled` defaults to `true`, so the countdown is active unless you set `enabled` to `false` in `app.config.json` or in the options passed to `provideSessionTimeout`. The default idle timeout is 30 minutes, and the default dialog timeout is 60 seconds. Values passed to `provideSessionTimeout` take precedence over values from `app.config.json`.
Applications can defer startup until another async condition is enabled:
```ts
provideSessionTimeout({
startWhen: () => featureService.isOn$('session-timeout-feature')
});
```
Whether the user clicks **Log out** in the countdown dialog or leaves it unanswered until the countdown elapses, the normal logout flow runs and the user is redirected to the configured IdP/login page. The same logout is broadcast to other tabs so every session ends together.
# OAuth2 Configuration
OAuth2 is a protocol that allows the application to authorize operations without exposing user credentials. The configuration includes several parameters essential for setting up OAuth2 authentication.
+2 -3
View File
@@ -90,7 +90,6 @@ export class YourSearchComponent implements OnInit {
ngOnInit() {
this.queryBuilder.resetToDefaults();
this.queryBuilder.updated.subscribe(() => void this.queryBuilder.execute());
this.queryBuilder.executed.subscribe((data) => {
this.queryBuilder.paging.skipCount = 0;
this.onSearchResultLoaded(data);
@@ -103,7 +102,7 @@ export class YourSearchComponent implements OnInit {
onSearchQueryChanged(string: string) {
this.queryBuilder.userQuery = decodeURIComponent(string);
this.queryBuilder.update();
void this.queryBuilder.execute();
}
async onPaginationChanged(pagination: Pagination) {
@@ -111,7 +110,7 @@ export class YourSearchComponent implements OnInit {
maxItems: pagination.maxItems,
skipCount: pagination.skipCount
};
this.queryBuilder.update();
void this.queryBuilder.execute();
}
}
```
@@ -16,6 +16,7 @@ This page describes how you can configure the search configuration.
- [Steps Involved In Search Configuration](#steps-involved-in-search-configuration)
- [Configuration](#configuration)
- [Extra fields and filter queries](#extra-fields-and-filter-queries)
- [Search modes and wildcards](#search-modes-and-wildcards)
- [Sorting](#sorting)
- [Categories and widgets](#categories-and-widgets)
- [Facet Fields](#facet-fields)
@@ -267,6 +268,44 @@ settings:
Note that the entries of the `filterQueries` array are joined using the `AND` operator.
### Search modes and wildcards
When a user types free text into a search input, the [Search Query Builder Service](../../content-services/services/search-query-builder.service.md) turns that text (its `userQuery`) into the final query according to the configured *search mode*:
- **regular** (default) - the user input is parsed into a field query. Each term is matched
against the fields listed in the `app:fields` array (falling back to `cm:name` when it is not
set), and a `*` wildcard is appended to each term when wildcards are enabled. Words that are
exactly `AND` or `OR` are preserved as logical operators.
- **formula** - the user input is passed through verbatim as an
[AFTS](https://docs.alfresco.com/content-services/latest/develop/search-api/) expression. Use
this mode when the caller already builds valid query syntax.
The `app:fields` entry lists the fields used to expand a `regular` user query:
```json
{
"search": {
...
"app:fields": ["cm:name", "cm:title", "cm:description"]
...
}
}
```
For example, with the configuration above and wildcards enabled, the user query `report` is
expanded to `(cm:name:"report*" OR cm:title:"report*" OR cm:description:"report*")`.
Wildcard matching is controlled by the top-level `search-wildcards-enabled` flag in
`app.config.json` (default `true`). When set to `false`, terms are matched exactly and the
trailing `*` is not added (this also disables the `searchPrefix`/`searchSuffix` of the
[Search text component](../content-services/components/search-text.component.md)):
```json
{
"search-wildcards-enabled": false
}
```
### Sorting
The Sorting configuration section consists of two blocks:
+2 -2
View File
@@ -61,8 +61,8 @@ backend services have been tested with each released version of ADF.
<!--7.0.0-alpha.3 start-->
- [AgentService](content-services/services/agent.service.md)
- [SearchAiService](content-services/services/search-ai.service.md)
- AgentService — removed, see [Notes](README.md#knowledge-discovery-removal-v900)
- SearchAiService — removed, see [Notes](README.md#knowledge-discovery-removal-v900)
<!--7.0.0-alpha.3 end-->
+1
View File
@@ -60,3 +60,4 @@ The pages linked below contain the audit for all third party dependencies of ADF
- [ADF 8.3.1](audit-info-8.3.1.md)
- [ADF 8.4.1](audit-info-8.4.1.md)
- [ADF 8.5.0](audit-info-8.5.0.md)
- [ADF 9.0.0](audit-info-9.0.0.md)
+23
View File
@@ -0,0 +1,23 @@
---
Title: Audit info, alfresco-ng2-components 9.0.0
---
# Audit information for alfresco-ng2-components 9.0.0
This page lists the security audit of the dependencies this project depends on.
## Risks
- Critical risk: 0
- High risk: 0
- Moderate risk: 0
- Low risk: 0
Dependencies analyzed: 297
## Libraries
| Severity | Module | Vulnerable versions |
| --- | --- | --- |
+2 -2
View File
@@ -98,8 +98,8 @@ Options:
const packageDir = path.dirname(packagePath);
// Use spawnSync with array arguments for safer command execution (prevents shell injection)
// Cross-platform: npm is available on PATH on all platforms (Windows, macOS, Linux)
const result = spawnSync('npm', ['audit', '--json', '--prod'], {
// Cross-platform: pnpm is available on PATH on all platforms (Windows, macOS, Linux)
const result = spawnSync('pnpm', ['audit', '--json', '--prod'], {
cwd: packageDir,
encoding: 'utf-8',
// shell: false is the default and more secure (no shell interpretation)
+12 -12
View File
@@ -11,19 +11,19 @@
"url": "https://github.com/Alfresco/alfresco-ng2-components/issues"
},
"peerDependencies": {
"@angular/animations": ">=14.1.3",
"@angular/cdk": ">=14.1.2",
"@angular/common": ">=14.1.3",
"@angular/compiler": ">=14.1.3",
"@angular/core": ">=14.1.3",
"@angular/forms": ">=14.1.3",
"@angular/material": ">=14.1.2",
"@angular/platform-browser": ">=14.1.3",
"@angular/platform-browser-dynamic": ">=14.1.3",
"@angular/router": ">=14.1.3",
"@alfresco/js-api": ">=9.5.0",
"@angular/animations": ">=20.3.25",
"@angular/cdk": ">=20.2.14",
"@angular/common": ">=20.3.25",
"@angular/compiler": ">=20.3.25",
"@angular/core": ">=20.3.25",
"@angular/forms": ">=20.3.25",
"@angular/material": ">=20.2.14",
"@angular/platform-browser": ">=20.3.25",
"@angular/platform-browser-dynamic": ">=20.3.25",
"@angular/router": ">=20.3.25",
"@alfresco/js-api": ">=10.0.0",
"@ngx-translate/core": ">=17.0.0",
"@alfresco/adf-core": ">=8.5.0"
"@alfresco/adf-core": ">=9.0.0"
},
"keywords": [
"content-services",
@@ -0,0 +1,7 @@
@mixin adf-error-icon {
font-size: 16px;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: text-bottom;
}
@@ -1,18 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './public-api';
@@ -1,67 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { AgentService } from './agent.service';
import { Agent, AgentPaging } from '@alfresco/js-api';
const agent1: Agent = {
id: '1',
name: 'HR Agent',
description: 'Your Claims Doc Agent streamlines the extraction, analysis, and management of data from insurance claims documents.',
avatarUrl: ''
};
const agent2: Agent = {
id: '2',
name: 'Policy Agent',
description: 'Your Claims Doc Agent streamlines the extraction, analysis, and management of data from insurance claims documents.',
avatarUrl: ''
};
const agentPagingObjectMock: AgentPaging = {
list: {
entries: [
{
entry: agent1
},
{
entry: agent2
}
]
}
};
const agentListMock: Agent[] = [agent1, agent2];
describe('AgentService', () => {
let agentService: AgentService;
beforeEach(() => {
agentService = TestBed.inject(AgentService);
});
it('should load agents', (done) => {
spyOn(agentService.agentsApi, 'getAgents').and.returnValue(Promise.resolve(agentPagingObjectMock));
agentService.getAgents().subscribe((pagingResponse) => {
expect(pagingResponse).toEqual(agentListMock);
expect(agentService.agentsApi.getAgents).toHaveBeenCalled();
done();
});
});
});
@@ -1,58 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { Injectable, inject } from '@angular/core';
import { Agent, AgentsApi, LazyApi } from '@alfresco/js-api';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AlfrescoApiService } from '../../services';
@Injectable({
providedIn: 'root'
})
export class AgentService {
private readonly apiService = inject(AlfrescoApiService);
private readonly agents = new BehaviorSubject<Agent[]>([]);
@LazyApi((self: AgentService) => new AgentsApi(self.apiService.getInstance()))
declare readonly agentsApi: AgentsApi;
agents$ = this.agents.asObservable();
/**
* Gets all agents from cache. If cache is empty, fetches agents from backend.
*
* @returns Agent[] list containing agents.
*/
getAgents(): Observable<Agent[]> {
return this.agents$.pipe(
switchMap((agentsList) => {
if (agentsList.length) {
return of(agentsList);
}
return from(this.agentsApi.getAgents()).pipe(
map((paging) => {
const agentEntries = paging.list.entries.map((agentEntry) => agentEntry.entry);
this.agents.next(agentEntries);
return agentEntries;
})
);
})
);
}
}
@@ -206,5 +206,9 @@ describe('ContentNodeDialogService', () => {
expect(testContentNodeSelectorComponentData.isSelectionValid(testData.node)).toBe(testData.expected);
});
});
it('should scope the search to folders by disabling files in the result', () => {
expect(testContentNodeSelectorComponentData.showFilesInResult).toBe(false);
});
});
});
@@ -154,7 +154,8 @@ export class ContentNodeDialogService {
where: '(isFolder=true)',
isSelectionValid: this.isCopyMoveSelectionValid.bind(this),
excludeSiteContent: excludeSiteContent || ContentNodeDialogService.nonDocumentSiteContent,
select
select,
showFilesInResult: false
};
const dialogRef = this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px');
@@ -196,7 +197,8 @@ export class ContentNodeDialogService {
imageResolver: this.imageResolver.bind(this),
isSelectionValid: this.hasAllowableOperationsOnNodeFolder.bind(this),
where: '(isFolder=true)',
select
select,
showFilesInResult: false
};
const dialogRef = this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px');
@@ -19,14 +19,13 @@ import { DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Node, NodeEntry, NodePaging, RequestScope, ResultSetPaging, SiteEntry, SitePaging, SitePagingList } from '@alfresco/js-api';
import { of } from 'rxjs';
import { of, Subject } from 'rxjs';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { DocumentListService } from '../../document-list/services/document-list.service';
import { DocumentListComponent } from '../../document-list/components/document-list.component';
import { CustomResourcesService } from '../../document-list/services/custom-resources.service';
import { NodeEntryEvent, ShareDataRow } from '../../document-list';
import { SearchQueryBuilderService } from '../../search';
import { mockSearchRequest } from '../../mock/search-query.mock';
import { SitesService } from '../../common/services/sites.service';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { UnitTestingUtils } from '../../../../../core/src/lib/testing/unit-testing-utils';
@@ -161,43 +160,70 @@ describe('ContentNodeSelectorPanelComponent', () => {
});
it('should the user query get updated when the user types in the search input', fakeAsync(() => {
const updateSpy = spyOn(searchQueryBuilderService, 'update');
typeToSearchBox('search-term');
tick(debounceSearch);
fixture.detectChanges();
expect(updateSpy).toHaveBeenCalled();
expect(searchSpy).toHaveBeenCalled();
expect(searchQueryBuilderService.userQuery).toEqual('(search-term*)');
expect(component.searchTerm).toEqual('search-term');
}));
it('should set the query builder search mode to formula when the user types in the search input', fakeAsync(() => {
typeToSearchBox('search-term');
tick(debounceSearch);
fixture.detectChanges();
expect(searchQueryBuilderService.searchMode).toEqual('formula');
}));
it('should add the wildcard suffix to the user query when wildcards are enabled', fakeAsync(() => {
spyOnProperty(searchQueryBuilderService, 'wildcardsEnabled', 'get').and.returnValue(true);
typeToSearchBox('search-term');
tick(debounceSearch);
fixture.detectChanges();
expect(searchQueryBuilderService.userQuery).toEqual('(search-term*)');
}));
it('should NOT add the wildcard suffix to the user query when wildcards are disabled', fakeAsync(() => {
spyOnProperty(searchQueryBuilderService, 'wildcardsEnabled', 'get').and.returnValue(false);
typeToSearchBox('search-term');
tick(debounceSearch);
fixture.detectChanges();
expect(searchQueryBuilderService.userQuery).toEqual('(search-term)');
}));
it('should perform a search when the search request gets updated and it is defined', fakeAsync(() => {
typeToSearchBox('search-term');
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
}));
it('should NOT perform a search and clear the results when the search request gets updated and it is NOT defined', async () => {
it('should NOT perform a search and clear the results when the search input is empty', fakeAsync(() => {
spyOn(component, 'clearSearch');
searchQueryBuilderService.userQuery = '';
searchQueryBuilderService.update();
typeToSearchBox('');
tick(debounceSearch);
fixture.detectChanges();
await fixture.whenStable();
expect(searchSpy).not.toHaveBeenCalled();
expect(component.clearSearch).toHaveBeenCalled();
});
}));
it('should reset the search term when clicking the clear icon', async () => {
component.searchTerm = 'search-term';
searchQueryBuilderService.userQuery = 'search-term';
spyOn(component, 'clearSearch');
spyOn(component, 'clear').and.callThrough();
fixture.detectChanges();
const clearIcon = getSearchIcon('clear');
@@ -207,7 +233,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(searchQueryBuilderService.userQuery).toEqual('');
expect(component.searchTerm).toEqual('');
expect(component.clearSearch).toHaveBeenCalled();
expect(component.clear).toHaveBeenCalled();
});
it('should load the results by calling the search api on search change', fakeAsync(() => {
@@ -216,22 +242,21 @@ describe('ContentNodeSelectorPanelComponent', () => {
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
}));
it('should the query include the show files filterQuery', fakeAsync(() => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
component.showFilesInResult = true;
typeToSearchBox('search-term');
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries.push({
query: `TYPE:'cm:folder' OR TYPE:'cm:content'`
});
const expectedRequest = `TYPE:'cm:folder' OR TYPE:'cm:content'`;
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
}));
it('should reset the currently chosen node in case of starting a new search', fakeAsync(() => {
@@ -253,6 +278,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
});
it('should perform a search when selecting a site with the correct query', fakeAsync(() => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
typeToSearchBox('search-term');
tick(debounceSearch);
@@ -261,14 +287,41 @@ describe('ContentNodeSelectorPanelComponent', () => {
component.siteChanged({ entry: { guid: 'namek' } } as SiteEntry);
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/namek'` }];
const expectedRequest = `ANCESTOR:'workspace://SpacesStore/namek'`;
expect(searchSpy.calls.count()).toBe(2);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
}));
it('should remove the previous site filter query when changing the selected site', fakeAsync(() => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
spyOn(searchQueryBuilderService, 'removeFilterQuery').and.callThrough();
typeToSearchBox('search-term');
tick(debounceSearch);
component.siteChanged({ entry: { guid: 'namek' } } as SiteEntry);
component.siteChanged({ entry: { guid: 'vegeta' } } as SiteEntry);
expect(searchQueryBuilderService.removeFilterQuery).toHaveBeenCalledWith(`ANCESTOR:'workspace://SpacesStore/namek'`);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(`ANCESTOR:'workspace://SpacesStore/vegeta'`);
expect(searchQueryBuilderService.filterQueries).toEqual([{ query: `ANCESTOR:'workspace://SpacesStore/vegeta'` }]);
}));
it('should browse the selected site folder without searching when no search term is present', fakeAsync(() => {
expect(searchSpy.calls.count()).toBe(0);
component.siteChanged({ entry: { guid: 'namek' } } as SiteEntry);
tick(debounceSearch);
expect(searchSpy).not.toHaveBeenCalled();
expect(component.searchTerm).toBe('');
expect(component.folderIdToShow).toBe('namek');
expect(component.showingSearchResults).toBe(false);
}));
it('should create the query with the right parameters on changing the site selectBox value from a custom dropdown menu', fakeAsync(() => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
component.dropdownSiteList = { list: { entries: [{ entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }] } } as SitePaging;
component.documentList.folderNode = { id: 'fakeNodeId', isFolder: true, path: {} } as Node;
fixture.detectChanges();
@@ -281,17 +334,12 @@ describe('ContentNodeSelectorPanelComponent', () => {
component.siteChanged({ entry: { guid: '-sites-' } } as SiteEntry);
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [
{
query: `ANCESTOR:'workspace://SpacesStore/-sites-' OR ANCESTOR:'workspace://SpacesStore/123456testId' OR ANCESTOR:'workspace://SpacesStore/09876543testId'`
}
];
const expectedRequest = `ANCESTOR:'workspace://SpacesStore/-sites-' OR ANCESTOR:'workspace://SpacesStore/123456testId' OR ANCESTOR:'workspace://SpacesStore/09876543testId'`;
expect(searchSpy).toHaveBeenCalled();
expect(searchSpy.calls.count()).toBe(2);
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
}));
it('should get the corresponding node ids on search when a known alias is selected from dropdown', fakeAsync(() => {
@@ -321,6 +369,29 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(getCorrespondingNodeIdsSpy.calls.mostRecent().args[0]).toEqual('-sites-');
}));
it('should execute the search only after the corresponding node ids filter query is applied for aliases', fakeAsync(() => {
component.documentList.folderNode = { id: 'fakeNodeId', isFolder: true, path: {} } as Node;
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
const nodeIds$ = new Subject<string[]>();
getCorrespondingNodeIdsSpy.and.returnValue(nodeIds$.asObservable());
typeToSearchBox('vegeta');
tick(debounceSearch);
searchSpy.calls.reset();
component.siteChanged({ entry: { guid: '-sites-' } } as SiteEntry);
expect(searchSpy).not.toHaveBeenCalled();
nodeIds$.next(['123456testId']);
nodeIds$.complete();
const expectedRequest = `ANCESTOR:'workspace://SpacesStore/-sites-' OR ANCESTOR:'workspace://SpacesStore/123456testId'`;
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
}));
it('should NOT get the corresponding node ids on search when NOTHING is selected from dropdown', fakeAsync(() => {
component.dropdownSiteList = { list: { entries: [{ entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }] } } as SitePaging;
fixture.detectChanges();
@@ -395,7 +466,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(component.clear).toHaveBeenCalled();
}));
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', async () => {
it('should reset the search term and user query when clearing the search', async () => {
component.chosenNode = [entry];
component.nodePaging = {
@@ -404,32 +475,31 @@ describe('ContentNodeSelectorPanelComponent', () => {
}
};
component.searchTerm = 'piccolo';
searchQueryBuilderService.userQuery = 'piccolo';
component.showingSearchResults = true;
component.clear();
expect(component.searchTerm).toBe('');
expect(component.nodePaging).toEqual(null);
expect(component.chosenNode).toBeNull();
expect(component.showingSearchResults).toBeFalsy();
expect(searchQueryBuilderService.userQuery).toBe('');
expect(searchSpy).toHaveBeenCalledWith(false);
});
it('should the query restrict the search to the currentFolderId in case is defined', fakeAsync(() => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
component.currentFolderId = 'my-root-id';
component.restrictRootToCurrentFolderId = true;
component.ngOnInit();
typeToSearchBox('search-term');
tick(debounceSearch);
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/my-root-id'` }];
const expectedRequest = `ANCESTOR:'workspace://SpacesStore/my-root-id'`;
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
}));
it('should emit showingSearch event with true while searching', async () => {
searchQueryBuilderService.userQuery = 'mock-search-term';
searchQueryBuilderService.update();
spyOn(customResourcesService, 'hasCorrespondingNodeIds').and.returnValue(true);
const showingSearchSpy = spyOn(component.showingSearch, 'emit');
@@ -454,8 +524,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(showingSearchSpy).toHaveBeenCalledWith(false);
}));
it('should emit showingResults event with false when clicking on the X (clear) icon', async () => {
const showingSearchSpy = spyOn(component.showingSearch, 'emit');
it('should re-run the query with an empty user query when clicking on the X (clear) icon', async () => {
component.chosenNode = [entry];
component.nodePaging = {
@@ -464,17 +533,16 @@ describe('ContentNodeSelectorPanelComponent', () => {
}
};
component.searchTerm = 'piccolo';
component.showingSearchResults = true;
searchQueryBuilderService.userQuery = 'piccolo';
component.clear();
expect(component.showingSearchResults).toBe(false);
expect(showingSearchSpy).toHaveBeenCalledWith(false);
expect(component.searchTerm).toBe('');
expect(searchQueryBuilderService.userQuery).toBe('');
expect(searchSpy).toHaveBeenCalledWith(false);
});
it('should emit showingResults event with false if search api fails', async () => {
searchQueryBuilderService.userQuery = 'mock-search-term';
searchQueryBuilderService.update();
getCorrespondingNodeIdsSpy.and.throwError('Failed');
const showingSearchSpy = spyOn(component.showingSearch, 'emit');
await searchQueryBuilderService.execute(true, { query: { query: 'search' } });
@@ -488,15 +556,17 @@ describe('ContentNodeSelectorPanelComponent', () => {
});
it('should the query restrict the search to the site and not to the currentFolderId in case is changed', async () => {
spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough();
component.searchTerm = 'search-term';
searchQueryBuilderService.userQuery = 'search-term*';
component.currentFolderId = 'my-root-id';
component.restrictRootToCurrentFolderId = true;
component.siteChanged({ entry: { guid: 'my-site-id' } } as SiteEntry);
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/my-site-id'` }];
const expectedRequest = `ANCESTOR:'workspace://SpacesStore/my-site-id'`;
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false);
expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest);
});
it('should restrict the breadcrumb to the currentFolderId in case restrictedRoot is true', async () => {
@@ -563,10 +633,12 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(searchSpy.calls.count()).toBe(2);
component.clear();
typeToSearchBox('');
tick(debounceSearch);
expect(component.searchTerm).toBe('');
expect(component.folderIdToShow).toBe('namek');
flush();
}));
it('should show the current folder content instead of search results if search was not performed', async () => {
@@ -638,25 +710,23 @@ describe('ContentNodeSelectorPanelComponent', () => {
fixture.detectChanges();
const documentList = testingUtils.getByCSS('[data-automation-id="content-node-selector-document-list"]');
expect(documentList).not.toBeNull();
expect(component.hasValidQuery).toEqual(true);
expect(documentList.componentInstance.currentFolderId).toBeNull();
done();
}, 300);
});
it('should not show the result list when results are returned but there is no search term typed', (done) => {
searchQueryBuilderService.userQuery = '';
searchQueryBuilderService.update();
it('should not run a search nor show the result list when there is no search term typed', fakeAsync(() => {
spyOn(component, 'clearSearch').and.callThrough();
setTimeout(() => {
triggerSearchResults(fakeResultSetPaging);
fixture.detectChanges();
typeToSearchBox('');
tick(debounceSearch);
fixture.detectChanges();
expect(component.hasValidQuery).toEqual(false);
expect(component.showingSearchResults).toEqual(false);
done();
}, 300);
});
expect(searchSpy).not.toHaveBeenCalled();
expect(component.clearSearch).toHaveBeenCalled();
expect(component.showingSearchResults).toEqual(false);
flush();
}));
it('should highlight the results when search was performed in the next timeframe', (done) => {
typeToSearchBox('My');
@@ -673,7 +743,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
}, 300);
});
it('should show the default text instead of result list if search was cleared', (done) => {
it('should reset the search term and re-run an empty query when the clear button is clicked', (done) => {
typeToSearchBox();
setTimeout(() => {
@@ -683,12 +753,13 @@ describe('ContentNodeSelectorPanelComponent', () => {
fixture.whenStable().then(() => {
const clearButton = getSearchIcon('clear');
expect(clearButton).not.toBeNull();
searchSpy.calls.reset();
clearButton.triggerEventHandler('click', {});
fixture.detectChanges();
const documentList = testingUtils.getByCSS('[data-automation-id="content-node-selector-document-list"]');
expect(documentList).not.toBeNull();
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
expect(component.searchTerm).toBe('');
expect(searchQueryBuilderService.userQuery).toBe('');
expect(searchSpy).toHaveBeenCalledWith(false);
done();
});
}, 300);
@@ -709,20 +780,20 @@ describe('ContentNodeSelectorPanelComponent', () => {
flush();
}));
it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => {
it('should reset the folder to the selected site and fall back to currentFolderId when site is undefined', () => {
component.siteChanged({ entry: { guid: 'Kame-Sennin Muten Roshi' } } as SiteEntry);
component.resetFolderToShow();
fixture.detectChanges();
let documentList = testingUtils.getByCSS('[data-automation-id="content-node-selector-document-list"]');
expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi');
component.siteChanged({ entry: { guid: undefined } } as SiteEntry);
component.resetFolderToShow();
fixture.detectChanges();
documentList = testingUtils.getByCSS('[data-automation-id="content-node-selector-document-list"]');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
done();
});
describe('Pagination "Load more" button', () => {
@@ -752,7 +823,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
});
it('should set its loading state to true to perform a new search', async () => {
component.prepareDialogForNewSearch(mockSearchRequest);
component.prepareDialogForNewSearch();
fixture.detectChanges();
await fixture.whenStable();
@@ -764,13 +835,14 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(paginationLoading).not.toBeNull();
});
it('Should infinite pagination target be null when we use it for search ', fakeAsync(() => {
it('Should keep the document list as infinite pagination target while searching', fakeAsync(() => {
component.showingSearchResults = true;
typeToSearchBox('shenron');
tick(debounceSearch);
fixture.detectChanges();
expect(component.target).toBeNull();
expect(component.target).toEqual(component.documentList);
flush();
}));
it('Should infinite pagination target be present when search finish', () => {
@@ -270,7 +270,6 @@ describe('ContentNodeSelectorPanelComponent', () => {
it('should not show the breadcrumb if search was performed as last action', async () => {
searchQueryBuilderService.userQuery = 'mock-search-term';
searchQueryBuilderService.update();
triggerSearchResults(fakeResultSetPaging);
fixture.detectChanges();
@@ -291,7 +290,6 @@ describe('ContentNodeSelectorPanelComponent', () => {
it('should show the breadcrumb in search results for a valid node selection', async () => {
searchQueryBuilderService.userQuery = 'mock-search-term';
searchQueryBuilderService.update();
triggerSearchResults(fakeResultSetPaging);
const chosenNode = new Node({ path: { elements: [{ name: 'one' }] } });
@@ -306,7 +304,6 @@ describe('ContentNodeSelectorPanelComponent', () => {
it('should show the breadcrumb in search results even for an invalid node selection', async () => {
component.isSelectionValid = (node: Node) => node.isFile;
searchQueryBuilderService.userQuery = 'mock-search-term';
searchQueryBuilderService.update();
triggerSearchResults(fakeResultSetPaging);
const chosenNode = new Node({ path: { elements: [{ name: 'fake-path' }] }, isFile: false, isFolder: true });
@@ -33,14 +33,15 @@ import {
} from '@alfresco/adf-core';
import { FileUploadCompleteEvent, FileUploadDeleteEvent, NodesApiService, SitesService, UploadService } from '../../common';
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { Node, NodeEntry, NodePaging, Pagination, RequestScope, SearchRequest, SiteEntry, SitePaging } from '@alfresco/js-api';
import { Node, NodeEntry, NodePaging, Pagination, RequestScope, SiteEntry, SitePaging } from '@alfresco/js-api';
import { DocumentListComponent } from '../../document-list/components/document-list.component';
import { RowFilter } from '../../document-list/data/row-filter.model';
import { ImageResolver } from '../../document-list/data/image-resolver.model';
import { CustomResourcesService } from '../../document-list/services/custom-resources.service';
import { ShareDataRow } from '../../document-list/data/share-data-row.model';
import { NodeEntryEvent } from '../../document-list/components/node.event';
import { debounceTime } from 'rxjs/operators';
import { debounceTime, map } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { ContentNodeSelectorPanelService } from './content-node-selector-panel.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { TranslatePipe } from '@ngx-translate/core';
@@ -110,6 +111,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
private showSearchField = true;
private showCounter = false;
private _emptyList = true;
private lastParentFilterQuery: string | null = null;
/** If true will restrict the search and breadcrumbs to the currentFolderId */
@Input()
@@ -292,7 +294,6 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
folderIdToShow: string | null = null;
breadcrumbFolderTitle: string | null = null;
startSiteGuid: string | null = null;
hasValidQuery: boolean = false;
showHeader = ShowHeaderMode.Never;
@ViewChild(InfinitePaginationComponent, { static: true })
@@ -334,26 +335,16 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
.pipe(debounceTime(this.debounceSearch), takeUntilDestroyed(this.destroyRef))
.subscribe((searchValue: string) => {
this.searchTerm = searchValue;
this.queryBuilderService.userQuery = searchValue.length > 0 ? `${searchValue}*` : searchValue;
this.queryBuilderService.update();
if (this.searchTerm) {
this.executeSearch(searchValue);
} else {
this.resetFolderToShow();
this.clearSearch();
}
});
this.queryBuilderService.updated.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((searchRequest) => {
if (searchRequest) {
this.hasValidQuery = true;
this.prepareDialogForNewSearch(searchRequest);
this.queryBuilderService.execute(false, searchRequest);
} else {
this.hasValidQuery = false;
this.resetFolderToShow();
this.clearSearch();
}
});
this.queryBuilderService.executed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((results: NodePaging) => {
if (this.hasValidQuery) {
this.showSearchResults(results);
}
this.showSearchResults(results);
});
this.userPreferencesService
@@ -451,7 +442,12 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
this.siteId = chosenSite.entry.guid;
this.setTitleIfCustomSite(chosenSite);
this.siteChange.emit(chosenSite.entry.title);
this.queryBuilderService.update();
if (this.searchTerm) {
this.executeSearch(this.searchTerm);
} else {
this.resetFolderToShow();
this.clearSearch();
}
}
/**
@@ -473,18 +469,11 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
/**
* Prepares the dialog for a new search
*
* @param searchRequest request options
*/
prepareDialogForNewSearch(searchRequest: SearchRequest): void {
this.target = searchRequest ? null : this.documentList;
if (this.target) {
this.infinitePaginationComponent.reset();
}
prepareDialogForNewSearch(): void {
this.folderIdToShow = null;
this.preselectedNodes = [];
this.loadingSearchResults = true;
this.addCorrespondingNodeIdsQuery();
this.resetChosenNode();
}
@@ -494,7 +483,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
clear(): void {
this.searchTerm = '';
this.queryBuilderService.userQuery = '';
this.queryBuilderService.update();
this.executeSearch(this.searchTerm);
}
/**
@@ -516,25 +505,35 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
this.showingSearch.emit(this.showingSearchResults);
}
private addCorrespondingNodeIdsQuery() {
let extraParentFiltering = '';
private addCorrespondingNodeIdsQuery(): Observable<void> {
if (this.customResourcesService.hasCorrespondingNodeIds(this.siteId)) {
this.customResourcesService.getCorrespondingNodeIds(this.siteId).subscribe((nodeIds) => {
if (nodeIds?.length) {
nodeIds
.filter((id) => id !== this.siteId)
.forEach((extraId) => {
extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`;
});
}
const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'${extraParentFiltering}` : '';
this.queryBuilderService.addFilterQuery(parentFiltering);
});
} else {
const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'` : '';
this.queryBuilderService.addFilterQuery(parentFiltering);
return this.customResourcesService.getCorrespondingNodeIds(this.siteId).pipe(
map((nodeIds) => {
let extraParentFiltering = '';
if (nodeIds?.length) {
nodeIds
.filter((id) => id !== this.siteId)
.forEach((extraId) => {
extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`;
});
}
const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'${extraParentFiltering}` : '';
this.setParentFilterQuery(parentFiltering);
})
);
}
const siteFilter = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'` : '';
this.setParentFilterQuery(siteFilter);
return of(undefined);
}
private setParentFilterQuery(parentFiltering: string) {
if (this.lastParentFilterQuery) {
this.queryBuilderService.removeFilterQuery(this.lastParentFilterQuery);
}
this.lastParentFilterQuery = parentFiltering || null;
this.queryBuilderService.addFilterQuery(parentFiltering);
}
private setSearchScopeToNodes() {
@@ -619,7 +618,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
this.queryBuilderService.paging.skipCount = pagination.skipCount;
if (this.searchTerm.length > 0) {
this.queryBuilderService.update();
this.executeSearch(this.searchTerm);
}
}
@@ -696,4 +695,14 @@ export class ContentNodeSelectorPanelComponent implements OnInit {
skipCount: this.DEFAULT_PAGINATION.skipCount
};
}
private executeSearch(searchValue: string): void {
this.prepareDialogForNewSearch();
this.queryBuilderService.searchMode = 'formula';
const wildcardSuffix = this.queryBuilderService.wildcardsEnabled ? '*' : '';
this.queryBuilderService.userQuery = searchValue.length > 0 ? `(${searchValue}${wildcardSuffix})` : searchValue;
this.addCorrespondingNodeIdsQuery()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.queryBuilderService.execute(false));
}
}
@@ -34,14 +34,12 @@ import { VersionCompatibilityService } from './version-compatibility/version-com
import { contentAuthLoaderFactory } from './auth-loader/content-auth-loader-factory';
import { ContentAuthLoaderService } from './auth-loader/content-auth-loader.service';
import { CONTENT_UPLOAD_DIRECTIVES } from './upload';
import { MaterialModule } from './material.module';
import { AlfrescoApiService } from './services/alfresco-api.service';
import { AlfrescoApiNoAuthService } from './api-factories/alfresco-api-no-auth.service';
import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-factories/alfresco-api-v2-loader.service';
@NgModule({
imports: [
MaterialModule,
MatDatetimepickerModule,
MatNativeDatetimeModule,
...CONTENT_TAG_DIRECTIVES,
@@ -59,7 +57,6 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto
],
providers: [provideTranslations('adf-content-services', 'assets/adf-content-services')],
exports: [
MaterialModule,
...CONTENT_TAG_DIRECTIVES,
...DOCUMENT_LIST_DIRECTIVES,
...CONTENT_UPLOAD_DIRECTIVES,
@@ -16,20 +16,10 @@
/>
@if (form.controls['name'].dirty && form.controls['name'].invalid) {
<mat-error>
@if (form.controls['name'].hasError('required')) {
<span>
{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
</span>
}
@else if (form.controls['name'].hasError('message')) {
<span>
{{ form.controls['name'].errors?.message | translate }}
</span>
}
<span class="adf-error-text"
>@if (form.controls['name'].errors?.required) {{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}} @else if (form.controls['name'].errors?.message) {{{ form.controls['name'].errors?.message | translate }}}</span>
</mat-error>
}
</mat-form-field>
<mat-form-field class="adf-full-width adf-folder-dialog-form-field">
@@ -23,6 +23,8 @@ import { SimpleChange } from '@angular/core';
import { SearchHeaderQueryBuilderService } from './../../../search/services/search-header-query-builder.service';
import { FilterHeaderComponent } from './filter-header.component';
import { provideRouter } from '@angular/router';
import { SearchCategory } from '@alfresco/adf-content-services';
import { NodePaging } from '@alfresco/js-api';
describe('FilterHeaderComponent', () => {
let fixture: ComponentFixture<FilterHeaderComponent>;
@@ -150,7 +152,7 @@ describe('FilterHeaderComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(Object.keys(queryBuilder.filterRawParams).length).toBe(0);
expect(queryBuilder.filterRawParams['name']).toBeUndefined();
component.value = { name: 'pinocchio' };
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
@@ -158,11 +160,41 @@ describe('FilterHeaderComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(Object.keys(queryBuilder.filterRawParams).length).toBe(1);
expect(queryBuilder.filterRawParams['name']).toBe('pinocchio');
expect(queryBuilder.queryFragments['name']).toBe('pinocchio');
});
it('should build a wildcard field query fragment for the queryName filter when wildcards are enabled', async () => {
spyOn(queryBuilder, 'setCurrentRootFolderId');
spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false);
spyOnProperty(queryBuilder, 'wildcardsEnabled', 'get').and.returnValue(true);
queryBuilder.categories = [{ id: 'queryName', component: { settings: { field: 'cm:name' } } } as SearchCategory];
component.value = { queryName: 'pinocchio' };
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.filterRawParams['queryName']).toBe('pinocchio');
expect(queryBuilder.queryFragments['queryName']).toBe(`cm:name:'*pinocchio*'`);
});
it('should build a non-wildcard field query fragment for the queryName filter when wildcards are disabled', async () => {
spyOn(queryBuilder, 'setCurrentRootFolderId');
spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false);
spyOnProperty(queryBuilder, 'wildcardsEnabled', 'get').and.returnValue(false);
queryBuilder.categories = [{ id: 'queryName', component: { settings: { field: 'cm:name' } } } as SearchCategory];
component.value = { queryName: 'pinocchio' };
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.queryFragments['queryName']).toBe(`cm:name:'pinocchio'`);
});
it('should emit filterSelection when a filter is changed', (done) => {
spyOn(queryBuilder, 'getActiveFilters').and.returnValue([{ key: 'name', value: 'pinocchio' }]);
@@ -193,7 +225,7 @@ describe('FilterHeaderComponent', () => {
it('should emit searchResultsReady when search query builder executes', (done) => {
fixture.detectChanges(); // Initialize component (triggers ngOnInit)
const mockNodePaging: any = { list: { entries: [] } };
const mockNodePaging: NodePaging = { list: { entries: [] } };
component.searchResultsReady.subscribe((nodePaging) => {
expect(nodePaging).toBe(mockNodePaging);
@@ -115,9 +115,16 @@ export class FilterHeaderComponent implements OnInit, OnChanges {
const operator = this.searchFilterQueryBuilder.getOperatorForFilterId(key) || 'OR';
this.searchFilterQueryBuilder.filterRawParams[key] = this.value[key];
this.searchFilterQueryBuilder.queryFragments[key] = Array.isArray(this.value[key])
? this.value[key].join(` ${operator} `)
: this.value[key];
if (key === 'queryName') {
const filterConfig = this.searchFilterQueryBuilder.categories.find((category) => category.id === key);
const wildcardSuffix = this.searchFilterQueryBuilder.wildcardsEnabled ? '*' : '';
this.searchFilterQueryBuilder.queryFragments[key] =
`${filterConfig.component.settings.field}:'${wildcardSuffix}${this.value[key]}${wildcardSuffix}'`;
} else {
this.searchFilterQueryBuilder.queryFragments[key] = Array.isArray(this.value[key])
? this.value[key].join(` ${operator} `)
: this.value[key];
}
});
}
this.searchFilterQueryBuilder.setCurrentRootFolderId(currentFolderId);
@@ -24,6 +24,9 @@ import {
FavoritePagingList,
NodeEntry,
NodePaging,
Person,
PersonEntry,
ResultSetPaging,
Site,
SiteEntry,
SiteMember,
@@ -194,7 +197,57 @@ describe('CustomResourcesService', () => {
spyOn(customResourcesService, 'getRecentFiles').and.stub();
customResourcesService.loadFolderByNodeId('-recent-', pagination, ['include'], 'where', ['filters']);
expect(customResourcesService.getRecentFiles).toHaveBeenCalledWith('-me-', pagination, ['filters']);
expect(customResourcesService.getRecentFiles).toHaveBeenCalledWith('-me-', pagination, ['filters'], ['include']);
});
});
describe('getRecentFiles', () => {
const pagination: PaginationModel = { maxItems: 100, skipCount: 0 };
beforeEach(() => {
spyOn(customResourcesService.peopleApi, 'getPerson').and.returnValue(
Promise.resolve(new PersonEntry({ entry: new Person({ id: 'user' }) }))
);
});
it('should pass includeFields through to the search request, keeping the defaults', (done) => {
const searchSpy = spyOn(customResourcesService.searchApi, 'search').and.returnValue(Promise.resolve(new ResultSetPaging()));
customResourcesService.getRecentFiles('-me-', pagination, undefined, ['isFavorite']).subscribe(() => {
expect(searchSpy).toHaveBeenCalledTimes(1);
expect(searchSpy.calls.mostRecent().args[0].include).toEqual([
'path',
'properties',
'allowableOperations',
'aspectNames',
'isFavorite'
]);
done();
});
});
it('should keep the default include fields when no includeFields are provided', (done) => {
const searchSpy = spyOn(customResourcesService.searchApi, 'search').and.returnValue(Promise.resolve(new ResultSetPaging()));
customResourcesService.getRecentFiles('-me-', pagination).subscribe(() => {
expect(searchSpy.calls.mostRecent().args[0].include).toEqual(['path', 'properties', 'allowableOperations', 'aspectNames']);
done();
});
});
it('should not duplicate an includeField that is already a default', (done) => {
const searchSpy = spyOn(customResourcesService.searchApi, 'search').and.returnValue(Promise.resolve(new ResultSetPaging()));
customResourcesService.getRecentFiles('-me-', pagination, undefined, ['properties', 'isFavorite']).subscribe(() => {
expect(searchSpy.calls.mostRecent().args[0].include).toEqual([
'path',
'properties',
'allowableOperations',
'aspectNames',
'isFavorite'
]);
done();
});
});
});
@@ -73,9 +73,10 @@ export class CustomResourcesService {
* @param personId ID of the user
* @param pagination Specifies how to paginate the results
* @param filters Specifies additional filters to apply (joined with **AND**)
* @param includeFields List of data field names to include in the results
* @returns List of nodes for the recently used files
*/
getRecentFiles(personId: string, pagination: PaginationModel, filters?: string[]): Observable<ResultSetPaging> {
getRecentFiles(personId: string, pagination: PaginationModel, filters?: string[], includeFields: string[] = []): Observable<ResultSetPaging> {
const defaultFilter = [
'TYPE:"content"',
'-PATH:"//cm:wiki/*"',
@@ -121,7 +122,7 @@ export class CustomResourcesService {
language: SEARCH_LANGUAGE.AFTS
},
filterQueries,
include: ['path', 'properties', 'allowableOperations', 'aspectNames'],
include: [...new Set(['path', 'properties', 'allowableOperations', 'aspectNames', ...includeFields])],
sort: [
{
type: 'FIELD',
@@ -380,7 +381,7 @@ export class CustomResourcesService {
} else if (nodeId === '-favorites-') {
return this.loadFavorites(pagination, includeFields, where);
} else if (nodeId === '-recent-') {
return this.getRecentFiles('-me-', pagination, filters);
return this.getRecentFiles('-me-', pagination, filters, includeFields);
} else {
return of(null);
}
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Aktionsmenü öffnen"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "{{ name }} ausgewählt",
"DESELECTED": "{{name}} Auswahl aufgehoben"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Anfrage zum Bibliotheksbeitritt abgeschickt"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Bitte wählen Sie nicht mehr als {{ maxFiles }} Dateien.",
"FOLDER_SELECTED": "Ordner sind nicht kompatibel mit KI Agents."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "{{ name }} zu Favoriten hinzugefügt",
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Request sent to join this library"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Please select no more than {{ maxFiles }} files.",
"FOLDER_SELECTED": "Folders are not compatible with AI Agents."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Added {{ name }} to favorites",
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Request sent to join this library"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Please select no more than {{ maxFiles }} files.",
"FOLDER_SELECTED": "Folders are not compatible with AI Agents."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Added {{ name }} to favorites",
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Abrir menú de acciones"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "{{name}} seleccionado",
"DESELECTED": "{{name}} deseleccionado"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Se ha enviado una solicitud para unirse a esta biblioteca"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "No seleccione más de {{ maxFiles }} ficheros.",
"FOLDER_SELECTED": "Las carpetas no son compatibles con los Agentes IA."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Añadido {{ name }} a favoritos",
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Ouvrir le menu actions"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "{{name}} sélectionné",
"DESELECTED": "{{name}} désélectionné"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Demande envoyée pour rejoindre cette bibliothèque"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Veuillez ne pas sélectionner plus de {{ maxFiles }} fichiers.",
"FOLDER_SELECTED": "Les dossiers ne sont pas compatibles avec les agents IA."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Ajout de {{ name }} aux favoris",
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Apri menu azioni"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "{{name}} selezionato",
"DESELECTED": "{{name}} deselezionato"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Richiesta di partecipazione alla libreria inviata"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Selezionare più di {{ maxFiles }} file.",
"FOLDER_SELECTED": "Le cartelle non sono compatibili con gli agenti IA."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Aggiunto {{ name }} ai preferiti",
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Otwórz menu akcji"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "Wybrano {{name}}",
"DESELECTED": "Odznaczono {{name}}"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Żądanie dołączenia do tej biblioteki zostało wysłane"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Proszę wybrać nie więcej niż {{ maxFiles }} plików.",
"FOLDER_SELECTED": "Foldery nie są kompatybilne z agentami AI."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Dodano {{ name }} do ulubionych",
+2 -10
View File
@@ -629,8 +629,8 @@
"TOOLTIP": "Abrir menu de ações"
},
"ARIA": {
"SELECTED": "{{ name }} selected",
"DESELECTED": "{{ name }} deselected"
"SELECTED": "{{name}} selecionado",
"DESELECTED": "{{name}} desmarcado"
}
},
"LIBRARY": {
@@ -729,14 +729,6 @@
"JOIN_REQUESTED": "Solicitação enviada para aderir a esta biblioteca"
}
},
"KNOWLEDGE_RETRIEVAL": {
"SEARCH": {
"WARNINGS": {
"TOO_MANY_FILES_SELECTED": "Selecione no máximo {{ maxFiles }} ficheiros.",
"FOLDER_SELECTED": "As pastas não são compatíveis com os Agentes de IA."
}
}
},
"NODE_FAVORITE_DIRECTIVE": {
"MESSAGES": {
"NODE_ADDED": "Adicionado {{ name }} aos favoritos",
@@ -1,92 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { MatRippleModule, MatOptionModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { MatTreeModule } from '@angular/material/tree';
import { MatBadgeModule } from '@angular/material/badge';
/** @deprecated this module is deprecated and will be removed in future versions */
@NgModule({
imports: [
MatButtonModule,
MatAutocompleteModule,
MatChipsModule,
MatDialogModule,
MatIconModule,
MatCardModule,
MatInputModule,
MatListModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatRippleModule,
MatMenuModule,
MatOptionModule,
MatExpansionModule,
MatSelectModule,
MatCheckboxModule,
MatDatepickerModule,
MatSlideToggleModule,
MatRadioModule,
MatSliderModule,
MatTreeModule,
MatBadgeModule
],
exports: [
MatButtonModule,
MatAutocompleteModule,
MatChipsModule,
MatDialogModule,
MatIconModule,
MatCardModule,
MatInputModule,
MatListModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatRippleModule,
MatMenuModule,
MatOptionModule,
MatExpansionModule,
MatSelectModule,
MatCheckboxModule,
MatDatepickerModule,
MatSlideToggleModule,
MatRadioModule,
MatSliderModule,
MatTreeModule,
MatBadgeModule
]
})
export class MaterialModule {}
@@ -1,18 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './public-api';
@@ -1,18 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './services';
@@ -1,18 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './prediction.service';
@@ -1,53 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { PredictionService } from './prediction.service';
import { TestBed } from '@angular/core/testing';
import { Prediction, PredictionEntry, PredictionPaging, PredictionPagingList, ReviewStatus } from '@alfresco/js-api';
describe('PredictionService', () => {
let service: PredictionService;
const mockPredictionPaging = (): PredictionPaging => {
const prediction = new Prediction();
prediction.id = 'test id';
const predictionEntry = new PredictionEntry({ entry: prediction });
const predictionPagingList = new PredictionPagingList({ entries: [predictionEntry] });
return new PredictionPaging({ list: predictionPagingList });
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: []
});
service = TestBed.inject(PredictionService);
});
it('should call getPredictions on PredictionsApi with nodeId', () => {
spyOn(service.predictionsApi, 'getPredictions').and.returnValue(Promise.resolve(mockPredictionPaging()));
service.getPredictions('test id');
expect(service.predictionsApi.getPredictions).toHaveBeenCalledWith('test id');
});
it('should call reviewPrediction on PredictionsApi with predictionId and reviewStatus', () => {
spyOn(service.predictionsApi, 'reviewPrediction').and.returnValue(Promise.resolve());
service.reviewPrediction('test id', ReviewStatus.CONFIRMED);
expect(service.predictionsApi.reviewPrediction).toHaveBeenCalledWith('test id', ReviewStatus.CONFIRMED);
});
});
@@ -1,50 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { Injectable, inject } from '@angular/core';
import { PredictionsApi, PredictionPaging, ReviewStatus, LazyApi } from '@alfresco/js-api';
import { from, Observable } from 'rxjs';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
@Injectable({ providedIn: 'root' })
export class PredictionService {
private readonly apiService = inject(AlfrescoApiService);
@LazyApi((self: PredictionService) => new PredictionsApi(self.apiService.getInstance()))
declare readonly predictionsApi: PredictionsApi;
/**
* Get predictions for a given node
*
* @param nodeId The identifier of node.
* @returns Observable<PredictionPaging>
*/
getPredictions(nodeId: string): Observable<PredictionPaging> {
return from(this.predictionsApi.getPredictions(nodeId));
}
/**
* Review a prediction
*
* @param predictionId The identifier of prediction.
* @param reviewStatus Review status to apply.
* @returns Observable<void>
*/
reviewPrediction(predictionId: string, reviewStatus: ReviewStatus): Observable<void> {
return from(this.predictionsApi.reviewPrediction(predictionId, reviewStatus));
}
}
@@ -1,18 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './public-api';
@@ -1,22 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface SearchAiInputState {
active: boolean;
selectedAgentId?: string;
searchTerm?: string;
}
@@ -1,19 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './services/search-ai.service';
export * from './models/search-ai-input-state';
@@ -1,246 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { AiAnswerEntry, KnowledgeRetrievalConfigEntry, Node, QuestionModel, QuestionRequest } from '@alfresco/js-api';
import { SearchAiService } from './search-ai.service';
import { SearchAiInputState } from '../models/search-ai-input-state';
import { TranslateService } from '@ngx-translate/core';
describe('SearchAiService', () => {
let service: SearchAiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: []
});
service = TestBed.inject(SearchAiService);
});
describe('ask', () => {
it('should load information about question', (done) => {
const question: QuestionModel = {
question: 'some question',
questionId: 'some id',
restrictionQuery: { nodesIds: ['nodeId1', 'nodeId2'] }
};
spyOn(service.searchAiApi, 'ask').and.returnValue(Promise.resolve(question));
const questionRequest: QuestionRequest = {
question: 'some question',
nodeIds: ['nodeId1', 'nodeId2'],
agentId: 'some id'
};
service.ask(questionRequest).subscribe((questionResponse) => {
expect(questionResponse).toBe(question);
expect(service.searchAiApi.ask).toHaveBeenCalledWith([questionRequest]);
done();
});
});
});
describe('getAnswer', () => {
it('should load information about question', (done) => {
const questionId = 'some id';
const answer: AiAnswerEntry = {
entry: {
answer: 'Some answer 1',
complete: true,
question: 'Some question',
objectReferences: [
{
objectId: 'some id 1',
references: [
{
referenceId: 'some reference id 1',
rank: 1,
rankScore: 0.005
},
{
referenceId: 'some reference id 2',
rank: 2,
rankScore: 0.004
}
]
},
{
objectId: 'some id 2',
references: [
{
referenceId: 'some reference id 3',
rank: 1,
rankScore: 0.005
},
{
referenceId: 'some reference id 4',
rank: 2,
rankScore: 0.004
}
]
}
]
}
};
spyOn(service.searchAiApi, 'getAnswer').and.returnValue(Promise.resolve(answer));
service.getAnswer(questionId).subscribe((answerResponse) => {
expect(answerResponse).toBe(answer);
expect(service.searchAiApi.getAnswer).toHaveBeenCalledWith(questionId);
done();
});
});
});
describe('getConfig', () => {
it('should load knowledge retrieval configuration', (done) => {
const config: KnowledgeRetrievalConfigEntry = {
entry: {
knowledgeRetrievalUrl: 'https://some-url'
}
};
spyOn(service.searchAiApi, 'getConfig').and.returnValue(Promise.resolve(config));
service.getConfig().subscribe((configResponse) => {
expect(configResponse).toBe(config);
expect(service.searchAiApi.getConfig).toHaveBeenCalled();
done();
});
});
});
describe('updateSearchAiInputState', () => {
it('should trigger toggleSearchAiInput$', () => {
const state: SearchAiInputState = {
active: true,
selectedAgentId: 'some id'
};
service.updateSearchAiInputState(state);
service.toggleSearchAiInput$.subscribe((receivedState) => {
expect(receivedState).toBe(state);
});
});
});
describe('checkSearchAvailability', () => {
let translateService: TranslateService;
const tooManyFilesSelectedError = 'Please select no more than 100 files.';
const folderSelectedError = 'Folders are not compatible with AI Agents.';
beforeEach(() => {
translateService = TestBed.inject(TranslateService);
spyOn(translateService, 'instant').and.callFake((key) => {
switch (key) {
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED':
return tooManyFilesSelectedError;
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED':
return folderSelectedError;
default:
return '';
}
});
});
it('should not return error if user did not select any files', () => {
expect(
service.checkSearchAvailability({
count: 0,
nodes: [],
libraries: [],
isEmpty: true
})
).toEqual('');
});
it('should return error for too many files selected', () => {
expect(
service.checkSearchAvailability({
count: 101,
nodes: [],
libraries: [],
isEmpty: false
})
).toBe(tooManyFilesSelectedError);
expect(translateService.instant).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED', {
maxFiles: 100,
key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED'
});
});
it('should return error for folder selected', () => {
expect(
service.checkSearchAvailability({
count: 1,
nodes: [
{
entry: {
isFolder: true
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(folderSelectedError);
});
it('should return error for folder and if non text mime type node is selected', () => {
expect(
service.checkSearchAvailability({
count: 1,
nodes: [
{
entry: {
isFolder: true,
content: {
mimeType: 'some mime type',
mimeTypeName: 'some mime type',
sizeInBytes: 100
}
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(folderSelectedError);
});
it('should return more than one error if more validators detected issues', () => {
expect(
service.checkSearchAvailability({
count: 101,
nodes: [
{
entry: {
isFolder: true,
content: {
mimeType: 'image/jpeg',
mimeTypeName: 'image/jpeg',
sizeInBytes: 100
}
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(`${tooManyFilesSelectedError} ${folderSelectedError}`);
});
});
});
@@ -1,105 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 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 { Injectable, inject } from '@angular/core';
import { AiAnswerEntry, KnowledgeRetrievalConfigEntry, LazyApi, QuestionModel, QuestionRequest, SearchAiApi } from '@alfresco/js-api';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { SelectionState } from '@alfresco/adf-extensions';
import { TranslateService } from '@ngx-translate/core';
import { SearchAiInputState } from '../models/search-ai-input-state';
import { AlfrescoApiService } from '../../services';
@Injectable({
providedIn: 'root'
})
export class SearchAiService {
private readonly apiService = inject(AlfrescoApiService);
private readonly translateService = inject(TranslateService);
private readonly toggleSearchAiInput = new BehaviorSubject<SearchAiInputState>({
active: false
});
@LazyApi((self: SearchAiService) => new SearchAiApi(self.apiService.getInstance()))
declare readonly searchAiApi: SearchAiApi;
toggleSearchAiInput$ = this.toggleSearchAiInput.asObservable();
/**
* Update the state of the search AI input.
*
* @param state The new state of the search AI input.
*/
updateSearchAiInputState(state: SearchAiInputState): void {
this.toggleSearchAiInput.next(state);
}
/**
* Ask a question to the AI.
*
* @param question The question to ask.
* @returns QuestionModel object containing information about questions.
*/
ask(question: QuestionRequest): Observable<QuestionModel> {
return from(this.searchAiApi.ask([question]));
}
/**
* Get an answer to specific question.
*
* @param questionId The ID of the question to get an answer for.
* @returns AiAnswerEntry object containing the answer.
*/
getAnswer(questionId: string): Observable<AiAnswerEntry> {
return from(this.searchAiApi.getAnswer(questionId));
}
/**
* Get the knowledge retrieval configuration.
*
* @returns KnowledgeRetrievalConfigEntry object containing the configuration.
*/
getConfig(): Observable<KnowledgeRetrievalConfigEntry> {
return from(this.searchAiApi.getConfig());
}
/**
* Check if using of search is possible (if all conditions are met).
*
* @param selectedNodesState information about selected nodes.
* @param maxSelectedNodes max number of selected nodes. Default 100.
* @returns string with error if any condition is not met, empty string otherwise.
*/
checkSearchAvailability(selectedNodesState: SelectionState, maxSelectedNodes = 100): string {
const messages: {
key: string;
[parameter: string]: number | string;
}[] = [];
if (selectedNodesState.count > maxSelectedNodes) {
messages.push({
key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED',
maxFiles: maxSelectedNodes
});
}
if (selectedNodesState.nodes.some((node) => node.entry.isFolder)) {
messages.push({
key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED'
});
}
return messages.map((message) => this.translateService.instant(message.key, message)).join(' ');
}
}
@@ -44,7 +44,7 @@ describe('SearchCheckListComponent', () => {
queryFragments: {},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy()
execute: jasmine.createSpy('execute')
} as any;
});
@@ -134,7 +134,7 @@ describe('SearchCheckListComponent', () => {
component.reset();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
@@ -226,7 +226,7 @@ describe('SearchCheckListComponent', () => {
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).not.toHaveBeenCalled();
expect(component.context.execute).not.toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', () => {
@@ -107,7 +107,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
this.clearOptions();
if (this.id && this.context && this.enableChangeUpdate) {
this.updateDisplayValue();
this.context.update();
this.context.execute();
}
}
@@ -128,7 +128,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
if (this.id && this.context) {
this.updateDisplayValue();
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -178,7 +178,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
this.context.queryFragments[this.id] = query;
this.updateDisplayValue();
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -309,6 +309,13 @@ describe('SearchChipAutocompleteInputComponent', () => {
expect(inputChangedSpy).toHaveBeenCalledOnceWith('test-value');
});
it('should not emit input value when input is empty', async () => {
const inputChangedSpy = spyOn(component.inputChanged, 'emit');
enterNewInputValue('');
await fixture.whenStable();
expect(inputChangedSpy).not.toHaveBeenCalled();
});
describe('isOptionSelected', () => {
beforeEach(() => {
component.autocompleteOptions = [{ value: 'option1' }, { value: 'option2' }];
@@ -108,8 +108,10 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnChanges {
takeUntilDestroyed(this.destroyRef)
)
.subscribe((value: string) => {
this.filteredOptions = this.filter(this.autocompleteOptions, value);
this.inputChanged.emit(value);
if (value) {
this.filteredOptions = this.filter(this.autocompleteOptions, value);
this.inputChanged.emit(value);
}
});
this.onReset$?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.reset());
this.selectedOptions = this.preselectedOptions ?? [];
@@ -79,7 +79,7 @@ describe('SearchDateRangeTabbedComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = {
hideDefaultAction: false,
@@ -210,7 +210,7 @@ describe('SearchDateRangeTabbedComponent', () => {
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(anyMockDate);
});
it('should trigger context.update() when values are submitted', () => {
it('should trigger context.execute() when values are submitted', () => {
component.onDateRangedValueChanged(betweenMockData, 'createdDate');
component.onDateRangedValueChanged(inLastMockData, 'modifiedDate');
fixture.detectChanges();
@@ -221,7 +221,7 @@ describe('SearchDateRangeTabbedComponent', () => {
`createdDate:['${formatISO(startOfDay(betweenMockData.betweenStartDate))}' TO '${formatISO(endOfDay(betweenMockData.betweenEndDate))}']` +
` AND modifiedDate:['${formatISO(startOfDay(inLastStartDate))}' TO '${formatISO(endOfToday())}']`;
expect(component.context.queryFragments['dateRange']).toEqual(query);
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should clear values and search filter when widget is reset', (done) => {
@@ -231,7 +231,7 @@ describe('SearchDateRangeTabbedComponent', () => {
expect(component.combinedDisplayValue).toBe('');
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.queryFragments['dateRange']).toEqual('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
component.fields.forEach((field) => expect(component.context.filterRawParams[field]).toBeUndefined());
done();
});
@@ -138,7 +138,7 @@ export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
this.context.queryFragments[this.id] = this.combinedQuery;
this.displayValue$.next(this.combinedDisplayValue);
if (this.id && this.context && updateContext) {
this.context.update();
this.context.execute();
}
}
onDateRangedValueChanged(value: Partial<SearchDateRange>, field: string) {
@@ -46,7 +46,7 @@ describe('SearchDatetimeRangeComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = { field: 'cm:created' };
});
@@ -128,7 +128,7 @@ describe('SearchDatetimeRangeComponent', () => {
component.reset();
expect(component.context.queryFragments.createdDatetimeRange).toEqual('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should update the query in UTC format when values change', async () => {
@@ -150,7 +150,7 @@ describe('SearchDatetimeRangeComponent', () => {
expect(component.context.queryFragments[component.id]).toEqual(expectedQuery);
expect(component.context.filterRawParams[component.id]).toEqual({ start: expectedFromDate, end: expectedToDate });
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should be able to update the query in UTC format from a GMT format', async () => {
@@ -175,7 +175,7 @@ describe('SearchDatetimeRangeComponent', () => {
expect(startDate).toContain('2021-02-24');
expect(endDate).toContain('2021-02-28');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
// Verify the query structure is correct without hardcoding exact timezone values
const query = component.context.queryFragments[component.id];
@@ -171,7 +171,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
filterParam.end = end;
this.updateDisplayValue();
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -242,7 +242,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
private updateQuery() {
if (this.id && this.context) {
this.updateDisplayValue();
this.context.update();
this.context.execute();
}
}
@@ -44,7 +44,7 @@ describe('SearchFacetFieldComponent', () => {
});
it('should update bucket model and query builder on facet toggle', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough();
const event: any = { checked: true };
@@ -57,12 +57,12 @@ describe('SearchFacetFieldComponent', () => {
expect(bucket.checked).toBeTruthy();
expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(facetField.field, bucket);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
});
it('should update bucket model and query builder on facet un-toggle', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
const event: any = { checked: false };
@@ -75,12 +75,12 @@ describe('SearchFacetFieldComponent', () => {
component.onToggleBucket(event, facetField, bucket);
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(facetField.field, bucket);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
});
it('should unselect facet query and update builder', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
const event: any = { checked: false };
@@ -94,17 +94,17 @@ describe('SearchFacetFieldComponent', () => {
expect(query.checked).toEqual(false);
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(facetField.field, query);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
});
it('should update query builder only when has bucket to unselect', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
const field: FacetField = { field: 'f1', label: 'f1' };
component.onToggleBucket({ checked: true } as any, field, null);
expect(queryBuilder.update).not.toHaveBeenCalled();
expect(queryBuilder.execute).not.toHaveBeenCalled();
});
it('should allow to to reset selected buckets', () => {
@@ -166,7 +166,7 @@ describe('SearchFacetFieldComponent', () => {
});
it('should update query builder upon resetting buckets', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
const buckets: FacetFieldBucket[] = [
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
@@ -183,6 +183,6 @@ describe('SearchFacetFieldComponent', () => {
fixture.detectChanges();
component.resetSelectedBuckets(field);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
});
});
@@ -69,7 +69,7 @@ export class SearchFacetFieldComponent implements FacetWidget {
this.searchFacetFiltersService.updateSelectedBuckets();
if (this.canUpdateOnChange) {
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
}
}
@@ -81,7 +81,7 @@ export class SearchFacetFieldComponent implements FacetWidget {
this.searchFacetFiltersService.updateSelectedBuckets();
if (this.canUpdateOnChange) {
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
}
}
@@ -101,7 +101,7 @@ export class SearchFacetFieldComponent implements FacetWidget {
}
this.searchFacetFiltersService.updateSelectedBuckets();
if (this.canUpdateOnChange) {
this.queryBuilder.update();
this.queryBuilder.execute();
}
}
}
@@ -125,11 +125,11 @@ export class SearchFacetFieldComponent implements FacetWidget {
reset(): void {
this.resetSelectedBuckets(this.field);
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
submitValues(): void {
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
}
@@ -47,7 +47,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = {
field: 'test',
@@ -102,7 +102,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
clearBtn.click();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.selectedOptions).toEqual([]);
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
@@ -116,7 +116,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
applyBtn.click();
fixture.detectChanges();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('test:"option2" OR test:"option1"');
expect(component.context.filterRawParams[component.id]).toEqual([{ value: 'option2' }, { value: 'option1' }]);
@@ -111,7 +111,6 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
this.selectedOptions = selectedOptions;
if (this.enableChangeUpdate) {
this.updateQuery();
this.context.update();
}
}
@@ -160,7 +159,7 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
}
this.context.queryFragments[this.id] = queryFragments.join(' OR ');
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -33,7 +33,7 @@ describe('SearchFacetTabbedContentComponent', () => {
let queryBuilder: SearchQueryBuilderService;
let searchFacetService: SearchFacetFiltersService;
let loader: HarnessLoader;
let queryBuilderUpdateSpy: jasmine.Spy;
let queryBuilderExecuteSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -44,7 +44,7 @@ describe('SearchFacetTabbedContentComponent', () => {
component = fixture.componentInstance;
queryBuilder = TestBed.inject(SearchQueryBuilderService);
searchFacetService = TestBed.inject(SearchFacetFiltersService);
queryBuilderUpdateSpy = spyOn(queryBuilder, 'update').and.stub();
queryBuilderExecuteSpy = spyOn(queryBuilder, 'execute').and.stub();
const facet1: FacetField = { type: 'field', label: 'field', field: 'field', buckets: new SearchFilterList() };
const facet2: FacetField = { type: 'field', label: 'field2', field: 'field2', buckets: new SearchFilterList() };
@@ -185,7 +185,7 @@ describe('SearchFacetTabbedContentComponent', () => {
spyOn(searchFacetService, 'updateSelectedBuckets').and.callThrough();
component.submitValues();
expect(component.submitValues).toHaveBeenCalled();
expect(queryBuilderUpdateSpy).toHaveBeenCalled();
expect(queryBuilderExecuteSpy).toHaveBeenCalled();
expect(component.updateDisplayValue).toHaveBeenCalled();
expect(searchFacetService.updateSelectedBuckets).toHaveBeenCalled();
});
@@ -193,12 +193,12 @@ describe('SearchFacetTabbedContentComponent', () => {
it('should update search query and display value on reset', () => {
spyOn(component, 'updateDisplayValue').and.callThrough();
component.reset();
expect(queryBuilderUpdateSpy).toHaveBeenCalled();
expect(queryBuilderExecuteSpy).toHaveBeenCalled();
expect(component.updateDisplayValue).toHaveBeenCalled();
});
it('should not call queryBuilder.update on options change', () => {
it('should not call queryBuilder.execute on options change', () => {
component.onOptionsChange([{ value: 'test' }], 'field');
expect(queryBuilderUpdateSpy).not.toHaveBeenCalled();
expect(queryBuilderExecuteSpy).not.toHaveBeenCalled();
});
});
@@ -111,14 +111,14 @@ export class SearchFacetTabbedContentComponent implements OnInit, OnChanges, Fac
this.resetSubject$.next();
this.updateUserFacetBuckets();
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
submitValues() {
this.updateUserFacetBuckets();
this.searchFacetFiltersService.updateSelectedBuckets();
this.updateDisplayValue();
this.queryBuilder.update();
this.queryBuilder.execute();
}
optionComparator(option1: AutocompleteOption, option2: AutocompleteOption): boolean {
@@ -41,7 +41,7 @@ describe('SearchFacetChipComponent', () => {
fixture = TestBed.createComponent(SearchFacetChipComponent);
component = fixture.componentInstance;
queryBuilder = TestBed.inject(SearchQueryBuilderService);
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
component.field = { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() };
fixture.detectChanges();
@@ -55,7 +55,7 @@ describe('SearchFacetChipComponent', () => {
const applyButton = await menu.getHarness(MatButtonHarness.with({ selector: '#apply-filter-button' }));
await applyButton.click();
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
});
it('should update search query on cancel click', async () => {
@@ -65,7 +65,7 @@ describe('SearchFacetChipComponent', () => {
const cancelButton = await menu.getHarness(MatButtonHarness.with({ selector: '#cancel-filter-button' }));
await cancelButton.click();
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
});
it('should display arrow down icon and not disable the chip when items are loaded', async () => {
@@ -223,7 +223,7 @@ describe('SearchFilterChipsComponent', () => {
});
it('should update query builder upon resetting selected queries', async () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
const queryResponse = {
@@ -247,7 +247,7 @@ describe('SearchFilterChipsComponent', () => {
facetField.resetSelectedBuckets(queryResponse);
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) {
expect(entry.checked).toEqual(false);
@@ -276,7 +276,7 @@ describe('SearchFilterChipsComponent', () => {
});
it('should be update the search query when name changed', async () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
appConfigService.config.search = searchFilter;
queryBuilder.resetToDefaults();
@@ -290,7 +290,7 @@ describe('SearchFilterChipsComponent', () => {
const inputElement = fixture.debugElement.query(By.css('[data-automation-id="search-field-Name"] input'));
inputElement.triggerEventHandler('change', { target: { value: '*' } });
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
queryBuilder.executed.next(mockSearchResult);
@@ -404,7 +404,7 @@ describe('SearchFilterChipsComponent', () => {
queryBuilder.executed.next(mockSearchResult);
fixture.detectChanges();
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Size facet queries"]`));
searchChip.triggerEventHandler('click', { stopPropagation: () => null });
@@ -441,7 +441,7 @@ describe('SearchFilterChipsComponent', () => {
await filteredMenu[0].check();
expect(await filteredMenu[0].getLabelText()).toEqual('Extra Small (10239)');
expect(queryBuilder.update).toHaveBeenCalledTimes(1);
expect(queryBuilder.execute).toHaveBeenCalledTimes(1);
});
});
});
@@ -45,7 +45,7 @@ describe('SearchWidgetChipComponent', () => {
queryBuilder = TestBed.inject(SearchQueryBuilderService);
fixture = TestBed.createComponent(SearchWidgetChipComponent);
component = fixture.componentInstance;
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
component.category = simpleCategories[1];
fixture.detectChanges();
@@ -60,7 +60,7 @@ describe('SearchWidgetChipComponent', () => {
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
applyButton.triggerEventHandler('click', {});
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
});
it('should update search query on cancel click', async () => {
@@ -69,7 +69,7 @@ describe('SearchWidgetChipComponent', () => {
const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button'));
applyButton.triggerEventHandler('click', {});
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
});
it('should display arrow down icon', async () => {
@@ -240,7 +240,7 @@ describe('SearchFilterComponent', () => {
});
it('should update query builder upon resetting selected queries', () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
const queryResponse = {
@@ -258,7 +258,7 @@ describe('SearchFilterComponent', () => {
facetField.resetSelectedBuckets(queryResponse);
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3);
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) {
expect(entry.checked).toEqual(false);
@@ -320,7 +320,7 @@ describe('SearchFilterComponent', () => {
});
it('should be update the search query when name changed', async () => {
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
appConfigService.config.search = searchFilter;
queryBuilder.resetToDefaults();
@@ -333,7 +333,7 @@ describe('SearchFilterComponent', () => {
const inputElement = fixture.debugElement.query(By.css('[data-automation-id="expansion-panel-Name"] input'));
inputElement.triggerEventHandler('change', { target: { value: '*' } });
expect(queryBuilder.update).toHaveBeenCalled();
expect(queryBuilder.execute).toHaveBeenCalled();
queryBuilder.executed.next(mockSearchResult);
fixture.detectChanges();
@@ -441,7 +441,7 @@ describe('SearchFilterComponent', () => {
queryBuilder.executed.next(mockSearchResult);
fixture.detectChanges();
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
const panel = await loader.getHarness(
MatExpansionPanelHarness.with({
@@ -470,7 +470,7 @@ describe('SearchFilterComponent', () => {
expect(labels).toEqual(stepOne);
await checkboxes[0].check();
expect(queryBuilder.update).toHaveBeenCalledTimes(1);
expect(queryBuilder.execute).toHaveBeenCalledTimes(1);
});
it('should preserve the filter state if other fields edited', async () => {
@@ -481,7 +481,7 @@ describe('SearchFilterComponent', () => {
fixture.detectChanges();
queryBuilder.executed.next(mockSearchResult);
fixture.detectChanges();
spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'execute').and.stub();
const inputElement = fixture.debugElement.query(By.css(`${panel1Selector} input`));
inputElement.nativeElement.value = 'my';
@@ -511,7 +511,7 @@ describe('SearchFilterComponent', () => {
const checkedOption = await panel1.getHarness(MatCheckboxHarness.with({ checked: true }));
expect(await checkedOption.getLabelText()).toBe('my1 (806)');
expect(queryBuilder.update).toHaveBeenCalledTimes(2);
expect(queryBuilder.execute).toHaveBeenCalledTimes(2);
});
it('should reset the query fragments when reset All is clicked', () => {
@@ -38,7 +38,7 @@ describe('SearchLogicalFilterComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = { field: 'field1,field2', allowUpdateOnChange: true, hideDefaultAction: false };
fixture.detectChanges();
@@ -136,7 +136,7 @@ describe('SearchLogicalFilterComponent', () => {
spyOn(component.displayValue$, 'next');
component.reset();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.getCurrentValue()).toEqual({ matchAll: '', matchAny: '', exclude: '', matchExact: '' });
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
@@ -145,7 +145,7 @@ describe('SearchLogicalFilterComponent', () => {
it('should form correct query from match all field', () => {
enterNewPhrase(' test1 test2 ', 0);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test1" AND field1:"test2") OR (field2:"test1" AND field2:"test2"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
@@ -153,7 +153,7 @@ describe('SearchLogicalFilterComponent', () => {
it('should form correct query from match any field', () => {
enterNewPhrase(' test3 test4', 1);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test3" OR field1:"test4") OR (field2:"test3" OR field2:"test4"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
@@ -161,7 +161,7 @@ describe('SearchLogicalFilterComponent', () => {
it('should form correct query from exclude field', () => {
enterNewPhrase('test5 test6 ', 2);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe(
'((NOT field1:"test5" AND NOT field1:"test6") AND (NOT field2:"test5" AND NOT field2:"test6"))'
);
@@ -171,7 +171,7 @@ describe('SearchLogicalFilterComponent', () => {
it('should form correct query from match exact field and trim it', () => {
enterNewPhrase(' test7 test8 ', 3);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test7 test8") OR (field2:"test7 test8"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
@@ -186,7 +186,7 @@ describe('SearchLogicalFilterComponent', () => {
const subQuery2 = '((field1:"test2") OR (field2:"test2"))';
const subQuery3 = '((NOT field1:"test3") AND (NOT field2:"test3"))';
const subQuery4 = '((field1:"test4") OR (field2:"test4"))';
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe(`${subQuery1} AND ${subQuery2} AND ${subQuery4} AND ${subQuery3}`);
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
@@ -128,7 +128,7 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
});
this.context.queryFragments[this.id] = query;
if (updateContext) {
this.context.update();
this.context.execute();
}
} else {
this.reset(updateContext);
@@ -154,7 +154,7 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
this.clearSearchInputs();
this.context.filterRawParams[this.id] = this.searchCondition;
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -37,7 +37,7 @@ describe('SearchNumberRangeComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
});
@@ -64,7 +64,7 @@ describe('SearchNumberRangeComponent', () => {
component.reset();
expect(component.context.queryFragments.contentSize).toEqual('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
@@ -82,7 +82,7 @@ describe('SearchNumberRangeComponent', () => {
const expectedQuery = 'cm:content.size:[10 TO 20]';
expect(component.context.queryFragments[component.id]).toEqual(expectedQuery);
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
expect(component.context.filterRawParams[component.id].from).toEqual('10');
expect(component.context.filterRawParams[component.id].to).toEqual('20');
});
@@ -123,7 +123,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
filterParam.from = model.from;
filterParam.to = model.to;
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -178,7 +178,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
this.context.filterRawParams[this.id] = undefined;
this.updateDisplayValue();
if (this.enableChangeUpdate && updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -186,7 +186,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
reset(updateContext = true) {
this.clear();
if (this.id && this.context && updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -72,7 +72,7 @@ describe('SearchPropertiesComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
});
@@ -208,7 +208,7 @@ describe('SearchPropertiesComponent', () => {
component.submitValues();
expect(component.displayValue$.next).not.toHaveBeenCalled();
expect(component.context.update).not.toHaveBeenCalled();
expect(component.context.execute).not.toHaveBeenCalled();
});
it('should not search when context is not set', () => {
@@ -227,7 +227,7 @@ describe('SearchPropertiesComponent', () => {
fileExtensions: undefined,
fileSizeCondition: { fileSize: null, fileSizeOperator: FileSizeOperator.AT_LEAST, fileSizeUnit: FileSizeUnit.KB }
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by at least KB by default when any size is typed', () => {
@@ -246,7 +246,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by at most MB after selecting proper options', () => {
@@ -271,7 +271,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.MB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by exactly GB after selecting proper options', () => {
@@ -296,7 +296,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.GB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by single file type', () => {
@@ -314,7 +314,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by multiple file types', () => {
@@ -331,7 +331,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should search by file size and type', () => {
@@ -351,7 +351,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
});
@@ -439,7 +439,7 @@ describe('SearchPropertiesComponent', () => {
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
});
@@ -484,7 +484,7 @@ describe('SearchPropertiesComponent', () => {
fileSizeUnit: FileSizeUnit.MB
}
});
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
});
@@ -206,7 +206,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
this.context.queryFragments[this.id] = '';
this.context.filterRawParams[this.id] = undefined;
if (updateContext) {
this.context.update();
this.context.execute();
}
}
this.reset$.next();
@@ -272,7 +272,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
this.displayValue$.next(displayedValue);
this.context.queryFragments[this.id] = query;
if (updateContext) {
this.context.update();
this.context.execute();
}
}
@@ -43,7 +43,7 @@ describe('SearchRadioComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = { options: sizeOptions } as any;
});
@@ -93,6 +93,15 @@ describe('SearchRadioComponent', () => {
expect(component.context.filterRawParams[component.id]).toBe(sizeOptions[0].value);
});
it('should call context.execute when reset is called', async () => {
const group = await loader.getHarness(MatRadioGroupHarness);
await group.checkRadioButton({ selector: `[data-automation-id="search-radio-${sizeOptions[2].name}"]` });
component.reset();
expect(component.context.execute).toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', async () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
@@ -114,7 +114,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
this.setValue(this.value);
this.updateDisplayValue();
if (updateContext) {
this.context.update();
this.context.execute();
}
}
@@ -129,7 +129,6 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
this.context.filterRawParams[this.id] = newValue;
if (this.enableChangeUpdate) {
this.updateDisplayValue();
this.context.update();
}
}
@@ -164,7 +163,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
this.setValue(initialValue);
this.updateDisplayValue();
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -36,7 +36,7 @@ describe('SearchSliderComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute')
} as any;
component.settings = {
field: 'field1',
@@ -64,7 +64,7 @@ describe('SearchSliderComponent', () => {
component.onChangedHandler();
expect(component.context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
expect(component.context.filterRawParams[component.id]).toEqual(10);
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
component.value = 20;
component.onChangedHandler();
@@ -81,7 +81,7 @@ describe('SearchSliderComponent', () => {
expect(component.value).toBe(10);
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBe(null);
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should reset to 0 if min not provided', () => {
@@ -93,7 +93,7 @@ describe('SearchSliderComponent', () => {
expect(component.value).toBe(0);
expect(component.context.queryFragments['slider']).toBe('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', async () => {
@@ -135,7 +135,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
this.context.queryFragments[this.id] = `${this.settings.field}:[0 TO ${value}]`;
}
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -22,11 +22,13 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatInputHarness } from '@angular/material/input/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { ReplaySubject } from 'rxjs';
import { AppConfigService } from '@alfresco/adf-core';
describe('SearchTextComponent', () => {
let loader: HarnessLoader;
let fixture: ComponentFixture<SearchTextComponent>;
let component: SearchTextComponent;
let appConfig: AppConfigService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -34,6 +36,7 @@ describe('SearchTextComponent', () => {
});
fixture = TestBed.createComponent(SearchTextComponent);
component = fixture.componentInstance;
appConfig = TestBed.inject(AppConfigService);
component.id = 'text';
component.settings = {
pattern: `cm:name:'(.*?)'`,
@@ -46,7 +49,10 @@ describe('SearchTextComponent', () => {
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
execute: jasmine.createSpy('execute'),
get wildcardsEnabled(): boolean {
return appConfig.get<boolean>('search-wildcards-enabled', true);
}
} as any;
loader = TestbedHarnessEnvironment.loader(fixture);
@@ -67,7 +73,10 @@ describe('SearchTextComponent', () => {
expect(component.value).toEqual('');
});
it('should update query builder on change', () => {
it('should update query builder on change when change updates are enabled', () => {
component.settings.allowUpdateOnChange = true;
fixture.detectChanges();
component.onChangedHandler({
target: {
value: 'top-secret.doc'
@@ -77,10 +86,27 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('top-secret.doc');
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'top-secret.doc'`);
expect(component.context.filterRawParams[component.id]).toBe('top-secret.doc');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.execute).toHaveBeenCalled();
});
it('should reset query builder', () => {
it('should not update query builder on change when change updates are disabled', () => {
fixture.detectChanges();
component.onChangedHandler({
target: {
value: 'top-secret.doc'
}
});
expect(component.value).toBe('top-secret.doc');
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.execute).not.toHaveBeenCalled();
});
it('should reset query builder when change updates are enabled', () => {
component.settings.allowUpdateOnChange = true;
fixture.detectChanges();
component.onChangedHandler({
target: {
value: 'top-secret.doc'
@@ -125,13 +151,21 @@ describe('SearchTextComponent', () => {
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
it('should update query with startValue on init, if provided', () => {
it('should set value from startValue on init, if provided', () => {
component.startValue = 'mock-start-value';
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'mock-start-value'`);
expect(component.value).toBe('mock-start-value');
expect(component.context.update).toHaveBeenCalled();
});
it('should build the query fragment from the current value when submitted', () => {
component.startValue = 'mock-start-value';
fixture.detectChanges();
component.submitValues();
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'mock-start-value'`);
expect(component.context.execute).toHaveBeenCalled();
});
it('should parse value and set query context as blank, and not call query update, if no start value was provided', () => {
@@ -141,7 +175,7 @@ describe('SearchTextComponent', () => {
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.value).toBe('secret.pdf');
expect(component.context.update).not.toHaveBeenCalled();
expect(component.context.execute).not.toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', async () => {
@@ -157,4 +191,30 @@ describe('SearchTextComponent', () => {
expect(component.context.filterRawParams[component.id]).toBe('secret.pdf');
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
it('should add the search prefix and suffix to the query fragment when wildcards are enabled', () => {
spyOn(appConfig, 'get').and.callFake((key: string, defaultValue?: any) => (key === 'search-wildcards-enabled' ? true : defaultValue));
component.settings.searchPrefix = '*';
component.settings.searchSuffix = '*';
fixture.detectChanges();
component.onChangedHandler({ target: { value: 'secret' } });
component.submitValues();
expect(component.context.wildcardsEnabled).toBeTrue();
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'*secret*'`);
});
it('should NOT add the search prefix and suffix to the query fragment when wildcards are disabled', () => {
spyOn(appConfig, 'get').and.callFake((key: string, defaultValue?: any) => (key === 'search-wildcards-enabled' ? false : defaultValue));
component.settings.searchPrefix = '*';
component.settings.searchSuffix = '*';
fixture.detectChanges();
component.onChangedHandler({ target: { value: 'secret' } });
component.submitValues();
expect(component.context.wildcardsEnabled).toBeFalse();
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'secret'`);
});
});
@@ -48,7 +48,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
context: SearchQueryBuilderService;
startValue: string;
isActive = false;
enableChangeUpdate = true;
enableChangeUpdate = false;
displayValue$ = new ReplaySubject<string>(1);
private readonly destroyRef = inject(DestroyRef);
@@ -121,7 +121,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
if (this.context?.queryFragments && this.settings?.field) {
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
if (updateContext) {
this.context.update();
this.context.execute();
}
}
}
@@ -141,14 +141,13 @@ export class SearchTextComponent implements SearchWidget, OnInit {
setValue(value: string) {
this.value = value;
this.displayValue$.next(this.value);
this.submitValues();
}
private getSearchPrefix(): string {
return this.settings.searchPrefix ? this.settings.searchPrefix : '';
return this.settings.searchPrefix && this.context.wildcardsEnabled ? this.settings.searchPrefix : '';
}
private getSearchSuffix(): string {
return this.settings.searchSuffix ? this.settings.searchSuffix : '';
return this.settings.searchSuffix && this.context.wildcardsEnabled ? this.settings.searchSuffix : '';
}
}
@@ -69,6 +69,12 @@ describe('BaseQueryBuilderService', () => {
{ ...mockSearchConfig, id: 'config-3', name: 'Config 3', default: false }
];
const configureAppConfig = (wildcards?: boolean): void => {
(appConfig.get as jasmine.Spy).and.callFake((key: string, defaultValue?: any) =>
key === 'search-wildcards-enabled' ? (wildcards ?? defaultValue) : false
);
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
@@ -89,19 +95,218 @@ describe('BaseQueryBuilderService', () => {
});
describe('userQuery', () => {
it('should set userQuery with parentheses', () => {
it('should set userQuery to raw value without wrapping', () => {
service.userQuery = 'test query';
expect(service.userQuery).toBe('(test query)');
expect(service.userQuery).toBe('test query');
});
it('should trim userQuery', () => {
service.userQuery = ' test query ';
expect(service.userQuery).toBe('(test query)');
it('should store userQuery in filterRawParams', () => {
service.userQuery = 'test';
expect(service.filterRawParams['userQuery']).toBe('test');
});
it('should return empty string for null input', () => {
service.userQuery = null;
it('should set parsedQuery when userQuery is set in regular mode', () => {
service.searchMode = 'regular';
service.userQuery = 'hello';
expect(service.parsedQuery).toBe('((cm:name:"hello*"))');
});
it('should set parsedQuery equal to userQuery in formula mode', () => {
service.searchMode = 'formula';
service.userQuery = '(cm:name:"test*")';
expect(service.parsedQuery).toBe('(cm:name:"test*")');
});
it('should clear userQuery and its raw param when set to empty', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'something';
service.userQuery = '';
expect(service.userQuery).toBe('');
expect(service.filterRawParams['userQuery']).toBe('');
});
});
describe('parsedQuery', () => {
it('should wrap a single term with field query and parentheses in regular mode', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello';
expect(service.parsedQuery).toContain('cm:name:"hello*"');
});
it('should join multiple terms with AND in regular mode when no operator present', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello world';
expect(service.parsedQuery).toContain(' AND ');
});
it('should preserve AND/OR operators in regular mode when present in query', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello AND world';
expect(service.parsedQuery).toBe('((cm:name:"hello*") AND (cm:name:"world*"))');
});
it('should store parsedQuery in filterRawParams', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello';
expect(service.filterRawParams['parsedQuery']).toBe(service.parsedQuery);
});
it('should not add wildcard suffix when wildcards are disabled', () => {
configureAppConfig(false);
service.searchMode = 'regular';
service.userQuery = 'hello';
expect(service.parsedQuery).toBe('((cm:name:"hello"))');
});
it('should build the exact single-term parsed query against the default cm:name field', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello';
expect(service.parsedQuery).toBe('((cm:name:"hello*"))');
});
it('should join each multi-term word with AND wrapped in parentheses', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello world';
expect(service.parsedQuery).toBe('((cm:name:"hello*") AND (cm:name:"world*"))');
});
it('should keep explicit operators as separators between terms', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello OR world';
expect(service.parsedQuery).toBe('((cm:name:"hello*") OR (cm:name:"world*"))');
});
it('should parse each term against every configured app:fields entry', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.config = { id: 'test-config', categories: [], 'app:fields': ['cm:name', 'cm:title'] } as any;
service.userQuery = 'hello';
expect(service.parsedQuery).toBe('((cm:name:"hello*" OR cm:title:"hello*"))');
});
it('should escape double quotes in the user query term', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'he"llo';
expect(service.parsedQuery).toBe('((cm:name:"he\\"llo*"))');
});
it('should escape backslashes in the user query term', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'he\\llo';
expect(service.parsedQuery).toBe('((cm:name:"he\\\\llo*"))');
});
it('should clear parsedQuery and its raw param when userQuery is set to empty', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'hello';
service.userQuery = '';
expect(service.parsedQuery).toBe('');
expect(service.filterRawParams['parsedQuery']).toBe('');
});
});
describe('searchMode', () => {
it('should default to regular mode', () => {
expect(service.searchMode).toBe('regular');
});
it('should store searchMode in filterRawParams', () => {
service.searchMode = 'formula';
expect(service.filterRawParams['searchMode']).toBe('formula');
});
it('should update filterRawParams when changed back to regular', () => {
service.searchMode = 'formula';
service.searchMode = 'regular';
expect(service.filterRawParams['searchMode']).toBe('regular');
});
it('should recompute parsedQuery when switching search mode', () => {
configureAppConfig(true);
service.searchMode = 'formula';
service.userQuery = 'hello';
expect(service.parsedQuery).toBe('hello');
service.searchMode = 'regular';
expect(service.parsedQuery).toBe('((cm:name:"hello*"))');
});
});
describe('wildcardsEnabled', () => {
it('should return true by default', () => {
configureAppConfig();
expect(service.wildcardsEnabled).toBe(true);
});
it('should return false when config disables wildcards', () => {
configureAppConfig(false);
expect(service.wildcardsEnabled).toBe(false);
});
});
describe('isOperator', () => {
it('should return true for AND', () => {
expect(service.isOperator('AND')).toBeTrue();
});
it('should return true for OR', () => {
expect(service.isOperator('OR')).toBeTrue();
});
it('should return true for AND with surrounding spaces', () => {
expect(service.isOperator(' AND ')).toBeTrue();
});
it('should return false for regular word', () => {
expect(service.isOperator('hello')).toBeFalse();
});
it('should return false for empty string', () => {
expect(service.isOperator('')).toBeFalse();
});
it('should return false for null/undefined', () => {
expect(service.isOperator(null)).toBeFalse();
expect(service.isOperator(undefined)).toBeFalse();
});
});
describe('selectedConfigurationId', () => {
it('should store selectedConfigurationId in filterRawParams when set', () => {
(appConfig.get as jasmine.Spy).and.returnValue(mockMultipleConfigs);
service.resetToDefaults();
service.selectedConfigurationId = 'config-2';
expect(service.filterRawParams['selectedConfigurationId']).toBe('config-2');
});
});
describe('encodedQuery', () => {
it('should return encoded query after encodeQuery is called', () => {
service.userQuery = 'test';
service.encodeQuery();
expect(service.encodedQuery).toBeTruthy();
});
it('should return a base64 encoded string of filterRawParams', () => {
service.userQuery = 'test';
service.encodeQuery();
const decoded = new TextDecoder().decode(Uint8Array.from(atob(service.encodedQuery), (char) => char.charCodeAt(0)));
const parsed = JSON.parse(decoded);
expect(parsed['userQuery']).toBe('test');
});
});
@@ -215,15 +420,27 @@ describe('BaseQueryBuilderService', () => {
expect(service.buildQuery()).toBeNull();
});
it('should build query with userQuery', () => {
it('should build query with parsedQuery in regular mode', () => {
configureAppConfig(true);
service.searchMode = 'regular';
service.userQuery = 'test';
const query = service.buildQuery();
expect(query).toBeTruthy();
expect(query.query.query).toBe('(test)');
expect(query.query.query).toContain('cm:name:"test*"');
});
it('should build query using userQuery directly in formula mode', () => {
service.searchMode = 'formula';
service.userQuery = '(cm:name:"test*")';
const query = service.buildQuery();
expect(query).toBeTruthy();
expect(query.query.query).toBe('(cm:name:"test*")');
});
it('should include scope in query when set', () => {
service.searchMode = 'formula';
service.userQuery = 'test';
service.setScope({ locations: 'nodes' });
const query = service.buildQuery();
@@ -233,6 +450,7 @@ describe('BaseQueryBuilderService', () => {
it('should include default includes when none configured', () => {
service.config = { id: 'test-config', categories: [] };
service.searchMode = 'formula';
service.userQuery = 'test';
const query = service.buildQuery();
@@ -241,29 +459,25 @@ describe('BaseQueryBuilderService', () => {
});
});
describe('update', () => {
it('should emit updated event with built query', (done) => {
describe('getFinalQuery', () => {
it('should skip query fragments that are empty match-all objects', () => {
service.searchMode = 'formula';
service.userQuery = 'test';
service.categories = [{ id: 'cat1', name: 'Cat1', enabled: true, expanded: false, component: { selector: 'test', settings: undefined } }];
service.queryFragments['cat1'] = { matchAll: '', matchAny: '', matchExact: '', exclude: '' };
service.updated.subscribe((query) => {
expect(query.query.query).toBe('(test)');
done();
});
service.update();
const query = service.buildQuery();
expect(query.query.query).toBe('test');
});
it('should emit updated event with provided query body', (done) => {
const customQuery = {
query: { query: 'custom query', language: 'afts' }
};
it('should include non-empty query fragments', () => {
service.searchMode = 'formula';
service.userQuery = 'test';
service.categories = [{ id: 'cat1', name: 'Cat1', enabled: true, expanded: false, component: { selector: 'test', settings: undefined } }];
service.queryFragments['cat1'] = 'cm:name:"hello"';
service.updated.subscribe((query) => {
expect(query.query.query).toBe('custom query');
done();
});
service.update(customQuery);
const query = service.buildQuery();
expect(query.query.query).toContain('cm:name:"hello"');
});
});
@@ -450,79 +664,24 @@ describe('BaseQueryBuilderService', () => {
expect(service.execute).toHaveBeenCalled();
});
});
describe('populateFilters and selectedConfiguration restoration', () => {
beforeEach(() => {
(appConfig.get as jasmine.Spy<<T>(key: string, defaultValue?: T) => T>).and.returnValue(mockMultipleConfigs);
service.resetToDefaults();
});
it('should restore selectedConfiguration from populateFilters', (done) => {
it('should not call execute when shouldExecute is false', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service, 'execute');
service.configUpdated.subscribe((config) => {
expect(config.name).toBe('Config 3');
done();
});
service.updateSelectedConfiguration('config-2', true, false);
service.populateFilters.next({ selectedConfigurationId: 'config-3', someOtherFilter: 'value' });
expect(service.execute).not.toHaveBeenCalled();
});
it('should reset to default configuration when populateFilters has no selectedConfigurationId', (done) => {
it('should not reset search options when resetFilters is false', () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.updateSelectedConfiguration('config-3');
service.queryFragments['someFilter'] = 'some value';
service.updateSelectedConfiguration('config-2', false, false);
setTimeout(() => {
service.configUpdated.subscribe((config) => {
expect(config.name).toBe('Config 1');
done();
});
service.populateFilters.next({ someOtherFilter: 'value' });
}, 0);
});
it('should not change configuration when populateFilters is empty', () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
const configUpdatedSpy = jasmine.createSpy('configUpdatedSpy');
service.configUpdated.subscribe(configUpdatedSpy);
configUpdatedSpy.calls.reset();
service.populateFilters.next({});
expect(configUpdatedSpy).not.toHaveBeenCalled();
});
it('should not change configuration when same configuration is already selected', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.updateSelectedConfiguration('config-2');
setTimeout(() => {
const configUpdatedSpy = jasmine.createSpy('configUpdatedSpy');
service.configUpdated.subscribe(configUpdatedSpy);
service.populateFilters.next({ selectedConfigurationId: 'config-2' });
setTimeout(() => {
expect(configUpdatedSpy).not.toHaveBeenCalled();
done();
}, 0);
}, 0);
});
it('should update filterRawParams when restoring configuration from populateFilters', (done) => {
service.configUpdated.subscribe(() => {
expect(service.filterRawParams['selectedConfigurationId']).toBe('config-2');
done();
});
service.populateFilters.next({ selectedConfigurationId: 'config-2' });
expect(service.queryFragments['someFilter']).toBe('some value');
});
});
@@ -577,5 +736,17 @@ describe('BaseQueryBuilderService', () => {
expect(service.categories.length).toBe(1);
expect(service.categories[0].id).toBe('cat1');
});
it('should reset userQuery when resetUserQuery is true (default)', () => {
service.userQuery = 'some query';
service.resetToDefaults(false, true);
expect(service.userQuery).toBe('');
});
it('should preserve userQuery when resetUserQuery is false', () => {
service.userQuery = 'some query';
service.resetToDefaults(false, false);
expect(service.userQuery).toBe('some query');
});
});
});
@@ -54,9 +54,6 @@ export abstract class BaseQueryBuilderService {
/* Stream that emits the event each time when search filter finishes loading initial value */
filterLoaded = new Subject<void>();
/* Stream that emits the query before search whenever user search */
updated = new Subject<SearchRequest>();
/* Stream that emits the results whenever user search */
executed = new Subject<ResultSetPaging>();
@@ -85,11 +82,13 @@ export abstract class BaseQueryBuilderService {
sorting: SearchSortingDefinition[] = [];
sortingOptions: SearchSortingDefinition[] = [];
private encodedQuery: string;
private _encodedQuery: string;
private scope: RequestScope;
private selectedConfigurationId: string;
private _userQuery = '';
private _selectedConfigurationId: string;
private _queryFragments: { [id: string]: string } = {};
private _parsedQuery: string;
private _userQuery: string;
private _searchMode: 'regular' | 'formula';
private readonly selectedConfigurationKey = 'selectedConfigurationId';
private readonly queryFragmentsHandler: ProxyHandler<{ [key: string]: any }> = {
@@ -111,13 +110,45 @@ export abstract class BaseQueryBuilderService {
this.queryFragmentsUpdate.next(this._queryFragments);
}
get encodedQuery(): string {
return this._encodedQuery;
}
get wildcardsEnabled(): boolean {
return this.appConfig.get<boolean>('search-wildcards-enabled', true);
}
get userQuery(): string {
return this._userQuery;
}
set userQuery(value: string) {
value = (value || '').trim();
this._userQuery = value ? `(${value})` : '';
this._userQuery = value;
this.filterRawParams['userQuery'] = value;
this.setParsedQuery();
}
get parsedQuery(): string {
return this._parsedQuery;
}
get searchMode(): 'regular' | 'formula' {
return this._searchMode;
}
set searchMode(value: 'regular' | 'formula') {
this._searchMode = value;
this.filterRawParams['searchMode'] = value;
this.setParsedQuery();
}
get selectedConfigurationId(): string {
return this._selectedConfigurationId;
}
set selectedConfigurationId(value: string) {
this._selectedConfigurationId = value;
this.filterRawParams[this.selectedConfigurationKey] = value;
}
config: SearchConfiguration = {
@@ -132,17 +163,17 @@ export abstract class BaseQueryBuilderService {
protected readonly appConfig: AppConfigService,
protected readonly alfrescoApiService: AlfrescoApiService
) {
this.searchMode = 'regular';
this.userQuery = '';
this.resetToDefaults();
this._queryFragments = this.createQueryFragmentsProxy({});
this.populateFilters.subscribe((filters) => this.handleSelectedConfigurationChange(filters));
}
public abstract loadConfiguration(): SearchConfiguration | SearchConfiguration[];
public abstract isFilterServiceActive(): boolean;
public resetToDefaults(withNavigate = false) {
public resetToDefaults(withNavigate = false, resetUserQuery = true) {
if (withNavigate) {
this.router.navigate([], {
queryParams: { q: null },
@@ -151,7 +182,7 @@ export abstract class BaseQueryBuilderService {
});
}
const currentConfig = this.getDefaultConfiguration();
this.resetSearchOptions();
this.resetSearchOptions(resetUserQuery);
this.configUpdated.next(currentConfig);
this.searchForms.next(this.getSearchFormDetails());
this.setUpSearchConfiguration(currentConfig);
@@ -170,23 +201,26 @@ export abstract class BaseQueryBuilderService {
return configurations;
}
public updateSelectedConfiguration(id: string): void {
public updateSelectedConfiguration(id: string, resetFilters = true, shouldExecute = true): void {
const currentConfig = this.loadConfiguration();
if (Array.isArray(currentConfig)) {
const selectedConfig = currentConfig.find((config) => config.id === id);
if (selectedConfig) {
if (resetFilters) {
this.resetSearchOptions(false);
}
this.selectedConfigurationId = id;
this.searchForms.next(this.getSearchFormDetails());
this.resetSearchOptions();
this.setUpSearchConfiguration(selectedConfig);
this.filterRawParams[this.selectedConfigurationKey] = id;
this.configUpdated.next(selectedConfig);
this.execute();
if (shouldExecute) {
this.execute(true);
}
}
}
}
private resetSearchOptions(): void {
private resetSearchOptions(resetUserQuery = true): void {
this.categories = [];
this.queryFragments = {};
this.filterQueries = [];
@@ -194,8 +228,10 @@ export abstract class BaseQueryBuilderService {
this.sortingOptions = [];
this.resetUserFacetBucket();
this.scope = null;
this.filterRawParams = {};
this._userQuery = '';
if (resetUserQuery) {
this.userQuery = '';
}
this.resetFilterRawParams(resetUserQuery);
this.populateFilters.next({});
}
@@ -229,6 +265,7 @@ export abstract class BaseQueryBuilderService {
this.categories = (this.config.categories || []).filter((category) => category.enabled);
this.filterQueries = this.config.filterQueries || [];
this.userFacetBuckets = {};
this.userQuery = this.filterRawParams['userQuery'] || '';
if (this.config.sorting) {
this.sorting = this.config.sorting.defaults || [];
this.sortingOptions = this.config.sorting.options || [];
@@ -355,16 +392,6 @@ export abstract class BaseQueryBuilderService {
return this.scope;
}
/**
* Builds the current query and triggers the `updated` event.
*
* @param queryBody query settings
*/
update(queryBody?: SearchRequest): void {
const query = queryBody ? queryBody : this.buildQuery();
this.updated.next(query);
}
/**
* Builds and executes the current query.
*
@@ -552,12 +579,9 @@ export abstract class BaseQueryBuilderService {
protected getFinalQuery(): string {
let query = '';
if (this.userQuery) {
this.filterRawParams['userQuery'] = this.userQuery;
}
this.categories.forEach((facet) => {
const customQuery = this.queryFragments[facet.id];
if (customQuery) {
if (customQuery && JSON.stringify(customQuery) !== JSON.stringify({ matchAll: '', matchAny: '', matchExact: '', exclude: '' })) {
if (query.length > 0) {
query += ' AND ';
}
@@ -565,7 +589,8 @@ export abstract class BaseQueryBuilderService {
}
});
let result = [this.userQuery, query].filter((entry) => entry).join(' AND ');
const parsedQuery = this.searchMode === 'regular' ? this.parsedQuery : this.userQuery;
let result = [parsedQuery, query].filter((entry) => entry).join(' AND ');
if (this.userFacetBuckets) {
Object.keys(this.userFacetBuckets).forEach((key) => {
@@ -626,10 +651,10 @@ export abstract class BaseQueryBuilderService {
*/
encodeQuery() {
try {
this.encodedQuery = btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(this.filterRawParams))));
this._encodedQuery = btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(this.filterRawParams))));
} catch (error) {
console.error('Failed to encode query parameters:', error);
this.encodedQuery = '';
this._encodedQuery = '';
}
}
@@ -640,7 +665,7 @@ export abstract class BaseQueryBuilderService {
this.encodeQuery();
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: { q: this.encodedQuery },
queryParams: { q: this._encodedQuery },
queryParamsHandling: 'merge'
});
}
@@ -653,50 +678,71 @@ export abstract class BaseQueryBuilderService {
*/
async navigateToSearch(query: string, searchUrl: string) {
this.userQuery = query;
this.encodeQuery();
await this.execute();
await this.router.navigate([searchUrl], {
queryParams: { q: this.encodedQuery },
queryParams: { q: this._encodedQuery },
queryParamsHandling: 'merge'
});
}
/**
* Checks if string is an AND or OR operator
*
* @param input string to check if it is an operator
* @returns boolean
*/
isOperator(input: string): boolean {
if (input) {
const operators = ['AND', 'OR'];
return operators.includes(input.trim());
}
return false;
}
private createQueryFragmentsProxy(target: { [key: string]: any }): { [key: string]: any } {
return new Proxy(target, this.queryFragmentsHandler);
}
private setSelectedConfiguration(id: string): void {
const currentConfig = this.loadConfiguration();
if (Array.isArray(currentConfig)) {
const selectedConfig = currentConfig.find((config) => config.id === id);
if (selectedConfig) {
this.selectedConfigurationId = id;
this.searchForms.next(this.getSearchFormDetails());
this.setUpSearchConfiguration(selectedConfig);
this.filterRawParams[this.selectedConfigurationKey] = id;
this.configUpdated.next(selectedConfig);
}
}
}
private handleSelectedConfigurationChange(filters: { [key: string]: string }): void {
if (Object.keys(filters ?? {}).length === 0) {
private setParsedQuery() {
if (!this.userQuery) {
this._parsedQuery = '';
this.filterRawParams['parsedQuery'] = '';
return;
}
const newSelectedConfig = filters?.[this.selectedConfigurationKey];
if (newSelectedConfig) {
if (newSelectedConfig !== this.selectedConfigurationId) {
this.setSelectedConfiguration(newSelectedConfig);
}
if (this.searchMode === 'formula') {
this._parsedQuery = this.userQuery;
} else {
const configurations = this.loadConfiguration();
if (Array.isArray(configurations)) {
const defaultConfig = configurations.find((config) => config.default);
if (defaultConfig && this.selectedConfigurationId !== defaultConfig.id) {
this.setSelectedConfiguration(defaultConfig.id);
}
const words = this.userQuery.split(/\s+/);
if (words.length > 1) {
const separator = words.some(this.isOperator) ? ' ' : ' AND ';
this._parsedQuery = '(' + words.map((term) => (this.isOperator(term) ? term : this.parseTermByFields(term))).join(separator) + ')';
} else {
this._parsedQuery = '(' + this.parseTermByFields(this.userQuery) + ')';
}
}
this.filterRawParams['parsedQuery'] = this._parsedQuery;
}
private parseTermByFields(term: string): string {
const suffix = this.wildcardsEnabled ? '*' : '';
const fields = this.config['app:fields'] || ['cm:name'];
const escapedTerm = this.escapeQueryTerm(term);
return '(' + fields.map((field) => `${field}:"${escapedTerm}${suffix}"`).join(' OR ') + ')';
}
private escapeQueryTerm(term: string): string {
return term.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
private resetFilterRawParams(resetUserQuery = true) {
this.filterRawParams = {
userQuery: resetUserQuery ? '' : this.userQuery,
parsedQuery: resetUserQuery ? '' : this.parsedQuery,
searchMode: this.searchMode,
[this.selectedConfigurationKey]: this.selectedConfigurationId,
logic: { matchAll: '', matchAny: '', matchExact: '', exclude: '' }
};
}
}
@@ -679,7 +679,7 @@ describe('SearchFacetFiltersService', () => {
it('should reset filters and call resetToDefaults', () => {
const resetToDefaultsSpy = spyOn(queryBuilder, 'resetToDefaults');
const updateSpy = spyOn(queryBuilder, 'update');
const executeSpy = spyOn(queryBuilder, 'execute');
const responseFacets = [{ field: 'field1', label: null }];
const selectedBuckets = [{ field: { field: 'field1', label: null }, bucket: { label: 'bucket1', count: 1, filterQuery: 'q1' } }];
@@ -695,8 +695,8 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.reset();
expect(resetToDefaultsSpy).toHaveBeenCalled();
expect(updateSpy).toHaveBeenCalled();
expect(resetToDefaultsSpy).toHaveBeenCalledWith(true, false);
expect(executeSpy).toHaveBeenCalled();
expect(searchFacetFiltersService.responseFacets).toEqual([]);
expect(searchFacetFiltersService.selectedBuckets).toEqual([]);
@@ -70,8 +70,6 @@ export class SearchFacetFiltersService {
this.responseFacets = null;
});
this.queryBuilder.updated.pipe(takeUntilDestroyed()).subscribe((query) => this.queryBuilder.execute(true, query));
this.queryBuilder.executed.pipe(takeUntilDestroyed()).subscribe((resultSetPaging: ResultSetPaging) => {
this.onDataLoaded(resultSetPaging);
this.searchService.dataLoaded.next(resultSetPaging);
@@ -397,7 +395,7 @@ export class SearchFacetFiltersService {
bucket.checked = false;
this.queryBuilder.removeUserFacetBucket(facetField?.field, bucket);
this.updateSelectedBuckets();
this.queryBuilder.update();
this.queryBuilder.execute();
}
}
@@ -432,7 +430,7 @@ export class SearchFacetFiltersService {
this.updateSelectedBuckets();
}
});
this.queryBuilder.update();
this.queryBuilder.execute();
}
resetQueryFragments() {
@@ -444,7 +442,7 @@ export class SearchFacetFiltersService {
this.responseFacets = [];
this.selectedBuckets = [];
this.tabbedFacet = null;
this.queryBuilder.resetToDefaults(true);
this.queryBuilder.update();
this.queryBuilder.resetToDefaults(true, false);
this.queryBuilder.execute();
}
}
@@ -231,19 +231,17 @@ describe('SearchHeaderQueryBuilderService', () => {
spyOn(router, 'navigate');
spyOn(console, 'error');
const searchUrl = 'search';
builderService.filterRawParams = {
userQuery: '((cm:name:"wąż*" OR cm:title:"wąż*" OR cm:description:"wąż*" OR TEXT:"wąż*" OR TAG:"wąż*"))'
};
builderService.encodeQuery();
const nonLatinQuery = '((cm:name:"wąż*" OR cm:title:"wąż*" OR cm:description:"wąż*" OR TEXT:"wąż*" OR TAG:"wąż*"))';
builderService.searchMode = 'formula';
await builderService.navigateToSearch(nonLatinQuery, searchUrl);
await builderService.navigateToSearch('', searchUrl);
expect(console.error).not.toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith([searchUrl], {
queryParams: {
q: 'eyJ1c2VyUXVlcnkiOiIoKGNtOm5hbWU6XCJ3xIXFvCpcIiBPUiBjbTp0aXRsZTpcInfEhcW8KlwiIE9SIGNtOmRlc2NyaXB0aW9uOlwid8SFxbwqXCIgT1IgVEVYVDpcInfEhcW8KlwiIE9SIFRBRzpcInfEhcW8KlwiKSkifQ=='
},
queryParamsHandling: 'merge'
});
const navigateArgs = (router.navigate as jasmine.Spy).calls.mostRecent().args;
expect(navigateArgs[0]).toEqual([searchUrl]);
expect(navigateArgs[1].queryParamsHandling).toBe('merge');
const decoded = JSON.parse(decodeURIComponent(escape(atob(navigateArgs[1].queryParams.q))));
expect(decoded.userQuery).toBe(nonLatinQuery);
});
});

Some files were not shown because too many files have changed in this diff Show More