mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[ACS-11909] [ADW] Link to file or folder plan for Type filter and Delete operation (#5272)
This commit is contained in:
@@ -185,6 +185,10 @@
|
|||||||
{
|
{
|
||||||
"name": "SEARCH.SEARCH_HEADER.FILTERS.TYPE.DOCUMENT",
|
"name": "SEARCH.SEARCH_HEADER.FILTERS.TYPE.DOCUMENT",
|
||||||
"value": "TYPE:'cm:content'"
|
"value": "TYPE:'cm:content'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SEARCH.SEARCH_HEADER.FILTERS.TYPE.LINK",
|
||||||
|
"value": "TYPE:'app:filelink' OR TYPE:'app:folderlink'"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,12 @@
|
|||||||
"YES_LABEL": "Delete",
|
"YES_LABEL": "Delete",
|
||||||
"NO_LABEL": "Keep"
|
"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": {
|
"CONFIRM_LEAVE": {
|
||||||
"TITLE": "Leave this library?",
|
"TITLE": "Leave this library?",
|
||||||
"MESSAGE": "Leaving will remove your access.",
|
"MESSAGE": "Leaving will remove your access.",
|
||||||
@@ -678,7 +684,8 @@
|
|||||||
"TYPE": {
|
"TYPE": {
|
||||||
"TITLE": "Check type",
|
"TITLE": "Check type",
|
||||||
"FOLDER": "Folder",
|
"FOLDER": "Folder",
|
||||||
"DOCUMENT": "Document"
|
"DOCUMENT": "Document",
|
||||||
|
"LINK": "Link"
|
||||||
},
|
},
|
||||||
"SIZE": {
|
"SIZE": {
|
||||||
"TITLE": "Content Size (range)",
|
"TITLE": "Content Size (range)",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ describe('FilesComponent', () => {
|
|||||||
let store: MockStore;
|
let store: MockStore;
|
||||||
let contentApi: ContentApiService;
|
let contentApi: ContentApiService;
|
||||||
let route: ActivatedRoute;
|
let route: ActivatedRoute;
|
||||||
|
let documentListService: DocumentListService;
|
||||||
let router: any = {
|
let router: any = {
|
||||||
url: '',
|
url: '',
|
||||||
navigate: jasmine.createSpy('navigate'),
|
navigate: jasmine.createSpy('navigate'),
|
||||||
@@ -111,7 +112,7 @@ describe('FilesComponent', () => {
|
|||||||
fixture = TestBed.createComponent(FilesComponent);
|
fixture = TestBed.createComponent(FilesComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
const documentListService: DocumentListService = TestBed.inject(DocumentListService);
|
documentListService = TestBed.inject(DocumentListService);
|
||||||
const fakeNodeEntry: NodeEntry = { entry: { id: 'fake-node-entry' } } as NodeEntry;
|
const fakeNodeEntry: NodeEntry = { entry: { id: 'fake-node-entry' } } as NodeEntry;
|
||||||
const fakeNodePaging: NodePaging = { list: { pagination: { count: 10, maxItems: 10, skipCount: 0 } } };
|
const fakeNodePaging: NodePaging = { list: { pagination: { count: 10, maxItems: 10, skipCount: 0 } } };
|
||||||
const documentLoaderNode = { children: fakeNodePaging, currentNode: fakeNodeEntry };
|
const documentLoaderNode = { children: fakeNodePaging, currentNode: fakeNodeEntry };
|
||||||
@@ -137,6 +138,10 @@ describe('FilesComponent', () => {
|
|||||||
spyContent.and.returnValue(of({ entry: node }));
|
spyContent.and.returnValue(of({ entry: node }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
describe('Current page is valid', () => {
|
describe('Current page is valid', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -251,6 +256,8 @@ describe('FilesComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component, 'reload');
|
spyOn(component, 'reload');
|
||||||
spyOn(component, 'reloadWithoutResettingSelection');
|
spyOn(component, 'reloadWithoutResettingSelection');
|
||||||
|
router.navigate['calls'].reset();
|
||||||
|
spyOn(searchHeaderQueryBuilderService, 'execute').and.returnValue(Promise.resolve());
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
|
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
|
||||||
@@ -296,6 +303,43 @@ describe('FilesComponent', () => {
|
|||||||
expect(component.reload).not.toHaveBeenCalled();
|
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(() => {
|
it('should call reloadWithoutResettingSelection on fileUploadComplete event if parent node match', fakeAsync(() => {
|
||||||
const file = { file: { options: { parentId: 'parentId' } } } as FileUploadCompleteEvent;
|
const file = { file: { options: { parentId: 'parentId' } } } as FileUploadCompleteEvent;
|
||||||
component.node = { id: 'parentId' } as Node;
|
component.node = { id: 'parentId' } as Node;
|
||||||
|
|||||||
@@ -266,7 +266,15 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
|||||||
this.navigate(route.id);
|
this.navigate(route.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFilterActive(): boolean {
|
||||||
|
return this.isFilterHeaderActive && !!this.queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
onFileUploadedEvent(event: FileUploadEvent) {
|
onFileUploadedEvent(event: FileUploadEvent) {
|
||||||
|
if (this.isFilterActive()) {
|
||||||
|
this.queryBuilderService.execute();
|
||||||
|
}
|
||||||
|
|
||||||
const node: NodeEntry = event.file.data;
|
const node: NodeEntry = event.file.data;
|
||||||
|
|
||||||
// check root and child nodes
|
// check root and child nodes
|
||||||
@@ -307,9 +315,13 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onContentAdded(nodes: NodeEntry[]) {
|
onContentAdded(nodes: NodeEntry[]) {
|
||||||
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
|
if (this.isFilterActive()) {
|
||||||
if (newNode) {
|
this.queryBuilderService.execute();
|
||||||
this.reload(this.selectedNode);
|
} else {
|
||||||
|
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
|
||||||
|
if (newNode) {
|
||||||
|
this.reload(this.selectedNode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
RestoreDeletedNodesAction,
|
RestoreDeletedNodesAction,
|
||||||
SetSelectedNodesAction,
|
SetSelectedNodesAction,
|
||||||
ShareNodeAction,
|
ShareNodeAction,
|
||||||
|
ShowLoaderAction,
|
||||||
UnlockWriteAction,
|
UnlockWriteAction,
|
||||||
ViewNodeVersionAction
|
ViewNodeVersionAction
|
||||||
} from '@alfresco/aca-shared/store';
|
} from '@alfresco/aca-shared/store';
|
||||||
@@ -917,6 +918,93 @@ describe('ContentManagementService', () => {
|
|||||||
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
|
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
|
||||||
expect(mockElement.focus).toHaveBeenCalled();
|
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', () => {
|
describe('Permanent Delete', () => {
|
||||||
|
|||||||
@@ -737,8 +737,40 @@ export class ContentManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteNodes(items: NodeEntry[], allowUndo = true, focusedElementOnCloseSelector?: string): void {
|
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);
|
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>[] = [];
|
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||||
|
|
||||||
items.forEach((node) => {
|
items.forEach((node) => {
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import {
|
|||||||
SetInfoDrawerStateAction,
|
SetInfoDrawerStateAction,
|
||||||
SetSelectedNodesAction,
|
SetSelectedNodesAction,
|
||||||
ShareNodeAction,
|
ShareNodeAction,
|
||||||
ShowLoaderAction,
|
|
||||||
UndoDeleteNodesAction,
|
UndoDeleteNodesAction,
|
||||||
UnlockWriteAction,
|
UnlockWriteAction,
|
||||||
UnshareNodesAction,
|
UnshareNodesAction,
|
||||||
@@ -223,7 +222,6 @@ describe('NodeEffects', () => {
|
|||||||
store.dispatch(new DeleteNodesAction([node]));
|
store.dispatch(new DeleteNodesAction([node]));
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction([node], true) }));
|
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);
|
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,7 +238,6 @@ describe('NodeEffects', () => {
|
|||||||
expect(store.dispatch).toHaveBeenCalledWith(
|
expect(store.dispatch).toHaveBeenCalledWith(
|
||||||
jasmine.objectContaining({ ...new DeleteNodesAction(null, true, { focusedElementOnCloseSelector: '.test-selector' }) })
|
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');
|
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, '.test-selector');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -250,7 +247,6 @@ describe('NodeEffects', () => {
|
|||||||
store.dispatch(new DeleteNodesAction(null));
|
store.dispatch(new DeleteNodesAction(null));
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...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();
|
expect(contentService.deleteNodes).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import {
|
|||||||
RestoreDeletedNodesAction,
|
RestoreDeletedNodesAction,
|
||||||
SetInfoDrawerStateAction,
|
SetInfoDrawerStateAction,
|
||||||
ShareNodeAction,
|
ShareNodeAction,
|
||||||
ShowLoaderAction,
|
|
||||||
UndoDeleteNodesAction,
|
UndoDeleteNodesAction,
|
||||||
UnlockWriteAction,
|
UnlockWriteAction,
|
||||||
UnshareNodesAction
|
UnshareNodesAction
|
||||||
@@ -166,7 +165,6 @@ export class NodeEffects {
|
|||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType<DeleteNodesAction>(NodeActionTypes.Delete),
|
ofType<DeleteNodesAction>(NodeActionTypes.Delete),
|
||||||
map((action) => {
|
map((action) => {
|
||||||
this.store.dispatch(new ShowLoaderAction(true));
|
|
||||||
if (action?.payload?.length > 0) {
|
if (action?.payload?.length > 0) {
|
||||||
this.contentService.deleteNodes(action.payload, action.allowUndo, action.configuration?.focusedElementOnCloseSelector);
|
this.contentService.deleteNodes(action.payload, action.allowUndo, action.configuration?.focusedElementOnCloseSelector);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user