From b2f1245c7d625246fad104197f02f40208f5b1f9 Mon Sep 17 00:00:00 2001 From: Dominik Iwanek <141320833+dominikiwanekhyland@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:18:55 +0100 Subject: [PATCH] [ACS-10475] [Search] UX improvements for search (#5042) --- .../copy-move-actions/src/tests/copy.e2e.ts | 64 ++-- .../copy-move-actions/src/tests/move.e2e.ts | 48 +-- .../src/tests/delete-undo-delete.e2e.ts | 34 +- .../src/tests/permanently-delete.e2e.ts | 20 +- .../delete-actions/src/tests/restore.e2e.ts | 18 +- .../edit-actions/src/tests/edit-folder.e2e.ts | 4 +- .../src/tests/create-rules.e2e.ts | 2 +- .../src/tests/library-actions.e2e.ts | 10 +- .../list-views/src/tests/empty-list.e2e.ts | 17 +- .../list-views/src/tests/permissions.e2e.ts | 15 +- .../navigation/src/tests/sidebar.e2e.ts | 10 +- .../search/src/tests/search-input.e2e.ts | 49 +-- .../src/tests/search-results-general.e2e.ts | 4 +- .../src/tests/search-results-libraries.e2e.ts | 30 +- .../share/unshare-file-search-results.e2e.ts | 30 +- .../search/search-results-general.e2e.ts | 10 +- .../src/tests/viewer.ts | 2 +- .../src/tests/viewer-version-actions.e2e.ts | 4 +- e2e/playwright/viewer/src/tests/viewer.e2e.ts | 9 +- projects/aca-content/assets/i18n/en.json | 6 +- .../src/lib/components/search/index.ts | 4 +- .../search/search-execution.service.spec.ts | 102 ++++++ .../search/search-execution.service.ts | 72 ++++ .../search/search-filter.service.spec.ts | 148 ++++++++ .../search/search-filter.service.ts | 167 +++++++++ .../search-in-menu.component.html | 76 ++++ .../search-in-menu.component.scss | 70 ++++ .../search-in-menu.component.spec.ts | 150 ++++++++ .../search-in-menu.component.ts | 178 +++++++++ .../search-input-control.component.html | 39 -- .../search-input-control.component.scss | 37 -- .../search-input-control.component.spec.ts | 198 ---------- .../search-input-control.component.ts | 180 --------- .../search-input/search-input.component.html | 70 ++-- .../search-input/search-input.component.scss | 90 +---- .../search-input.component.spec.ts | 346 ++++++++++++------ .../search-input/search-input.component.ts | 322 ++++------------ .../search-libraries-results.component.html | 3 +- .../search-libraries-results.component.scss | 4 +- ...search-libraries-results.component.spec.ts | 6 - .../search-libraries-results.component.ts | 9 +- .../search/search-navigation.service.ts | 31 +- .../search-results.component.html | 3 +- .../search-results.component.scss | 4 +- projects/aca-content/src/lib/ui/theme.scss | 11 + .../src/lib/ui/variables/variables.scss | 2 - .../src/base-config/playwright.config.ts | 2 +- .../manageRules/manage-rules.component.ts | 4 +- .../page-objects/components/search/index.ts | 1 - .../search/search-in-dialog.components.ts | 88 +++++ .../search/search-input.component.ts | 10 +- .../search/search-overlay.components.ts | 93 ----- .../src/page-objects/pages/search.page.ts | 42 ++- .../components/toolbar/toolbar.component.scss | 2 +- .../src/lib/services/app.service.ts | 12 +- ...ading-trailing-operators.validator.spec.ts | 70 ---- ...no-leading-trailing-operators.validator.ts | 43 --- .../no-whitespace.validator.spec.ts | 50 --- .../lib/validators/no-whitespace.validator.ts | 37 -- projects/aca-shared/src/public-api.ts | 3 - 60 files changed, 1640 insertions(+), 1525 deletions(-) create mode 100644 projects/aca-content/src/lib/components/search/search-execution.service.spec.ts create mode 100644 projects/aca-content/src/lib/components/search/search-execution.service.ts create mode 100644 projects/aca-content/src/lib/components/search/search-filter.service.spec.ts create mode 100644 projects/aca-content/src/lib/components/search/search-filter.service.ts create mode 100644 projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.html create mode 100644 projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.scss create mode 100644 projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.spec.ts create mode 100644 projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.ts delete mode 100644 projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html delete mode 100644 projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.scss delete mode 100644 projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts delete mode 100644 projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts create mode 100644 projects/aca-playwright-shared/src/page-objects/components/search/search-in-dialog.components.ts delete mode 100644 projects/aca-playwright-shared/src/page-objects/components/search/search-overlay.components.ts delete mode 100644 projects/aca-shared/src/lib/validators/no-leading-trailing-operators.validator.spec.ts delete mode 100644 projects/aca-shared/src/lib/validators/no-leading-trailing-operators.validator.ts delete mode 100644 projects/aca-shared/src/lib/validators/no-whitespace.validator.spec.ts delete mode 100644 projects/aca-shared/src/lib/validators/no-whitespace.validator.ts diff --git a/e2e/playwright/copy-move-actions/src/tests/copy.e2e.ts b/e2e/playwright/copy-move-actions/src/tests/copy.e2e.ts index 812e7c9cc..1d1942554 100644 --- a/e2e/playwright/copy-move-actions/src/tests/copy.e2e.ts +++ b/e2e/playwright/copy-move-actions/src/tests/copy.e2e.ts @@ -85,34 +85,34 @@ test.describe('Copy actions', () => { test('[XAT-4941] Copy a file', async ({ personalFiles }) => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFile); await copyContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4942] Copy a folder with content', async ({ personalFiles }) => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); }); test('[XAT-4943] Copy multiple items', async ({ personalFiles }) => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder, sourceFile], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4944] Copy a file with a name that already exists on the destination', async ({ personalFiles }) => { @@ -120,11 +120,11 @@ test.describe('Copy actions', () => { const expectedNameForCopiedFile = sourceFile.replace('.', '-1.'); await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFile); await copyContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(true); }); test('[XAT-4945] Copy a folder with a name that already exists on the destination', async ({ personalFiles }) => { @@ -133,14 +133,14 @@ test.describe('Copy actions', () => { const expectedNameForCopiedFile = sourceFileInsideFolder.replace('.', '-1.'); await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(true); }); test('[XAT-4947] Copy locked file', async ({ personalFiles }) => { @@ -148,10 +148,10 @@ test.describe('Copy actions', () => { await nodesApi.lockNodes([sourceFileId], lockType); await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFile); await copyContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4948] Copy folder that contains locked file', async ({ personalFiles }) => { @@ -159,33 +159,33 @@ test.describe('Copy actions', () => { await nodesApi.lockNodes([sourceFileInsideFolderId], lockType); await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); }); test('[XAT-4949] Undo copy of files', async ({ personalFiles }) => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFile); await copyContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); await personalFiles.snackBar.actionButton.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(false); }); test('[XAT-4950] Undo copy of folders', async ({ personalFiles }) => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); await personalFiles.snackBar.actionButton.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(false); }); test('[XAT-4951] Undo copy of a file when a file with same name already exists on the destination', async ({ personalFiles }) => { @@ -194,11 +194,11 @@ test.describe('Copy actions', () => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFile); await copyContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); await personalFiles.snackBar.actionButton.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(false); }); test('[XAT-4952] Undo copy of a folder when a folder with same name already exists on the destination', async ({ personalFiles }) => { @@ -208,13 +208,13 @@ test.describe('Copy actions', () => { await Utils.reloadPageIfRowNotVisible(personalFiles, sourceFolder); await copyContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); await personalFiles.snackBar.actionButton.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(false); }); }); diff --git a/e2e/playwright/copy-move-actions/src/tests/move.e2e.ts b/e2e/playwright/copy-move-actions/src/tests/move.e2e.ts index cd0771e4c..be1a04946 100644 --- a/e2e/playwright/copy-move-actions/src/tests/move.e2e.ts +++ b/e2e/playwright/copy-move-actions/src/tests/move.e2e.ts @@ -80,9 +80,9 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Moved 1 item.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(false); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4998] Move multiple items', async ({ personalFiles }) => { @@ -91,11 +91,11 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Moved 2 items.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeFalsy(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(false); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(false); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4999] Move a file with a name that already exists on the destination', async ({ personalFiles }) => { @@ -106,10 +106,10 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Move unsuccessful, a file with the same name already exists.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(false); }); test('[XAT-5000] Move a folder with a name that already exists on the destination', async ({ personalFiles }) => { @@ -121,12 +121,12 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Move unsuccessful, a file with the same name already exists.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); - expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(expectedNameForCopiedFile)).toBe(false); }); test('[XAT-4989] Move locked file', async ({ personalFiles }) => { @@ -137,9 +137,9 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Moved 1 item.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(false); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); }); test('[XAT-4992] Undo move files', async ({ personalFiles, trashPage }) => { @@ -147,11 +147,11 @@ test.describe('Move actions', () => { await moveContentInPersonalFiles(personalFiles, [sourceFile], destinationFolder); await personalFiles.snackBar.actionButton.click(); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFile)).toBe(false); await trashPage.navigate(); - expect(await trashPage.dataTable.isItemPresent(sourceFile)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(sourceFile)).toBe(false); }); test('[XAT-4993] Undo move folder', async ({ personalFiles, trashPage }) => { @@ -159,11 +159,11 @@ test.describe('Move actions', () => { await moveContentInPersonalFiles(personalFiles, [sourceFolder], destinationFolder); await personalFiles.snackBar.actionButton.click(); await personalFiles.spinner.waitForReload(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(false); await trashPage.navigate(); - expect(await trashPage.dataTable.isItemPresent(sourceFolder)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(sourceFolder)).toBe(false); }); async function moveFolderWithContent(personalFiles: PersonalFilesPage, lockedFile: boolean) { @@ -176,11 +176,11 @@ test.describe('Move actions', () => { const msg = await personalFiles.snackBar.message.innerText(); expect.soft(msg).toContain('Moved 1 item.'); await personalFiles.snackBar.closeIcon.click(); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeFalsy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(false); await personalFiles.dataTable.performClickFolderOrFileToOpen(destinationFolder); - expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBeTruthy(); + expect.soft(await personalFiles.dataTable.isItemPresent(sourceFolder)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(sourceFolder); - expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(sourceFileInsideFolder)).toBe(true); } [ diff --git a/e2e/playwright/delete-actions/src/tests/delete-undo-delete.e2e.ts b/e2e/playwright/delete-actions/src/tests/delete-undo-delete.e2e.ts index 543954f3b..2dbf7b48a 100755 --- a/e2e/playwright/delete-actions/src/tests/delete-undo-delete.e2e.ts +++ b/e2e/playwright/delete-actions/src/tests/delete-undo-delete.e2e.ts @@ -128,11 +128,11 @@ test.describe('Delete and undo delete', () => { const action = await personalFiles.snackBar.getSnackBarActionText(); expect(action).toContain('Undo'); await personalFiles.snackBar.closeIcon.click(); - expect(await personalFiles.dataTable.isItemPresent(file1)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(file1)).toBe(false); items--; expect(await personalFiles.pagination.getMaxRange()).toEqual(` Showing 1-${items} of ${items} `); await trashPage.navigate(); - expect(await personalFiles.dataTable.isItemPresent(file1)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file1)).toBe(true); }); test('[XAT-5017] Delete multiple files and check snackbar notification', async ({ personalFiles, trashPage }) => { @@ -147,11 +147,11 @@ test.describe('Delete and undo delete', () => { await personalFiles.matMenu.clickMenuItem('Delete'); await personalFiles.snackBar.verifySnackBarActionText(`Deleted 2 items`); await personalFiles.snackBar.closeIcon.click(); - expect(await personalFiles.dataTable.isItemPresent(file2)).toBeFalsy(); - expect(await personalFiles.dataTable.isItemPresent(file3)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(file2)).toBe(false); + expect(await personalFiles.dataTable.isItemPresent(file3)).toBe(false); await trashPage.navigate(); - expect(await personalFiles.dataTable.isItemPresent(file2)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(file3)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file2)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(file3)).toBe(true); }); test('[XAT-5018] Delete a folder with content', async ({ personalFiles, trashPage }) => { @@ -160,12 +160,12 @@ test.describe('Delete and undo delete', () => { await personalFiles.acaHeader.clickMoreActions(); await personalFiles.matMenu.clickMenuItem('Delete'); await personalFiles.snackBar.closeIcon.click(); - expect(await personalFiles.dataTable.isItemPresent(folder1)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(folder1)).toBe(false); items--; expect(await personalFiles.pagination.getMaxRange()).toEqual(` Showing 1-${items} of ${items} `); await trashPage.navigate(); - expect(await personalFiles.dataTable.isItemPresent(folder1)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(file1InFolder)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(folder1)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(file1InFolder)).toBe(false); }); test(`[XAT-5019] Delete a folder containing files that can't be deleted (e.g. locked files)`, async ({ personalFiles, trashPage }) => { @@ -174,10 +174,10 @@ test.describe('Delete and undo delete', () => { await personalFiles.matMenu.clickMenuItem('Delete'); await personalFiles.snackBar.verifySnackBarActionText(`${folder2} couldn't be deleted`); await expect(personalFiles.snackBar.actionButton).toBeHidden(); - expect(await personalFiles.dataTable.isItemPresent(folder2)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(folder2)).toBe(true); await trashPage.navigate(); - expect(await personalFiles.dataTable.isItemPresent(folder2)).toBeFalsy(); - expect(await personalFiles.dataTable.isItemPresent(fileLocked1)).toBeFalsy(); + expect(await personalFiles.dataTable.isItemPresent(folder2)).toBe(false); + expect(await personalFiles.dataTable.isItemPresent(fileLocked1)).toBe(false); }); test('[XAT-5020] Notification on unsuccessful multiple items deletion - some items fail to delete', async ({ personalFiles }) => { @@ -206,7 +206,7 @@ test.describe('Delete and undo delete', () => { await personalFiles.snackBar.clickSnackBarAction(); await personalFiles.dataTable.spinnerWaitForReload(); - expect(await personalFiles.dataTable.isItemPresent(file5)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file5)).toBe(true); expect(await personalFiles.pagination.getRange()).toContain(`1-${items} of ${items}`); }); @@ -216,10 +216,10 @@ test.describe('Delete and undo delete', () => { await personalFiles.matMenu.clickMenuItem('Delete'); await personalFiles.snackBar.clickSnackBarAction(); await personalFiles.dataTable.spinnerWaitForReload(); - expect(await personalFiles.dataTable.isItemPresent(folder6)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(folder6)).toBe(true); await personalFiles.dataTable.performClickFolderOrFileToOpen(folder6); await personalFiles.dataTable.spinnerWaitForReload(); - expect(await personalFiles.dataTable.isItemPresent(file2InFolder)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file2InFolder)).toBe(true); }); test('[XAT-5023] Undo delete of multiple files restores all of the files', async ({ personalFiles }) => { @@ -228,8 +228,8 @@ test.describe('Delete and undo delete', () => { await personalFiles.matMenu.clickMenuItem('Delete'); await personalFiles.snackBar.clickSnackBarAction(); await personalFiles.dataTable.spinnerWaitForReload(); - expect(await personalFiles.dataTable.isItemPresent(file6)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(file7)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file6)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(file7)).toBe(true); }); }); }); diff --git a/e2e/playwright/delete-actions/src/tests/permanently-delete.e2e.ts b/e2e/playwright/delete-actions/src/tests/permanently-delete.e2e.ts index 6bfda01f3..1f7ac5caf 100755 --- a/e2e/playwright/delete-actions/src/tests/permanently-delete.e2e.ts +++ b/e2e/playwright/delete-actions/src/tests/permanently-delete.e2e.ts @@ -85,7 +85,7 @@ test.describe('Delete and undo delete', () => { await trashPage.deleteDialog.deleteButton.click(); await trashPage.snackBar.verifySnackBarActionText(`${file1} deleted`); - expect(await trashPage.dataTable.isItemPresent(file1)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(file1)).toBe(false); }); test('[XAT-5104] Permanently delete a folder', async ({ trashPage }) => { @@ -94,7 +94,7 @@ test.describe('Delete and undo delete', () => { await trashPage.deleteDialog.deleteButton.click(); await trashPage.snackBar.verifySnackBarActionText(`${folder1} deleted`); - expect(await trashPage.dataTable.isItemPresent(folder1)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(folder1)).toBe(false); }); test('[XAT-5108] Permanently delete a library', async ({ trashPage }) => { @@ -103,7 +103,7 @@ test.describe('Delete and undo delete', () => { await trashPage.deleteDialog.deleteButton.click(); await trashPage.snackBar.verifySnackBarActionText(`${site} deleted`); - expect(await trashPage.dataTable.isItemPresent(site)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(site)).toBe(false); }); test('[XAT-5105] Permanently delete multiple items', async ({ trashPage }) => { @@ -112,8 +112,8 @@ test.describe('Delete and undo delete', () => { await trashPage.deleteDialog.deleteButton.click(); await trashPage.snackBar.verifySnackBarActionText(`2 items deleted`); - expect(await trashPage.dataTable.isItemPresent(file2)).toBeFalsy(); - expect(await trashPage.dataTable.isItemPresent(folder2)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(file2)).toBe(false); + expect(await trashPage.dataTable.isItemPresent(folder2)).toBe(false); }); test('[XAT-5106] Permanently delete - Confirmation dialog UI', async ({ trashPage }) => { @@ -121,11 +121,11 @@ test.describe('Delete and undo delete', () => { await trashPage.acaHeader.permanentlyDeleteButton.click(); await trashPage.deleteDialog.waitForDialog(); - expect(await trashPage.deleteDialog.isDialogOpen()).toBeTruthy(); + expect(await trashPage.deleteDialog.isDialogOpen()).toBe(true); expect(await trashPage.deleteDialog.getDialogTitle()).toContain('Delete from trash'); expect(await trashPage.deleteDialog.getDialogDescription()).toContain('This will permanently remove the selected item(s)'); - expect(await trashPage.deleteDialog.isDeleteEnabled()).toBeTruthy(); - expect(await trashPage.deleteDialog.isKeepEnabled()).toBeTruthy(); + expect(await trashPage.deleteDialog.isDeleteEnabled()).toBe(true); + expect(await trashPage.deleteDialog.isKeepEnabled()).toBe(true); }); test('[XAT-5107] ""Keep"" action cancels the deletion', async ({ trashPage }) => { @@ -133,9 +133,9 @@ test.describe('Delete and undo delete', () => { await trashPage.acaHeader.permanentlyDeleteButton.click(); await trashPage.deleteDialog.waitForDialog(); - expect(await trashPage.deleteDialog.isKeepEnabled()).toBeTruthy(); + expect(await trashPage.deleteDialog.isKeepEnabled()).toBe(true); await trashPage.deleteDialog.keepButton.click(); - expect(await trashPage.dataTable.isItemPresent(file3)).toBeTruthy(); + expect(await trashPage.dataTable.isItemPresent(file3)).toBe(true); }); }); }); diff --git a/e2e/playwright/delete-actions/src/tests/restore.e2e.ts b/e2e/playwright/delete-actions/src/tests/restore.e2e.ts index 73501ce09..3d2a49220 100755 --- a/e2e/playwright/delete-actions/src/tests/restore.e2e.ts +++ b/e2e/playwright/delete-actions/src/tests/restore.e2e.ts @@ -88,25 +88,25 @@ test.describe('Restore from Trash', () => { await trashPage.snackBar.verifySnackBarActionText(`${nodeName} restored`); const action = await trashPage.snackBar.getSnackBarActionText(); expect(action).toContain('View'); - expect(await trashPage.dataTable.isItemPresent(nodeName)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(nodeName)).toBe(false); } test('[XAT-5109] Restore file', async ({ trashPage, personalFiles }) => { await restoreNode(trashPage, file1); await personalFiles.navigate(); - expect(await personalFiles.dataTable.isItemPresent(file1)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file1)).toBe(true); }); test('[XAT-5110] Restore folder', async ({ trashPage, personalFiles }) => { await restoreNode(trashPage, folder1); await personalFiles.navigate(); - expect(await personalFiles.dataTable.isItemPresent(folder1)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(folder1)).toBe(true); }); test('[XAT-5117] Restore library', async ({ trashPage, myLibrariesPage }) => { await restoreNode(trashPage, site1); await myLibrariesPage.navigate(); - expect(await myLibrariesPage.dataTable.isItemPresent(site1)).toBeTruthy(); + expect(await myLibrariesPage.dataTable.isItemPresent(site1)).toBe(true); }); test('[XAT-5111] Restore multiple items', async ({ trashPage, personalFiles }) => { @@ -115,11 +115,11 @@ test.describe('Restore from Trash', () => { await trashPage.snackBar.verifySnackBarActionText(`Restore successful`); const action = await trashPage.snackBar.getSnackBarActionText(); expect(action).not.toContain('View'); - expect(await trashPage.dataTable.isItemPresent(file2)).toBeFalsy(); - expect(await trashPage.dataTable.isItemPresent(folder2)).toBeFalsy(); + expect(await trashPage.dataTable.isItemPresent(file2)).toBe(false); + expect(await trashPage.dataTable.isItemPresent(folder2)).toBe(false); await personalFiles.navigate(); - expect(await personalFiles.dataTable.isItemPresent(file2)).toBeTruthy(); - expect(await personalFiles.dataTable.isItemPresent(folder2)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(file2)).toBe(true); + expect(await personalFiles.dataTable.isItemPresent(folder2)).toBe(true); }); test('[XAT-5112] Restore file - View from notification', async ({ trashPage, personalFiles }) => { @@ -127,7 +127,7 @@ test.describe('Restore from Trash', () => { await trashPage.acaHeader.restoreButton.click(); await trashPage.snackBar.clickSnackBarAction(); await trashPage.dataTable.spinnerWaitForReload(); - expect(await trashPage.sidenav.isActive('Personal Files')).toBeTruthy(); + expect(await trashPage.sidenav.isActive('Personal Files')).toBe(true); expect(personalFiles.page.url()).toContain(APP_ROUTES.PERSONAL_FILES); }); }); diff --git a/e2e/playwright/edit-actions/src/tests/edit-folder.e2e.ts b/e2e/playwright/edit-actions/src/tests/edit-folder.e2e.ts index d913598ff..1758856b1 100755 --- a/e2e/playwright/edit-actions/src/tests/edit-folder.e2e.ts +++ b/e2e/playwright/edit-actions/src/tests/edit-folder.e2e.ts @@ -92,7 +92,7 @@ test.describe('Edit folder', () => { await personalFiles.editDialog.updateButton.click(); await expect(personalFiles.editDialog.editDialog).toBeHidden(); - expect(await personalFiles.dataTable.isItemPresent(folderNameEdited)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(folderNameEdited)).toBe(true); const description = await nodesApi.getNodeProperty(folderNameToEditId, 'cm:description'); expect(description).toEqual(folderDescriptionEdited); }); @@ -159,7 +159,7 @@ test.describe('Edit folder', () => { await personalFiles.editDialog.updateButton.click(); await expect(personalFiles.editDialog.editDialog).toBeHidden(); - expect(await personalFiles.dataTable.isItemPresent(folderName)).toBeTruthy(); + expect(await personalFiles.dataTable.isItemPresent(folderName)).toBe(true); }); }); }); diff --git a/e2e/playwright/folder-rules/src/tests/create-rules.e2e.ts b/e2e/playwright/folder-rules/src/tests/create-rules.e2e.ts index 9aca1a07c..a8b432aca 100644 --- a/e2e/playwright/folder-rules/src/tests/create-rules.e2e.ts +++ b/e2e/playwright/folder-rules/src/tests/create-rules.e2e.ts @@ -117,7 +117,7 @@ test.describe('Folder Rules Actions', () => { await nodesPage.rulesToolbar.clickCreateRuleButton(); await expect(nodesPage.manageRulesDialog.createRuleButton).toBeDisabled(); await nodesPage.manageRulesDialog.cancelRuleButton.click(); - expect(nodesPage.manageRules.checkIfRuleListEmpty()).toBeTruthy(); + expect(await nodesPage.manageRules.checkIfRuleListEmpty()).toBe(true); }); test('[XAT-887] Create a disabled rule', async ({ personalFiles, nodesPage }) => { diff --git a/e2e/playwright/library-actions/src/tests/library-actions.e2e.ts b/e2e/playwright/library-actions/src/tests/library-actions.e2e.ts index 09bc891cc..2fcf410ae 100644 --- a/e2e/playwright/library-actions/src/tests/library-actions.e2e.ts +++ b/e2e/playwright/library-actions/src/tests/library-actions.e2e.ts @@ -380,10 +380,7 @@ test.describe('Library actions ', () => { const myLibrariesHeader = myLibrariesPage.acaHeader; const libraryMenu = myLibrariesPage.matMenu; - await myLibrariesHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.searchLibrariesOption.click(); - await searchPage.searchOverlay.searchFor(adminLibrary4); + await searchPage.searchWithin(adminLibrary4, 'libraries'); await expect(libraryTable.getRowByName(adminLibrary4)).toBeVisible(); await libraryTable.getRowByName(adminLibrary4).click(); @@ -397,10 +394,7 @@ test.describe('Library actions ', () => { const searchHeader = searchPage.acaHeader; const libraryMenu = searchPage.matMenu; - await searchHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.searchLibrariesOption.click(); - await searchPage.searchOverlay.searchFor(user2Library4); + await searchPage.searchWithin(user2Library4, 'libraries'); await expect(libraryTable.getRowByName(user2Library4)).toBeVisible(); await searchPage.reload({ waitUntil: domContentLoadedString }); await libraryTable.getRowByName(user2Library4).click(); diff --git a/e2e/playwright/list-views/src/tests/empty-list.e2e.ts b/e2e/playwright/list-views/src/tests/empty-list.e2e.ts index e4317a374..b25c8f2f4 100755 --- a/e2e/playwright/list-views/src/tests/empty-list.e2e.ts +++ b/e2e/playwright/list-views/src/tests/empty-list.e2e.ts @@ -51,12 +51,12 @@ test.describe('Empty list views', () => { async function checkPaginationForTabs(searchPage: SearchPage, tab: string, personalFiles: PersonalFilesPage) { await searchPage.sidenav.openPanel(tab); - expect(await personalFiles.pagination.isRangePresent()).toBeFalsy(); - expect(await personalFiles.pagination.isMaxItemsPresent()).toBeFalsy(); - expect(await personalFiles.pagination.isCurrentPagePresent()).toBeFalsy(); - expect(await personalFiles.pagination.isTotalPagesPresent()).toBeFalsy(); - expect(await personalFiles.pagination.isPreviousButtonPresent()).toBeFalsy(); - expect(await personalFiles.pagination.isNextButtonPresent()).toBeFalsy(); + expect(await personalFiles.pagination.isRangePresent()).toBe(false); + expect(await personalFiles.pagination.isMaxItemsPresent()).toBe(false); + expect(await personalFiles.pagination.isCurrentPagePresent()).toBe(false); + expect(await personalFiles.pagination.isTotalPagesPresent()).toBe(false); + expect(await personalFiles.pagination.isPreviousButtonPresent()).toBe(false); + expect(await personalFiles.pagination.isNextButtonPresent()).toBe(false); } test('[XAT-4402] Empty My Libraries', async ({ myLibrariesPage }) => { @@ -99,10 +99,7 @@ test.describe('Empty list views', () => { }); test('[XAT-4590] Search Page - Pagination control is not displayed on empty page load', async ({ personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor('InvalidText'); + await searchPage.searchWithin('InvalidText', 'filesAndFolders'); await searchPage.reload({ waitUntil: 'domcontentloaded' }); await searchPage.dataTable.spinnerWaitForReload(); diff --git a/e2e/playwright/list-views/src/tests/permissions.e2e.ts b/e2e/playwright/list-views/src/tests/permissions.e2e.ts index 9bcbc0157..12a29e3e3 100755 --- a/e2e/playwright/list-views/src/tests/permissions.e2e.ts +++ b/e2e/playwright/list-views/src/tests/permissions.e2e.ts @@ -120,12 +120,8 @@ test.describe('Special permissions', () => { await expect(sharedPage.dataTable.getRowByName(sitePrivate)).toHaveCount(0); }); - test('[XAT-17775] Search - File is not displayed if the user no longer has permissions on it', async ({ personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(fileName); - await searchPage.dataTable.spinnerWaitForReload(); + test('[XAT-17775] Search - File is not displayed if the user no longer has permissions on it', async ({ searchPage }) => { + await searchPage.searchWithin(fileName, 'filesAndFolders'); expect(await searchPage.dataTable.getRowsCount(), 'Incorrect number of items').toBe(1); @@ -204,14 +200,9 @@ test.describe('Special permissions', () => { }); test(`[XAT-5612] Search Page - Location column is empty if the user doesn't have permissions on the file's parent folder`, async ({ - personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(fileName); - await searchPage.dataTable.spinnerWaitForReload(); + await searchPage.searchWithin(fileName, 'filesAndFolders'); expect(await searchPage.dataTable.getRowsCount(), 'Incorrect number of items').toBe(1); expect(await searchPage.dataTable.getItemLocationText(fileName)).toContain('You do not have permission to view the location of this document.'); diff --git a/e2e/playwright/navigation/src/tests/sidebar.e2e.ts b/e2e/playwright/navigation/src/tests/sidebar.e2e.ts index 584bc94aa..5741253e4 100644 --- a/e2e/playwright/navigation/src/tests/sidebar.e2e.ts +++ b/e2e/playwright/navigation/src/tests/sidebar.e2e.ts @@ -46,11 +46,11 @@ test.describe('Sidebar', () => { expect(await myLibrariesPage.sidenav.isActive(SIDEBAR_LABELS.MY_LIBRARIES), 'My Libraries link not active').toBe(true); }); - test('[XAT-5387] The sidenav can be expanded when search results page is displayed', async ({ personalFiles }) => { + test('[XAT-5387] The sidenav can be collapsed when search results page is displayed', async ({ personalFiles }) => { await personalFiles.navigate({ remoteUrl: `#/search;q=test` }); - await expect(personalFiles.sidenav.expandedSidenav, 'Sidebar expanded').toBeHidden(); - await personalFiles.sidenav.expandSideNav(); - await expect(personalFiles.sidenav.expandedSidenav, 'Sidebar not expanded').toBeVisible(); + await expect(personalFiles.sidenav.expandedSidenav, 'Sidebar expanded').toBeVisible(); + await personalFiles.sidenav.collapseSideNav(); + await expect(personalFiles.sidenav.expandedSidenav, 'Sidebar not collapsed').toBeHidden(); }); test('[XAT-5384] Sidenav state is preserved on page refresh', async ({ personalFiles }) => { @@ -79,7 +79,7 @@ test.describe('Sidebar', () => { searchPage }) => { await personalFiles.navigate({ remoteUrl: `#/search;q=test` }); - await searchPage.searchInput.searchCloseButton.click(); + await searchPage.searchInputComponent.searchCloseButton.click(); await searchPage.sidenav.expandedSidenav.waitFor({ state: 'attached' }); await expect(personalFiles.sidenav.expandedSidenav, 'Sidebar not expanded').toBeVisible(); }); diff --git a/e2e/playwright/search/src/tests/search-input.e2e.ts b/e2e/playwright/search/src/tests/search-input.e2e.ts index 6a225e065..530d17ebd 100644 --- a/e2e/playwright/search/src/tests/search-input.e2e.ts +++ b/e2e/playwright/search/src/tests/search-input.e2e.ts @@ -44,50 +44,31 @@ test.describe('Search - Input', () => { test('[XAT-5563] Search icon is displayed in toolbar and clicking on it displays search input container', async ({ searchPage }) => { await searchPage.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - - await expect(searchPage.searchOverlay.searchInputControl).toBeVisible(); + await expect(searchPage.searchInputComponent.searchInput).toBeVisible(); }); test('[XAT-5564] Search options are displayed when clicking in the search input', async ({ searchPage }) => { await searchPage.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); + await searchPage.searchInputComponent.searchInButton.click(); - await expect(searchPage.searchOverlay.searchOptions).toBeVisible(); - await expect(searchPage.searchOverlay.searchFilesOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchFoldersOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchLibrariesOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchFilesOption).not.toBeChecked(); - await expect(searchPage.searchOverlay.searchFoldersOption).not.toBeChecked(); - await expect(searchPage.searchOverlay.searchLibrariesOption).not.toBeChecked(); + await expect(searchPage.searchInDialog.filesAndFoldersRadioButton).toBeVisible(); + await expect(searchPage.searchInDialog.filesCheckbox).toBeVisible(); + await expect(searchPage.searchInDialog.foldersCheckbox).toBeVisible(); + await expect(searchPage.searchInDialog.librariesRadioButton).toBeVisible(); }); test('[XAT-5565] Search options are correctly enabled / disabled', async ({ searchPage }) => { await searchPage.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); + await searchPage.searchInputComponent.searchInButton.click(); - await searchPage.searchOverlay.searchFilesOption.click(); - await expect(searchPage.searchOverlay.searchFoldersOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchLibrariesOption).toBeDisabled(); + await searchPage.searchInDialog.filesAndFoldersRadioButton.click(); + await expect(searchPage.searchInDialog.filesCheckbox).toBeChecked(); + await expect(searchPage.searchInDialog.foldersCheckbox).toBeChecked(); + await expect(searchPage.searchInDialog.librariesRadioButton).not.toBeChecked(); - await searchPage.searchOverlay.searchFilesOption.click(); - await expect(searchPage.searchOverlay.searchFoldersOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchLibrariesOption).toBeEnabled(); - - await searchPage.searchOverlay.searchFoldersOption.click(); - await expect(searchPage.searchOverlay.searchFilesOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchLibrariesOption).toBeDisabled(); - - await searchPage.searchOverlay.searchFoldersOption.click(); - await expect(searchPage.searchOverlay.searchFilesOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchLibrariesOption).toBeEnabled(); - - await searchPage.searchOverlay.searchLibrariesOption.click(); - await expect(searchPage.searchOverlay.searchFilesOption).toBeDisabled(); - await expect(searchPage.searchOverlay.searchFoldersOption).toBeDisabled(); - - await searchPage.searchOverlay.searchLibrariesOption.click(); - await expect(searchPage.searchOverlay.searchFilesOption).toBeEnabled(); - await expect(searchPage.searchOverlay.searchFoldersOption).toBeEnabled(); + await searchPage.searchInDialog.librariesRadioButton.click(); + await expect(searchPage.searchInDialog.foldersCheckbox).toBeHidden(); + await expect(searchPage.searchInDialog.filesCheckbox).toBeHidden(); + await expect(searchPage.searchInDialog.filesAndFoldersRadioButton).not.toBeChecked(); }); }); diff --git a/e2e/playwright/search/src/tests/search-results-general.e2e.ts b/e2e/playwright/search/src/tests/search-results-general.e2e.ts index b19cff061..2b3e8e324 100644 --- a/e2e/playwright/search/src/tests/search-results-general.e2e.ts +++ b/e2e/playwright/search/src/tests/search-results-general.e2e.ts @@ -82,9 +82,7 @@ test.describe('Search Results - General', () => { expect(await searchPage.dataTable.isItemPresent(file)).toBe(true); expect(await searchPage.dataTable.isItemPresent(folder)).toBe(false); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.searchFor(folder); - await searchPage.dataTable.progressBarWaitForReload(); + await searchPage.searchWithin(folder, 'filesAndFolders'); expect(await searchPage.dataTable.isItemPresent(file)).toBe(false); expect(await searchPage.dataTable.isItemPresent(folder)).toBe(true); diff --git a/e2e/playwright/search/src/tests/search-results-libraries.e2e.ts b/e2e/playwright/search/src/tests/search-results-libraries.e2e.ts index 817a87257..06dbce470 100644 --- a/e2e/playwright/search/src/tests/search-results-libraries.e2e.ts +++ b/e2e/playwright/search/src/tests/search-results-libraries.e2e.ts @@ -143,29 +143,29 @@ test.describe('Search Results - General', () => { test('[XAT-5594] Search library - full name match', async ({ searchPage }) => { await searchPage.searchWithin(site1.name, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(site1.name)).toBeTruthy(); - expect(await searchPage.dataTable.isItemPresent(site2.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site3.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site4.name)).toBeFalsy(); + expect(await searchPage.dataTable.isItemPresent(site1.name)).toBe(true); + expect(await searchPage.dataTable.isItemPresent(site2.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site3.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site4.name)).toBe(false); }); test('[XAT-5595] Search library - partial name match', async ({ searchPage }) => { await fileActionsApi.waitForNodes(site3.id, { expect: 1 }); await searchPage.searchWithin(`lib-${random}`, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(site1.name)).toBeTruthy(); - expect(await searchPage.dataTable.isItemPresent(site2.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site3.name)).toBeTruthy(); - expect(await searchPage.dataTable.isItemPresent(site4.name)).toBeFalsy(); + expect(await searchPage.dataTable.isItemPresent(site1.name)).toBe(true); + expect(await searchPage.dataTable.isItemPresent(site2.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site3.name)).toBe(true); + expect(await searchPage.dataTable.isItemPresent(site4.name)).toBe(false); }); test('[XAT-5596] Search library - description match', async ({ searchPage }) => { await searchPage.searchWithin(site4.description, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(site1.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site2.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site3.name)).toBeFalsy(); - expect(await searchPage.dataTable.isItemPresent(site4.name)).toBeTruthy(); + expect(await searchPage.dataTable.isItemPresent(site1.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site2.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site3.name)).toBe(false); + expect(await searchPage.dataTable.isItemPresent(site4.name)).toBe(true); }); test('[XAT-5598] Libraries Results page columns', async ({ searchPage }) => { @@ -209,13 +209,13 @@ test.describe('Search Results - General', () => { test('[XAT-5601] Private sites are not displayed when user is not a member', async ({ searchPage }) => { await searchPage.searchWithin(`admin-${random}-site`, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(adminPrivate)).toBeFalsy(); + expect(await searchPage.dataTable.isItemPresent(adminPrivate)).toBe(false); }); test('[XAT-5602] Search libraries with special characters', async ({ searchPage }) => { await searchPage.searchWithin(siteRussian.name, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(siteRussian.name)).toBeTruthy(); + expect(await searchPage.dataTable.isItemPresent(siteRussian.name)).toBe(true); }); test('[XAT-5591] Empty Search results - Libraries', async ({ searchPage }) => { @@ -223,7 +223,7 @@ test.describe('Search Results - General', () => { /* cspell:disable-next-line */ await searchPage.searchWithin('qwertyuiop', 'files'); - expect(await searchPage.dataTable.isEmpty()).toBeTruthy(); + expect(await searchPage.dataTable.isEmpty()).toBe(true); expect(await searchPage.dataTable.emptySearchText.textContent()).toContain('Your search returned 0 results'); }); }); diff --git a/e2e/playwright/share-action/src/tests/share/unshare-file-search-results.e2e.ts b/e2e/playwright/share-action/src/tests/share/unshare-file-search-results.e2e.ts index cb4cc6d3a..bbe826dba 100755 --- a/e2e/playwright/share-action/src/tests/share/unshare-file-search-results.e2e.ts +++ b/e2e/playwright/share-action/src/tests/share/unshare-file-search-results.e2e.ts @@ -114,10 +114,7 @@ test.describe('Unshare a file from Search Results', () => { }); test('[XAT-5197] Unshare dialog UI', async ({ personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(file1); + await searchPage.searchWithin(file1, 'filesAndFolders'); await personalFiles.dataTable.selectItems(file1); await personalFiles.acaHeader.shareButton.click(); @@ -135,10 +132,7 @@ test.describe('Unshare a file from Search Results', () => { }); test('[XAT-5198] Unshare a file', async ({ personalFiles, searchPage, nodesApiAction, page }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(file2); + await searchPage.searchWithin(file2, 'filesAndFolders'); await personalFiles.dataTable.selectItems(file2); await personalFiles.acaHeader.shareButton.click(); @@ -154,10 +148,7 @@ test.describe('Unshare a file from Search Results', () => { }); test('[XAT-5199] Cancel the Unshare action', async ({ personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(file3); + await searchPage.searchWithin(file3, 'filesAndFolders'); await personalFiles.dataTable.selectItems(file3); await personalFiles.acaHeader.shareButton.click(); @@ -172,10 +163,7 @@ test.describe('Unshare a file from Search Results', () => { }); test('[XAT-5200] Unshare a file from the context menu', async ({ personalFiles, searchPage, nodesApiAction, page }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(file4); + await searchPage.searchWithin(file4, 'filesAndFolders'); await personalFiles.dataTable.rightClickOnItem(file4); await personalFiles.matMenu.clickMenuItem('Shared Link Settings'); @@ -191,10 +179,7 @@ test.describe('Unshare a file from Search Results', () => { }); test('[XAT-5227] Unshare a file as consumer - file shared by other user - Search Page', async ({ personalFiles, searchPage, nodesApiAction }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(fileSite1); + await searchPage.searchWithin(fileSite1, 'filesAndFolders'); await personalFiles.dataTable.selectItems(fileSite1); await personalFiles.acaHeader.shareButton.click(); @@ -211,10 +196,7 @@ test.describe('Unshare a file from Search Results', () => { nodesApiAction, page }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(fileSite2); + await searchPage.searchWithin(fileSite2, 'filesAndFolders'); await personalFiles.dataTable.selectItems(fileSite2); await personalFiles.acaHeader.shareButton.click(); diff --git a/e2e/playwright/smoke-test/src/tests/search/search-results-general.e2e.ts b/e2e/playwright/smoke-test/src/tests/search/search-results-general.e2e.ts index 7ddb48234..b848c2816 100644 --- a/e2e/playwright/smoke-test/src/tests/search/search-results-general.e2e.ts +++ b/e2e/playwright/smoke-test/src/tests/search/search-results-general.e2e.ts @@ -63,15 +63,15 @@ test.describe('Search Results - General', () => { test('[XAT-17734] Search with Folders options are checked', async ({ searchPage }) => { await searchPage.searchWithin(`*${random}`, 'filesAndFolders'); - expect(await searchPage.dataTable.isItemPresent(file)).toBeTruthy(); - expect(await searchPage.dataTable.isItemPresent(folder)).toBeTruthy(); - expect(await searchPage.dataTable.isItemPresent(site)).toBeFalsy(); + expect(await searchPage.dataTable.isItemPresent(file)).toBe(true); + expect(await searchPage.dataTable.isItemPresent(folder)).toBe(true); + expect(await searchPage.dataTable.isItemPresent(site)).toBe(false); }); test('[XAT-17735] Search with an URL containing a search query', async ({ searchPage, personalFiles }) => { await searchPage.searchWithin(site, 'libraries'); - expect(await searchPage.dataTable.isItemPresent(site)).toBeTruthy(); + expect(await searchPage.dataTable.isItemPresent(site)).toBe(true); const url = searchPage.page.url(); @@ -79,6 +79,6 @@ test.describe('Search Results - General', () => { await personalFiles.page.goto(url); await searchPage.dataTable.progressBarWaitForReload(); - expect(await searchPage.dataTable.isItemPresent(site)).toBeTruthy(); + expect(await searchPage.dataTable.isItemPresent(site)).toBe(true); }); }); diff --git a/e2e/playwright/special-permissions-actions-available/src/tests/viewer.ts b/e2e/playwright/special-permissions-actions-available/src/tests/viewer.ts index 2323c5979..e1d6d9dab 100644 --- a/e2e/playwright/special-permissions-actions-available/src/tests/viewer.ts +++ b/e2e/playwright/special-permissions-actions-available/src/tests/viewer.ts @@ -331,7 +331,7 @@ export function viewerTests(userConsumer: string, siteName: string) { await loginPage.navigate(); await loginPage.loginUser({ username: userConsumer, password: userConsumer }); await searchPage.searchWithin(item, 'filesAndFolders'); - await searchPage.searchInput.performDoubleClickFolderOrFileToOpen(item); + await searchPage.dataTable.performClickFolderOrFileToOpen(item); expect(await searchPage.viewer.isViewerOpened(), 'Viewer is not opened').toBe(true); await searchPage.viewer.verifyViewerPrimaryActions(expectedToolbarPrimary); await searchPage.acaHeader.clickViewerMoreActions(); diff --git a/e2e/playwright/viewer/src/tests/viewer-version-actions.e2e.ts b/e2e/playwright/viewer/src/tests/viewer-version-actions.e2e.ts index be6dbd31e..708f8d9cb 100644 --- a/e2e/playwright/viewer/src/tests/viewer-version-actions.e2e.ts +++ b/e2e/playwright/viewer/src/tests/viewer-version-actions.e2e.ts @@ -194,9 +194,7 @@ test.describe('Version actions', () => { test.describe('on Search', () => { test.beforeEach(async ({ loginPage, searchPage }) => { await Utils.tryLoginUser(loginPage, username, username, 'beforeEach failed'); - await searchPage.navigate(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.searchFor(filenameAfterUpdate); + await searchPage.searchWithin(filenameAfterUpdate, 'files'); await viewFirstFileVersion(searchPage); }); diff --git a/e2e/playwright/viewer/src/tests/viewer.e2e.ts b/e2e/playwright/viewer/src/tests/viewer.e2e.ts index 21d4f2e19..7fd3303b6 100644 --- a/e2e/playwright/viewer/src/tests/viewer.e2e.ts +++ b/e2e/playwright/viewer/src/tests/viewer.e2e.ts @@ -180,13 +180,10 @@ test.describe('viewer file', () => { expect(await recentFilesPage.viewer.isFileTitleDisplayed(), 'File title is not displayed').toBe(true); }); - test('[XAT-5482] Viewer opens for a file from Search Results', async ({ personalFiles, searchPage }) => { - await personalFiles.acaHeader.searchButton.click(); - await searchPage.clickSearchButton(); - await searchPage.searchOverlay.checkFilesAndFolders(); - await searchPage.searchOverlay.searchFor(randomDocxName); + test('[XAT-5482] Viewer opens for a file from Search Results', async ({ searchPage }) => { + await searchPage.searchWithin(randomDocxName, 'filesAndFolders'); await searchPage.dataTable.goThroughPagesLookingForRowWithName(randomDocxName); - await searchPage.searchInput.performDoubleClickFolderOrFileToOpen(randomDocxName); + await searchPage.dataTable.performClickFolderOrFileToOpen(randomDocxName); expect(await searchPage.viewer.isViewerOpened(), 'Viewer is not opened').toBe(true); expect(await searchPage.viewer.isCloseButtonDisplayed(), 'Close button is not displayed').toBe(true); expect(await searchPage.viewer.isFileTitleDisplayed(), 'File title is not displayed').toBe(true); diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index a787dded1..479edb174 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -601,7 +601,11 @@ "MIN_LENGTH": "Search input must have at least 2 alphanumeric characters.", "REQUIRED": "Search input is required.", "WHITESPACE": "Search input cannot be only whitespace.", - "OPERATORS": "Search input cannot begin with, end with or contain only operators." + "OPERATORS": "Search input cannot begin with, end with or contain only operators.", + "IN_PREFIX": "In", + "FILES_AND_FOLDERS": "Files and folders", + "RESET": "Reset", + "SEARCH_IN": "Search in" }, "SORT": { "SORTING_OPTION": "Sort by", diff --git a/projects/aca-content/src/lib/components/search/index.ts b/projects/aca-content/src/lib/components/search/index.ts index cbad922bf..be60b738d 100644 --- a/projects/aca-content/src/lib/components/search/index.ts +++ b/projects/aca-content/src/lib/components/search/index.ts @@ -27,7 +27,6 @@ import { SearchLibrariesResultsComponent } from './search-libraries-results/sear import { SearchResultsComponent } from './search-results/search-results.component'; import { SearchResultsRowComponent } from './search-results-row/search-results-row.component'; import { SearchInputComponent } from './search-input/search-input.component'; -import { SearchInputControlComponent } from './search-input-control/search-input-control.component'; /* @deprecated import standalone components instead */ export const APP_SEARCH_DIRECTIVES = [ @@ -35,6 +34,5 @@ export const APP_SEARCH_DIRECTIVES = [ SearchLibrariesResultsComponent, SearchResultsComponent, SearchResultsRowComponent, - SearchInputComponent, - SearchInputControlComponent + SearchInputComponent ] as const; diff --git a/projects/aca-content/src/lib/components/search/search-execution.service.spec.ts b/projects/aca-content/src/lib/components/search/search-execution.service.spec.ts new file mode 100644 index 000000000..a6bfe518b --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-execution.service.spec.ts @@ -0,0 +1,102 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { TestBed } from '@angular/core/testing'; +import { Store } from '@ngrx/store'; +import { SearchExecutionService } from './search-execution.service'; +import { SearchFilterService } from './search-filter.service'; +import { SearchNavigationService } from './search-navigation.service'; +import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { SearchLibrariesQueryBuilderService } from './search-libraries-results/search-libraries-query-builder.service'; +import { AppStore, SearchActionTypes } from '@alfresco/aca-shared/store'; + +describe('SearchExecutionService', () => { + let service: SearchExecutionService; + let store: jasmine.SpyObj>; + let filterService: jasmine.SpyObj; + let navigationService: jasmine.SpyObj; + let queryBuilder: jasmine.SpyObj; + let queryLibrariesBuilder: jasmine.SpyObj; + + beforeEach(() => { + store = jasmine.createSpyObj('Store', ['dispatch']); + filterService = jasmine.createSpyObj('SearchFilterService', ['applyContentFilters', 'isLibrariesChecked'], { + searchOptions: [] + }); + navigationService = jasmine.createSpyObj('SearchNavigationService', ['isSameSearchTerm'], { + onSearchResults: false, + onLibrariesSearchResults: false + }); + queryBuilder = jasmine.createSpyObj('SearchQueryBuilderService', ['update']); + queryLibrariesBuilder = jasmine.createSpyObj('SearchLibrariesQueryBuilderService', ['update']); + + TestBed.configureTestingModule({ + providers: [ + SearchExecutionService, + { provide: Store, useValue: store }, + { provide: SearchFilterService, useValue: filterService }, + { provide: SearchNavigationService, useValue: navigationService }, + { provide: SearchQueryBuilderService, useValue: queryBuilder }, + { provide: SearchLibrariesQueryBuilderService, useValue: queryLibrariesBuilder } + ] + }); + + service = TestBed.inject(SearchExecutionService); + }); + + it('should do nothing for empty/null search term', () => { + service.execute(''); + service.execute(null); + service.execute(' '); + expect(filterService.applyContentFilters).not.toHaveBeenCalled(); + }); + + it('should apply content filters before executing', () => { + filterService.isLibrariesChecked.and.returnValue(false); + service.execute('test'); + expect(filterService.applyContentFilters).toHaveBeenCalled(); + }); + + describe('content search', () => { + beforeEach(() => { + filterService.isLibrariesChecked.and.returnValue(false); + }); + + it('should dispatch SearchByTermAction for new content search', () => { + service.execute('test'); + expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ type: SearchActionTypes.SearchByTerm, payload: 'test' })); + }); + }); + + describe('libraries search', () => { + beforeEach(() => { + filterService.isLibrariesChecked.and.returnValue(true); + }); + + it('should dispatch SearchByTermAction for new libraries search', () => { + service.execute('test'); + expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ type: SearchActionTypes.SearchByTerm, payload: 'test' })); + }); + }); +}); diff --git a/projects/aca-content/src/lib/components/search/search-execution.service.ts b/projects/aca-content/src/lib/components/search/search-execution.service.ts new file mode 100644 index 000000000..b8f774161 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-execution.service.ts @@ -0,0 +1,72 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppStore, SearchByTermAction } from '@alfresco/aca-shared/store'; +import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { SearchFilterService } from './search-filter.service'; +import { SearchNavigationService } from './search-navigation.service'; +import { SearchLibrariesQueryBuilderService } from './search-libraries-results/search-libraries-query-builder.service'; + +@Injectable({ providedIn: 'root' }) +export class SearchExecutionService { + constructor( + private readonly store: Store, + private readonly queryBuilder: SearchQueryBuilderService, + private readonly queryLibrariesBuilder: SearchLibrariesQueryBuilderService, + private readonly filterService: SearchFilterService, + private readonly searchNavigationService: SearchNavigationService + ) {} + + execute(searchedWord: string) { + if (!searchedWord?.trim()) { + return; + } + + this.filterService.applyContentFilters(); + + if (this.filterService.isLibrariesChecked()) { + this.executeLibrariesSearch(searchedWord); + } else { + this.executeContentSearch(searchedWord); + } + } + + private executeLibrariesSearch(searchedWord: string) { + if (this.searchNavigationService.onLibrariesSearchResults && this.searchNavigationService.isSameSearchTerm(searchedWord)) { + this.queryLibrariesBuilder.update(); + } else { + this.store.dispatch(new SearchByTermAction(searchedWord, this.filterService.searchOptions)); + } + } + + private executeContentSearch(searchedWord: string) { + if (this.searchNavigationService.onSearchResults && this.searchNavigationService.isSameSearchTerm(searchedWord)) { + this.queryBuilder.update(); + } else { + this.store.dispatch(new SearchByTermAction(searchedWord, this.filterService.searchOptions)); + } + } +} diff --git a/projects/aca-content/src/lib/components/search/search-filter.service.spec.ts b/projects/aca-content/src/lib/components/search/search-filter.service.spec.ts new file mode 100644 index 000000000..df522efc9 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-filter.service.spec.ts @@ -0,0 +1,148 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { TestBed } from '@angular/core/testing'; +import { SearchFilterService } from './search-filter.service'; +import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; + +describe('SearchFilterService', () => { + let service: SearchFilterService; + let queryBuilder: jasmine.SpyObj; + + beforeEach(() => { + queryBuilder = jasmine.createSpyObj('SearchQueryBuilderService', ['addFilterQuery', 'removeFilterQuery']); + + TestBed.configureTestingModule({ + providers: [SearchFilterService, { provide: SearchQueryBuilderService, useValue: queryBuilder }] + }); + + service = TestBed.inject(SearchFilterService); + }); + + describe('getSearchInLabel', () => { + it('should return libraries label in libraries mode', () => { + service.searchInMode = 'libraries'; + expect(service.getSearchInLabel()).toBe('SEARCH.INPUT.LIBRARIES'); + }); + + it('should return filter and folders label when both checked', () => { + service.searchInMode = 'content'; + service.filesChecked = true; + service.foldersChecked = true; + expect(service.getSearchInLabel()).toBe('SEARCH.INPUT.FILES_AND_FOLDERS'); + }); + + it('should return files label when only files checked', () => { + service.searchInMode = 'content'; + service.filesChecked = true; + service.foldersChecked = false; + expect(service.getSearchInLabel()).toBe('SEARCH.INPUT.FILES'); + }); + + it('should return folders when only folders checked', () => { + service.searchInMode = 'content'; + service.filesChecked = false; + service.foldersChecked = true; + expect(service.getSearchInLabel()).toBe('SEARCH.INPUT.FOLDERS'); + }); + }); + + describe('validateSearchTerm', () => { + it('should return error for empty term', () => { + expect(service.validateSearchTerm('')).toBe('SEARCH.INPUT.REQUIRED'); + }); + + it('should return error for null/undefined', () => { + expect(service.validateSearchTerm(null)).toBe('SEARCH.INPUT.REQUIRED'); + }); + + it('should return error for whitespace-only term', () => { + expect(service.validateSearchTerm(' ')).toBe('SEARCH.INPUT.WHITESPACE'); + }); + + it('should return error for term starting with operator', () => { + expect(service.validateSearchTerm('+test')).toBe('SEARCH.INPUT.OPERATORS'); + }); + + it('should return error for single char in libraries mode', () => { + service.searchInMode = 'libraries'; + expect(service.validateSearchTerm('a')).toBe('SEARCH.INPUT.MIN_LENGTH'); + }); + + it('should return null for valid term', () => { + expect(service.validateSearchTerm('valid search')).toBeNull(); + }); + }); + + describe('applyContentFilters', () => { + it('should add folder filter when only folders checked in content mode', () => { + service.searchInMode = 'content'; + service.filesChecked = false; + service.foldersChecked = true; + service.applyContentFilters(); + + expect(queryBuilder.addFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:folder'"); + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:content'"); + }); + + it('should add content filter when only files checked in content mode', () => { + service.searchInMode = 'content'; + service.filesChecked = true; + service.foldersChecked = false; + service.applyContentFilters(); + + expect(queryBuilder.addFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:content'"); + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:folder'"); + }); + + it('should remove all content filters when both checked', () => { + service.searchInMode = 'content'; + service.filesChecked = true; + service.foldersChecked = true; + service.applyContentFilters(); + + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:content'"); + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:folder'"); + }); + + it('should remove content filters in libraries mode', () => { + service.searchInMode = 'libraries'; + service.filesChecked = false; + service.foldersChecked = false; + service.applyContentFilters(); + + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:content'"); + expect(queryBuilder.removeFilterQuery).toHaveBeenCalledWith("+TYPE:'cm:folder'"); + }); + }); + + describe('initForLibrariesRoute', () => { + it('should set libraries mode and uncheck files/folders', () => { + service.initForLibrariesRoute(); + expect(service.searchInMode).toBe('libraries'); + expect(service.filesChecked).toBeFalse(); + expect(service.foldersChecked).toBeFalse(); + }); + }); +}); diff --git a/projects/aca-content/src/lib/components/search/search-filter.service.ts b/projects/aca-content/src/lib/components/search/search-filter.service.ts new file mode 100644 index 000000000..bf055ccb8 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-filter.service.ts @@ -0,0 +1,167 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { SearchOptionIds, SearchOptionModel } from '@alfresco/aca-shared/store'; +import { isOperator } from '../../utils/aca-search-utils'; + +@Injectable({ providedIn: 'root' }) +export class SearchFilterService { + searchInMode: 'content' | 'libraries' = 'content'; + filesChecked = true; + foldersChecked = true; + + searchOptions: SearchOptionModel[] = [ + { + id: SearchOptionIds.Files, + key: 'SEARCH.INPUT.FILES', + value: false, + shouldDisable: () => this.isLibrariesChecked() + }, + { + id: SearchOptionIds.Folders, + key: 'SEARCH.INPUT.FOLDERS', + value: false, + shouldDisable: () => this.isLibrariesChecked() + }, + { + id: SearchOptionIds.Libraries, + key: 'SEARCH.INPUT.LIBRARIES', + value: false, + shouldDisable: () => this.isContentChecked() + } + ]; + + constructor(private readonly queryBuilder: SearchQueryBuilderService) {} + + isFilesChecked(): boolean { + return this.getOptionValue(SearchOptionIds.Files); + } + + isFoldersChecked(): boolean { + return this.getOptionValue(SearchOptionIds.Folders); + } + + isLibrariesChecked(): boolean { + return this.getOptionValue(SearchOptionIds.Libraries); + } + + isContentChecked(): boolean { + return this.isFilesChecked() || this.isFoldersChecked(); + } + + getSearchInLabel(): string { + if (this.searchInMode === SearchOptionIds.Libraries) { + return 'SEARCH.INPUT.LIBRARIES'; + } + if (this.filesChecked === this.foldersChecked) { + return 'SEARCH.INPUT.FILES_AND_FOLDERS'; + } + if (this.filesChecked) { + return 'SEARCH.INPUT.FILES'; + } + if (this.foldersChecked) { + return 'SEARCH.INPUT.FOLDERS'; + } + return 'SEARCH.INPUT.FILES_AND_FOLDERS'; + } + + validateSearchTerm(term: string): string | null { + if (!term) { + return 'SEARCH.INPUT.REQUIRED'; + } + if (/^\s+$/.test(term)) { + return 'SEARCH.INPUT.WHITESPACE'; + } + + const words = term.trim().split(/\s+/); + if (isOperator(words[0]) || isOperator(words[words.length - 1])) { + return 'SEARCH.INPUT.OPERATORS'; + } + + if (/^[+\-|!(){}[\]^"~*?:\\/]/.test(term) || /[+\-|!(){}[\]^"~*?:\\/]$/.test(term)) { + return 'SEARCH.INPUT.OPERATORS'; + } + if (this.searchInMode === SearchOptionIds.Libraries && term.length < 2) { + return 'SEARCH.INPUT.MIN_LENGTH'; + } + return null; + } + + syncSearchOptionsFromState() { + const filesOption = this.searchOptions.find((opt) => opt.id === SearchOptionIds.Files); + const foldersOption = this.searchOptions.find((opt) => opt.id === SearchOptionIds.Folders); + const librariesOption = this.searchOptions.find((opt) => opt.id === SearchOptionIds.Libraries); + + if (this.searchInMode === SearchOptionIds.Libraries) { + filesOption.value = false; + foldersOption.value = false; + librariesOption.value = true; + } else { + filesOption.value = this.filesChecked && !this.foldersChecked; + foldersOption.value = this.foldersChecked && !this.filesChecked; + librariesOption.value = false; + } + } + + applyContentFilters() { + this.syncSearchOptionsFromState(); + + if (this.isLibrariesChecked()) { + this.removeContentFilters(); + return; + } + + if (this.isFoldersChecked() && !this.isFilesChecked()) { + this.setContentFilter(SearchOptionIds.Folders); + } else if (this.isFilesChecked() && !this.isFoldersChecked()) { + this.setContentFilter(SearchOptionIds.Files); + } else { + this.removeContentFilters(); + } + } + + removeContentFilters() { + this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Files}'`); + this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Folders}'`); + } + + initForLibrariesRoute() { + this.searchInMode = SearchOptionIds.Libraries; + this.filesChecked = false; + this.foldersChecked = false; + } + + private getOptionValue(optionId: string): boolean { + const item = this.searchOptions.find((opt) => opt.id === optionId); + return !!item && item.value; + } + + private setContentFilter(option: SearchOptionIds.Folders | SearchOptionIds.Files) { + const oppositeOption = option === SearchOptionIds.Folders ? SearchOptionIds.Files : SearchOptionIds.Folders; + this.queryBuilder.addFilterQuery(`+TYPE:'cm:${option}'`); + this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${oppositeOption}'`); + } +} diff --git a/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.html b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.html new file mode 100644 index 000000000..2bcce1664 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.html @@ -0,0 +1,76 @@ + + + +
+
+ {{ 'SEARCH.INPUT.SEARCH_IN' | translate }} + +
+ +
+ + + {{ 'SEARCH.INPUT.FILES_AND_FOLDERS' | translate }} + + + @if (searchInMode === 'content') { +
+ + {{ 'SEARCH.INPUT.FILES' | translate }} + + + {{ 'SEARCH.INPUT.FOLDERS' | translate }} + +
+ } + + + {{ 'SEARCH.INPUT.LIBRARIES' | translate }} + +
+
+ + + +
+ + +
+
+
diff --git a/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.scss b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.scss new file mode 100644 index 000000000..d3ef3e38d --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.scss @@ -0,0 +1,70 @@ +@use '../../../ui/mat-selectors' as ms; + +aca-search-in-menu { + .aca-search-input--search-in-button { + border-radius: 16px; + height: 32px; + padding: 0 8px 0 12px; + white-space: nowrap; + min-width: unset; + background-color: var(--theme-dropdown-color); + border: none; + + &#{ms.$mat-icon-no-color} { + font-size: 18px; + width: 18px; + height: 18px; + margin-left: 2px; + } + + &:hover { + background-color: var(--theme-dropdown-background-hover, rgba(0, 0, 0, 0.04)); + } + } +} + +.aca-search-in-panel { + padding: 0; + border: none; + background-color: var(--theme-white-background); + min-width: 512px; + box-shadow: 0 2px 4px var(--theme-grey-divider-color); + border-radius: 12px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 8px 8px 16px; + font-weight: bold; + border-bottom: 1px solid var(--theme-grey-background-color); + } + + &__title { + font-size: 16px; + font-weight: bold; + } + + &__content { + padding: 24px; + } + + &__radio { + display: block; + } + + &__checkboxes { + display: flex; + flex-direction: column; + gap: 8px; + padding-left: 28px; + padding-bottom: 4px; + margin: 10px 0; + } + + &__actions { + display: flex; + justify-content: space-between; + padding: 8px 16px; + } +} diff --git a/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.spec.ts b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.spec.ts new file mode 100644 index 000000000..3dba4dd66 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.spec.ts @@ -0,0 +1,150 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SearchInMenuComponent } from './search-in-menu.component'; +import { SearchFilterService } from '../search-filter.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppTestingModule } from '../../../testing/app-testing.module'; + +describe('SearchInMenuComponent', () => { + let fixture: ComponentFixture; + let component: SearchInMenuComponent; + let filterService: jasmine.SpyObj; + + beforeEach(async () => { + filterService = jasmine.createSpyObj('SearchFilterService', ['getSearchInLabel']); + filterService.getSearchInLabel.and.returnValue('SEARCH.INPUT.FILES_AND_FOLDERS'); + filterService.searchInMode = 'content'; + filterService.filesChecked = true; + filterService.foldersChecked = true; + + await TestBed.configureTestingModule({ + imports: [SearchInMenuComponent, AppTestingModule, NoopAnimationsModule, TranslateModule.forRoot()], + providers: [{ provide: SearchFilterService, useValue: filterService }] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchInMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should initialize state from filterService on init', () => { + expect(component.searchInMode).toBe('content'); + expect(component.filesChecked).toBeTrue(); + expect(component.foldersChecked).toBeTrue(); + }); + + it('should uncheck files/folders when switching to libraries mode', () => { + component.searchInMode = 'libraries'; + component.onSearchInModeChange(); + expect(component.filesChecked).toBeFalse(); + expect(component.foldersChecked).toBeFalse(); + }); + + it('should check files/folders when switching back to content mode', () => { + component.searchInMode = 'libraries'; + component.onSearchInModeChange(); + component.searchInMode = 'content'; + component.onSearchInModeChange(); + expect(component.filesChecked).toBeTrue(); + expect(component.foldersChecked).toBeTrue(); + }); + + it('should check folders when unchecking files and both would be unchecked', () => { + component.filesChecked = false; + component.foldersChecked = true; + component.onFilesCheckedChange(); + expect(component.foldersChecked).toBeTrue(); + + component.foldersChecked = false; + component.filesChecked = false; + component.onFilesCheckedChange(); + expect(component.foldersChecked).toBeTrue(); + }); + + it('should check files when unchecking folders and both would be unchecked', () => { + component.foldersChecked = false; + component.filesChecked = true; + component.onFoldersCheckedChange(); + expect(component.filesChecked).toBeTrue(); + + component.filesChecked = false; + component.foldersChecked = false; + component.onFoldersCheckedChange(); + expect(component.filesChecked).toBeTrue(); + }); + + it('should not change other checkbox when at least one remains checked', () => { + component.filesChecked = false; + component.foldersChecked = true; + component.onFilesCheckedChange(); + expect(component.foldersChecked).toBeTrue(); + + component.filesChecked = true; + component.foldersChecked = false; + component.onFoldersCheckedChange(); + expect(component.filesChecked).toBeTrue(); + }); + + it('should commit state to service and emit on apply', () => { + spyOn(component.filtersApplied, 'emit'); + component.searchInMode = 'libraries'; + component.filesChecked = false; + component.foldersChecked = false; + + component.apply(); + + expect(filterService.searchInMode).toBe('libraries'); + expect(filterService.filesChecked).toBeFalse(); + expect(filterService.foldersChecked).toBeFalse(); + expect(component.filtersApplied.emit).toHaveBeenCalled(); + }); + + it('should revert to last applied state on close', () => { + component.apply(); + + component.searchInMode = 'libraries'; + component.filesChecked = false; + component.foldersChecked = false; + component.close(); + + expect(component.searchInMode).toBe('content'); + expect(component.filesChecked).toBeTrue(); + expect(component.foldersChecked).toBeTrue(); + }); + + it('should reset to defaults', () => { + component.searchInMode = 'libraries'; + component.filesChecked = false; + component.foldersChecked = false; + + component.reset(); + + expect(component.searchInMode).toBe('content'); + expect(component.filesChecked).toBeTrue(); + expect(component.foldersChecked).toBeTrue(); + }); +}); diff --git a/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.ts b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.ts new file mode 100644 index 000000000..5526462a5 --- /dev/null +++ b/projects/aca-content/src/lib/components/search/search-in-menu/search-in-menu.component.ts @@ -0,0 +1,178 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Component, ElementRef, EventEmitter, inject, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe } from '@ngx-translate/core'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatDividerModule } from '@angular/material/divider'; +import { SearchOptionIds } from '@alfresco/aca-shared/store'; +import { SearchFilterService } from '../search-filter.service'; +import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; + +@Component({ + selector: 'aca-search-in-menu', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TranslatePipe, + OverlayModule, + MatButtonModule, + MatIconModule, + MatCheckboxModule, + MatRadioModule, + MatDividerModule + ], + templateUrl: './search-in-menu.component.html', + styleUrls: ['./search-in-menu.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchInMenuComponent implements OnInit { + readonly SearchOptionIds = SearchOptionIds; + + @Output() readonly filtersApplied = new EventEmitter(); + + @ViewChild('searchInPanel') searchInPanel: ElementRef; + @ViewChild('overlayOrigin', { read: ElementRef }) triggerButton: ElementRef; + + isOpen = false; + searchInMode: 'content' | 'libraries' = 'content'; + filesChecked = true; + foldersChecked = true; + + readonly filterService = inject(SearchFilterService); + private readonly focusTrapFactory = inject(ConfigurableFocusTrapFactory); + + private focusTrap: ConfigurableFocusTrap; + private pendingSearchInMode: 'content' | 'libraries' = 'content'; + private pendingFilesChecked = true; + private pendingFoldersChecked = true; + + ngOnInit() { + this.initializeSavedValues(); + } + + toggle(): void { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + this.isOpen = true; + setTimeout(() => { + if (this.searchInPanel) { + this.focusTrap = this.focusTrapFactory.create(this.searchInPanel.nativeElement); + this.focusTrap.focusInitialElement(); + } + }); + } + + close() { + this.searchInMode = this.pendingSearchInMode; + this.filesChecked = this.pendingFilesChecked; + this.foldersChecked = this.pendingFoldersChecked; + this.destroyFocusTrap(); + this.isOpen = false; + this.focusTrigger(); + } + + onPanelDetached() { + this.destroyFocusTrap(); + this.isOpen = false; + } + + onSearchInModeChange() { + if (this.searchInMode === SearchOptionIds.Libraries) { + this.filesChecked = false; + this.foldersChecked = false; + } else { + this.filesChecked = true; + this.foldersChecked = true; + } + } + + onFilesCheckedChange() { + if (!this.filesChecked && !this.foldersChecked) { + this.foldersChecked = true; + } + } + + onFoldersCheckedChange() { + if (!this.filesChecked && !this.foldersChecked) { + this.filesChecked = true; + } + } + + apply() { + this.commitToService(); + this.pendingSearchInMode = this.searchInMode; + this.pendingFilesChecked = this.filesChecked; + this.pendingFoldersChecked = this.foldersChecked; + this.destroyFocusTrap(); + this.isOpen = false; + this.focusTrigger(); + this.filtersApplied.emit(); + } + + reset() { + this.searchInMode = 'content'; + this.filesChecked = true; + this.foldersChecked = true; + } + + private destroyFocusTrap() { + if (this.focusTrap) { + this.focusTrap.destroy(); + this.focusTrap = null; + } + } + + private initializeSavedValues() { + this.searchInMode = this.filterService.searchInMode; + this.filesChecked = this.filterService.filesChecked; + this.foldersChecked = this.filterService.foldersChecked; + this.pendingSearchInMode = this.searchInMode; + this.pendingFilesChecked = this.filesChecked; + this.pendingFoldersChecked = this.foldersChecked; + } + + private commitToService() { + this.filterService.searchInMode = this.searchInMode; + this.filterService.filesChecked = this.filesChecked; + this.filterService.foldersChecked = this.foldersChecked; + } + + private focusTrigger() { + setTimeout(() => this.triggerButton?.nativeElement?.focus()); + } +} diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html deleted file mode 100644 index 3f378a6d5..000000000 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.html +++ /dev/null @@ -1,39 +0,0 @@ -
- - - - - - -
- -
-
-
diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.scss b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.scss deleted file mode 100644 index 2b70f289d..000000000 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.scss +++ /dev/null @@ -1,37 +0,0 @@ -$search-height: 64px; - -.app-search-container { - font-size: 16px; - box-sizing: border-box; - height: $search-height; - display: flex; - flex-direction: row; - align-items: center; - - .app-suffix-icon { - width: 18px; - height: 18px; - font-size: 18px; - } - - /* stylelint-disable-next-line selector-class-pattern */ - .mdc-notched-outline { - display: none; - } - - .app-input-form-field { - letter-spacing: -0.7px; - font-size: 16px; - width: 100%; - height: $search-height; - line-height: calc($search-height / 2); - - &-input { - letter-spacing: -0.7px; - } - - .app-search-input-control-clear-button { - margin-right: 1.5px; - } - } -} diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts deleted file mode 100644 index 76195eb67..000000000 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -/*! - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Alfresco Example Content Application - * - * This file is part of the Alfresco Example Content Application. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * The Alfresco Example Content Application is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Alfresco Example Content Application is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * from Hyland Software. If not, see . - */ - -import { SearchInputControlComponent } from './search-input-control.component'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { AppTestingModule } from '../../../testing/app-testing.module'; -import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; -import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; -import { of } from 'rxjs'; -import { UnitTestingUtils } from '@alfresco/adf-core'; -import { HarnessLoader } from '@angular/cdk/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; - -describe('SearchInputControlComponent', () => { - let fixture: ComponentFixture; - let component: SearchInputControlComponent; - let unitTestingUtils: UnitTestingUtils; - let loader: HarnessLoader; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [AppTestingModule, SearchInputControlComponent], - schemas: [NO_ERRORS_SCHEMA] - }); - fixture = TestBed.createComponent(SearchInputControlComponent); - component = fixture.componentInstance; - loader = TestbedHarnessEnvironment.loader(fixture); - unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); - fixture.detectChanges(); - }); - - /** - * Sets the input value of the search input control component. - */ - function setInputValue(value: string) { - component.searchTerm = value; - fixture.detectChanges(); - } - - it('should emit submit event if form is valid', () => { - component.searchTerm = 'valid'; - spyOn(component.submit, 'emit'); - - component.searchSubmit(); - - expect(component.submit.emit).toHaveBeenCalledWith('valid'); - }); - - it('should not emit submit event if form is invalid', () => { - component.searchTerm = ''; - spyOn(component.submit, 'emit'); - - component.searchSubmit(); - - expect(component.submit.emit).not.toHaveBeenCalled(); - }); - - it('should emit searchChange event on inputChange', () => { - let emittedSearchTerm = ''; - component.searchChange.subscribe((searchTerm) => (emittedSearchTerm = searchTerm)); - component.searchTerm = 'mock-search-term'; - expect(emittedSearchTerm).toBe('mock-search-term'); - }); - - it('should emit searchChange event on clear', () => { - let emittedSearchTerm: string = null; - component.searchChange.subscribe((searchTerm) => (emittedSearchTerm = searchTerm)); - component.clear(); - expect(emittedSearchTerm).toBe(''); - }); - - it('should clear searchTerm', () => { - setInputValue('c'); - component.clear(); - expect(component.searchTerm).toBe(''); - }); - - it('should check if searchTerm has a length less than 2', () => { - setInputValue('d'); - expect(component.isTermTooShort()).toBe(true); - setInputValue('dd'); - expect(component.isTermTooShort()).toBe(false); - }); - - it('should mark searchFieldFormControl as untouched on blur', async () => { - spyOn(component.searchFieldFormControl, 'markAsUntouched').and.callThrough(); - const input = await unitTestingUtils.getMatInput(); - await input.setValue('test'); - expect(component.searchFieldFormControl.touched).toBeTrue(); - - await input.blur(); - expect(component.searchFieldFormControl.markAsUntouched).toHaveBeenCalled(); - expect(component.searchFieldFormControl.touched).toBeFalse(); - }); - - describe('ngOnInit', () => { - let route: ActivatedRoute; - let router: Router; - - beforeEach(() => { - route = TestBed.inject(ActivatedRoute); - router = TestBed.inject(Router); - spyOnProperty(router, 'events').and.returnValue(of(new NavigationStart(1, ''))); - }); - - it('should set * as value when url params has q parameter and input is empty', fakeAsync(() => { - spyOn(component.searchFieldFormControl, 'setValue'); - route.queryParams = of({ q: 'someQueryParams' }); - - component.ngOnInit(); - tick(); - expect(component.searchFieldFormControl.setValue).toHaveBeenCalledWith('*'); - })); - - it('should not set * as value when url params has missing q parameter and input is empty', fakeAsync(() => { - spyOn(component.searchFieldFormControl, 'setValue'); - route.queryParams = of({ otherQueryParam: 'someQueryParams' }); - - component.ngOnInit(); - tick(); - expect(component.searchFieldFormControl.setValue).not.toHaveBeenCalled(); - })); - - it('should not set * as value when url params has q parameter and input is not empty', fakeAsync(() => { - component.searchFieldFormControl.setValue('some value'); - spyOn(component.searchFieldFormControl, 'setValue'); - route.queryParams = of({ q: 'someQueryParams' }); - - component.ngOnInit(); - tick(); - expect(component.searchFieldFormControl.setValue).not.toHaveBeenCalled(); - })); - }); - - describe('validation error messages', () => { - beforeEach(() => { - spyOn(component.validationError, 'emit'); - }); - - it('should emit correct validation error message for whitespace validator', () => { - setInputValue(' '); - expect(component.validationError.emit).toHaveBeenCalledWith('SEARCH.INPUT.WHITESPACE'); - }); - - it('should emit correct validation error message for operators validator', () => { - setInputValue('AND word'); - expect(component.validationError.emit).toHaveBeenCalledWith('SEARCH.INPUT.OPERATORS'); - }); - - it('should emit correct validation error message for required validator', () => { - setInputValue(''); - expect(component.validationError.emit).toHaveBeenCalledWith('SEARCH.INPUT.REQUIRED'); - }); - - it('should update validation error when hasLibrariesConstraint changes from false to true with short search term', () => { - setInputValue('a'); - expect(component.validationError.emit).toHaveBeenCalledWith(''); - - component.hasLibrariesConstraint = true; - component.ngOnChanges({ - hasLibrariesConstraint: new SimpleChange(false, true, false) - }); - expect(component.validationError.emit).toHaveBeenCalledWith('SEARCH.INPUT.MIN_LENGTH'); - }); - - it('should clear validation error when hasLibrariesConstraint changes from true to false with short search term', () => { - component.hasLibrariesConstraint = true; - setInputValue('a'); - expect(component.validationError.emit).toHaveBeenCalledWith('SEARCH.INPUT.MIN_LENGTH'); - - component.hasLibrariesConstraint = false; - component.ngOnChanges({ - hasLibrariesConstraint: new SimpleChange(true, false, false) - }); - expect(component.validationError.emit).toHaveBeenCalledWith(''); - }); - }); -}); diff --git a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts b/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts deleted file mode 100644 index c09000623..000000000 --- a/projects/aca-content/src/lib/components/search/search-input-control/search-input-control.component.ts +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Alfresco Example Content Application - * - * This file is part of the Alfresco Example Content Application. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * The Alfresco Example Content Application is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Alfresco Example Content Application is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * from Hyland Software. If not, see . - */ - -import { - Component, - EventEmitter, - Input, - Output, - ViewEncapsulation, - ViewChild, - ElementRef, - OnInit, - inject, - DestroyRef, - OnChanges, - SimpleChanges -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { FormControl, FormsModule, ReactiveFormsModule, StatusChangeEvent, TouchedChangeEvent, Validators } from '@angular/forms'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { noWhitespaceValidator, noLeadingTrailingOperatorsValidator } from '@alfresco/aca-shared'; -import { combineLatest } from 'rxjs'; -import { filter, startWith } from 'rxjs/operators'; -import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; - -@Component({ - imports: [CommonModule, TranslatePipe, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule], - selector: 'app-search-input-control', - templateUrl: './search-input-control.component.html', - styleUrls: ['./search-input-control.component.scss'], - encapsulation: ViewEncapsulation.None, - host: { class: 'app-search-control' } -}) -export class SearchInputControlComponent implements OnInit, OnChanges { - private readonly destroyRef = inject(DestroyRef); - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); - - /** Type of the input field to render, e.g. "search" or "text" (default). */ - @Input() - inputType = 'text'; - - /** - * Indicates whether the search is constrained by libraries. - * If true, specific error messaging or validation behavior may be triggered. - */ - @Input() - hasLibrariesConstraint = false; - - /** Emitted when the search is submitted pressing ENTER button. - * The search term is provided as value of the event. - */ - @Output() - // eslint-disable-next-line @angular-eslint/no-output-native - submit: EventEmitter = new EventEmitter(); - - /** Emitted when the search term is changed. The search term is provided - * in the 'value' property of the returned object. If the term is less - * than three characters in length then the term is truncated to an empty - * string. - */ - @Output() - searchChange: EventEmitter = new EventEmitter(); - - /** Emitted when the input control has a validation error. */ - @Output() - validationError = new EventEmitter(); - - @ViewChild('searchInput', { static: true }) - searchInput: ElementRef; - - searchFieldFormControl = new FormControl('', [Validators.required, noWhitespaceValidator(), noLeadingTrailingOperatorsValidator()]); - - get searchTerm(): string { - return this.searchFieldFormControl.value.replace('text:', 'TEXT:'); - } - - set searchTerm(value: string) { - this.searchFieldFormControl.setValue(value); - } - - ngOnInit() { - this.searchFieldFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((searchTermValue) => { - this.searchFieldFormControl.markAsTouched(); - this.searchChange.emit(searchTermValue); - }); - - this.searchFieldFormControl.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { - if (event instanceof TouchedChangeEvent || event instanceof StatusChangeEvent) { - if (this.searchFieldFormControl.touched) { - this.emitValidationError(); - } else { - this.validationError.emit(''); - } - } - }); - combineLatest([ - this.route.queryParams, - this.router.events.pipe( - filter((e): e is NavigationStart => e instanceof NavigationStart), - startWith(null) - ) - ]) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(([params]) => { - if (params['q'] && !this.searchFieldFormControl.value) { - setTimeout(() => this.searchFieldFormControl.setValue('*')); - } - }); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes['hasLibrariesConstraint'] && !changes['hasLibrariesConstraint'].firstChange) { - this.emitValidationError(); - } - } - - searchSubmit() { - this.searchFieldFormControl.markAsTouched(); - - const trimmedTerm = this.searchTerm?.trim(); - if (this.searchFieldFormControl.valid && trimmedTerm) { - this.submit.emit(trimmedTerm); - } - } - - clear() { - this.searchTerm = ''; - this.searchChange.emit(''); - } - - onBlur() { - this.searchFieldFormControl.markAsUntouched(); - } - - isTermTooShort() { - return this.searchTerm.trim()?.length < 2; - } - - emitValidationError(): void { - const errors = this.searchFieldFormControl.errors; - if (errors?.whitespace) { - this.validationError.emit('SEARCH.INPUT.WHITESPACE'); - } else if (errors?.operators) { - this.validationError.emit('SEARCH.INPUT.OPERATORS'); - } else if (errors?.required) { - this.validationError.emit('SEARCH.INPUT.REQUIRED'); - } else if (this.hasLibrariesConstraint && this.isTermTooShort()) { - this.validationError.emit('SEARCH.INPUT.MIN_LENGTH'); - } else { - this.validationError.emit(''); - } - } -} diff --git a/projects/aca-content/src/lib/components/search/search-input/search-input.component.html b/projects/aca-content/src/lib/components/search/search-input/search-input.component.html index 345a4cb04..62bd4deb3 100644 --- a/projects/aca-content/src/lib/components/search/search-input/search-input.component.html +++ b/projects/aca-content/src/lib/components/search/search-input/search-input.component.html @@ -1,65 +1,45 @@ -