mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[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:
co-authored by
Adam Świderski
parent
0f7bd89263
commit
4204745227
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
+25
@@ -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>
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
}
|
||||
+532
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+32
-16
@@ -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);
|
||||
|
||||
+1
-1
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-138
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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>
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+285
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+72
@@ -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
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -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, ''));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-70
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user