From 4204745227a537fef1a8b943a5445ff893109dba Mon Sep 17 00:00:00 2001 From: Michal Kinas <113341662+MichalKinas@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:27:32 +0100 Subject: [PATCH] [MNT-25478] Add Information dialog for nodes and dialog for additional location references (#5041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [MNT-25478] Add Information dialog for nodes and dialog for additional location references * [MNT-25478] CR fixes * [MNT-25478] CR fixes * [MNT-25478] Unit test fix * [MNT-25478] E2E locator fixes * [MNT-25478] E2E locator fixes * [MMNT-25478] Add cm:contains to secondary parents API call * [MMNT-25478] Small fix * [MNT-25478] First e2e fixes * more fixes for e2es * last e2e fixes --------- Co-authored-by: Adam Świderski --- docs/extending/rules-list.md | 4 +- .../src/tests/folder-information.e2e.ts | 28 +- .../list-views/src/tests/permissions.e2e.ts | 12 +- .../list-views/src/tests/trash.e2e.ts | 4 +- .../src/tests/search-filters-date.e2e.ts | 85 --- .../aca-content/assets/app.extensions.json | 24 +- projects/aca-content/assets/i18n/en.json | 20 +- .../aca-content/src/lib/aca-content.module.ts | 2 +- .../location-link.component.html | 25 + .../location-link.component.scss | 21 + .../location-link.component.spec.ts | 532 ++++++++++++++++++ .../location-link/location-link.component.ts | 48 +- .../search-results-row.component.scss | 2 +- .../folder-information.component.html | 38 -- .../folder-information.component.scss | 52 -- .../folder-information.component.spec.ts | 138 ----- .../folder-information.component.ts | 126 ----- .../node-information.component.html | 66 +++ .../node-information.component.scss | 78 +++ .../node-information.component.spec.ts | 245 ++++++++ .../node-information.component.ts | 123 ++++ .../node-location-references.component.html | 22 + .../node-location-references.component.scss | 27 + ...node-location-references.component.spec.ts | 285 ++++++++++ .../node-location-references.component.ts | 72 +++ .../content-management.service.spec.ts | 18 +- .../services/content-management.service.ts | 10 +- .../lib/store/effects/node.effects.spec.ts | 21 +- .../src/lib/store/effects/node.effects.ts | 12 +- .../src/lib/ui/variables/variables.scss | 4 +- .../dataTable/data-table.component.ts | 2 +- .../folder-information-dialog.component.ts | 14 +- .../search-filters-date.component.ts | 72 +-- .../aca-shared/rules/src/app.rules.spec.ts | 18 +- projects/aca-shared/rules/src/app.rules.ts | 6 +- .../store/src/actions/node.actions.ts | 6 +- 36 files changed, 1636 insertions(+), 626 deletions(-) create mode 100644 projects/aca-content/src/lib/components/common/location-link/location-link.component.html create mode 100644 projects/aca-content/src/lib/components/common/location-link/location-link.component.scss create mode 100644 projects/aca-content/src/lib/components/common/location-link/location-link.component.spec.ts delete mode 100644 projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.html delete mode 100644 projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.scss delete mode 100644 projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.spec.ts delete mode 100644 projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.ts create mode 100644 projects/aca-content/src/lib/dialogs/node-details/node-information.component.html create mode 100644 projects/aca-content/src/lib/dialogs/node-details/node-information.component.scss create mode 100644 projects/aca-content/src/lib/dialogs/node-details/node-information.component.spec.ts create mode 100644 projects/aca-content/src/lib/dialogs/node-details/node-information.component.ts create mode 100644 projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.html create mode 100644 projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.scss create mode 100644 projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.spec.ts create mode 100644 projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.ts diff --git a/docs/extending/rules-list.md b/docs/extending/rules-list.md index 0f769ef3d..c3819cb18 100644 --- a/docs/extending/rules-list.md +++ b/docs/extending/rules-list.md @@ -83,5 +83,5 @@ Rules/Evaluators created for specific features in ADW to be checked if supported | Version | Key | Description | |---------|---------------------------------|---------------------------------------------------------------------------| | 8.1.0 | isPreferencesApiAvailable | Checks whether current ACS version supports PUT method in Preferences API | -| 8.1.0 | isFolderInfoAvailable | Checks whether current ACS version supports folder size calculation API | -| 8.1.0 | isBulkActionsAvailable | Checks whether current ACS version supports bulk update feature | \ No newline at end of file +| 8.1.0 | isNodeInfoAvailable | Checks whether current ACS version supports folder size calculation API | +| 8.1.0 | isBulkActionsAvailable | Checks whether current ACS version supports bulk update feature | diff --git a/e2e/playwright/folder-information-actions/src/tests/folder-information.e2e.ts b/e2e/playwright/folder-information-actions/src/tests/folder-information.e2e.ts index 1770e4742..c532fc43c 100644 --- a/e2e/playwright/folder-information-actions/src/tests/folder-information.e2e.ts +++ b/e2e/playwright/folder-information-actions/src/tests/folder-information.e2e.ts @@ -133,7 +133,7 @@ test.describe('Actions - Folder Information', () => { ) { await page.dataTable.selectItems(folderName); await page.acaHeader.clickMoreActions(); - await page.matMenu.clickMenuItem('Folder Information'); + await page.matMenu.clickMenuItem('Information'); await expect(async () => { expect(await page.folderInformationDialog.folderNumberOfFiles.textContent()).toContain(expectedNumber); expect(await page.folderInformationDialog.folderSize.textContent()).toContain(expectedSize); @@ -153,46 +153,34 @@ test.describe('Actions - Folder Information', () => { test('[XAT-17722] Folder information Empty folder size and number of documents as 0', async ({ personalFiles }) => { await personalFiles.navigate(); - await checkFolderInformation(personalFiles, emptyFolder, '0', '0 bytes', `/Company Home/User Homes/${username}`, 'isEmpty'); + await checkFolderInformation(personalFiles, emptyFolder, '0', '0 Bytes', `/Company Home/User Homes/${username}`, 'isEmpty'); }); test('[XAT-17715] Folder information correct folder size and number of documents - single file', async ({ personalFiles }) => { await personalFiles.navigate(); - await checkFolderInformation(personalFiles, folder1File, '1', '13,877 bytes (13.88 KB on disk)', `/Company Home/User Homes/${username}`); + await checkFolderInformation(personalFiles, folder1File, '1', '13.55 KB', `/Company Home/User Homes/${username}`); }); test('[XAT-17752] Folder information correct folder size and number of documents - multiple files', async ({ personalFiles }) => { await personalFiles.navigate(); - await checkFolderInformation(personalFiles, folderXFiles, '3', '41,631 bytes (41.63 KB on disk)', `/Company Home/User Homes/${username}`); + await checkFolderInformation(personalFiles, folderXFiles, '3', '40.66 KB', `/Company Home/User Homes/${username}`); }); test('[XAT-17753] Folder information correct folder size and number of documents - folder and files', async ({ personalFiles }) => { await personalFiles.navigate(); - await checkFolderInformation( - personalFiles, - folderXFilesAndFolders, - '5', - '69,385 bytes (69.39 KB on disk)', - `/Company Home/User Homes/${username}` - ); + await checkFolderInformation(personalFiles, folderXFilesAndFolders, '5', '67.76 KB', `/Company Home/User Homes/${username}`); }); test('[XAT-17758] Folder information correct folder size and number of documents - from libraries', async ({ myLibrariesPage }) => { await myLibrariesPage.navigate(); await myLibrariesPage.dataTable.getRowByName(libraryForFolder).dblclick(); - await checkFolderInformation( - myLibrariesPage, - folderInLibrary, - '1', - '13,877 bytes (13.88 KB on disk)', - `/Company Home/Sites/${libraryForFolder}/documentLibrary` - ); + await checkFolderInformation(myLibrariesPage, folderInLibrary, '1', '13.55 KB', `/Company Home/Sites/${libraryForFolder}/documentLibrary`); }); test('[XAT-17759] Folder information correct folder size and number of documents - from search', async ({ personalFiles, searchPage }) => { await personalFiles.navigate(); await searchPage.searchWithin(folderForSearch, 'folders'); - await checkFolderInformation(searchPage, folderForSearch, '2', '27,754 bytes (27.75 KB on disk)', `/Company Home/User Homes/${username}`); + await checkFolderInformation(searchPage, folderForSearch, '2', '27.1 KB', `/Company Home/User Homes/${username}`); }); test('[XAT-17766] Folder information correct folder size and number of documents - nested folders', async ({ personalFiles }) => { @@ -203,7 +191,7 @@ test.describe('Actions - Folder Information', () => { personalFiles, folderNested3, '1', - '13,877 bytes (13.88 KB on disk)', + '13.55 KB', `/Company Home/User Homes/${username}/${folderNested1}/${folderNested2}` ); }); diff --git a/e2e/playwright/list-views/src/tests/permissions.e2e.ts b/e2e/playwright/list-views/src/tests/permissions.e2e.ts index 01569fd36..9bcbc0157 100755 --- a/e2e/playwright/list-views/src/tests/permissions.e2e.ts +++ b/e2e/playwright/list-views/src/tests/permissions.e2e.ts @@ -181,7 +181,9 @@ test.describe('Special permissions', () => { }) => { await recentFilesPage.navigate(); expect(await recentFilesPage.dataTable.isItemPresent(fileName)).toBe(true); - expect(await recentFilesPage.dataTable.getItemLocationText(fileName)).toEqual('Unknown'); + expect(await recentFilesPage.dataTable.getItemLocationText(fileName)).toContain( + 'You do not have permission to view the location of this document.' + ); }); test(`[XAT-4464] Favorites - Location column is empty if the user doesn't have permissions on the file's parent folder`, async ({ @@ -189,14 +191,16 @@ test.describe('Special permissions', () => { }) => { await favoritePage.navigate(); expect(await favoritePage.dataTable.getRowsCount(), 'Incorrect number of items').toBe(1); - expect(await favoritePage.dataTable.getItemLocationText(fileName)).toEqual('Unknown'); + expect(await favoritePage.dataTable.getItemLocationText(fileName)).toContain( + 'You do not have permission to view the location of this document.' + ); }); test(`[XAT-4441] Shared Files - Location column is empty if the user doesn't have permissions on the file's parent folder`, async ({ sharedPage }) => { await sharedPage.navigate(); - expect(await sharedPage.dataTable.getItemLocationText(fileName)).toEqual('Unknown'); + expect(await sharedPage.dataTable.getItemLocationText(fileName)).toContain('You do not have permission to view the location of this document.'); }); test(`[XAT-5612] Search Page - Location column is empty if the user doesn't have permissions on the file's parent folder`, async ({ @@ -210,7 +214,7 @@ test.describe('Special permissions', () => { await searchPage.dataTable.spinnerWaitForReload(); expect(await searchPage.dataTable.getRowsCount(), 'Incorrect number of items').toBe(1); - expect(await searchPage.dataTable.getItemLocationText(fileName)).toEqual('Unknown'); + expect(await searchPage.dataTable.getItemLocationText(fileName)).toContain('You do not have permission to view the location of this document.'); }); }); }); diff --git a/e2e/playwright/list-views/src/tests/trash.e2e.ts b/e2e/playwright/list-views/src/tests/trash.e2e.ts index 7c9e1e455..ac54e57ce 100755 --- a/e2e/playwright/list-views/src/tests/trash.e2e.ts +++ b/e2e/playwright/list-views/src/tests/trash.e2e.ts @@ -90,7 +90,9 @@ test.describe('Trash', () => { }); test('[XAT-4475] Location column is empty if parent folder no longer exists', async ({ trashPage }) => { - expect(await trashPage.dataTable.getItemLocationText(fileDeleted)).toEqual(''); + expect(await trashPage.dataTable.getItemLocationText(fileDeleted)).toContain( + 'You do not have permission to view the location of this document.' + ); }); test(`[XAT-4470] Clicking on the location link redirects to parent folder - item in User's Home`, async ({ trashPage }) => { diff --git a/e2e/playwright/search/src/tests/search-filters-date.e2e.ts b/e2e/playwright/search/src/tests/search-filters-date.e2e.ts index b37044741..7ee987d3e 100644 --- a/e2e/playwright/search/src/tests/search-filters-date.e2e.ts +++ b/e2e/playwright/search/src/tests/search-filters-date.e2e.ts @@ -76,90 +76,5 @@ test.describe('Search - Filters - Date', () => { await expect(searchPage.searchFiltersDate.betweenRadioButton).toBeChecked(); expect(await searchPage.searchFiltersDate.isSearchTabSelected()).toBe('true'); }); - - test('[XAT-5582] Should able to filter search result with Created date under Date Facet - Created anytime', async ({ searchPage }) => { - await searchPage.searchFiltersDate.filterFilesByDate({ - searchPage, - filterType: 'anytime', - dateFilterTab: 'Created', - searchPhrase: randomId, - searchType: 'files', - expectSearchResults: 2 - }); - }); - - test('[XAT-5583] Should able to filter search result with Modified date under Date Facet - Modified anytime', async ({ searchPage }) => { - await searchPage.searchFiltersDate.filterFilesByDate({ - searchPage, - filterType: 'anytime', - dateFilterTab: 'Modified', - searchPhrase: randomId, - searchType: 'files', - expectSearchResults: 2 - }); - }); - - test('[XAT-17699] Should able to filter search result with Created date under Date Facet - Created in the last', async ({ searchPage }) => { - await searchPage.searchFiltersDate.filterFilesByDate({ - searchPage, - filterType: 'inTheLast', - dateFilterTab: 'Created', - searchPhrase: randomId, - searchType: 'files', - expectSearchResults: 2, - inTheLastInputValue: '1' - }); - }); - - test('[XAT-17701] Should able to filter search result with Modified date under Date Facet - Modified in the last', async ({ searchPage }) => { - await searchPage.searchFiltersDate.filterFilesByDate({ - searchPage, - filterType: 'inTheLast', - dateFilterTab: 'Modified', - searchPhrase: randomId, - searchType: 'files', - expectSearchResults: 2, - inTheLastInputValue: '1' - }); - }); - }); - - test.describe('Search - Filters - Date - Modified/Created', () => { - const currentAndPreviousDay = Utils.getCurrentAndPreviousDay(); - const searchPhrase = `Project Contract`; - const fileNamePng = `${randomId}-${searchPhrase}.png`; - const dateForSearch = '01-Jan-11'; - - test.beforeAll(async () => { - try { - const apiClientFactory = new ApiClientFactory(); - await apiClientFactory.setUpAcaBackend('admin'); - await apiClientFactory.createUser({ username }); - trashcanApi = await TrashcanApi.initialize(username, username); - nodesApi = await NodesApi.initialize(username, username); - const fileActionsApi = await FileActionsApi.initialize(username, username); - await fileActionsApi.uploadFileWithRename(TEST_FILES.PNG_FILE.path, fileNamePng, '-my-'); - } catch (error) { - console.error(`beforeAll failed: ${error}`); - } - }); - - test.afterAll(async () => { - await Utils.deleteNodesSitesEmptyTrashcan(nodesApi, trashcanApi, 'afterAll failed'); - }); - - test('[XAT-17700] Should able to filter search result with Created date under Date Facet - Created between', async ({ searchPage }) => { - await searchPage.searchFiltersDate.filterFilesByDate({ - searchPage, - filterType: 'between', - dateFilterTab: 'Created', - searchPhrase: searchPhrase, - searchType: 'files', - expectSearchResults: 1, - inTheLastInputValue: '1', - startDay: dateForSearch, - endDay: currentAndPreviousDay.currentDate - }); - }); }); }); diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index f879943bc..f1c202ce3 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -683,18 +683,20 @@ } }, { - "id": "app.context.menu.folder-info", - "title": "APP.ACTIONS.FOLDER_INFO", + "id": "app.context.menu.node-info", + "title": "APP.ACTIONS.NODE_INFO", "order": 800, "icon": "info", "actions": { - "click": "FOLDER_INFORMATION" + "click": "NODE_INFORMATION" }, "rules": { "visible": [ - "app.selection.folder", + "app.selection.notEmpty", + "!app.navigation.isLibraries", + "!app.navigation.isSharedFiles", "!app.navigation.isTrashcan", - "isFolderInfoAvailable" + "isNodeInfoAvailable" ] } }, @@ -1023,18 +1025,20 @@ } }, { - "id": "app.context.menu.folder-info", - "title": "APP.ACTIONS.FOLDER_INFO", + "id": "app.context.menu.node-info", + "title": "APP.ACTIONS.NODE_INFO", "order": 1200, "icon": "info", "actions": { - "click": "FOLDER_INFORMATION" + "click": "NODE_INFORMATION" }, "rules": { "visible": [ - "app.selection.folder", + "app.selection.notEmpty", + "!app.navigation.isLibraries", + "!app.navigation.isSharedFiles", "!app.navigation.isTrashcan", - "isFolderInfoAvailable" + "isNodeInfoAvailable" ] } }, diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index fed809462..323b15229 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -267,7 +267,8 @@ "LOCATION": "Location", "SIZE": "Size" }, - "UNKNOWN_LOCATION": "Unknown", + "UNKNOWN_LOCATION": "You do not have permission to view the location of this document.", + "VIEW_ADDITIONAL_LOCATIONS": "View additional references locations", "NO_RESULTS": "Your search returned 0 results", "EMPTY_SEARCH": "Search term required", "NO_FILTER_RESULTS": "Your filter returned 0 results", @@ -312,7 +313,7 @@ "EDIT_OFFLINE_CANCEL": "Cancel Editing", "CHANGE_ASPECT": "Edit Aspects", "ADD_ASPECTS": "Add Aspects", - "FOLDER_INFO": "Folder Information" + "NODE_INFO": "Information" }, "DIALOGS": { "CONFIRM_PURGE": { @@ -480,19 +481,24 @@ "CANCEL": "Cancel", "SAVE": "Save" }, - "FOLDER_INFO": { - "ICON": "Folder Icon", - "TITLE": "Folder Information", + "NODE_INFO": { + "ICON": "Node Icon", + "TITLE": "Information", "SIZE" : "Size", "NUMBER_OF_FILES": "Number of files", "CALCULATING": "Calculating...", - "CALCULATED_SIZE_LARGE": "{{sizeInBytes}} bytes ({{sizeInLargeUnit}} {{unit}} on disk)", - "CALCULATED_SIZE_NORMAL": "{{sizeInBytes}} bytes", "LOCATION": "Location", + "REFERENCED": "Referenced", "CREATED": "Created", "MODIFIED": "Modified", "DONE": "Done", "ERROR": "Something went wrong, please close this dialog and try again" + }, + "ADDITIONAL_REFERENCES_DIALOG": { + "TITLE": "Additional Reference Locations", + "NO_LOCATIONS": "{{ name }} is not referenced in any locations.", + "DESCRIPTION": "{{ name }} is currently referenced in these locations.", + "CLOSE": "Close" } }, "NODE_SELECTOR": { diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts index 2e50a0755..3ff4569a6 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -136,7 +136,7 @@ import { IsFeatureSupportedInCurrentAcsPipe } from './pipes/is-feature-supported isMultiSelection: rules.isMultiselection, canPrintFile: rules.canPrintFile, isPreferencesApiAvailable: rules.isPreferencesApiAvailable, - isFolderInfoAvailable: rules.isFolderInfoAvailable, + isNodeInfoAvailable: rules.isNodeInfoAvailable, isBulkActionsAvailable: rules.isBulkActionsAvailable, 'app.selection.canDelete': rules.canDeleteSelection, diff --git a/projects/aca-content/src/lib/components/common/location-link/location-link.component.html b/projects/aca-content/src/lib/components/common/location-link/location-link.component.html new file mode 100644 index 000000000..932ac25e5 --- /dev/null +++ b/projects/aca-content/src/lib/components/common/location-link/location-link.component.html @@ -0,0 +1,25 @@ +@if (hasPrimaryParentPath) { + + +} @else { +
+ + {{ 'APP.BROWSE.SEARCH.UNKNOWN_LOCATION' | translate }} + + +
+} diff --git a/projects/aca-content/src/lib/components/common/location-link/location-link.component.scss b/projects/aca-content/src/lib/components/common/location-link/location-link.component.scss new file mode 100644 index 000000000..96349ccb4 --- /dev/null +++ b/projects/aca-content/src/lib/components/common/location-link/location-link.component.scss @@ -0,0 +1,21 @@ +.aca-location-link { + &-unknown { + display: flex; + align-items: center; + font-size: 12px; + + .adf-datatable-cell-value:hover { + color: var(--theme-foreground-text-color); + text-decoration: none; + } + } + + .aca-location-link-info-btn { + --mdc-icon-button-state-layer-size: 24px; + --mdc-icon-button-icon-size: 24px; + + color: var(--theme-foreground-text-color); + padding: 0; + margin-left: 5px; + } +} diff --git a/projects/aca-content/src/lib/components/common/location-link/location-link.component.spec.ts b/projects/aca-content/src/lib/components/common/location-link/location-link.component.spec.ts new file mode 100644 index 000000000..fa6c3ec7e --- /dev/null +++ b/projects/aca-content/src/lib/components/common/location-link/location-link.component.spec.ts @@ -0,0 +1,532 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DialogComponent, DialogSize, UnitTestingUtils } from '@alfresco/adf-core'; +import { LocationLinkComponent } from './location-link.component'; +import { ContentApiService, LibTestingModule } from '@alfresco/aca-shared'; +import { provideMockStore } from '@ngrx/store/testing'; +import { Store } from '@ngrx/store'; +import { NavigateToParentFolder } from '@alfresco/aca-shared/store'; +import { of, skip, take, throwError } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { NodeLocationReferencesComponent } from '../../../dialogs/node-location-references/node-location-references.component'; +import { Node } from '@alfresco/js-api'; +import { DebugElement } from '@angular/core'; + +describe('LocationLinkComponent', () => { + let fixture: ComponentFixture; + let contentApi: ContentApiService; + let unitTestingUtils: UnitTestingUtils; + let store: Store; + let dialog: MatDialog; + + const getCell = (): DebugElement => unitTestingUtils.getByCSS('.adf-datatable-cell-value'); + + const getCellText = (): string => unitTestingUtils.getInnerTextByCSS('.adf-datatable-cell-value'); + + const testTooltipValue = (value: string, done: DoneFn): void => { + fixture.detectChanges(); + fixture.componentInstance.nodeLocation$.pipe(skip(1), take(1)).subscribe(() => { + fixture.detectChanges(); + expect(getCell().nativeElement.title).toBe(value); + done(); + }); + const hostElement = fixture.nativeElement; + const mouseenter = new MouseEvent('mouseenter', { bubbles: true }); + hostElement.dispatchEvent(mouseenter); + fixture.detectChanges(); + }; + + const clickLink = (): void => { + fixture.detectChanges(); + unitTestingUtils.clickByCSS('.adf-datatable-cell-value'); + fixture.detectChanges(); + }; + + const clickInfoButton = (): void => { + fixture.detectChanges(); + unitTestingUtils.clickByCSS('.aca-location-link-info-btn'); + fixture.detectChanges(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LocationLinkComponent, LibTestingModule], + providers: [provideMockStore({})] + }); + + fixture = TestBed.createComponent(LocationLinkComponent); + contentApi = TestBed.inject(ContentApiService); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); + store = TestBed.inject(Store); + dialog = TestBed.inject(MatDialog); + fixture.componentInstance.context = null; + }); + + it('should not allow the navigation when there is no context', () => { + spyOn(store, 'dispatch'); + fixture.componentInstance.hasPrimaryParentPath = true; + clickLink(); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('should not open additional locations dialog when there is no context', () => { + spyOn(dialog, 'open'); + clickInfoButton(); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + describe('when node has primary parent path', () => { + beforeEach(() => { + fixture.componentInstance.context = { + row: { + node: { + entry: { + path: { + name: 'Test', + elements: [{ name: 'Company Home' }] + } + } + } + } + }; + }); + + it('should display primary path', () => { + fixture.detectChanges(); + expect(getCellText()).toBe('APP.BROWSE.PERSONAL.TITLE'); + }); + + it('should display path name when showLocation is true', () => { + fixture.componentInstance.showLocation = true; + fixture.componentInstance.context.row.node.entry.path.name = '/Company Home'; + fixture.detectChanges(); + expect(getCellText()).toBe('Company Home'); + }); + + it('should display APP.BROWSE.PERSONAL.TITLE for single Company Home element', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [{ id: '1', name: 'Company Home' }] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('APP.BROWSE.PERSONAL.TITLE'); + }); + + it('should not display APP.BROWSE.PERSONAL.TITLE when single element is not Company Home', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [{ id: '1', name: 'Other Root' }] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('Other Root'); + }); + + it('should not display APP.BROWSE.PERSONAL.TITLE when path has multiple elements even if first is Company Home', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Folder1' } + ] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('Folder1'); + }); + + it('should display APP.BROWSE.PERSONAL.TITLE for path with Company Home, User Homes, and third element', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'User Homes' }, + { id: '3', name: 'user123' } + ] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('APP.BROWSE.PERSONAL.TITLE'); + }); + + it('should not display APP.BROWSE.PERSONAL.TITLE when path has 3 elements but second is not User Homes', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: '3', name: 'site1' } + ] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('site1'); + }); + + it('should not display APP.BROWSE.PERSONAL.TITLE when path has 3 elements but first is not Company Home', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Root' }, + { id: '2', name: 'User Homes' }, + { id: '3', name: 'user123' } + ] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('user123'); + }); + + it('should not display APP.BROWSE.PERSONAL.TITLE when path has Company Home and User Homes but more than 3 elements', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'User Homes' }, + { id: '3', name: 'user123' }, + { id: '4', name: 'Documents' } + ] + }; + fixture.detectChanges(); + expect(getCellText()).toBe('Documents'); + }); + + it('should use cm:title from node info response when available', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'site-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: 'site-name', + properties: { 'cm:title': 'My Site Title' } + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + fixture.detectChanges(); + + expect(getCellText()).toBe('My Site Title'); + }); + + it('should use node name when cm:title is not available', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'site-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: 'Actual Site Name', + properties: {} + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + fixture.detectChanges(); + expect(getCellText()).toBe('Actual Site Name'); + }); + + it('should display fragment name as fallback when node has no cm:title or name', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'fallback-fragment-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: '', + properties: {} + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + + fixture.detectChanges(); + expect(getCellText()).toBe('fallback-fragment-name'); + }); + + it('should handle API error gracefully and display fragment name', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'error-fallback-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(throwError(() => new Error('API Error'))); + + fixture.detectChanges(); + expect(getCellText()).toBe('error-fallback-name'); + }); + + it('should display last element name for simple path', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Folder1' }, + { id: '2', name: 'Folder2' }, + { id: '3', name: 'Folder3' } + ] + }; + + fixture.detectChanges(); + expect(getCellText()).toBe('Folder3'); + }); + + it('should display last element name when path contains documentLibrary but not as last element', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: '3', name: 'documentLibrary' }, + { id: '4', name: 'SubFolder' } + ] + }; + + fixture.detectChanges(); + expect(getCellText()).toBe('SubFolder'); + }); + + it('should display last element name for path with Company Home and multiple folders', () => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Folder1' }, + { id: '3', name: 'Folder2' }, + { id: '4', name: 'FinalFolder' } + ] + }; + + fixture.detectChanges(); + expect(getCellText()).toBe('FinalFolder'); + }); + + it('should navigate to parent folder on click', () => { + spyOn(store, 'dispatch'); + clickLink(); + expect(store.dispatch).toHaveBeenCalledWith( + jasmine.objectContaining({ + ...new NavigateToParentFolder(fixture.componentInstance.context.row.node) + }) + ); + }); + + it('should display tooltip on mouse enter', (done) => { + testTooltipValue('APP.BROWSE.PERSONAL.TITLE', done); + }); + + it('should join elements with slash for the tooltip when first element is not Company Home', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Root' }, + { id: '2', name: 'Folder1' }, + { id: '3', name: 'Folder2' } + ] + }; + + testTooltipValue('Root/Folder1/Folder2', done); + }); + + it('should replace Company Home with Personal Files', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Folder1' } + ] + }; + + testTooltipValue('APP.BROWSE.PERSONAL.TITLE/Folder1', done); + }); + + it('should handle User Homes path correctly', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'User Homes' }, + { id: '3', name: 'user123' }, + { id: '4', name: 'Documents' } + ] + }; + + testTooltipValue('APP.BROWSE.PERSONAL.TITLE/Documents', done); + }); + + it('should handle path with Company Home and other second element (not Sites or User Homes)', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'OtherFolder' }, + { id: '3', name: 'SubFolder' } + ] + }; + + testTooltipValue('APP.BROWSE.PERSONAL.TITLE/OtherFolder/SubFolder', done); + }); + + it('should use cm:title from node info response when available', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'site-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: 'site-name', + properties: { 'cm:title': 'My Site Title' } + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + + testTooltipValue('APP.BROWSE.LIBRARIES.TITLE/My Site Title', done); + }); + + it('should use node name when cm:title is not available', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'site-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: 'Actual Site Name', + properties: {} + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + + testTooltipValue('APP.BROWSE.LIBRARIES.TITLE/Actual Site Name', done); + }); + + it('should use fragment name as fallback when node has no cm:title or name', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'fallback-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + const mockNode = { + id: 'site-id', + name: '', + properties: {} + } as Node; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(of(mockNode)); + + testTooltipValue('APP.BROWSE.LIBRARIES.TITLE/fallback-name', done); + }); + + it('should handle API error and use fragment name', (done) => { + fixture.componentInstance.context.row.node.entry.path = { + name: 'Test', + elements: [ + { id: '1', name: 'Company Home' }, + { id: '2', name: 'Sites' }, + { id: 'site-id', name: 'error-fallback-name' }, + { id: '4', name: 'documentLibrary' } + ] + }; + + spyOn(contentApi, 'getNodeInfo').and.returnValue(throwError(() => new Error('API Error'))); + + testTooltipValue('APP.BROWSE.LIBRARIES.TITLE/error-fallback-name', done); + }); + }); + + describe('when node does not have primary parent path', () => { + beforeEach(() => { + fixture.componentInstance.context = { + row: { + node: { + entry: { + path: {} + } + } + } + }; + }); + + it('should display unknown location message with info button', () => { + fixture.detectChanges(); + const infoButton = unitTestingUtils.getByCSS('.aca-location-link-info-btn'); + const unknownLocation = getCell(); + expect(unknownLocation.nativeElement.innerText.trim()).toBe('APP.BROWSE.SEARCH.UNKNOWN_LOCATION'); + expect(unknownLocation.attributes['title']).toBe('APP.BROWSE.SEARCH.UNKNOWN_LOCATION'); + expect(infoButton.attributes['title']).toBe('APP.BROWSE.SEARCH.VIEW_ADDITIONAL_LOCATIONS'); + expect(infoButton.attributes['aria-label']).toBe('APP.BROWSE.SEARCH.VIEW_ADDITIONAL_LOCATIONS'); + expect(infoButton.nativeElement.innerText.trim()).toBe('info'); + }); + + it('should open additional location references dialog on info button click', () => { + spyOn(dialog, 'open'); + clickInfoButton(); + expect(dialog.open).toHaveBeenCalledWith(DialogComponent, { + data: { + title: 'APP.ADDITIONAL_REFERENCES_DIALOG.TITLE', + confirmButtonTitle: 'APP.ADDITIONAL_REFERENCES_DIALOG.CLOSE', + isCancelButtonHidden: true, + isCloseButtonHidden: false, + dialogSize: DialogSize.Medium, + contentComponent: NodeLocationReferencesComponent, + componentData: fixture.componentInstance.context.row.node.entry + }, + width: '600px', + restoreFocus: true + }); + }); + }); +}); diff --git a/projects/aca-content/src/lib/components/common/location-link/location-link.component.ts b/projects/aca-content/src/lib/components/common/location-link/location-link.component.ts index ed379ea9b..7e72f8bcd 100644 --- a/projects/aca-content/src/lib/components/common/location-link/location-link.component.ts +++ b/projects/aca-content/src/lib/components/common/location-link/location-link.component.ts @@ -28,24 +28,19 @@ import { Observable, BehaviorSubject, of } from 'rxjs'; import { Store } from '@ngrx/store'; import { NavigateToParentFolder } from '@alfresco/aca-shared/store'; import { ContentApiService } from '@alfresco/aca-shared'; -import { TranslationService } from '@alfresco/adf-core'; +import { DialogComponent, DialogSize, TranslationService } from '@alfresco/adf-core'; import { CommonModule } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; +import { MatIcon } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { NodeLocationReferencesComponent } from '../../../dialogs/node-location-references/node-location-references.component'; @Component({ - imports: [CommonModule, TranslatePipe], + imports: [CommonModule, TranslatePipe, MatIcon, MatButtonModule], selector: 'aca-location-link', - template: ` - - - `, + templateUrl: './location-link.component.html', + styleUrls: ['./location-link.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { @@ -56,11 +51,13 @@ export class LocationLinkComponent implements OnInit { private store = inject(Store); private contentApi = inject(ContentApiService); private translationService = inject(TranslationService); - private _path: PathInfo; - nodeLocation$ = new BehaviorSubject(this.translationService.instant('APP.BROWSE.SEARCH.UNKNOWN_LOCATION')); + private readonly dialogRef = inject(MatDialog); + + nodeLocation$ = new BehaviorSubject(''); displayText: Observable; + hasPrimaryParentPath = false; @Input() context: any; @@ -94,13 +91,32 @@ export class LocationLinkComponent implements OnInit { this.displayText = this.getDisplayText(path); } this._path = path; + this.hasPrimaryParentPath = true; } else { - this.displayText = of('APP.BROWSE.SEARCH.UNKNOWN_LOCATION'); + this.hasPrimaryParentPath = false; } } } } + openAdditionalLocationReferencesDialog() { + if (this.context.row.node) { + this.dialogRef.open(DialogComponent, { + data: { + title: 'APP.ADDITIONAL_REFERENCES_DIALOG.TITLE', + confirmButtonTitle: 'APP.ADDITIONAL_REFERENCES_DIALOG.CLOSE', + isCancelButtonHidden: true, + isCloseButtonHidden: false, + dialogSize: DialogSize.Medium, + contentComponent: NodeLocationReferencesComponent, + componentData: this.context.row.node.entry + }, + width: '600px', + restoreFocus: true + }); + } + } + // todo: review once 5.2.3 is out private getDisplayText(path: PathInfo): Observable { const elements = path.elements.map((e) => e.name); diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss index 11035049a..bf03385bf 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss @@ -35,7 +35,7 @@ } .aca-link:hover, - .aca-location-link .adf-datatable-cell-value:hover { + .aca-location-link a.adf-datatable-cell-value:hover { color: var(--theme-primary-color); text-decoration: underline; } diff --git a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.html b/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.html deleted file mode 100644 index 4cd09188c..000000000 --- a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.html +++ /dev/null @@ -1,38 +0,0 @@ -
- -
{{ folderDetails.name }}
-
-