[ACS-11909] [ADW] Link to file or folder plan for Type filter and Delete operation (#5272)

This commit is contained in:
Dominik Iwanek
2026-07-10 14:25:42 +02:00
committed by GitHub
parent 357c61bf3c
commit b9677ec6b6
8 changed files with 193 additions and 12 deletions
+8 -1
View File
@@ -331,6 +331,12 @@
"YES_LABEL": "Delete",
"NO_LABEL": "Keep"
},
"CONFIRM_DELETE_LINK": {
"TITLE": "Delete link?",
"MESSAGE": "Links are permanently deleted and can't be restored from the trash.",
"YES_LABEL": "Delete",
"NO_LABEL": "Cancel"
},
"CONFIRM_LEAVE": {
"TITLE": "Leave this library?",
"MESSAGE": "Leaving will remove your access.",
@@ -678,7 +684,8 @@
"TYPE": {
"TITLE": "Check type",
"FOLDER": "Folder",
"DOCUMENT": "Document"
"DOCUMENT": "Document",
"LINK": "Link"
},
"SIZE": {
"TITLE": "Content Size (range)",
@@ -58,6 +58,7 @@ describe('FilesComponent', () => {
let store: MockStore;
let contentApi: ContentApiService;
let route: ActivatedRoute;
let documentListService: DocumentListService;
let router: any = {
url: '',
navigate: jasmine.createSpy('navigate'),
@@ -111,7 +112,7 @@ describe('FilesComponent', () => {
fixture = TestBed.createComponent(FilesComponent);
component = fixture.componentInstance;
const documentListService: DocumentListService = TestBed.inject(DocumentListService);
documentListService = TestBed.inject(DocumentListService);
const fakeNodeEntry: NodeEntry = { entry: { id: 'fake-node-entry' } } as NodeEntry;
const fakeNodePaging: NodePaging = { list: { pagination: { count: 10, maxItems: 10, skipCount: 0 } } };
const documentLoaderNode = { children: fakeNodePaging, currentNode: fakeNodeEntry };
@@ -137,6 +138,10 @@ describe('FilesComponent', () => {
spyContent.and.returnValue(of({ entry: node }));
});
afterEach(() => {
fixture.destroy();
});
describe('Current page is valid', () => {
beforeEach(() => {
fixture.detectChanges();
@@ -251,6 +256,8 @@ describe('FilesComponent', () => {
beforeEach(() => {
spyOn(component, 'reload');
spyOn(component, 'reloadWithoutResettingSelection');
router.navigate['calls'].reset();
spyOn(searchHeaderQueryBuilderService, 'execute').and.returnValue(Promise.resolve());
fixture.detectChanges();
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
@@ -296,6 +303,43 @@ describe('FilesComponent', () => {
expect(component.reload).not.toHaveBeenCalled();
});
it('should execute filter query when contentLinked emits and filters are active', () => {
const nodes = [{ entry: { parentId: 'different-id' } }] as NodeEntry[];
component.node = { id: '1' } as Node;
component.isFilterHeaderActive = true;
component.queryParams = { checkList: 'TYPE:"app:filelink"' };
nodeActionsService.contentLinked.next({ succeeded: nodes, failed: [] });
expect(searchHeaderQueryBuilderService.execute).toHaveBeenCalled();
expect(component.reload).not.toHaveBeenCalled();
});
it('should call regular reload when contentLinked emits and filters are not active', () => {
const nodes = [{ entry: { parentId: '1' } }] as NodeEntry[];
component.node = { id: '1' } as Node;
component.isFilterHeaderActive = false;
component.queryParams = null;
nodeActionsService.contentLinked.next({ succeeded: nodes, failed: [] });
expect(component.reload).toHaveBeenCalled();
});
it('should call regular reload when contentLinked emits and isFilterHeaderActive is true but queryParams is null', () => {
const nodes = [{ entry: { parentId: '1' } }] as NodeEntry[];
component.node = { id: '1' } as Node;
component.isFilterHeaderActive = true;
component.queryParams = null;
nodeActionsService.contentLinked.next({ succeeded: nodes, failed: [] });
expect(component.reload).toHaveBeenCalled();
});
it('should call reloadWithoutResettingSelection on fileUploadComplete event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } } as FileUploadCompleteEvent;
component.node = { id: 'parentId' } as Node;
@@ -266,7 +266,15 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.navigate(route.id);
}
isFilterActive(): boolean {
return this.isFilterHeaderActive && !!this.queryParams;
}
onFileUploadedEvent(event: FileUploadEvent) {
if (this.isFilterActive()) {
this.queryBuilderService.execute();
}
const node: NodeEntry = event.file.data;
// check root and child nodes
@@ -307,9 +315,13 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
onContentAdded(nodes: NodeEntry[]) {
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
if (newNode) {
this.reload(this.selectedNode);
if (this.isFilterActive()) {
this.queryBuilderService.execute();
} else {
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
if (newNode) {
this.reload(this.selectedNode);
}
}
}
@@ -37,6 +37,7 @@ import {
RestoreDeletedNodesAction,
SetSelectedNodesAction,
ShareNodeAction,
ShowLoaderAction,
UnlockWriteAction,
ViewNodeVersionAction
} from '@alfresco/aca-shared/store';
@@ -917,6 +918,93 @@ describe('ContentManagementService', () => {
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
describe('link nodes', () => {
it('should open a confirmation dialog when the selection contains a link', () => {
const dialogOpenSpy = spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(false)
} as MatDialogRef<MatDialog>);
const selection = [{ entry: { id: '1', name: 'link.url', nodeType: 'app:filelink' } }] as NodeEntry[];
store.dispatch(new DeleteNodesAction(selection));
expect(dialogOpenSpy).toHaveBeenCalledWith(
ConfirmDialogComponent,
jasmine.objectContaining({
data: jasmine.objectContaining({ title: 'APP.DIALOGS.CONFIRM_DELETE_LINK.TITLE' })
})
);
});
it('should delete the link when the confirmation dialog is accepted', () => {
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(true)
} as MatDialogRef<MatDialog>);
const deleteNodeSpy = spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'link.url', nodeType: 'app:filelink' } }] as NodeEntry[];
store.dispatch(new DeleteNodesAction(selection));
expect(deleteNodeSpy).toHaveBeenCalledWith('1');
});
it('should not delete the link when the confirmation dialog is cancelled', () => {
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(false)
} as MatDialogRef<MatDialog>);
const deleteNodeSpy = spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'link.url', nodeType: 'app:filelink' } }] as NodeEntry[];
store.dispatch(new DeleteNodesAction(selection));
expect(deleteNodeSpy).not.toHaveBeenCalled();
});
it('should not raise the confirmation dialog for regular nodes', () => {
const dialogOpenSpy = spyOn(dialog, 'open');
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'name1', nodeType: 'cm:content' } }] as NodeEntry[];
store.dispatch(new DeleteNodesAction(selection));
expect(dialogOpenSpy).not.toHaveBeenCalled();
});
it('should not offer undo when every deleted node is a link', () => {
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(true)
} as MatDialogRef<MatDialog>);
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'link.url', nodeType: 'app:filelink' } }] as NodeEntry[];
store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[1]).toBeNull();
});
it('should dispatch ShowLoaderAction(true) when link deletion is confirmed', () => {
const dispatchSpy = spyOn(store, 'dispatch');
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(true)
} as MatDialogRef<MatDialog>);
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'link.url', nodeType: 'app:filelink' } }] as NodeEntry[];
contentManagementService.deleteNodes(selection);
expect(dispatchSpy).toHaveBeenCalledWith(jasmine.any(ShowLoaderAction));
});
it('should dispatch ShowLoaderAction(true) for regular node deletion', () => {
const dispatchSpy = spyOn(store, 'dispatch');
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const selection = [{ entry: { id: '1', name: 'name1', nodeType: 'cm:content' } }] as NodeEntry[];
contentManagementService.deleteNodes(selection);
expect(dispatchSpy).toHaveBeenCalledWith(jasmine.any(ShowLoaderAction));
});
});
});
describe('Permanent Delete', () => {
@@ -737,8 +737,40 @@ export class ContentManagementService {
}
deleteNodes(items: NodeEntry[], allowUndo = true, focusedElementOnCloseSelector?: string): void {
const containsLink = items.some((node) => this.isLinkNode(node));
if (containsLink) {
const dialogRef = this.dialogRef.open(ConfirmDialogComponent, {
data: {
title: 'APP.DIALOGS.CONFIRM_DELETE_LINK.TITLE',
message: 'APP.DIALOGS.CONFIRM_DELETE_LINK.MESSAGE',
yesLabel: 'APP.DIALOGS.CONFIRM_DELETE_LINK.YES_LABEL',
noLabel: 'APP.DIALOGS.CONFIRM_DELETE_LINK.NO_LABEL'
},
minWidth: '250px'
});
dialogRef.afterClosed().subscribe((result) => {
if (result === true) {
this.store.dispatch(new ShowLoaderAction(true));
this.deleteNodesBatch(items, allowUndo, focusedElementOnCloseSelector);
} else {
this.focusAfterClose(focusedElementOnCloseSelector);
}
});
} else {
this.store.dispatch(new ShowLoaderAction(true));
this.deleteNodesBatch(items, allowUndo, focusedElementOnCloseSelector);
}
}
private isLinkNode(node: NodeEntry): boolean {
return node.entry.nodeType === 'app:filelink' || node.entry.nodeType === 'app:folderlink';
}
private deleteNodesBatch(items: NodeEntry[], allowUndo = true, focusedElementOnCloseSelector?: string): void {
this.focusAfterClose(focusedElementOnCloseSelector);
const canUndo = allowUndo && !items.every((node) => node.entry.nodeType === 'app:filelink' || node.entry.nodeType === 'app:folderlink');
const canUndo = allowUndo && !items.every((node) => this.isLinkNode(node));
const batch: Observable<DeletedNodeInfo>[] = [];
items.forEach((node) => {
@@ -49,7 +49,6 @@ import {
SetInfoDrawerStateAction,
SetSelectedNodesAction,
ShareNodeAction,
ShowLoaderAction,
UndoDeleteNodesAction,
UnlockWriteAction,
UnshareNodesAction,
@@ -223,7 +222,6 @@ describe('NodeEffects', () => {
store.dispatch(new DeleteNodesAction([node]));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction([node], true) }));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) }));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, undefined);
});
@@ -240,7 +238,6 @@ describe('NodeEffects', () => {
expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({ ...new DeleteNodesAction(null, true, { focusedElementOnCloseSelector: '.test-selector' }) })
);
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) }));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, '.test-selector');
}));
@@ -250,7 +247,6 @@ describe('NodeEffects', () => {
store.dispatch(new DeleteNodesAction(null));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction(null) }));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) }));
expect(contentService.deleteNodes).not.toHaveBeenCalled();
});
});
@@ -52,7 +52,6 @@ import {
RestoreDeletedNodesAction,
SetInfoDrawerStateAction,
ShareNodeAction,
ShowLoaderAction,
UndoDeleteNodesAction,
UnlockWriteAction,
UnshareNodesAction
@@ -166,7 +165,6 @@ export class NodeEffects {
this.actions$.pipe(
ofType<DeleteNodesAction>(NodeActionTypes.Delete),
map((action) => {
this.store.dispatch(new ShowLoaderAction(true));
if (action?.payload?.length > 0) {
this.contentService.deleteNodes(action.payload, action.allowUndo, action.configuration?.focusedElementOnCloseSelector);
} else {