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 @@
-
-
-
-
-
{{ 'APP.FOLDER_INFO.NUMBER_OF_FILES' | translate }}
-
{{ folderDetails.numberOfFiles }}
-
-
-
-
{{ 'APP.FOLDER_INFO.SIZE' | translate }}
-
{{ folderDetails.size }}
-
-
-
-
{{ 'APP.FOLDER_INFO.LOCATION' | translate }}
-
{{ folderDetails.location }}
-
-
-
-
{{ 'APP.FOLDER_INFO.CREATED' | translate }}
-
{{ folderDetails.created | adfTimeAgo }}
-
-
-
-
{{ 'APP.FOLDER_INFO.MODIFIED' | translate }}
-
{{ folderDetails.modified | adfTimeAgo }}
-
-
diff --git a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.scss b/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.scss
deleted file mode 100644
index 04418ee9a..000000000
--- a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-$folder-icon-color: #d9e021;
-
-.app-folder-info {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--theme-border-color);
- border-radius: 12px;
-
- .aca-folder-info {
- &-header {
- display: flex;
- flex-direction: row;
- align-items: center;
- column-gap: 10px;
- padding: 20px;
-
- .aca-folder-icon {
- display: flex;
- flex-direction: column;
- vertical-align: middle;
- color: $folder-icon-color;
- }
-
- .aca-folder-title {
- flex: 1;
- font-weight: bold;
- }
- }
-
- &-body {
- display: flex;
- flex-direction: column;
- flex: 1;
- padding: 10px 20px;
-
- .aca-folder-info-item {
- display: flex;
- flex-direction: row;
- padding: 20px 0;
-
- &-label {
- width: 30%;
- color: var(--theme-text-color);
- }
-
- &-value {
- width: 70%;
- }
- }
- }
- }
-}
diff --git a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.spec.ts b/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.spec.ts
deleted file mode 100644
index 7a26bf520..000000000
--- a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.spec.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/*!
- * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
- *
- * Alfresco Example Content Application
- *
- * This file is part of the Alfresco Example Content Application.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Alfresco Example Content Application is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * from Hyland Software. If not, see .
- */
-
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { FolderInformationComponent } from './folder-information.component';
-import { DIALOG_COMPONENT_DATA, RedirectAuthService, UnitTestingUtils } from '@alfresco/adf-core';
-import { ContentService, NodesApiService } from '@alfresco/adf-content-services';
-import { EMPTY, Observable, of, Subject } from 'rxjs';
-import { LibTestingModule } from '@alfresco/aca-shared';
-import { JobIdBodyEntry, SizeDetails, SizeDetailsEntry, Node } from '@alfresco/js-api';
-
-describe('FolderInformationComponent', () => {
- let fixture: ComponentFixture;
- let nodeService: NodesApiService;
- let unitTestingUtils: UnitTestingUtils;
- let initiateFolderSizeCalculationSpy: jasmine.Spy<(nodeId: string) => Observable>;
- let getFolderSizeInfoSpy: jasmine.Spy<(nodeId: string, jobId: string) => Observable>;
-
- const getNumberOfFiles = (): string => unitTestingUtils.getInnerTextByDataAutomationId('folder-info-number-of-files');
-
- const mockSub = new Subject();
- const dialogData = {
- name: 'mock-folder',
- id: 'mock-folder-id',
- path: {
- name: 'mock-folder-path'
- },
- createdAt: new Date(2024, 1, 1, 11, 11),
- modifiedAt: new Date(2024, 2, 2, 22, 22)
- } as Node;
- const mockSizeDetailsEntry: SizeDetailsEntry = {
- entry: {
- id: 'mock-id',
- sizeInBytes: '1',
- calculatedAt: 'mock-date',
- numberOfFiles: 1,
- status: SizeDetails.StatusEnum.COMPLETE,
- jobId: 'mock-job-id'
- }
- };
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [FolderInformationComponent, LibTestingModule],
- providers: [
- { provide: DIALOG_COMPONENT_DATA, useValue: dialogData },
- { provide: RedirectAuthService, useValue: { onLogin: EMPTY, onTokenReceived: EMPTY } }
- ]
- });
- fixture = TestBed.createComponent(FolderInformationComponent);
- nodeService = TestBed.inject(NodesApiService);
- unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
- spyOn(TestBed.inject(ContentService), 'getNodeIcon').and.returnValue('./assets/images/ft_ic_folder.svg');
- initiateFolderSizeCalculationSpy = spyOn(nodeService, 'initiateFolderSizeCalculation').and.returnValue(mockSub.asObservable());
- getFolderSizeInfoSpy = spyOn(nodeService, 'getFolderSizeInfo').and.returnValue(EMPTY);
- });
-
- it('should render all information in init', () => {
- fixture.detectChanges();
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-name')).toBe('mock-folder');
- expect(getNumberOfFiles()).toBe('APP.FOLDER_INFO.CALCULATING');
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-size')).toBe('APP.FOLDER_INFO.CALCULATING');
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-location')).toBe('mock-folder-path');
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-creation-date')).toBe('01/02/2024 11:11');
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-modify-date')).toBe('02/03/2024 22:22');
- });
-
- it('should make API call on init to start folder size calculation', () => {
- fixture.detectChanges();
- expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
- });
-
- it('should fetch folder size only when the initial folder size calculation request is completed and one second has passed', fakeAsync(() => {
- fixture.detectChanges();
- expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
- expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
- mockSub.next({ entry: { jobId: 'mock-job-id' } });
- expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
- tick(1000);
- expect(getFolderSizeInfoSpy).toHaveBeenCalled();
- }));
-
- it('should make repeated calls to get folder size info, if the response returned from the API is IN_PROGRESS', fakeAsync(() => {
- mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.IN_PROGRESS;
- getFolderSizeInfoSpy.and.returnValue(of(mockSizeDetailsEntry));
- fixture.detectChanges();
- expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
- mockSub.next({ entry: { jobId: 'mock-job-id' } });
- tick(1000);
- expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(1);
- tick(5000);
- expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(2);
- tick(5000);
- expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(3);
- mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.COMPLETE;
- tick(5000);
- expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(4);
- tick(5000);
- expect(getFolderSizeInfoSpy).not.toHaveBeenCalledTimes(5);
- expect(fixture.componentInstance.folderDetails.numberOfFiles).toBe(1);
- }));
-
- it('should not make new API request, and display error message if response returned from API is neither COMPLETE, nor IN_PROGRESS', fakeAsync(() => {
- mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.NOT_INITIATED;
- getFolderSizeInfoSpy.and.returnValue(of(mockSizeDetailsEntry));
- fixture.detectChanges();
- mockSub.next({ entry: { jobId: 'mock-job-id' } });
- tick(1000);
- fixture.detectChanges();
- expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(1);
- fixture.detectChanges();
- expect(getNumberOfFiles()).toBe('APP.FOLDER_INFO.ERROR');
- expect(unitTestingUtils.getInnerTextByDataAutomationId('folder-info-size')).toBe('APP.FOLDER_INFO.ERROR');
- tick(5000);
- expect(getFolderSizeInfoSpy).not.toHaveBeenCalledTimes(2);
- }));
-});
diff --git a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.ts b/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.ts
deleted file mode 100644
index 59c0b29c1..000000000
--- a/projects/aca-content/src/lib/dialogs/folder-details/folder-information.component.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/*!
- * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
- *
- * Alfresco Example Content Application
- *
- * This file is part of the Alfresco Example Content Application.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Alfresco Example Content Application is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * from Hyland Software. If not, see .
- */
-
-import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { DIALOG_COMPONENT_DATA, IconComponent, LocalizedDatePipe, TimeAgoPipe } from '@alfresco/adf-core';
-import { Node, SizeDetails } from '@alfresco/js-api';
-import { MatDividerModule } from '@angular/material/divider';
-import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { ContentService, NodesApiService } from '@alfresco/adf-content-services';
-import { catchError, concatMap, delay, expand, first, switchMap } from 'rxjs/operators';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { EMPTY, of, timer } from 'rxjs';
-
-const MEMORY_UNIT_LIST = ['bytes', 'KB', 'MB', 'GB', 'TB'];
-
-class FolderDetails {
- name: string;
- size: string;
- location: string;
- created: Date;
- modified: Date;
- icon: string;
- numberOfFiles: string | number;
-}
-
-@Component({
- selector: 'app-folder-info',
- imports: [CommonModule, MatDividerModule, TimeAgoPipe, TranslatePipe, LocalizedDatePipe, IconComponent],
- templateUrl: './folder-information.component.html',
- styleUrls: ['./folder-information.component.scss'],
- encapsulation: ViewEncapsulation.None,
- host: { class: 'app-folder-info' }
-})
-export class FolderInformationComponent implements OnInit {
- readonly contentService = inject(ContentService);
- readonly nodesService = inject(NodesApiService);
- readonly translateService = inject(TranslateService);
-
- private readonly destroyRef = inject(DestroyRef);
-
- data: Node = inject(DIALOG_COMPONENT_DATA);
- folderDetails = new FolderDetails();
-
- ngOnInit() {
- this.folderDetails.name = this.data.name;
- this.folderDetails.location = this.data.path.name;
- this.folderDetails.created = this.data.createdAt;
- this.folderDetails.modified = this.data.modifiedAt;
- this.folderDetails.icon = this.contentService.getNodeIcon(this.data);
- this.folderDetails.size = this.translateService.instant('APP.FOLDER_INFO.CALCULATING');
- this.folderDetails.numberOfFiles = this.translateService.instant('APP.FOLDER_INFO.CALCULATING');
-
- this.nodesService
- .initiateFolderSizeCalculation(this.data.id)
- .pipe(
- first(),
- delay(1000),
- switchMap((jobIdEntry) => {
- return this.nodesService.getFolderSizeInfo(this.data.id, jobIdEntry.entry.jobId).pipe(
- expand((result) =>
- result.entry.status === SizeDetails.StatusEnum.IN_PROGRESS
- ? timer(5000).pipe(concatMap(() => this.nodesService.getFolderSizeInfo(this.data.id, jobIdEntry.entry.jobId)))
- : EMPTY
- ),
- catchError(() => {
- this.folderDetails.size = this.translateService.instant('APP.FOLDER_INFO.ERROR');
- return of(null);
- })
- );
- }),
- takeUntilDestroyed(this.destroyRef),
- catchError(() => {
- this.folderDetails.numberOfFiles = this.translateService.instant('APP.FOLDER_INFO.ERROR');
- this.folderDetails.size = this.translateService.instant('APP.FOLDER_INFO.ERROR');
- return of(null);
- })
- )
- .subscribe((folderInfo) => {
- if (folderInfo?.entry?.status === SizeDetails.StatusEnum.COMPLETE) {
- let size = parseFloat(folderInfo.entry.sizeInBytes);
- let unitIndex = 0;
- let isMoreThanBytes = false;
- while (size > 1000) {
- isMoreThanBytes = true;
- size = size / 1000;
- unitIndex++;
- }
- const params = {
- sizeInBytes: parseFloat(folderInfo.entry.sizeInBytes).toLocaleString('en'),
- sizeInLargeUnit: size.toFixed(2),
- unit: MEMORY_UNIT_LIST[unitIndex]
- };
- this.folderDetails.size = this.translateService.instant(
- isMoreThanBytes ? 'APP.FOLDER_INFO.CALCULATED_SIZE_LARGE' : 'APP.FOLDER_INFO.CALCULATED_SIZE_NORMAL',
- params
- );
- this.folderDetails.numberOfFiles = folderInfo.entry.numberOfFiles;
- } else if (folderInfo?.entry?.status !== SizeDetails.StatusEnum.IN_PROGRESS) {
- this.folderDetails.size = this.translateService.instant('APP.FOLDER_INFO.ERROR');
- this.folderDetails.numberOfFiles = this.translateService.instant('APP.FOLDER_INFO.ERROR');
- }
- });
- }
-}
diff --git a/projects/aca-content/src/lib/dialogs/node-details/node-information.component.html b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.html
new file mode 100644
index 000000000..36aaa1c2e
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.html
@@ -0,0 +1,66 @@
+
+
+
+ @if (node.isFolder) {
+
+
{{ 'APP.NODE_INFO.NUMBER_OF_FILES' | translate }}
+
{{ nodeDetails.numberOfFiles }}
+
+
+ }
+
+
{{ 'APP.NODE_INFO.SIZE' | translate }}
+
+ {{ nodeDetails.size }}
+
+
+
+
0}">
+
+
{{ 'APP.NODE_INFO.LOCATION' | translate }}
+
+
+
+ @if (nodeDetails.location) {
+ {{ nodeDetails.location }}
+ } @else {
+ {{ 'APP.BROWSE.SEARCH.UNKNOWN_LOCATION' | translate }}
+ }
+
+
+
+ @if (nodeDetails.secondaryParentsPaths?.length > 0) {
+
+
{{ 'APP.NODE_INFO.REFERENCED' | translate }}
+
+ @for (secondaryParentPath of nodeDetails.secondaryParentsPaths; track $index) {
+
+
+
+ {{ secondaryParentPath }}
+
+
+ }
+
+
+ }
+
+
+
+
{{ 'APP.NODE_INFO.CREATED' | translate }}
+
{{ nodeDetails.created | adfTimeAgo }}
+
+
+
+
{{ 'APP.NODE_INFO.MODIFIED' | translate }}
+
{{ nodeDetails.modified | adfTimeAgo }}
+
+
diff --git a/projects/aca-content/src/lib/dialogs/node-details/node-information.component.scss b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.scss
new file mode 100644
index 000000000..547ce8965
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.scss
@@ -0,0 +1,78 @@
+.app-node-info {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--theme-border-color);
+ border-radius: 12px;
+
+ .aca-node-icon {
+ display: flex;
+ flex-direction: column;
+ vertical-align: middle;
+ color: var(--theme-app-blue-folder);
+ }
+
+ .aca-node-info {
+ &-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ column-gap: 10px;
+ padding: 20px;
+
+ .aca-node-title {
+ flex: 1;
+ font-weight: bold;
+ }
+ }
+
+ &-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding: 10px 20px;
+
+ .aca-node-info-item {
+ display: flex;
+ flex-direction: row;
+ padding: 20px 0;
+
+ .aca-node-info-secondary-parents-container {
+ display: flex;
+ width: 70%;
+ flex-direction: column;
+ gap: 10px;
+
+ .aca-node-info-secondary-parent {
+ display: flex;
+ }
+ }
+
+ &-label {
+ width: 30%;
+ color: var(--theme-text-color);
+ }
+
+ &-value {
+ display: flex;
+ width: 70%;
+ }
+
+ .aca-node-info-location {
+ margin-left: 10px;
+ }
+ }
+
+ .aca-node-location-group {
+ .aca-node-info-item {
+ &:first-of-type {
+ padding-bottom: 10px;
+ }
+
+ &:last-of-type {
+ padding-top: 10px;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/projects/aca-content/src/lib/dialogs/node-details/node-information.component.spec.ts b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.spec.ts
new file mode 100644
index 000000000..b994785a3
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.spec.ts
@@ -0,0 +1,245 @@
+/*!
+ * 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, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { NodeInformationComponent } from './node-information.component';
+import { DIALOG_COMPONENT_DATA, RedirectAuthService, UnitTestingUtils } from '@alfresco/adf-core';
+import { NodesApiService } from '@alfresco/adf-content-services';
+import { EMPTY, Observable, of, Subject } from 'rxjs';
+import { LibTestingModule } from '@alfresco/aca-shared';
+import { JobIdBodyEntry, SizeDetails, SizeDetailsEntry, Node, NodeAssociationPaging, NodeAssociation } from '@alfresco/js-api';
+
+describe('NodeInformationComponent', () => {
+ let fixture: ComponentFixture;
+ let nodeService: NodesApiService;
+ let unitTestingUtils: UnitTestingUtils;
+ let initiateFolderSizeCalculationSpy: jasmine.Spy<(nodeId: string) => Observable>;
+ let getFolderSizeInfoSpy: jasmine.Spy<(nodeId: string, jobId: string) => Observable>;
+ let listParentsSpy: jasmine.Spy<(nodeId: string, opts: any) => Observable>;
+
+ const getNumberOfFiles = (): string => unitTestingUtils.getInnerTextByDataAutomationId('node-info-number-of-files');
+ const getNodeIcon = (): HTMLImageElement => unitTestingUtils.getByCSS('.aca-node-icon').nativeElement;
+ const getNodeName = (): string => unitTestingUtils.getInnerTextByDataAutomationId('node-info-name');
+ const getNodeSize = (): string => unitTestingUtils.getInnerTextByDataAutomationId('node-info-size');
+ const getNodeLocation = (): string => unitTestingUtils.getInnerTextByCSS('.aca-node-info-location');
+ const getNodeCreationDate = (): string => unitTestingUtils.getInnerTextByDataAutomationId('node-info-creation-date');
+ const getNodeModifyDate = (): string => unitTestingUtils.getInnerTextByDataAutomationId('node-info-modify-date');
+
+ const mockSub = new Subject();
+ const mockFolder = {
+ name: 'mock-folder',
+ id: 'mock-folder-id',
+ path: {
+ name: 'mock-folder-path'
+ },
+ isFolder: true,
+ createdAt: new Date(2024, 1, 1, 11, 11),
+ modifiedAt: new Date(2024, 2, 2, 22, 22)
+ } as Node;
+ const mockFile = {
+ name: 'mock-file',
+ id: 'mock-file-id',
+ path: {
+ name: 'mock-file-path'
+ },
+ isFolder: false,
+ isFile: true,
+ content: {
+ mimeType: 'text/plain',
+ sizeInBytes: 1000
+ },
+ createdAt: new Date(2024, 1, 1, 11, 11),
+ modifiedAt: new Date(2024, 2, 2, 22, 22)
+ } as Node;
+ const mockSizeDetailsEntry: SizeDetailsEntry = {
+ entry: {
+ id: 'mock-id',
+ sizeInBytes: '1',
+ calculatedAt: 'mock-date',
+ numberOfFiles: 1,
+ status: SizeDetails.StatusEnum.COMPLETE,
+ jobId: 'mock-job-id'
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [NodeInformationComponent, LibTestingModule],
+ providers: [{ provide: RedirectAuthService, useValue: { onLogin: EMPTY, onTokenReceived: EMPTY } }]
+ });
+ });
+
+ describe('folder node', () => {
+ beforeEach(() => {
+ TestBed.overrideProvider(DIALOG_COMPONENT_DATA, { useValue: mockFolder });
+ fixture = TestBed.createComponent(NodeInformationComponent);
+ nodeService = TestBed.inject(NodesApiService);
+ unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
+ initiateFolderSizeCalculationSpy = spyOn(nodeService, 'initiateFolderSizeCalculation').and.returnValue(mockSub.asObservable());
+ getFolderSizeInfoSpy = spyOn(nodeService, 'getFolderSizeInfo').and.returnValue(EMPTY);
+ listParentsSpy = spyOn(nodeService, 'listParents').and.returnValue(of({ list: { entries: [] } } as NodeAssociationPaging));
+ });
+
+ it('should display folder icon for folders', () => {
+ fixture.detectChanges();
+ const icon: HTMLImageElement = getNodeIcon();
+
+ expect(icon.alt).toBe('APP.NODE_INFO.ICON');
+ expect(icon.src).toContain('ft_ic_folder.svg');
+ });
+
+ it('should render all information in init', () => {
+ fixture.detectChanges();
+ expect(getNodeName()).toBe('mock-folder');
+ expect(getNumberOfFiles()).toBe('APP.NODE_INFO.CALCULATING');
+ expect(getNodeSize()).toBe('APP.NODE_INFO.CALCULATING');
+ expect(getNodeLocation()).toBe('mock-folder-path');
+ expect(getNodeCreationDate()).toBe('01/02/2024 11:11');
+ expect(getNodeModifyDate()).toBe('02/03/2024 22:22');
+ });
+
+ it('should make API call on init to start folder size calculation', () => {
+ fixture.detectChanges();
+ expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
+ });
+
+ it('should fetch folder size only when the initial folder size calculation request is completed and one second has passed', fakeAsync(() => {
+ fixture.detectChanges();
+ expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
+ expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
+ mockSub.next({ entry: { jobId: 'mock-job-id' } });
+ expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
+ tick(1000);
+ expect(getFolderSizeInfoSpy).toHaveBeenCalled();
+ }));
+
+ it('should make repeated calls to get folder size info, if the response returned from the API is IN_PROGRESS', fakeAsync(() => {
+ mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.IN_PROGRESS;
+ getFolderSizeInfoSpy.and.returnValue(of(mockSizeDetailsEntry));
+ fixture.detectChanges();
+ expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
+ mockSub.next({ entry: { jobId: 'mock-job-id' } });
+ tick(1000);
+ expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(1);
+ tick(5000);
+ expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(2);
+ tick(5000);
+ expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(3);
+ mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.COMPLETE;
+ tick(5000);
+ expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(4);
+ tick(5000);
+ expect(getFolderSizeInfoSpy).not.toHaveBeenCalledTimes(5);
+ expect(fixture.componentInstance.nodeDetails.numberOfFiles).toBe(1);
+ }));
+
+ it('should not make new API request, and display error message if response returned from API is neither COMPLETE, nor IN_PROGRESS', fakeAsync(() => {
+ mockSizeDetailsEntry.entry.status = SizeDetails.StatusEnum.NOT_INITIATED;
+ getFolderSizeInfoSpy.and.returnValue(of(mockSizeDetailsEntry));
+ fixture.detectChanges();
+ mockSub.next({ entry: { jobId: 'mock-job-id' } });
+ tick(1000);
+ fixture.detectChanges();
+ expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(1);
+ fixture.detectChanges();
+ expect(getNumberOfFiles()).toBe('APP.NODE_INFO.ERROR');
+ expect(getNodeSize()).toBe('APP.NODE_INFO.ERROR');
+ tick(5000);
+ expect(getFolderSizeInfoSpy).not.toHaveBeenCalledTimes(2);
+ }));
+ });
+
+ describe('file node', () => {
+ const secondaryParent: NodeAssociation = {
+ id: 'mock-secondary-parent-id',
+ name: 'mock-secondary-parent',
+ nodeType: 'cm:folder',
+ isFolder: true,
+ isFile: false,
+ path: { name: 'mock-secondary-parent-path' },
+ modifiedAt: new Date(),
+ modifiedByUser: { id: 'mock-user-id', displayName: 'mock-user' },
+ createdAt: new Date(),
+ createdByUser: { id: 'mock-user-id', displayName: 'mock-user' }
+ };
+
+ const otherSecondaryParent: NodeAssociation = {
+ id: 'mock-other-secondary-parent-id',
+ name: 'mock-other-secondary-parent',
+ nodeType: 'cm:folder',
+ isFolder: true,
+ isFile: false,
+ path: { name: 'mock-other-secondary-parent-path' },
+ modifiedAt: new Date(),
+ modifiedByUser: { id: 'mock-user-id', displayName: 'mock-user' },
+ createdAt: new Date(),
+ createdByUser: { id: 'mock-user-id', displayName: 'mock-user' }
+ };
+
+ beforeEach(() => {
+ TestBed.overrideProvider(DIALOG_COMPONENT_DATA, { useValue: mockFile });
+ fixture = TestBed.createComponent(NodeInformationComponent);
+ nodeService = TestBed.inject(NodesApiService);
+ unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
+ listParentsSpy = spyOn(nodeService, 'listParents').and.returnValue(
+ of({ list: { entries: [{ entry: secondaryParent }, { entry: otherSecondaryParent }] } } as NodeAssociationPaging)
+ );
+ });
+
+ it('should display file icon based on the mime type', () => {
+ fixture.detectChanges();
+ const icon: HTMLImageElement = getNodeIcon();
+
+ expect(icon.alt).toBe('APP.NODE_INFO.ICON');
+ expect(icon.src).toContain('ft_ic_document.svg');
+ });
+
+ it('should render all information in init', () => {
+ fixture.detectChanges();
+ expect(getNodeName()).toBe('mock-file');
+ expect(getNodeSize()).toBe('1000 CORE.FILE_SIZE.BYTES');
+ expect(getNodeLocation()).toBe('mock-file-path');
+ expect(getNodeCreationDate()).toBe('01/02/2024 11:11');
+ expect(getNodeModifyDate()).toBe('02/03/2024 22:22');
+ });
+
+ it('should call API to fetch secondary parent paths', () => {
+ fixture.detectChanges();
+ expect(listParentsSpy).toHaveBeenCalledWith('mock-file-id', { where: `(isPrimary=false and assocType='cm:contains')`, include: ['path'] });
+ });
+
+ it('should display secondary parent paths if there are any', () => {
+ fixture.detectChanges();
+
+ const locationLabels = unitTestingUtils.getAllByCSS('.aca-node-location-group .aca-node-info-item-label');
+ const secondaryPaths = unitTestingUtils.getAllByCSS('.aca-node-location-group .aca-node-info-secondary-parent .aca-node-info-location');
+ expect(locationLabels.length).toBe(2);
+ expect(secondaryPaths.length).toBe(2);
+ expect(locationLabels[0].nativeElement.innerText).toBe('APP.NODE_INFO.LOCATION');
+ expect(locationLabels[1].nativeElement.innerText).toBe('APP.NODE_INFO.REFERENCED');
+ expect(secondaryPaths[0].nativeElement.innerText).toBe('mock-secondary-parent-path');
+ expect(secondaryPaths[1].nativeElement.innerText).toBe('mock-other-secondary-parent-path');
+ });
+ });
+});
diff --git a/projects/aca-content/src/lib/dialogs/node-details/node-information.component.ts b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.ts
new file mode 100644
index 000000000..d9a5286ca
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-details/node-information.component.ts
@@ -0,0 +1,123 @@
+/*!
+ * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
+ *
+ * Alfresco Example Content Application
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * from Hyland Software. If not, see .
+ */
+
+import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
+import { CommonModule, NgOptimizedImage } from '@angular/common';
+import { DIALOG_COMPONENT_DATA, LocalizedDatePipe, TimeAgoPipe, IconComponent, FileSizePipe, TranslationService } from '@alfresco/adf-core';
+import { Node, SizeDetails } from '@alfresco/js-api';
+import { MatDividerModule } from '@angular/material/divider';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ContentService, NodesApiService } from '@alfresco/adf-content-services';
+import { catchError, concatMap, delay, expand, first, switchMap, take } from 'rxjs/operators';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { EMPTY, of, timer } from 'rxjs';
+
+class NodeDetails {
+ name: string;
+ size: string;
+ location: string;
+ secondaryParentsPaths: string[];
+ created: Date;
+ modified: Date;
+ icon: string;
+ numberOfFiles: string | number;
+}
+
+@Component({
+ selector: 'app-node-info',
+ imports: [CommonModule, MatDividerModule, TimeAgoPipe, TranslatePipe, LocalizedDatePipe, IconComponent, NgOptimizedImage],
+ templateUrl: './node-information.component.html',
+ styleUrls: ['./node-information.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'app-node-info' }
+})
+export class NodeInformationComponent implements OnInit {
+ readonly contentService = inject(ContentService);
+ readonly nodesService = inject(NodesApiService);
+ readonly translateService = inject(TranslateService);
+ readonly translationService = inject(TranslationService);
+
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly fileSizePipe = new FileSizePipe(this.translationService);
+
+ node: Node = inject(DIALOG_COMPONENT_DATA);
+ nodeDetails = new NodeDetails();
+
+ ngOnInit() {
+ this.nodeDetails.name = this.node.name;
+ this.nodeDetails.location = this.node.path.name;
+ this.nodeDetails.created = this.node.createdAt;
+ this.nodeDetails.modified = this.node.modifiedAt;
+ this.nodeDetails.icon = this.contentService.getNodeIcon(this.node);
+ this.nodesService
+ .listParents(this.node.id, { where: `(isPrimary=false and assocType='cm:contains')`, include: ['path'] })
+ .pipe(
+ take(1),
+ catchError(() => of(null))
+ )
+ .subscribe((parents) => {
+ this.nodeDetails.secondaryParentsPaths = parents != null ? parents.list.entries.map((entry) => entry.entry.path.name) : [];
+ });
+ if (this.node.isFolder) {
+ this.nodeDetails.size = this.translateService.instant('APP.NODE_INFO.CALCULATING');
+ this.nodeDetails.numberOfFiles = this.translateService.instant('APP.NODE_INFO.CALCULATING');
+ this.nodesService
+ .initiateFolderSizeCalculation(this.node.id)
+ .pipe(
+ first(),
+ delay(1000),
+ switchMap((jobIdEntry) => {
+ return this.nodesService.getFolderSizeInfo(this.node.id, jobIdEntry.entry.jobId).pipe(
+ expand((result) =>
+ result.entry.status === SizeDetails.StatusEnum.IN_PROGRESS
+ ? timer(5000).pipe(concatMap(() => this.nodesService.getFolderSizeInfo(this.node.id, jobIdEntry.entry.jobId)))
+ : EMPTY
+ ),
+ catchError(() => {
+ this.nodeDetails.size = this.translateService.instant('APP.NODE_INFO.ERROR');
+ return of(null);
+ })
+ );
+ }),
+ takeUntilDestroyed(this.destroyRef),
+ catchError(() => {
+ this.nodeDetails.numberOfFiles = this.translateService.instant('APP.NODE_INFO.ERROR');
+ this.nodeDetails.size = this.translateService.instant('APP.NODE_INFO.ERROR');
+ return of(null);
+ })
+ )
+ .subscribe((folderInfo) => {
+ if (folderInfo?.entry?.status === SizeDetails.StatusEnum.COMPLETE) {
+ this.nodeDetails.size = this.fileSizePipe.transform(folderInfo.entry.sizeInBytes);
+ this.nodeDetails.numberOfFiles = folderInfo.entry.numberOfFiles;
+ } else if (folderInfo?.entry?.status !== SizeDetails.StatusEnum.IN_PROGRESS) {
+ this.nodeDetails.size = this.translateService.instant('APP.NODE_INFO.ERROR');
+ this.nodeDetails.numberOfFiles = this.translateService.instant('APP.NODE_INFO.ERROR');
+ }
+ });
+ } else {
+ this.nodeDetails.size = this.fileSizePipe.transform(this.node.content.sizeInBytes);
+ }
+ }
+}
diff --git a/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.html b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.html
new file mode 100644
index 000000000..c8215cf9d
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.html
@@ -0,0 +1,22 @@
+@if (additionalReferenceLocations.length > 0) {
+
+ {{ 'APP.ADDITIONAL_REFERENCES_DIALOG.DESCRIPTION' | translate : { name: node.name} }}
+
+ @for (location of additionalReferenceLocations; track $index) {
+
+ }
+} @else {
+
+ {{ 'APP.ADDITIONAL_REFERENCES_DIALOG.NO_LOCATIONS' | translate : { name: node.name } }}
+
+}
diff --git a/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.scss b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.scss
new file mode 100644
index 000000000..85c61f99d
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.scss
@@ -0,0 +1,27 @@
+.app-node-location-reference {
+ display: flex;
+ margin: 15px 0;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ &-link {
+ color: var(--theme-foreground-text-color);
+ }
+
+ &-title {
+ font-size: medium;
+ font-weight: 200;
+ margin-bottom: 20px;
+ }
+
+ &-no-locations {
+ margin-bottom: 1px;
+ }
+
+ &-icon {
+ color: var(--theme-app-blue-folder);
+ margin-right: 15px;
+ }
+}
diff --git a/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.spec.ts b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.spec.ts
new file mode 100644
index 000000000..38b8a5f50
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.spec.ts
@@ -0,0 +1,285 @@
+/*!
+ * 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 { DIALOG_COMPONENT_DATA, UnitTestingUtils } from '@alfresco/adf-core';
+import { NodesApiService } from '@alfresco/adf-content-services';
+import { LibTestingModule } from '@alfresco/aca-shared';
+import { Node, NodeAssociation } from '@alfresco/js-api';
+import { NodeLocationReferencesComponent } from './node-location-references.component';
+import { of, throwError } from 'rxjs';
+import { Router } from '@angular/router';
+
+describe('NodeLocationReferencesComponent', () => {
+ let fixture: ComponentFixture;
+ let nodeService: NodesApiService;
+ let unitTestingUtils: UnitTestingUtils;
+ let router: Router;
+
+ const checkNavigationUrl = (event: Event, node: NodeAssociation, expectedUrl: string): void => {
+ spyOn(router, 'parseUrl').and.returnValue({ root: { children: [] } } as any);
+ spyOn(router, 'serializeUrl');
+ spyOn(window, 'open');
+
+ fixture.componentInstance.goToLocation(event, node);
+
+ expect(router.parseUrl).toHaveBeenCalledWith(expectedUrl);
+ };
+
+ const getNoLocationsMessage = (): string => unitTestingUtils.getInnerTextByCSS('.app-node-location-reference-no-locations');
+
+ const verifyIfNavigationHappened = (event: Event, node: NodeAssociation): void => {
+ spyOn(window, 'open');
+ fixture.componentInstance.goToLocation(event, node);
+ expect(window.open).not.toHaveBeenCalled();
+ };
+
+ const mockNode = {
+ name: 'mock-file',
+ id: 'mock-file-id',
+ path: {
+ name: 'mock-file-path'
+ },
+ isFolder: false,
+ isFile: true,
+ content: {
+ mimeType: 'text/plain',
+ sizeInBytes: 1000
+ },
+ createdAt: new Date(2024, 1, 1, 11, 11),
+ modifiedAt: new Date(2024, 2, 2, 22, 22)
+ } as Node;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [NodeLocationReferencesComponent, LibTestingModule],
+ providers: [{ provide: DIALOG_COMPONENT_DATA, useValue: mockNode }]
+ });
+
+ fixture = TestBed.createComponent(NodeLocationReferencesComponent);
+ nodeService = TestBed.inject(NodesApiService);
+ router = TestBed.inject(Router);
+ unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
+ });
+
+ it('should display proper message when there are no additional reference locations', () => {
+ spyOn(nodeService, 'listParents').and.returnValue(of({ list: { entries: [] } }));
+ fixture.detectChanges();
+
+ expect(getNoLocationsMessage()).toBe('APP.ADDITIONAL_REFERENCES_DIALOG.NO_LOCATIONS');
+ });
+
+ it('should display proper message when list parents api fails', () => {
+ spyOn(nodeService, 'listParents').and.returnValue(throwError(() => new Error('API Error')));
+ fixture.detectChanges();
+
+ expect(getNoLocationsMessage()).toBe('APP.ADDITIONAL_REFERENCES_DIALOG.NO_LOCATIONS');
+ });
+
+ describe('Additional reference locations', () => {
+ let additionalLocation1: NodeAssociation;
+ let additionalLocation2: NodeAssociation;
+ const mockEvent = jasmine.createSpyObj('MouseEvent', ['preventDefault']);
+
+ beforeEach(() => {
+ additionalLocation1 = {
+ name: 'additional-location-1',
+ id: 'additional-location-1-id',
+ nodeType: 'cm:folder',
+ isFolder: true,
+ isFile: false,
+ modifiedAt: new Date(2024, 3, 3, 15, 15),
+ modifiedByUser: {
+ id: 'user-id',
+ displayName: 'User Name'
+ },
+ createdAt: new Date(2024, 3, 3, 14, 14),
+ createdByUser: {
+ id: 'user-id',
+ displayName: 'User Name'
+ },
+ path: {
+ name: 'additional-location-1-path',
+ elements: [{ name: 'Company Home' }, { name: 'Sites' }, { name: 'My Site' }]
+ }
+ };
+ additionalLocation2 = {
+ name: 'additional-location-2',
+ id: 'additional-location-2-id',
+ nodeType: 'cm:file',
+ isFolder: false,
+ isFile: true,
+ modifiedAt: new Date(2024, 3, 3, 15, 15),
+ modifiedByUser: {
+ id: 'user-id',
+ displayName: 'User Name'
+ },
+ createdAt: new Date(2024, 3, 3, 14, 14),
+ createdByUser: {
+ id: 'user-id',
+ displayName: 'User Name'
+ },
+ path: {
+ name: 'additional-location-2-path',
+ elements: [{ name: 'Company Home' }, { name: 'Sites' }, { name: 'My Site 2' }]
+ }
+ };
+ spyOn(nodeService, 'listParents').and.returnValue(of({ list: { entries: [{ entry: additionalLocation1 }, { entry: additionalLocation2 }] } }));
+ fixture.detectChanges();
+ });
+
+ it('should display proper title for additional reference locations', () => {
+ expect(unitTestingUtils.getInnerTextByCSS('.app-node-location-reference-title')).toBe('APP.ADDITIONAL_REFERENCES_DIALOG.DESCRIPTION');
+ });
+
+ it('should display folder icon and correct link for additional reference location', () => {
+ const link: HTMLAnchorElement = unitTestingUtils.getByCSS('.app-node-location-reference-link').nativeElement;
+
+ expect(unitTestingUtils.getInnerTextByCSS('.app-node-location-reference-icon')).toBe('folder');
+ expect(link.getAttribute('title')).toBe('additional-location-1-path');
+ expect(link.innerText).toBe('additional-location-1-path');
+ });
+
+ it('should prevent default event behavior when navigating to a location', () => {
+ fixture.componentInstance.goToLocation(mockEvent, additionalLocation1);
+
+ expect(mockEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should navigate to location when reference link is clicked', () => {
+ spyOn(router, 'parseUrl').and.returnValue({ root: { children: [] } } as any);
+ spyOn(router, 'serializeUrl').and.returnValue('#/libraries/additional-location-1-id');
+ spyOn(window, 'open');
+ spyOn(fixture.componentInstance, 'goToLocation').and.callThrough();
+
+ unitTestingUtils.clickByCSS('.app-node-location-reference-link');
+
+ expect(fixture.componentInstance.goToLocation).toHaveBeenCalledWith(jasmine.any(MouseEvent), additionalLocation1);
+ expect(router.parseUrl).toHaveBeenCalledWith('#/libraries/additional-location-1-id');
+ expect(router.serializeUrl).toHaveBeenCalled();
+ expect(window.open).toHaveBeenCalledWith('#/libraries/additional-location-1-id', '_blank');
+ });
+
+ it('should navigate to libraries area when path has Sites as second element', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '/Company Home/Sites/My Site',
+ elements: [{ name: 'Company Home' }, { name: 'Sites' }, { name: 'My Site' }]
+ }
+ } as NodeAssociation;
+
+ checkNavigationUrl(mockEvent, node, '#/libraries/test-node-id');
+ });
+
+ it('should navigate to libraries area when path has more than 2 elements with Sites as second', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '/Company Home/Sites/My Site/Folder1',
+ elements: [{ name: 'Company Home' }, { name: 'Sites' }, { name: 'My Site' }, { name: 'Folder1' }]
+ }
+ } as NodeAssociation;
+
+ checkNavigationUrl(mockEvent, node, '#/libraries/test-node-id');
+ });
+
+ it('should navigate to personal-files area when path has User Homes as second element', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '/Company Home/User Homes/user123',
+ elements: [{ name: 'Company Home' }, { name: 'User Homes' }, { name: 'user123' }]
+ }
+ } as NodeAssociation;
+
+ checkNavigationUrl(mockEvent, node, '#/personal-files/test-node-id');
+ });
+
+ it('should navigate to personal-files area when path has only one element', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '/Company Home',
+ elements: [{ name: 'Company Home' }]
+ }
+ } as NodeAssociation;
+
+ checkNavigationUrl(mockEvent, node, '#/personal-files/test-node-id');
+ });
+
+ it('should not open window when path is undefined', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: undefined
+ } as NodeAssociation;
+
+ verifyIfNavigationHappened(mockEvent, node);
+ });
+
+ it('should not open window when path.name is missing', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: undefined,
+ elements: [{ name: 'Company Home' }]
+ }
+ } as NodeAssociation;
+
+ verifyIfNavigationHappened(mockEvent, node);
+ });
+
+ it('should not open window when path.elements is missing', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '/Company Home',
+ elements: undefined
+ }
+ } as NodeAssociation;
+
+ verifyIfNavigationHappened(mockEvent, node);
+ });
+
+ it('should not open window when path.name is empty string', () => {
+ const node: NodeAssociation = {
+ id: 'test-node-id',
+ name: 'Test Node',
+ path: {
+ name: '',
+ elements: [{ name: 'Company Home' }]
+ }
+ } as NodeAssociation;
+
+ verifyIfNavigationHappened(mockEvent, node);
+ });
+ });
+});
diff --git a/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.ts b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.ts
new file mode 100644
index 000000000..bda60b61e
--- /dev/null
+++ b/projects/aca-content/src/lib/dialogs/node-location-references/node-location-references.component.ts
@@ -0,0 +1,72 @@
+/*!
+ * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
+ *
+ * Alfresco Example Content Application
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * from Hyland Software. If not, see .
+ */
+
+import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { NodesApiService } from '@alfresco/adf-content-services';
+import { DIALOG_COMPONENT_DATA, IconComponent } from '@alfresco/adf-core';
+import { Node, NodeAssociation } from '@alfresco/js-api';
+import { Router } from '@angular/router';
+import { catchError, of, take } from 'rxjs';
+import { TranslatePipe } from '@ngx-translate/core';
+
+@Component({
+ selector: 'app-node-location-references',
+ imports: [CommonModule, IconComponent, TranslatePipe],
+ templateUrl: './node-location-references.component.html',
+ styleUrls: ['./node-location-references.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class NodeLocationReferencesComponent implements OnInit {
+ node: Node = inject(DIALOG_COMPONENT_DATA);
+ additionalReferenceLocations: NodeAssociation[] = [];
+
+ private readonly nodesService = inject(NodesApiService);
+ private readonly router = inject(Router);
+
+ ngOnInit(): void {
+ this.nodesService
+ .listParents(this.node.id, { where: `(isPrimary=false and assocType='cm:contains')`, include: ['path'] })
+ .pipe(
+ take(1),
+ catchError(() => of({ list: { entries: [] } }))
+ )
+ .subscribe((response) => {
+ this.additionalReferenceLocations = response.list.entries.map((entry) => entry.entry);
+ });
+ }
+
+ goToLocation($event: Event, node: NodeAssociation) {
+ $event.preventDefault();
+ let link: string[] = [];
+ const { path } = node;
+
+ if (path?.name && path?.elements) {
+ const area = path && path.elements.length >= 2 && path.elements[1].name === 'Sites' ? 'libraries' : 'personal-files';
+ link = ['#', area, node.id];
+ const url = this.router.serializeUrl(this.router.parseUrl(link.join('/')));
+ window.open(url, '_blank');
+ }
+ }
+}
diff --git a/projects/aca-content/src/lib/services/content-management.service.spec.ts b/projects/aca-content/src/lib/services/content-management.service.spec.ts
index f632934c2..7d75bf3c3 100644
--- a/projects/aca-content/src/lib/services/content-management.service.spec.ts
+++ b/projects/aca-content/src/lib/services/content-management.service.spec.ts
@@ -60,7 +60,7 @@ import {
NodesApiService,
ViewVersion
} from '@alfresco/adf-content-services';
-import { FolderInformationComponent } from '../dialogs/folder-details/folder-information.component';
+import { NodeInformationComponent } from '../dialogs/node-details/node-information.component';
import { provideEffects } from '@ngrx/effects';
import { ActivatedRoute, Router } from '@angular/router';
import { EventEmitter } from '@angular/core';
@@ -1973,14 +1973,14 @@ describe('ContentManagementService', () => {
});
});
- describe('folderInformationDialog', () => {
- it('should open folder information dialog', () => {
+ describe('nodeInformationDialog', () => {
+ it('should open node information dialog', () => {
spyOn(dialog, 'open');
const fakeNode: NodeEntry = {
entry: {
- id: 'folder-node-id',
- name: 'mock-folder-name',
+ id: 'node-id',
+ name: 'mock-node-name',
nodeType: 'fake-node-type',
isFolder: true,
isFile: false,
@@ -1991,15 +1991,15 @@ describe('ContentManagementService', () => {
}
};
- contentManagementService.showFolderInformation(fakeNode);
+ contentManagementService.showNodeInformation(fakeNode);
expect(dialog.open).toHaveBeenCalledWith(DialogComponent, {
data: {
- title: 'APP.FOLDER_INFO.TITLE',
- confirmButtonTitle: 'APP.FOLDER_INFO.DONE',
+ title: 'APP.NODE_INFO.TITLE',
+ confirmButtonTitle: 'APP.NODE_INFO.DONE',
isCancelButtonHidden: true,
isCloseButtonHidden: false,
dialogSize: DialogSize.Large,
- contentComponent: FolderInformationComponent,
+ contentComponent: NodeInformationComponent,
componentData: fakeNode.entry
},
width: '700px'
diff --git a/projects/aca-content/src/lib/services/content-management.service.ts b/projects/aca-content/src/lib/services/content-management.service.ts
index 08ec47323..e0e4c782c 100644
--- a/projects/aca-content/src/lib/services/content-management.service.ts
+++ b/projects/aca-content/src/lib/services/content-management.service.ts
@@ -57,7 +57,7 @@ import { forkJoin, Observable, of, zip } from 'rxjs';
import { catchError, map, mergeMap, take, tap } from 'rxjs/operators';
import { NodeActionsService } from './node-actions.service';
import { ActivatedRoute, Router } from '@angular/router';
-import { FolderInformationComponent } from '../dialogs/folder-details/folder-information.component';
+import { NodeInformationComponent } from '../dialogs/node-details/node-information.component';
interface RestoredNode {
status: number;
@@ -1168,15 +1168,15 @@ export class ContentManagementService {
}
}
- showFolderInformation(node: NodeEntry) {
+ showNodeInformation(node: NodeEntry) {
this.dialogRef.open(DialogComponent, {
data: {
- title: 'APP.FOLDER_INFO.TITLE',
- confirmButtonTitle: 'APP.FOLDER_INFO.DONE',
+ title: 'APP.NODE_INFO.TITLE',
+ confirmButtonTitle: 'APP.NODE_INFO.DONE',
isCancelButtonHidden: true,
isCloseButtonHidden: false,
dialogSize: DialogSize.Large,
- contentComponent: FolderInformationComponent,
+ contentComponent: NodeInformationComponent,
componentData: node.entry
},
width: '700px'
diff --git a/projects/aca-content/src/lib/store/effects/node.effects.spec.ts b/projects/aca-content/src/lib/store/effects/node.effects.spec.ts
index bcb45182d..267eae99b 100644
--- a/projects/aca-content/src/lib/store/effects/node.effects.spec.ts
+++ b/projects/aca-content/src/lib/store/effects/node.effects.spec.ts
@@ -34,7 +34,7 @@ import {
DeleteNodesAction,
EditFolderAction,
ExpandInfoDrawerAction,
- FolderInformationAction,
+ NodeInformationAction,
FullscreenViewerAction,
ManageAspectsAction,
ManagePermissionsAction,
@@ -569,8 +569,8 @@ describe('NodeEffects', () => {
});
});
- describe('folderInformation$', () => {
- it('should call folder information dialog', () => {
+ describe('nodeInformation$', () => {
+ it('should call node information dialog', () => {
const node: NodeEntry = {
entry: {
id: 'folder-node-id',
@@ -584,16 +584,15 @@ describe('NodeEffects', () => {
createdByUser: new UserInfo()
}
};
- spyOn(contentService, 'showFolderInformation').and.stub();
+ spyOn(contentService, 'showNodeInformation').and.stub();
- store.dispatch(new FolderInformationAction(node));
+ store.dispatch(new NodeInformationAction(node));
- expect(contentService.showFolderInformation).toHaveBeenCalledWith(node);
+ expect(contentService.showNodeInformation).toHaveBeenCalledWith(node);
});
- it('should call folder information dialog from the active folder selection', fakeAsync(() => {
- spyOn(contentService, 'showFolderInformation').and.stub();
-
+ it('should call node information dialog from the active selection', fakeAsync(() => {
+ spyOn(contentService, 'showNodeInformation').and.stub();
const node: NodeEntry = {
entry: {
id: 'folder-node-id',
@@ -611,9 +610,9 @@ describe('NodeEffects', () => {
tick(100);
- store.dispatch(new FolderInformationAction(null));
+ store.dispatch(new NodeInformationAction(null));
- expect(contentService.showFolderInformation).toHaveBeenCalledWith(node);
+ expect(contentService.showNodeInformation).toHaveBeenCalledWith(node);
}));
});
});
diff --git a/projects/aca-content/src/lib/store/effects/node.effects.ts b/projects/aca-content/src/lib/store/effects/node.effects.ts
index 649f048bf..38ed0e71c 100644
--- a/projects/aca-content/src/lib/store/effects/node.effects.ts
+++ b/projects/aca-content/src/lib/store/effects/node.effects.ts
@@ -52,7 +52,7 @@ import {
UndoDeleteNodesAction,
UnlockWriteAction,
UnshareNodesAction,
- FolderInformationAction
+ NodeInformationAction
} from '@alfresco/aca-shared/store';
import { ContentManagementService } from '../../services/content-management.service';
import { RenditionService } from '@alfresco/adf-content-services';
@@ -462,20 +462,20 @@ export class NodeEffects {
{ dispatch: false }
);
- folderInformation$ = createEffect(
+ nodeInformation$ = createEffect(
() =>
this.actions$.pipe(
- ofType(NodeActionTypes.FolderInformation),
+ ofType(NodeActionTypes.NodeInformation),
map((action) => {
if (action?.payload) {
- this.contentService.showFolderInformation(action.payload);
+ this.contentService.showNodeInformation(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
- if (selection && !selection.isEmpty && selection.folder.entry) {
- this.contentService.showFolderInformation(selection.folder);
+ if (selection && !selection.isEmpty) {
+ this.contentService.showNodeInformation(selection.nodes[0]);
}
});
}
diff --git a/projects/aca-content/src/lib/ui/variables/variables.scss b/projects/aca-content/src/lib/ui/variables/variables.scss
index 6584ada98..d4af1824a 100644
--- a/projects/aca-content/src/lib/ui/variables/variables.scss
+++ b/projects/aca-content/src/lib/ui/variables/variables.scss
@@ -54,6 +54,7 @@ $light-grey-1: #d5d5d5;
$light-grey-2: #d9d9d9;
$light-grey-3: #dedede;
$user-profile-background: #595959;
+$app-blue-folder: #2a7de1;
// CSS Variables
$defaults: (
@@ -110,7 +111,8 @@ $defaults: (
--theme-light-grey-1-color: $light-grey-1,
--theme-light-grey-2-color: $light-grey-2,
--theme-light-grey-3-color: $light-grey-3,
- --theme-user-profile-background-color: $user-profile-background
+ --theme-user-profile-background-color: $user-profile-background,
+ --theme-app-blue-folder: $app-blue-folder
);
// propagates SCSS variables into the CSS variables scope
diff --git a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
index eb2a81a60..48e9c29ae 100644
--- a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
+++ b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
@@ -253,7 +253,7 @@ export class DataTableComponent extends BaseComponent {
}
async getItemLocationText(name: string): Promise {
- await this.getItemLocationEl(name).locator('a').waitFor({ state: 'attached' });
+ await this.getItemLocationEl(name).waitFor({ state: 'attached' });
return this.getItemLocationEl(name).innerText();
}
diff --git a/projects/aca-playwright-shared/src/page-objects/components/dialogs/folder-information-dialog.component.ts b/projects/aca-playwright-shared/src/page-objects/components/dialogs/folder-information-dialog.component.ts
index 3b0235486..144d21e7f 100644
--- a/projects/aca-playwright-shared/src/page-objects/components/dialogs/folder-information-dialog.component.ts
+++ b/projects/aca-playwright-shared/src/page-objects/components/dialogs/folder-information-dialog.component.ts
@@ -32,19 +32,19 @@ export class FolderInformationDialogComponent extends BaseComponent {
super(page, FolderInformationDialogComponent.rootElement);
}
- folderName = this.getChild('.aca-folder-info-header');
+ folderName = this.getChild('.aca-node-info-header');
doneButton = this.getChild('[data-automation-id="adf-dialog-actions-confirm"]');
- folderNumberOfFiles = this.getChild('[data-automation-id="folder-info-number-of-files"]');
- folderSize = this.getChild('[data-automation-id="folder-info-size"]');
- folderLocation = this.getChild('[data-automation-id="folder-info-location"]');
- folderCreationDate = this.getChild('[data-automation-id="folder-info-creation-date"]');
- folderModifiedDate = this.getChild('[data-automation-id="folder-info-modify-date"]');
+ folderNumberOfFiles = this.getChild('[data-automation-id="node-info-number-of-files"]');
+ folderSize = this.getChild('[data-automation-id="node-info-size"]');
+ folderLocation = this.getChild('[data-automation-id="node-info-location"]');
+ folderCreationDate = this.getChild('[data-automation-id="node-info-creation-date"]');
+ folderModifiedDate = this.getChild('[data-automation-id="node-info-modify-date"]');
async getFolderSizeNumber(): Promise {
const textContent = await this.folderSize.textContent();
if (!textContent) {
throw new Error('Folder size text content is null or undefined');
}
- return parseInt(textContent.split(' ')[0].replace(/,/g, ''), 10);
+ return parseFloat(textContent.trim().split(/\s+/)[0].replace(/,/g, ''));
}
}
diff --git a/projects/aca-playwright-shared/src/page-objects/components/search/search-filters/search-filters-date.component.ts b/projects/aca-playwright-shared/src/page-objects/components/search/search-filters/search-filters-date.component.ts
index 86e9b03e9..ae0ea52b9 100644
--- a/projects/aca-playwright-shared/src/page-objects/components/search/search-filters/search-filters-date.component.ts
+++ b/projects/aca-playwright-shared/src/page-objects/components/search/search-filters/search-filters-date.component.ts
@@ -23,23 +23,11 @@
*/
import { BaseComponent } from '../../base.component';
-import { Page, expect } from '@playwright/test';
-import { SearchPage, SearchType } from '../../../pages';
+import { Page } from '@playwright/test';
+import { SearchPage } from '../../../pages';
type FilterTab = 'Created' | 'Modified';
-interface FilterFilesByDateParams {
- searchPage: SearchPage;
- filterType: 'anytime' | 'inTheLast' | 'between';
- dateFilterTab: FilterTab;
- searchPhrase: string;
- searchType: SearchType;
- expectSearchResults: number;
- inTheLastInputValue?: string;
- startDay?: string;
- endDay?: string;
-}
-
export class SearchFiltersDate extends BaseComponent {
private static rootElement = '.adf-search-filter-menu-card';
@@ -75,62 +63,6 @@ export class SearchFiltersDate extends BaseComponent {
await page.page.waitForTimeout(2000);
}
- /**
- * Method used in cases where we want to filter search results by Date
- *
- * @param searchPage page context for the test
- * @param filterType filter type for the Date filter
- * @param dateFilterTab tab in Date filter. Either Created or Modified
- * @param searchPhrase search phrase for the search
- * @param searchType type of items we want in search results e.g. files or folders
- * @param expectSearchResults expect a number of search results
- * @param inTheLastInputValue a value for 'in the last' input. e.g. in the last X days
- * @param startDay start day for time-frame search. DD-MMMM-YY
- * @param endDay end day for time-frame search. DD-MMMM-YY
- */
- async filterFilesByDate(params: FilterFilesByDateParams) {
- const { searchPage, filterType, dateFilterTab, searchPhrase, searchType, expectSearchResults, inTheLastInputValue, startDay, endDay } = params;
-
- await searchPage.searchWithin(searchPhrase, searchType);
- await searchPage.dataTable.progressBarWaitForReload();
- await searchPage.searchFilters.dateFilter.click();
-
- if (dateFilterTab === 'Modified') {
- await searchPage.searchFiltersDate.openCreatedModifiedTab(searchPage, 'Modified');
- }
-
- switch (filterType) {
- case 'anytime':
- await searchPage.searchFiltersDate.anytimeButton.click();
- break;
- case 'inTheLast':
- await searchPage.searchFiltersDate.inTheLastButton.click();
- await searchPage.searchFiltersDate.inTheLastInput.fill(inTheLastInputValue);
- break;
- case 'between':
- await searchPage.searchFiltersDate.betweenButton.click();
- await searchPage.searchFiltersDate.betweenStartDate.fill(startDay);
- await searchPage.searchFiltersDate.betweenEndDate.fill(endDay);
- break;
- default:
- throw new Error('Invalid filter type');
- }
-
- await searchPage.searchFilters.menuCardApply.click();
- await searchPage.dataTable.progressBarWaitForReload();
- expect(await searchPage.dataTable.getRowsCount()).toEqual(expectSearchResults);
-
- let dateText: string;
- if (filterType === 'between') {
- if (dateFilterTab === 'Modified') {
- dateText = `Modified: ${startDay} - ${endDay}`;
- } else {
- dateText = `Created: ${startDay} - ${endDay}`;
- }
- await expect(searchPage.searchFilters.dateFilter).toContainText(dateText, { ignoreCase: true });
- }
- }
-
async isModifiedTabSelected(): Promise {
return this.modifiedTab.getAttribute('aria-selected');
}
diff --git a/projects/aca-shared/rules/src/app.rules.spec.ts b/projects/aca-shared/rules/src/app.rules.spec.ts
index 90fca2dff..70f431ea5 100644
--- a/projects/aca-shared/rules/src/app.rules.spec.ts
+++ b/projects/aca-shared/rules/src/app.rules.spec.ts
@@ -23,7 +23,7 @@
*/
import * as app from './app.rules';
-import { createVersionRule, getFileExtension, isPreferencesApiAvailable, isFolderInfoAvailable, isBulkActionsAvailable } from './app.rules';
+import { createVersionRule, getFileExtension, isPreferencesApiAvailable, isNodeInfoAvailable, isBulkActionsAvailable } from './app.rules';
import { TestRuleContext } from './test-rule-context';
import { NodeEntry, RepositoryInfo, StatusInfo } from '@alfresco/js-api';
import { ProfileState, RuleContext } from '@alfresco/adf-extensions';
@@ -1240,24 +1240,24 @@ describe('Versions compatibility', () => {
});
});
- describe('isFolderInfoAvailable', () => {
+ describe('isNodeInfoAvailable', () => {
it('should return true if ACS version is equal to minimal version', () => {
- expect(isFolderInfoAvailable(makeContext('25.1.0'))).toBe(true);
+ expect(isNodeInfoAvailable(makeContext('25.1.0'))).toBe(true);
});
it('should return true if ACS version is greater than minimal version', () => {
- expect(isFolderInfoAvailable(makeContext('25.2.0'))).toBe(true);
- expect(isFolderInfoAvailable(makeContext('26.0.0'))).toBe(true);
+ expect(isNodeInfoAvailable(makeContext('25.2.0'))).toBe(true);
+ expect(isNodeInfoAvailable(makeContext('26.0.0'))).toBe(true);
});
it('should return false if ACS version is less than minimal version', () => {
- expect(isFolderInfoAvailable(makeContext('22.0.0'))).toBe(false);
- expect(isFolderInfoAvailable(makeContext('23.2.0'))).toBe(false);
+ expect(isNodeInfoAvailable(makeContext('22.0.0'))).toBe(false);
+ expect(isNodeInfoAvailable(makeContext('23.2.0'))).toBe(false);
});
it('should return false if ACS version is missing', () => {
- expect(isFolderInfoAvailable(makeContext())).toBe(false);
- expect(isFolderInfoAvailable({ repository: {} } as any)).toBe(false);
+ expect(isNodeInfoAvailable(makeContext())).toBe(false);
+ expect(isNodeInfoAvailable({ repository: {} } as any)).toBe(false);
});
});
diff --git a/projects/aca-shared/rules/src/app.rules.ts b/projects/aca-shared/rules/src/app.rules.ts
index 2c8bc6957..369c39627 100644
--- a/projects/aca-shared/rules/src/app.rules.ts
+++ b/projects/aca-shared/rules/src/app.rules.ts
@@ -490,10 +490,10 @@ export function canOpenWithOffice(context: AcaRuleContext): boolean {
export const isPreferencesApiAvailable = createVersionRule('25.1.0');
/**
- * Checks if folder info modal is supported by current ACS version.
- * JSON ref: `isFolderInfoAvailable`
+ * Checks if node info modal is supported by current ACS version.
+ * JSON ref: `isNodeInfoAvailable`
*/
-export const isFolderInfoAvailable = createVersionRule('23.4.0');
+export const isNodeInfoAvailable = createVersionRule('23.4.0');
/**
* Checks if bulk update feature for legal holds is supported by current ACS version.
diff --git a/projects/aca-shared/store/src/actions/node.actions.ts b/projects/aca-shared/store/src/actions/node.actions.ts
index eb2733eb5..200331858 100644
--- a/projects/aca-shared/store/src/actions/node.actions.ts
+++ b/projects/aca-shared/store/src/actions/node.actions.ts
@@ -39,7 +39,7 @@ export enum NodeActionTypes {
Unshare = 'UNSHARE_NODES',
Copy = 'COPY_NODES',
Move = 'MOVE_NODES',
- FolderInformation = 'FOLDER_INFORMATION',
+ NodeInformation = 'NODE_INFORMATION',
ManagePermissions = 'MANAGE_PERMISSIONS',
PrintFile = 'PRINT_FILE',
ManageVersions = 'MANAGE_VERSIONS',
@@ -219,8 +219,8 @@ export class ManageRulesAction implements Action {
constructor(public payload: NodeEntry) {}
}
-export class FolderInformationAction implements Action {
- readonly type = NodeActionTypes.FolderInformation;
+export class NodeInformationAction implements Action {
+ readonly type = NodeActionTypes.NodeInformation;
constructor(public payload: NodeEntry) {}
}