[ACS-10475] [Search] UX improvements for search (#5042)

This commit is contained in:
Dominik Iwanek
2026-03-13 16:18:55 +01:00
committed by GitHub
parent 683eab8be4
commit b2f1245c7d
60 changed files with 1640 additions and 1525 deletions
@@ -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);
});
});
@@ -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);
}
[
@@ -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);
});
});
});
@@ -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);
});
});
});
@@ -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);
});
});
@@ -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);
});
});
});
@@ -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 }) => {
@@ -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();
@@ -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();
@@ -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.');
@@ -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();
});
@@ -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();
});
});
@@ -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);
@@ -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');
});
});
@@ -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();
@@ -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);
});
});
@@ -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();
@@ -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);
});
@@ -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);
+5 -1
View File
@@ -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",
@@ -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;
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Store<AppStore>>;
let filterService: jasmine.SpyObj<SearchFilterService>;
let navigationService: jasmine.SpyObj<SearchNavigationService>;
let queryBuilder: jasmine.SpyObj<SearchQueryBuilderService>;
let queryLibrariesBuilder: jasmine.SpyObj<SearchLibrariesQueryBuilderService>;
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' }));
});
});
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AppStore>,
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));
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SearchQueryBuilderService>;
beforeEach(() => {
queryBuilder = jasmine.createSpyObj<SearchQueryBuilderService>('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();
});
});
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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}'`);
}
}
@@ -0,0 +1,76 @@
<button
mat-stroked-button
class="aca-search-input--search-in-button"
cdkOverlayOrigin
#overlayOrigin="cdkOverlayOrigin"
(click)="toggle()"
[title]="'SEARCH.INPUT.SEARCH_IN' | translate"
[attr.aria-label]="'SEARCH.INPUT.SEARCH_IN' | translate"
[attr.aria-expanded]="isOpen"
aria-haspopup="dialog"
>
{{ 'SEARCH.INPUT.IN_PREFIX' | translate }}: {{ filterService.getSearchInLabel() | translate }}
<mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="overlayOrigin"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayHasBackdrop]="true"
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
(backdropClick)="close()"
(detach)="onPanelDetached()">
<fieldset
#searchInPanel
class="aca-search-in-panel"
role="group"
[attr.aria-label]="'SEARCH.INPUT.SEARCH_IN' | translate"
(keydown.escape)="close()"
(click)="$event.stopPropagation()">
<div class="aca-search-in-panel__header">
<span class="aca-search-in-panel__title" id="searchInTitle">{{ 'SEARCH.INPUT.SEARCH_IN' | translate }}</span>
<button mat-icon-button (click)="close()" [title]="'COMMON.CLOSE' | translate" [attr.aria-label]="'COMMON.CLOSE' | translate">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="aca-search-in-panel__content">
<mat-radio-group [(ngModel)]="searchInMode" (change)="onSearchInModeChange()">
<mat-radio-button value="content" class="aca-search-in-panel__radio">
{{ 'SEARCH.INPUT.FILES_AND_FOLDERS' | translate }}
</mat-radio-button>
@if (searchInMode === 'content') {
<div class="aca-search-in-panel__checkboxes">
<mat-checkbox
[id]="SearchOptionIds.Files"
[(ngModel)]="filesChecked"
(change)="onFilesCheckedChange()"
>
{{ 'SEARCH.INPUT.FILES' | translate }}
</mat-checkbox>
<mat-checkbox
[id]="SearchOptionIds.Folders"
[(ngModel)]="foldersChecked"
(change)="onFoldersCheckedChange()"
>
{{ 'SEARCH.INPUT.FOLDERS' | translate }}
</mat-checkbox>
</div>
}
<mat-radio-button [value]="SearchOptionIds.Libraries" class="aca-search-in-panel__radio">
{{ 'SEARCH.INPUT.LIBRARIES' | translate }}
</mat-radio-button>
</mat-radio-group>
</div>
<mat-divider />
<div class="aca-search-in-panel__actions">
<button mat-button (click)="reset()">{{ 'SEARCH.INPUT.RESET' | translate }}</button>
<button mat-flat-button color="primary" (click)="apply()">{{ 'SEARCH.FILTER.BUTTONS.APPLY' | translate }}</button>
</div>
</fieldset>
</ng-template>
@@ -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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SearchInMenuComponent>;
let component: SearchInMenuComponent;
let filterService: jasmine.SpyObj<SearchFilterService>;
beforeEach(async () => {
filterService = jasmine.createSpyObj<SearchFilterService>('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();
});
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<void>();
@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());
}
}
@@ -1,39 +0,0 @@
<div class="app-search-container">
<mat-form-field class="app-input-form-field">
<button
mat-icon-button
matPrefix
class="app-search-button"
(click)="searchSubmit()"
(keydown.enter)="searchSubmit()"
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
>
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
</button>
<input
matInput
#searchInput
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
[type]="inputType"
id="app-control-input"
[formControl]="searchFieldFormControl"
(keydown.enter)="searchSubmit()"
(blur)="onBlur()"
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
autocomplete="off"
/>
<div matSuffix>
<button
mat-icon-button
(click)="clear()"
[attr.aria-label]="'COMMON.CLEAR' | translate"
[attr.title]="'COMMON.CLEAR' | translate"
class="app-search-input-control-clear-button">
<mat-icon *ngIf="searchFieldFormControl.value.length" class="app-suffix-icon">clear</mat-icon>
</button>
</div>
</mat-form-field>
</div>
@@ -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;
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SearchInputControlComponent>;
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('');
});
});
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<any> = 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<string> = new EventEmitter();
/** Emitted when the input control has a validation error. */
@Output()
validationError = new EventEmitter<string>();
@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('');
}
}
}
@@ -1,65 +1,45 @@
<div class="app-search-container"
role="search"
[matMenuTriggerFor]="searchOptionsMenu"
(menuOpened)="onMenuOpened()"
(menuClosed)="syncInputValues()"
>
<div class="app-search-container" role="search">
<mat-form-field class="app-input-form-field">
<button class="aca-search-input--search-button" mat-icon-button matPrefix (click)="searchByOption()" [title]="'SEARCH.BUTTON.TOOLTIP' | translate">
<button
class="aca-search-input--search-button"
mat-icon-button
matPrefix
(click)="executeSearch()"
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
>
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
</button>
<input
class="app-search-input"
matInput
#searchInputField
class="app-search-input"
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
[type]="'text'"
[readonly]="true"
[value]="searchedWord"
type="text"
[(ngModel)]="searchedWord"
(keydown.enter)="onSearchSubmit(searchedWord)"
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
autocomplete="off"
/>
<div matSuffix>
<mat-icon class="app-suffix-icon">arrow_drop_down</mat-icon>
<div matSuffix class="aca-search-input--suffix">
<aca-search-in-menu (filtersApplied)="onFiltersApplied()" />
<button
class="aca-search-input--close-button"
mat-icon-button
[attr.aria-label]="'SEARCH.CLOSE_BUTTON.ARIA_LABEL' | translate"
[attr.title]="'SEARCH.CLOSE_BUTTON.ARIA_LABEL' | translate"
(click)="exitSearch()">
<mat-icon class="app-suffix-icon">close</mat-icon>
(click)="exitSearch()"
>
<mat-icon>close</mat-icon>
</button>
</div>
</mat-form-field>
</div>
<mat-menu #searchOptionsMenu="matMenu" [overlapTrigger]="true" class="app-search-options-menu">
<div (keydown.tab)="$event.stopPropagation()" (keydown.shift.tab)="$event.stopPropagation()" tabindex=0>
<div cdkTrapFocus>
<app-search-input-control
#searchInputControl
(click)="$event.stopPropagation()"
(submit)="onSearchSubmit($event)"
(searchChange)="onSearchChange($event)"
(validationError)="error = $event"
[hasLibrariesConstraint]="hasLibrariesConstraint"
/>
<mat-error *ngIf="error" class="app-search-error" role="alert" aria-live="assertive">
{{ error | translate }}
</mat-error>
<div id="search-options" class="app-search-options">
<mat-checkbox *ngFor="let option of searchOptions"
id="{{ option.id }}"
[(ngModel)]="option.value"
[disabled]="option.shouldDisable()"
(change)="searchByOption()"
(keyup.enter)="$event.stopPropagation()"
(click)="$event.stopPropagation()"
>
{{ option.key | translate }}
</mat-checkbox>
</div>
</div>
</div>
</mat-menu>
@if (error) {
<mat-error class="app-search-error" role="alert" aria-live="assertive">
{{ error | translate }}
</mat-error>
}
</div>
@@ -1,73 +1,43 @@
$search-width: 594px;
$search-height: 64px;
$search-background: var(--theme-search-background-color);
$search-height: 48px;
$search-border-radius: 4px;
.app-search-container {
color: var(--theme-foreground-text-color);
width: 100%;
max-width: $search-width;
height: $search-height;
border: none;
background: none;
padding: 0;
&:focus-visible {
outline: 2px solid var(--theme-blue-button-color);
border-radius: 4px;
}
&:focus-within {
label {
display: none;
}
}
.app-search-input {
&:focus-within {
outline: 2px solid var(--theme-blue-button-color);
border-radius: 4px;
}
font-size: 16px;
letter-spacing: -0.7px;
}
}
.app-search-options-menu {
background-color: var(--theme-dialog-background-color);
width: $search-width;
/* stylelint-disable-next-line declaration-no-important */
min-width: $search-width !important;
border-radius: $search-border-radius;
/* stylelint-disable-next-line selector-class-pattern */
.mdc-notched-outline {
display: none;
}
}
.aca-search-input {
width: 100%;
max-width: $search-width;
background-color: $search-background;
flex: 1;
min-width: 350px;
border-radius: $search-border-radius;
height: $search-height;
.app-suffix-icon {
width: 18px;
height: 18px;
font-size: 18px;
.app-input-form-field {
width: 100%;
height: $search-height;
font-size: 16px;
line-height: calc($search-height / 2);
}
}
.app-search-options {
color: var(--theme-text-color);
border-top: 1px solid var(--theme-divider-color);
padding: 25px;
.aca-search-input--suffix {
display: flex;
flex-direction: row;
column-gap: 24px;
}
.app-search-error {
position: absolute;
align-items: center;
gap: 4px;
padding-left: 17px;
margin-top: 4px;
font-size: 12px;
}
@media screen and (max-width: 959px) {
@@ -80,26 +50,6 @@ $search-border-radius: 4px;
.app-search-container {
max-width: $search-width-small;
}
.app-search-options-menu {
width: $search-width-small;
}
.app-search-options {
padding-left: 20px;
label {
max-width: 105px;
}
.app-search-options-checkbox {
padding: 3px 20px 3px 0;
label {
padding: 0;
}
}
}
}
@media screen and (max-width: 599px) {
@@ -112,14 +62,6 @@ $search-border-radius: 4px;
.app-search-container {
max-width: $search-width-xsmall;
}
.app-search-options-menu {
width: $search-width-xsmall;
}
.app-search-options label {
max-width: 180px;
}
}
@media screen and (max-width: 380px) {
@@ -23,178 +23,282 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnitTestingUtils } from '@alfresco/adf-core';
import { MatError } from '@angular/material/form-field';
import { AppStore } from '@alfresco/aca-shared/store';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { SearchInputComponent } from './search-input.component';
import { Store } from '@ngrx/store';
import { of, Subject } from 'rxjs';
import { NavigationStart, Router, RouterEvent } from '@angular/router';
import { Subject } from 'rxjs';
import { ActivatedRoute, Event, NavigationStart, Params, Router } from '@angular/router';
import { SearchConfiguration, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatMenuHarness } from '@angular/material/menu/testing';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SearchNavigationService } from '../search-navigation.service';
import { SearchFilterService } from '../search-filter.service';
import { SearchExecutionService } from '../search-execution.service';
import { AppHookService } from '@alfresco/aca-shared';
import { UnitTestingUtils } from '@alfresco/adf-core';
describe('SearchInputComponent', () => {
let fixture: ComponentFixture<SearchInputComponent>;
let component: SearchInputComponent;
let store: jasmine.SpyObj<Store<AppStore>>;
let unitTestingUtils: UnitTestingUtils;
let loader: HarnessLoader;
let router: Router;
const routerEventsSubject = new Subject<RouterEvent>();
let searchExecutionService: jasmine.SpyObj<SearchExecutionService>;
let searchFilterService: jasmine.SpyObj<SearchFilterService>;
let searchNavigationService: jasmine.SpyObj<SearchNavigationService>;
let testingUtils: UnitTestingUtils;
const routerEventsSubject = new Subject<Event>();
const configUpdatedSubject = new Subject<SearchConfiguration>();
function getFirstError(): string {
const error = unitTestingUtils.getByDirective(MatError);
return error?.nativeElement.textContent.trim();
}
async function openMenu(): Promise<MatMenuHarness> {
const menu = await loader.getHarness(MatMenuHarness);
await menu.open();
return menu;
}
function getCheckbox(id: string): Promise<MatCheckboxHarness> {
const overlayLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
return overlayLoader.getHarness(MatCheckboxHarness.with({ selector: `#${id}` }));
}
async function uncheckAllCheckboxes() {
const checkboxIds = ['libraries', 'folder', 'content'];
for (const id of checkboxIds) {
try {
const checkbox = await getCheckbox(id);
if (await checkbox.isChecked()) {
await checkbox.uncheck();
fixture.detectChanges();
}
} catch (err) {
fail(`Checkbox with id ${id} not found`);
throw err;
}
}
}
const queryParamsSubject = new Subject<Params>();
const library400ErrorSubject = new Subject<void>();
beforeEach(async () => {
const storeSpy = jasmine.createSpyObj<Store<AppStore>>('Store', ['dispatch', 'pipe']);
const queryBuilderSpy = {
configUpdated: configUpdatedSubject,
removeFilterQuery: () => {}
removeFilterQuery: jasmine.createSpy('removeFilterQuery'),
addFilterQuery: jasmine.createSpy('addFilterQuery'),
update: jasmine.createSpy('update')
} as Partial<SearchQueryBuilderService>;
searchExecutionService = jasmine.createSpyObj<SearchExecutionService>('SearchExecutionService', ['execute']);
searchFilterService = jasmine.createSpyObj<SearchFilterService>('SearchFilterService', [
'validateSearchTerm',
'removeContentFilters',
'initForLibrariesRoute',
'getSearchInLabel'
]);
searchFilterService.getSearchInLabel.and.returnValue('some-label');
searchNavigationService = jasmine.createSpyObj<SearchNavigationService>('SearchNavigationService', ['navigateBack', 'getUrlSearchTerm'], {
onLibrariesSearchResults: false
});
searchNavigationService.getUrlSearchTerm.and.returnValue(null);
searchFilterService.validateSearchTerm.and.returnValue(null);
const appHookServiceMock = {
library400Error: library400ErrorSubject.asObservable()
};
const activatedRouteMock = {
queryParams: queryParamsSubject.asObservable()
};
await TestBed.configureTestingModule({
imports: [AppTestingModule, SearchInputComponent],
imports: [AppTestingModule, SearchInputComponent, NoopAnimationsModule],
providers: [
{ provide: Store, useValue: storeSpy },
{ provide: SearchQueryBuilderService, useValue: queryBuilderSpy }
{ provide: SearchQueryBuilderService, useValue: queryBuilderSpy },
{ provide: SearchExecutionService, useValue: searchExecutionService },
{ provide: SearchFilterService, useValue: searchFilterService },
{ provide: SearchNavigationService, useValue: searchNavigationService },
{ provide: AppHookService, useValue: appHookServiceMock },
{ provide: ActivatedRoute, useValue: activatedRouteMock }
]
}).compileComponents();
fixture = TestBed.createComponent(SearchInputComponent);
component = fixture.componentInstance;
store = TestBed.inject(Store) as jasmine.SpyObj<Store<AppStore>>;
router = TestBed.inject(Router);
store.pipe.and.returnValue(of([]));
fixture.detectChanges();
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
loader = TestbedHarnessEnvironment.loader(fixture);
Object.defineProperty(router, 'events', {
get: () => routerEventsSubject.asObservable()
});
fixture = TestBed.createComponent(SearchInputComponent);
component = fixture.componentInstance;
testingUtils = new UnitTestingUtils(fixture.debugElement);
fixture.detectChanges();
});
it('should show required error when field is empty and touched', async () => {
await openMenu();
component.searchInputControl.searchFieldFormControl.setValue('');
component.searchInputControl.searchFieldFormControl.markAsTouched();
fixture.detectChanges();
expect(getFirstError()).toBe('SEARCH.INPUT.REQUIRED');
it('should render a directly editable search input', () => {
const input = testingUtils.getInputByCSS('.app-search-input');
expect(input).toBeTruthy();
expect(input.readOnly).toBeFalse();
});
it('should not show error when field has value', async () => {
await openMenu();
component.searchInputControl.searchFieldFormControl.setValue('not giving up');
component.searchInputControl.searchFieldFormControl.markAsTouched();
fixture.detectChanges();
const error = unitTestingUtils.getByDirective(MatError);
expect(error).toBeNull();
it('should render the search-in menu component', () => {
const searchInMenu = testingUtils.getByCSS('aca-search-in-menu');
expect(searchInMenu).toBeTruthy();
});
it('should not show error when field is untouched', async () => {
await openMenu();
component.searchInputControl.searchFieldFormControl.setValue('');
component.searchInputControl.searchFieldFormControl.markAsUntouched();
fixture.detectChanges();
const error = unitTestingUtils.getByDirective(MatError);
expect(error).toBeNull();
it('should render the close button', () => {
const closeButton = testingUtils.getByCSS('.aca-search-input--close-button');
expect(closeButton).toBeTruthy();
});
it('should dispatch action when Libraries checkbox selected and term is entered', async () => {
await openMenu();
const checkbox = await getCheckbox('libraries');
await checkbox.check();
fixture.detectChanges();
component.onSearchSubmit({ target: { value: 'happy faces only' } });
expect(store.dispatch).toHaveBeenCalled();
it('should render the search button', () => {
const searchButton = testingUtils.getByCSS('.aca-search-input--search-button');
expect(searchButton).toBeTruthy();
});
it('should not dispatch SearchByTermAction when no checkboxes are selected and term is empty', async () => {
store.dispatch.calls.reset();
describe('onSearchSubmit', () => {
it('should execute search on submit with valid term', () => {
component.onSearchSubmit('happy faces only');
expect(searchExecutionService.execute).toHaveBeenCalledWith('happy faces only');
});
await openMenu();
await uncheckAllCheckboxes();
it('should not execute search when search term is empty', () => {
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
component.onSearchSubmit('');
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
expect(component.searchOptions.every((option) => !option.value)).toBeTrue();
it('should not execute search when search term is whitespace', () => {
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
component.onSearchSubmit(' ');
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
component.searchedWord = '';
fixture.detectChanges();
it('should set error when validation fails', () => {
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
component.onSearchSubmit('');
expect(component.error).toBe('SEARCH.ERRORS.EMPTY_QUERY');
});
component.onSearchSubmit({ target: { value: '' } });
expect(store.dispatch).not.toHaveBeenCalled();
it('should clear error on valid submission', () => {
component.error = 'some error';
component.onSearchSubmit('valid term');
expect(component.error).toBe('');
});
it('should trim whitespace from search term', () => {
component.onSearchSubmit(' hello ');
expect(component.searchedWord).toBe('hello');
expect(searchExecutionService.execute).toHaveBeenCalledWith('hello');
});
});
describe('onFiltersApplied', () => {
it('should execute search when searchedWord is set and valid', () => {
component.searchedWord = 'test';
component.onFiltersApplied();
expect(searchExecutionService.execute).toHaveBeenCalledWith('test');
});
it('should not execute search when searchedWord is empty', () => {
component.searchedWord = '';
component.onFiltersApplied();
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
it('should not execute search when searchedWord is null', () => {
component.searchedWord = null;
component.onFiltersApplied();
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
it('should not execute search when searchedWord is whitespace', () => {
component.searchedWord = ' ';
component.onFiltersApplied();
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
it('should set error when validation fails', () => {
component.searchedWord = 'test';
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.SOME_ERROR');
component.onFiltersApplied();
expect(component.error).toBe('SEARCH.ERRORS.SOME_ERROR');
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
});
describe('executeSearch', () => {
it('should reset has400LibraryError flag', () => {
component.has400LibraryError = true;
component.searchedWord = 'test';
component.executeSearch();
expect(component.has400LibraryError).toBeFalse();
});
it('should call searchExecutionService.execute with searchedWord', () => {
component.searchedWord = 'test query';
component.executeSearch();
expect(searchExecutionService.execute).toHaveBeenCalledWith('test query');
});
});
describe('exitSearch', () => {
it('should call searchNavigationService.navigateBack', () => {
component.exitSearch();
expect(searchNavigationService.navigateBack).toHaveBeenCalled();
});
});
describe('ngOnDestroy', () => {
it('should call removeContentFilters on destroy', () => {
component.ngOnDestroy();
expect(searchFilterService.removeContentFilters).toHaveBeenCalled();
});
});
describe('library400Error handling', () => {
it('should set has400LibraryError when library400Error emits', () => {
expect(component.has400LibraryError).toBeFalse();
library400ErrorSubject.next();
expect(component.has400LibraryError).toBeTrue();
});
});
describe('route params subscription', () => {
it('should update searchedWord from query params', () => {
const encodedQuery = btoa(JSON.stringify({ userQuery: '(cm:name:"hello*")' }));
queryParamsSubject.next({ q: encodedQuery });
expect(component.searchedWord).toBeTruthy();
});
it('should not update searchedWord when q param is missing', () => {
component.searchedWord = null;
queryParamsSubject.next({});
expect(component.searchedWord).toBeNull();
});
});
describe('queryBuilder configUpdated handling', () => {
it('should call searchByOption when searchedWord set and navigation has query params', () => {
spyOn(component, 'searchByOption').and.stub();
component.ngOnInit();
it('should execute search when searchedWord is set and navigation has query params', () => {
component.searchedWord = 'term';
routerEventsSubject.next(new NavigationStart(1, '/path?q=term'));
configUpdatedSubject.next({
id: 'config1'
});
configUpdatedSubject.next({ id: 'config1' } as SearchConfiguration);
expect(component.searchByOption).toHaveBeenCalled();
expect(searchExecutionService.execute).toHaveBeenCalledWith('term');
});
it('should NOT call searchByOption when searchedWord set and navigation has NO query params', () => {
it('should NOT execute search when navigation has no query params', () => {
searchExecutionService.execute.calls.reset();
component.searchedWord = 'term';
routerEventsSubject.next(new NavigationStart(1, '/path'));
configUpdatedSubject.next({} as SearchConfiguration);
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
configUpdatedSubject.next({
id: 'config1'
});
spyOn(component, 'searchByOption').and.stub();
it('should NOT execute search when searchedWord is not set', () => {
searchExecutionService.execute.calls.reset();
component.searchedWord = null;
routerEventsSubject.next(new NavigationStart(1, '/path?q=term'));
configUpdatedSubject.next({} as SearchConfiguration);
expect(searchExecutionService.execute).not.toHaveBeenCalled();
});
});
component.ngOnInit();
describe('initSearchState', () => {
it('should reset has400LibraryError on init', () => {
component.has400LibraryError = true;
component['initSearchState']();
expect(component.has400LibraryError).toBeFalse();
});
expect(component.searchByOption).not.toHaveBeenCalled();
it('should call initForLibrariesRoute when on libraries search results', () => {
Object.defineProperty(searchNavigationService, 'onLibrariesSearchResults', { get: () => true });
component['initSearchState']();
expect(searchFilterService.initForLibrariesRoute).toHaveBeenCalled();
});
});
describe('error display', () => {
it('should show error message when error is set', () => {
component.error = 'SEARCH.ERRORS.EMPTY_QUERY';
fixture.detectChanges();
const errorEl = testingUtils.getByCSS('.app-search-error');
expect(errorEl).toBeTruthy();
});
it('should not show error message when error is empty', () => {
component.error = '';
fixture.detectChanges();
const errorEl = testingUtils.getByCSS('.app-search-error');
expect(errorEl).toBeFalsy();
});
});
});
@@ -22,55 +22,29 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { AppHookService, AppService } from '@alfresco/aca-shared';
import { AppStore, SearchByTermAction, SearchOptionIds, SearchOptionModel } from '@alfresco/aca-shared/store';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { AppConfigService, NotificationService } from '@alfresco/adf-core';
import { Component, DestroyRef, inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import {
ActivatedRoute,
NavigationSkipped,
NavigationStart,
Params,
PRIMARY_OUTLET,
Router,
UrlSegment,
UrlSegmentGroup,
UrlTree
} from '@angular/router';
import { Store } from '@ngrx/store';
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
import { AppHookService } from '@alfresco/aca-shared';
import { AppConfigService } from '@alfresco/adf-core';
import { Component, DestroyRef, ElementRef, inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, NavigationSkipped, NavigationStart, Params, Router } from '@angular/router';
import { SearchNavigationService } from '../search-navigation.service';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
import { SearchFilterService } from '../search-filter.service';
import { SearchExecutionService } from '../search-execution.service';
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 { A11yModule } from '@angular/cdk/a11y';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { FormsModule } from '@angular/forms';
import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils';
import { SearchInMenuComponent } from '../search-in-menu/search-in-menu.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { merge } from 'rxjs/internal/observable/merge';
import { filter, map, startWith, withLatestFrom } from 'rxjs';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils';
@Component({
imports: [
CommonModule,
TranslatePipe,
MatMenuModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
A11yModule,
MatCheckboxModule,
FormsModule,
SearchInputControlComponent
],
imports: [CommonModule, TranslatePipe, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, FormsModule, SearchInMenuComponent],
selector: 'aca-search-input',
templateUrl: './search-input.component.html',
styleUrls: ['./search-input.component.scss'],
@@ -78,61 +52,79 @@ import { filter, map, startWith, withLatestFrom } from 'rxjs';
host: { class: 'aca-search-input' }
})
export class SearchInputComponent implements OnInit, OnDestroy {
private readonly notificationService = inject(NotificationService);
has400LibraryError = false;
hasLibrariesConstraint = false;
searchOnChange: boolean;
isTrimmedWordEmpty = false;
searchedWord: string = null;
error = '';
searchedWord: string = null;
searchOptions: Array<SearchOptionModel> = [
{
id: SearchOptionIds.Files,
key: 'SEARCH.INPUT.FILES',
value: false,
shouldDisable: this.isLibrariesChecked.bind(this)
},
{
id: SearchOptionIds.Folders,
key: 'SEARCH.INPUT.FOLDERS',
value: false,
shouldDisable: this.isLibrariesChecked.bind(this)
},
{
id: SearchOptionIds.Libraries,
key: 'SEARCH.INPUT.LIBRARIES',
value: this.onLibrariesSearchResults,
shouldDisable: this.isContentChecked.bind(this)
}
];
@ViewChild('searchInputControl', { static: true })
searchInputControl: SearchInputControlComponent;
@ViewChild(MatMenuTrigger, { static: true })
trigger: MatMenuTrigger;
@ViewChild('searchInputField')
searchInputField: ElementRef<HTMLInputElement>;
private readonly destroyRef = inject(DestroyRef);
constructor(
private readonly queryBuilder: SearchQueryBuilderService,
private readonly queryLibrariesBuilder: SearchLibrariesQueryBuilderService,
private readonly config: AppConfigService,
private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly store: Store<AppStore>,
private readonly appHookService: AppHookService,
private readonly appService: AppService,
public readonly searchInputService: SearchNavigationService
private readonly filterService: SearchFilterService,
private readonly searchExecutionService: SearchExecutionService,
public readonly searchNavigationService: SearchNavigationService
) {
this.searchOnChange = this.config.get<boolean>('search.aca:triggeredOnChange', true);
}
ngOnInit() {
this.showInputValue();
this.initSearchState();
this.subscribeToRouteParams();
this.appHookService.library400Error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.has400LibraryError = true;
});
}
ngOnDestroy(): void {
this.filterService.removeContentFilters();
}
exitSearch() {
this.searchNavigationService.navigateBack();
}
onSearchSubmit(searchTerm: string) {
const trimmedTerm = searchTerm?.trim();
const validationError = this.filterService.validateSearchTerm(trimmedTerm);
if (validationError) {
this.error = validationError;
return;
}
this.searchedWord = trimmedTerm;
this.error = '';
this.executeSearch();
}
onFiltersApplied() {
if (!this.searchedWord?.trim()) {
return;
}
const validationError = this.filterService.validateSearchTerm(this.searchedWord.trim());
if (!validationError) {
this.executeSearch();
} else {
this.error = validationError;
}
}
executeSearch() {
this.has400LibraryError = false;
this.searchExecutionService.execute(this.searchedWord);
}
private subscribeToRouteParams() {
merge(
this.route.queryParams,
this.router.events.pipe(
@@ -144,9 +136,8 @@ export class SearchInputComponent implements OnInit, OnDestroy {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params: Params) => {
const encodedQuery = params['q'];
if (encodedQuery && this.searchInputControl) {
if (encodedQuery) {
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
this.searchInputControl.searchTerm = this.searchedWord;
}
});
@@ -163,190 +154,17 @@ export class SearchInputComponent implements OnInit, OnDestroy {
.subscribe(([, navigationStartEvent]) => {
const hasQueryParams = navigationStartEvent?.url.includes('?');
if (this.searchedWord && hasQueryParams) {
this.searchByOption();
this.searchExecutionService.execute(this.searchedWord);
}
});
this.appHookService.library400Error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.has400LibraryError = true;
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
});
}
exitSearch() {
this.searchInputService.navigateBack();
}
showInputValue() {
this.appService.setAppNavbarMode('collapsed');
private initSearchState() {
this.has400LibraryError = false;
this.searchedWord = this.getUrlSearchTerm();
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
this.searchedWord = this.searchNavigationService.getUrlSearchTerm();
if (this.searchInputControl) {
this.searchInputControl.searchTerm = this.searchedWord;
if (this.searchNavigationService.onLibrariesSearchResults) {
this.filterService.initForLibrariesRoute();
}
}
ngOnDestroy(): void {
this.appService.setAppNavbarMode('expanded');
this.removeContentFilters();
}
onMenuOpened() {
if (this.searchInputControl) {
this.searchInputControl.searchInput?.nativeElement?.focus();
}
}
/**
* Called when the user submits the search, e.g. hits enter or clicks submit
*
* @param event Parameters relating to the search
*/
onSearchSubmit(event: any) {
const searchTerm = event.target ? (event.target as HTMLInputElement).value : event;
const trimmedTerm = searchTerm.trim();
if (trimmedTerm) {
this.searchedWord = trimmedTerm;
if (this.isLibrariesChecked() && this.searchInputControl.isTermTooShort()) {
return;
} else {
this.searchByOption();
}
} else {
this.notificationService.showError('APP.BROWSE.SEARCH.EMPTY_SEARCH');
}
setTimeout(() => {
this.trigger?.closeMenu();
});
}
onSearchChange(searchTerm: string) {
if (!this.searchOnChange) {
return;
}
this.has400LibraryError = false;
this.searchedWord = searchTerm;
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
}
searchByOption() {
this.syncInputValues();
this.has400LibraryError = false;
this.searchInputControl.emitValidationError();
if (!this.searchedWord.trim()) {
return;
}
if (this.isLibrariesChecked()) {
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
if (this.hasLibrariesConstraint) {
return;
}
if (this.onLibrariesSearchResults && this.isSameSearchTerm()) {
this.queryLibrariesBuilder.update();
} else if (this.searchedWord) {
this.store.dispatch(new SearchByTermAction(this.searchedWord, this.searchOptions));
}
} else {
if (this.isFoldersChecked() && !this.isFilesChecked()) {
this.filterContent(SearchOptionIds.Folders);
} else if (this.isFilesChecked() && !this.isFoldersChecked()) {
this.filterContent(SearchOptionIds.Files);
} else {
this.removeContentFilters();
}
if (this.onSearchResults && this.isSameSearchTerm()) {
this.queryBuilder.update();
} else if (this.searchedWord) {
this.store.dispatch(new SearchByTermAction(this.searchedWord, this.searchOptions));
}
}
}
get onLibrariesSearchResults() {
return this.router?.url.indexOf('/search-libraries') === 0;
}
get onSearchResults() {
return !this.onLibrariesSearchResults && this.router?.url.indexOf('/search') === 0;
}
isFilesChecked(): boolean {
return this.isOptionChecked(SearchOptionIds.Files);
}
isFoldersChecked(): boolean {
return this.isOptionChecked(SearchOptionIds.Folders);
}
isLibrariesChecked(): boolean {
return this.isOptionChecked(SearchOptionIds.Libraries);
}
isOptionChecked(optionId: string): boolean {
const libItem = this.searchOptions.find((item) => item.id === optionId);
return !!libItem && libItem.value;
}
isContentChecked(): boolean {
return this.isFilesChecked() || this.isFoldersChecked();
}
evaluateLibrariesConstraint(): boolean {
if (this.isLibrariesChecked()) {
return this.has400LibraryError || this.searchInputControl.isTermTooShort();
}
return false;
}
filterContent(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}'`);
}
removeContentFilters() {
this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Files}'`);
this.queryBuilder.removeFilterQuery(`+TYPE:'cm:${SearchOptionIds.Folders}'`);
}
syncInputValues() {
if (this.searchInputControl.searchTerm !== this.searchedWord) {
if (this.searchInputControl.searchTerm) {
this.searchedWord = this.searchInputControl.searchTerm;
} else {
this.searchInputControl.searchTerm = this.searchedWord;
}
}
}
getUrlSearchTerm(): string {
let searchTerm = '';
if (this.onSearchResults || this.onLibrariesSearchResults) {
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (urlSegmentGroup) {
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
searchTerm = extractSearchedWordFromEncodedQuery(urlSegments[0].parameters['q']);
}
}
return searchTerm;
}
isSameSearchTerm(): boolean {
const urlSearchTerm = this.getUrlSearchTerm();
return this.searchedWord === urlSearchTerm;
}
}
@@ -1,8 +1,7 @@
<aca-page-layout>
<div class="aca-page-layout-header">
<aca-search-input />
<div class="aca-search-toolbar-spacer"></div>
<aca-toolbar [items]="actions" />
<aca-toolbar class="aca-search-results__toolbar-actions" [items]="actions" />
</div>
<div class="aca-page-layout-content">
@@ -1,8 +1,8 @@
@import '../../../ui/mixins';
aca-search-results {
.aca-search-toolbar-spacer {
width: 100%;
.aca-search-results__toolbar-actions {
margin-left: 8px;
}
.adf-search-results {
@@ -75,12 +75,6 @@ describe('SearchLibrariesResultsComponent', () => {
expect(component.onSearchResultLoaded).toHaveBeenCalledWith(emptyPage);
});
it('should collapsed sidenav by default', () => {
component.ngOnInit();
expect(appServiceMock.setAppNavbarMode).toHaveBeenCalledWith('collapsed');
});
it('should extract searched word from query params', (done) => {
route.queryParams = of({ q: encodeQuery({ userQuery: 'cm:name:"test*"' }) });
route.queryParams.subscribe(() => {
@@ -29,7 +29,6 @@ import { ActivatedRoute, Params } from '@angular/router';
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
import {
AppHookService,
AppService,
ContextActionsDirective,
InfoDrawerComponent,
PageComponent,
@@ -81,10 +80,9 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
columns: DocumentListPresetRef[] = [];
constructor(
private librariesQueryBuilder: SearchLibrariesQueryBuilderService,
private route: ActivatedRoute,
private appHookService: AppHookService,
private appService: AppService
private readonly librariesQueryBuilder: SearchLibrariesQueryBuilderService,
private readonly route: ActivatedRoute,
private readonly appHookService: AppHookService
) {
super();
@@ -95,7 +93,6 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
}
ngOnInit() {
this.appService.setAppNavbarMode('collapsed');
super.ngOnInit();
this.columns = this.extensions.documentListPresets.searchLibraries || [];
@@ -23,7 +23,8 @@
*/
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
import { extractSearchedWordFromEncodedQuery } from '../../utils/aca-search-utils';
@Injectable({
providedIn: 'root'
@@ -35,6 +36,14 @@ export class SearchNavigationService {
return this._previousRoute;
}
get onLibrariesSearchResults(): boolean {
return this.router?.url.indexOf('/search-libraries') === 0;
}
get onSearchResults(): boolean {
return !this.onLibrariesSearchResults && this.router?.url.indexOf('/search') === 0;
}
constructor(private router: Router) {}
saveRoute(route: string): void {
@@ -53,4 +62,24 @@ export class SearchNavigationService {
this.saveRoute(this.router.url);
this.router.navigate(['/search']);
}
getUrlSearchTerm(): string {
if (!this.onSearchResults && !this.onLibrariesSearchResults) {
return '';
}
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (urlSegmentGroup) {
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
return extractSearchedWordFromEncodedQuery(urlSegments[0].parameters['q']);
}
return '';
}
isSameSearchTerm(searchedWord: string): boolean {
return searchedWord === this.getUrlSearchTerm();
}
}
@@ -4,8 +4,7 @@
<div class="aca-header-container">
<aca-search-input />
<aca-bulk-actions-dropdown *ngIf="bulkActions && ('isBulkActionsAvailable' | isFeatureSupportedInCurrentAcs | async)" [items]="bulkActions" />
<div class="aca-search-toolbar-spacer"></div>
<aca-toolbar [items]="actions" />
<aca-toolbar class="aca-search-results__toolbar-actions" [items]="actions" />
</div>
</div>
@@ -8,8 +8,8 @@ aca-search-results {
}
}
.aca-search-toolbar-spacer {
width: 100%;
.aca-search-results__toolbar-actions {
margin-left: 8px;
}
.adf-search-results {
@@ -30,6 +30,17 @@
subscript-text-size: 12px
)
);
#{ms.$mat-radio-button} {
@include mat.radio-overrides(
(
selected-icon-color: var(--theme-blue-checkbox-color),
selected-hover-icon-color: var(--theme-blue-checkbox-color),
selected-focus-icon-color: var(--theme-blue-checkbox-color),
selected-pressed-icon-color: var(--theme-blue-checkbox-color)
)
);
}
}
.adf-search-properties-form,
@@ -59,7 +59,6 @@ $app-blue-folder: #2a7de1;
// CSS Variables
$defaults: (
--theme-background-color: $background-color,
--theme-search-background-color: darken($background-color, 1),
--theme-text-color: mat.m2-get-color-from-palette($foreground, text, 0.54),
--theme-sidenav-text-color: mat.m2-get-color-from-palette($foreground, text),
--theme-sidenav-hovered-text-color: mat.m2-get-color-from-palette($foreground, text),
@@ -71,7 +70,6 @@ $defaults: (
--theme-foreground-text-color: mat.m2-get-color-from-palette($foreground, text, 0.72),
--theme-foreground-text-bold-color: mat.m2-get-color-from-palette($foreground, text, 0.87),
--theme-secondary-text-color: mat.m2-get-color-from-palette($foreground, secondary-text),
--theme-divider-color: mat.m2-get-color-from-palette($foreground, divider, 0.07),
--theme-dialog-background-color: mat.m2-get-color-from-palette($background, dialog),
--theme-header-text-color: mat.m2-get-color-from-palette($foreground, text, 0.87),
--new-button-font-size: 0.9rem,
@@ -167,7 +167,7 @@ export const getGlobalConfig: PlaywrightTestConfig = {
testMatch: ['**/*.e2e.ts'],
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
actionTimeout: 40 * 1000,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: env.PLAYWRIGHT_E2E_HOST,
headless: env.PLAYWRIGHT_HEADLESS === 'true' || !!env.CI,
@@ -34,7 +34,7 @@ export class ManageRules extends BaseComponent {
public ruleDetailsDeleteButton = this.getChild('#delete-rule-btn');
public ruleDetailsEditButton = this.getChild('#edit-rule-btn');
public ruleDetailsPerformActionsDiv = this.getChild('adf-card-view-selectitem [data-automation-id="select-box"]');
public rulesEmptyListTitle = this.getChild('.adf-empty-content__title');
public rulesEmptyList = this.getChild('adf-empty-content');
public ruleActions = this.getChild('aca-rule-action');
public ruleConditionsInGroup = this.getChild('aca-rule-composite-condition aca-rule-simple-condition');
public ruleDescription = this.getChild('.aca-manage-rules__container__rule-details__header__title__description');
@@ -51,7 +51,7 @@ export class ManageRules extends BaseComponent {
}
async checkIfRuleListEmpty(): Promise<boolean> {
return this.rulesEmptyListTitle.isVisible();
return this.rulesEmptyList.isVisible();
}
async checkIfRuleIsOnTheList(ruleName: string): Promise<void> {
@@ -26,5 +26,4 @@ export * from './search-filters';
export * from './search-filters.component';
export * from './search-input.component';
export * from './search-menu-card.component';
export * from './search-overlay.components';
export * from './search-sorting-picker.components';
@@ -0,0 +1,88 @@
/*!
* 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 <http://www.gnu.org/licenses/>.
*/
import { Page } from '@playwright/test';
import { BaseComponent } from '../base.component';
export class SearchInDialogComponent extends BaseComponent {
private static readonly rootElement = '.aca-search-in-panel';
public filesAndFoldersRadioButton = this.getChild('.aca-search-in-panel__radio label').getByText('Files and folders');
public librariesRadioButton = this.getChild('.aca-search-in-panel__radio label').getByText('Libraries');
public filesCheckbox = this.getChild('.aca-search-in-panel__checkboxes').getByLabel('Files');
public foldersCheckbox = this.getChild('.aca-search-in-panel__checkboxes').getByLabel('Folders');
public applyButton = this.getChild('button', { hasText: 'Apply' });
readonly resetButton = this.getChild('button', { hasText: 'Reset' });
constructor(page: Page, rootElement = SearchInDialogComponent.rootElement) {
super(page, rootElement);
}
async isFoldersOptionChecked() {
return this.foldersCheckbox.isChecked();
}
async isFilesOptionChecked() {
return this.filesCheckbox.isChecked();
}
async resetOptions() {
await this.resetButton.click();
}
async checkOnlyFolders(): Promise<void> {
await this.resetOptions();
if (await this.isFilesOptionChecked()) {
await this.filesCheckbox.click();
}
if (!(await this.isFoldersOptionChecked())) {
await this.foldersCheckbox.click();
}
}
async checkOnlyFiles(): Promise<void> {
await this.resetOptions();
if (await this.isFoldersOptionChecked()) {
await this.foldersCheckbox.click();
}
if (!(await this.isFilesOptionChecked())) {
await this.filesCheckbox.click();
}
}
async checkLibraries(): Promise<void> {
await this.resetOptions();
await this.librariesRadioButton.click();
}
async checkFilesAndFolders(): Promise<void> {
await this.resetOptions();
if (!(await this.isFilesOptionChecked())) {
await this.filesCheckbox.click();
}
if (!(await this.isFoldersOptionChecked())) {
await this.foldersCheckbox.click();
}
}
}
@@ -24,12 +24,13 @@
import { Locator, Page } from '@playwright/test';
import { BaseComponent } from '.././base.component';
import { timeouts } from '../../../utils';
export class SearchInputComponent extends BaseComponent {
private static rootElement = 'aca-page-layout';
public searchInput = this.getChild('aca-search-input input');
public searchButton = this.page.locator('.aca-search-input--search-button');
public searchCloseButton = this.page.locator('.aca-search-input--close-button');
public searchInButton = this.getChild('aca-search-in-menu button');
/**
* Method used in cases where user have possibility to navigate "inside" the element (it's clickable and has link attribute).
@@ -43,9 +44,8 @@ export class SearchInputComponent extends BaseComponent {
super(page, rootElement);
}
async performDoubleClickFolderOrFileToOpen(name: string): Promise<void> {
await this.getCellLinkByName(name).waitFor({ state: 'visible', timeout: timeouts.medium });
await this.getCellLinkByName(name).dblclick();
await this.spinnerWaitForReload();
async searchFor(searchText: string): Promise<void> {
await this.searchInput.fill(searchText);
await this.searchButton.click();
}
}
@@ -1,93 +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 <http://www.gnu.org/licenses/>.
*/
import { Page } from '@playwright/test';
import { BaseComponent } from '.././base.component';
export class SearchOverlayComponent extends BaseComponent {
private static rootElement = '.cdk-overlay-pane';
public searchFilesOption = this.getChild('input#content-input');
public searchFoldersOption = this.getChild('input#folder-input');
public searchLibrariesOption = this.getChild('input#libraries-input');
public searchInput = this.page.locator('#app-control-input');
public searchButton = this.page.locator('app-search-input-control button[title="Search"]');
public searchInputControl = this.page.locator('.app-search-control');
public searchOptions = this.page.locator('.app-search-options');
constructor(page: Page, rootElement = SearchOverlayComponent.rootElement) {
super(page, rootElement);
}
async isFoldersOptionChecked() {
return this.searchFoldersOption.isChecked();
}
async isFilesOptionChecked() {
return this.searchFilesOption.isChecked();
}
async isLibrariesOptionChecked() {
return this.searchLibrariesOption.isChecked();
}
async clearOptions() {
if (await this.isFilesOptionChecked()) {
await this.searchFilesOption.click();
}
if (await this.isFoldersOptionChecked()) {
await this.searchFoldersOption.click();
}
if (await this.isLibrariesOptionChecked()) {
await this.searchLibrariesOption.click();
}
}
async checkOnlyFolders(): Promise<void> {
await this.clearOptions();
await this.searchFoldersOption.click();
}
async checkOnlyFiles(): Promise<void> {
await this.clearOptions();
await this.searchFilesOption.click();
}
async checkLibraries(): Promise<void> {
await this.clearOptions();
await this.searchLibrariesOption.click();
}
async checkFilesAndFolders(): Promise<void> {
await this.clearOptions();
await this.searchFilesOption.click();
await this.searchFoldersOption.click();
}
async searchFor(input: string): Promise<void> {
await this.searchInput.clear();
await this.searchInput.fill(input);
await this.searchButton.click({ force: true });
}
}
@@ -26,24 +26,24 @@ import { Page } from '@playwright/test';
import { BasePage } from './base.page';
import {
DataTableComponent,
FolderInformationDialogComponent,
MatMenuComponent,
ViewerComponent,
SearchInputComponent,
SearchOverlayComponent,
SidenavComponent,
SearchSortingPicker,
SearchFilters,
SearchFiltersTags,
SearchFiltersCategories,
SearchFiltersDate,
SearchFiltersLocation,
SearchFiltersLogic,
SearchFiltersProperties,
FolderInformationDialogComponent,
SearchMenuCard
SearchFiltersTags,
SearchInputComponent,
SearchMenuCard,
SearchSortingPicker,
SidenavComponent,
ViewerComponent
} from '../components';
import { AcaHeader } from '../components/aca-header.component';
import { AdfConfirmDialogComponent, AdfFolderDialogComponent, UploadNewVersionDialog, ManageVersionsDialog } from '../components/dialogs';
import { AdfConfirmDialogComponent, AdfFolderDialogComponent, ManageVersionsDialog, UploadNewVersionDialog } from '../components/dialogs';
import { SearchInDialogComponent } from '../components/search/search-in-dialog.components';
export type SearchType = 'files' | 'folders' | 'filesAndFolders' | 'libraries';
@@ -59,8 +59,8 @@ export class SearchPage extends BasePage {
public folderDialog = new AdfFolderDialogComponent(this.page);
public dataTable = new DataTableComponent(this.page);
public viewer = new ViewerComponent(this.page);
public searchInput = new SearchInputComponent(this.page);
public searchOverlay = new SearchOverlayComponent(this.page);
public searchInputComponent = new SearchInputComponent(this.page);
public searchInDialog = new SearchInDialogComponent(this.page);
public searchSortingPicker = new SearchSortingPicker(this.page);
public searchFilters = new SearchFilters(this.page);
public searchFiltersTags = new SearchFiltersTags(this.page);
@@ -77,29 +77,33 @@ export class SearchPage extends BasePage {
public searchMenuCard = new SearchMenuCard(this.page);
async searchWithin(searchText: string, searchType?: SearchType): Promise<void> {
await this.acaHeader.searchButton.click();
await this.clickSearchButton();
if (!(await this.searchInputComponent.searchInput.isVisible())) {
await this.acaHeader.searchButton.click();
}
await this.searchInputComponent.searchInButton.click();
switch (searchType) {
case 'files':
await this.searchOverlay.checkOnlyFiles();
await this.searchInDialog.checkOnlyFiles();
break;
case 'folders':
await this.searchOverlay.checkOnlyFolders();
await this.searchInDialog.checkOnlyFolders();
break;
case 'filesAndFolders':
await this.searchOverlay.checkFilesAndFolders();
await this.searchInDialog.checkFilesAndFolders();
break;
case 'libraries':
await this.searchOverlay.checkLibraries();
await this.searchInDialog.checkLibraries();
break;
default:
break;
}
await this.searchOverlay.searchFor(searchText);
await this.searchInDialog.applyButton.click();
await this.clickSearchButton();
await this.searchInputComponent.searchFor(searchText);
await this.dataTable.progressBarWaitForReload();
}
async clickSearchButton() {
await this.searchInput.searchButton.click({ force: true });
await this.searchInputComponent.searchButton.click({ force: true });
}
}
@@ -1,6 +1,6 @@
aca-toolbar {
.aca-toolbar {
min-height: 48px;
height: 56px;
color: var(--adf-theme-foreground-text-color-064);
background-color: inherit;
border: none;
@@ -24,14 +24,14 @@
import { inject, Injectable } from '@angular/core';
import {
AuthenticationService,
AppConfigService,
PageTitleService,
UserPreferencesService,
AuthenticationService,
NotificationService,
StorageService
PageTitleService,
StorageService,
UserPreferencesService
} from '@alfresco/adf-core';
import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import {
AlfrescoApiService,
FileUploadErrorEvent,
@@ -72,7 +72,7 @@ export class AppService implements ShellAppService {
toggleAppNavBar$ = new Subject<void>();
hideSidenavConditions = ['/preview/'];
minimizeSidenavConditions = ['/search'];
minimizeSidenavConditions = [];
/**
* Whether `withCredentials` mode is enabled.
@@ -1,70 +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 <http://www.gnu.org/licenses/>.
*/
import { FormControl } from '@angular/forms';
import { noLeadingTrailingOperatorsValidator } from './no-leading-trailing-operators.validator';
describe('noLeadingTrailingOperatorsValidator', () => {
const validatorFn = noLeadingTrailingOperatorsValidator();
it('should return null for valid input without operators', () => {
const control = new FormControl('valid input');
expect(validatorFn(control)).toBeNull();
});
it('should return null for empty input', () => {
const control = new FormControl('');
expect(validatorFn(control)).toBeNull();
});
it('should return null for valid input with operators', () => {
const control = new FormControl('valid AND input OR phrase');
expect(validatorFn(control)).toBeNull();
});
it('should return error for input with leading AND operator', () => {
const control = new FormControl('AND input');
expect(validatorFn(control)).toEqual({ operators: true });
});
it('should return error for input with leading OR operator', () => {
const control = new FormControl('OR input');
expect(validatorFn(control)).toEqual({ operators: true });
});
it('should return error for input with trailing AND operator', () => {
const control = new FormControl('input AND');
expect(validatorFn(control)).toEqual({ operators: true });
});
it('should return error for input with trailing OR operator', () => {
const control = new FormControl('input OR');
expect(validatorFn(control)).toEqual({ operators: true });
});
it('should return error for input with only operator', () => {
const control = new FormControl('AND OR');
expect(validatorFn(control)).toEqual({ operators: true });
});
});
@@ -1,43 +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 <http://www.gnu.org/licenses/>.
*/
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
const isOperator = (word: string): boolean => {
const operators = ['AND', 'OR'];
return operators.includes(word.trim());
};
export const noLeadingTrailingOperatorsValidator = (): ValidatorFn => {
return (control: AbstractControl<string>): ValidationErrors | null => {
const rawValue = control.value;
if (!rawValue) {
return null;
}
const words = rawValue.trim().split(/\s+/);
return isOperator(words[0]) || isOperator(words[words.length - 1]) ? { operators: true } : null;
};
};
@@ -1,50 +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 <http://www.gnu.org/licenses/>.
*/
import { FormControl } from '@angular/forms';
import { noWhitespaceValidator } from './no-whitespace.validator';
describe('noWhitespaceValidator', () => {
const validatorFn = noWhitespaceValidator();
it('should return null for valid non-whitespace input', () => {
const control = new FormControl('valid input');
expect(validatorFn(control)).toBeNull();
});
it('should return error for input with only spaces', () => {
const control = new FormControl(' ');
expect(validatorFn(control)).toEqual({ whitespace: true });
});
it('should return error for empty string', () => {
const control = new FormControl('');
expect(validatorFn(control)).toBeNull();
});
it('should return null for input with leading and trailing spaces but valid content inside', () => {
const control = new FormControl(' valid ');
expect(validatorFn(control)).toBeNull();
});
});
@@ -1,37 +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 <http://www.gnu.org/licenses/>.
*/
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const noWhitespaceValidator = (): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const rawValue = control.value;
if (!rawValue) {
return null;
}
const trimmedValue = rawValue.toString().trim();
return trimmedValue.length === 0 ? { whitespace: true } : null;
};
};
-3
View File
@@ -64,6 +64,3 @@ export * from './lib/services/navigation-history.service';
export * from './lib/testing/lib-testing-module';
export * from './lib/utils/node.utils';
export * from './lib/validators/no-whitespace.validator';
export * from './lib/validators/no-leading-trailing-operators.validator';