mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[ACS-10475] [Search] UX improvements for search (#5042)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}'`);
|
||||
}
|
||||
}
|
||||
+76
@@ -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>
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
+150
@@ -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();
|
||||
});
|
||||
});
|
||||
+178
@@ -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());
|
||||
}
|
||||
}
|
||||
-39
@@ -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>
|
||||
-37
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-198
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
-180
@@ -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('');
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-45
@@ -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>
|
||||
|
||||
+16
-74
@@ -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) {
|
||||
|
||||
+225
-121
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+70
-252
@@ -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
-2
@@ -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">
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
-6
@@ -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(() => {
|
||||
|
||||
+3
-6
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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>
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+88
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
-93
@@ -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.
|
||||
|
||||
-70
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user