mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-16 18:13:06 +00:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3496363b8e | ||
|
|
d25038dc59 | ||
|
|
c348ea32c0 | ||
|
|
0505b1d5c3 | ||
|
|
0a19be090e | ||
|
|
b69c3ae1ae | ||
|
|
ce907417b7 | ||
|
|
4c7765e3fa | ||
|
|
407a64996e | ||
|
|
c0a102f87f | ||
|
|
066ef30516 | ||
|
|
01cea60490 | ||
|
|
f438c6ce69 | ||
|
|
09ef05038c | ||
|
|
c7bd56f244 | ||
|
|
b86e41515e | ||
|
|
0b35118dee | ||
|
|
fe2b3d85dd | ||
|
|
b0dc85422c | ||
|
|
23f0392c12 | ||
|
|
997303e6a7 | ||
|
|
45d1818f04 | ||
|
|
2f50af623d | ||
|
|
9c56712328 | ||
|
|
448e667c52 | ||
|
|
18e602aa85 | ||
|
|
53ef0cfc00 | ||
|
|
4cd8648a05 | ||
|
|
7f57b3936b | ||
|
|
5a30baad99 | ||
|
|
5050c3d974 | ||
|
|
d8b36606e2 | ||
|
|
37f8fe47ac | ||
|
|
af0d1e4ead |
@@ -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 }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
+374
-185
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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-->
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
| --- | --- | --- |
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
+143
-71
@@ -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', () => {
|
||||
|
||||
-3
@@ -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 });
|
||||
|
||||
+57
-48
@@ -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">
|
||||
|
||||
+35
-3
@@ -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);
|
||||
|
||||
+10
-3
@@ -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);
|
||||
|
||||
+54
-1
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(' ');
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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', () => {
|
||||
|
||||
+3
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -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' }];
|
||||
|
||||
+4
-2
@@ -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 ?? [];
|
||||
|
||||
+4
-4
@@ -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();
|
||||
});
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+4
-4
@@ -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];
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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' }]);
|
||||
|
||||
|
||||
+1
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+3
-3
@@ -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 () => {
|
||||
|
||||
+6
-6
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -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 () => {
|
||||
|
||||
+8
-8
@@ -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', () => {
|
||||
|
||||
+7
-7
@@ -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());
|
||||
});
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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');
|
||||
});
|
||||
|
||||
+3
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -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();
|
||||
|
||||
+2
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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 () => {
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-8
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+9
-11
@@ -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
Reference in New Issue
Block a user