[MNT-25478] Add Information dialog for nodes and dialog for additional location references (#5041)

* [MNT-25478] Add Information dialog for nodes and dialog for additional location references

* [MNT-25478] CR fixes

* [MNT-25478] CR fixes

* [MNT-25478] Unit test fix

* [MNT-25478] E2E locator fixes

* [MNT-25478] E2E locator fixes

* [MMNT-25478] Add cm:contains to secondary parents API call

* [MMNT-25478] Small fix

* [MNT-25478] First e2e fixes

* more fixes for e2es

* last e2e fixes

---------

Co-authored-by: Adam Świderski <adam.tomasz.swiderski@gmail.com>
This commit is contained in:
Michal Kinas
2026-02-18 12:27:32 +01:00
committed by GitHub
co-authored by Adam Świderski
parent 0f7bd89263
commit 4204745227
36 changed files with 1636 additions and 626 deletions
+2 -2
View File
@@ -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 |
| 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 |
@@ -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}`
);
});
@@ -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.');
});
});
});
@@ -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 }) => {
@@ -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
});
});
});
});
+14 -10
View File
@@ -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"
]
}
},
+13 -7
View File
@@ -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": {
@@ -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,
@@ -0,0 +1,25 @@
@if (hasPrimaryParentPath) {
<a
href="#"
[title]="nodeLocation$ | async"
(click)="goToLocation($event)"
(keydown.enter)="goToLocation($event)"
class="adf-datatable-cell-value"
[innerHTML]="displayText | async | translate">
</a>
} @else {
<div>
<span
class="adf-datatable-cell-value"
[title]="'APP.BROWSE.SEARCH.UNKNOWN_LOCATION' | translate">
{{ 'APP.BROWSE.SEARCH.UNKNOWN_LOCATION' | translate }}
</span>
<button mat-icon-button
[title]="'APP.BROWSE.SEARCH.VIEW_ADDITIONAL_LOCATIONS' | translate"
[attr.aria-label]="'APP.BROWSE.SEARCH.VIEW_ADDITIONAL_LOCATIONS' | translate"
class="aca-location-link-info-btn"
(click)="openAdditionalLocationReferencesDialog()">
<mat-icon>info</mat-icon>
</button>
</div>
}
@@ -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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<LocationLinkComponent>;
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
});
});
});
});
@@ -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: `
<a
href="#"
[title]="nodeLocation$ | async"
(click)="goToLocation($event)"
(keydown.enter)="goToLocation($event)"
class="adf-datatable-cell-value"
[innerHTML]="displayText | async | translate"
>
</a>
`,
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<string>;
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<string> {
const elements = path.elements.map((e) => e.name);
@@ -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;
}
@@ -1,38 +0,0 @@
<div class="aca-folder-info-header">
<adf-icon value="folder" class="aca-folder-icon" fontSet="material-icons"/>
<div class="aca-folder-title" data-automation-id="folder-info-name">{{ folderDetails.name }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-folder-info-body">
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.NUMBER_OF_FILES' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-number-of-files">{{ folderDetails.numberOfFiles }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.SIZE' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-size">{{ folderDetails.size }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.LOCATION' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-location">{{ folderDetails.location }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.CREATED' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-creation-date"
[title]="folderDetails.created | adfLocalizedDate">{{ folderDetails.created | adfTimeAgo }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.MODIFIED' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-modify-date"
[title]="folderDetails.modified | adfLocalizedDate">{{ folderDetails.modified | adfTimeAgo }}</div>
</div>
</div>
@@ -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%;
}
}
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<FolderInformationComponent>;
let nodeService: NodesApiService;
let unitTestingUtils: UnitTestingUtils;
let initiateFolderSizeCalculationSpy: jasmine.Spy<(nodeId: string) => Observable<JobIdBodyEntry>>;
let getFolderSizeInfoSpy: jasmine.Spy<(nodeId: string, jobId: string) => Observable<SizeDetailsEntry>>;
const getNumberOfFiles = (): string => unitTestingUtils.getInnerTextByDataAutomationId('folder-info-number-of-files');
const mockSub = new Subject<JobIdBodyEntry>();
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);
}));
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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');
}
});
}
}
@@ -0,0 +1,66 @@
<div class="aca-node-info-header">
<img class="aca-node-icon" alt="{{ 'APP.NODE_INFO.ICON' | translate }}" [ngSrc]="nodeDetails.icon" width="24" height="24"/>
<div class="aca-node-title" data-automation-id="node-info-name">{{ nodeDetails.name }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-node-info-body">
@if (node.isFolder) {
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.NUMBER_OF_FILES' | translate }}</div>
<div class="aca-node-info-item-value"
data-automation-id="node-info-number-of-files">{{ nodeDetails.numberOfFiles }}</div>
</div>
<mat-divider aria-hidden="true"/>
}
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.SIZE' | translate }}</div>
<div class="aca-node-info-item-value" data-automation-id="node-info-size">
{{ nodeDetails.size }}
</div>
</div>
<mat-divider aria-hidden="true"/>
<div [ngClass]="{'aca-node-location-group': nodeDetails.secondaryParentsPaths?.length > 0}">
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.LOCATION' | translate }}</div>
<div class="aca-node-info-item-value" data-automation-id="node-info-location">
<adf-icon value="folder" class="aca-node-icon" fontSet="material-icons"/>
<div class="aca-node-info-location">
@if (nodeDetails.location) {
{{ nodeDetails.location }}
} @else {
{{ 'APP.BROWSE.SEARCH.UNKNOWN_LOCATION' | translate }}
}
</div>
</div>
</div>
@if (nodeDetails.secondaryParentsPaths?.length > 0) {
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.REFERENCED' | translate }}</div>
<div class="aca-node-info-secondary-parents-container" data-automation-id="node-info-secondary-parents">
@for (secondaryParentPath of nodeDetails.secondaryParentsPaths; track $index) {
<div class="aca-node-info-secondary-parent" [attr.data-automation-id]="'node-info-secondary-parent-' + $index">
<adf-icon value="folder" class="aca-node-icon" fontSet="material-icons"/>
<div class="aca-node-info-location">
{{ secondaryParentPath }}
</div>
</div>
}
</div>
</div>
}
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.CREATED' | translate }}</div>
<div class="aca-node-info-item-value"
data-automation-id="node-info-creation-date"
[title]="nodeDetails.created | adfLocalizedDate">{{ nodeDetails.created | adfTimeAgo }}</div>
</div>
<mat-divider aria-hidden="true"/>
<div class="aca-node-info-item">
<div class="aca-node-info-item-label">{{ 'APP.NODE_INFO.MODIFIED' | translate }}</div>
<div class="aca-node-info-item-value"
data-automation-id="node-info-modify-date"
[title]="nodeDetails.modified | adfLocalizedDate">{{ nodeDetails.modified | adfTimeAgo }}</div>
</div>
</div>
@@ -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;
}
}
}
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<NodeInformationComponent>;
let nodeService: NodesApiService;
let unitTestingUtils: UnitTestingUtils;
let initiateFolderSizeCalculationSpy: jasmine.Spy<(nodeId: string) => Observable<JobIdBodyEntry>>;
let getFolderSizeInfoSpy: jasmine.Spy<(nodeId: string, jobId: string) => Observable<SizeDetailsEntry>>;
let listParentsSpy: jasmine.Spy<(nodeId: string, opts: any) => Observable<NodeAssociationPaging>>;
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<JobIdBodyEntry>();
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');
});
});
});
@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}
@@ -0,0 +1,22 @@
@if (additionalReferenceLocations.length > 0) {
<div class="app-node-location-reference-title">
{{ 'APP.ADDITIONAL_REFERENCES_DIALOG.DESCRIPTION' | translate : { name: node.name} }}
</div>
@for (location of additionalReferenceLocations; track $index) {
<div class="app-node-location-reference">
<adf-icon value="folder" class="app-node-location-reference-icon" fontSet="material-icons"/>
<a
href="#"
[title]="location.path.name"
(click)="goToLocation($event, location)"
(keydown.enter)="goToLocation($event, location)"
class="app-node-location-reference-link">
{{ location.path.name }}
</a>
</div>
}
} @else {
<div class="app-node-location-reference-title app-node-location-reference-no-locations">
{{ 'APP.ADDITIONAL_REFERENCES_DIALOG.NO_LOCATIONS' | translate : { name: node.name } }}
</div>
}
@@ -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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<NodeLocationReferencesComponent>;
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);
});
});
});
@@ -0,0 +1,72 @@
/*!
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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');
}
}
}
@@ -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'
@@ -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'
@@ -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);
}));
});
});
@@ -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<FolderInformationAction>(NodeActionTypes.FolderInformation),
ofType<NodeInformationAction>(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]);
}
});
}
@@ -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
@@ -253,7 +253,7 @@ export class DataTableComponent extends BaseComponent {
}
async getItemLocationText(name: string): Promise<string> {
await this.getItemLocationEl(name).locator('a').waitFor({ state: 'attached' });
await this.getItemLocationEl(name).waitFor({ state: 'attached' });
return this.getItemLocationEl(name).innerText();
}
@@ -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<number> {
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, ''));
}
}
@@ -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<string> {
return this.modifiedTab.getAttribute('aria-selected');
}
@@ -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);
});
});
+3 -3
View File
@@ -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.
@@ -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) {}
}