mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[MNT-25522] Add support for 'Link document' functionality (#5195)
* [MNT-25522] Add 'Link document' functionality * [MNT-25522] address copilot comments * [MNT-25522] cr fixes * [MNT-25522] address code smell
This commit is contained in:
@@ -554,6 +554,24 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.toolbar.locate",
|
||||||
|
"title": "APP.ACTIONS.LOCATE_LINKED_ITEM",
|
||||||
|
"order": 250,
|
||||||
|
"icon": "my_location",
|
||||||
|
"actions": {
|
||||||
|
"click": "LOCATE_LINKED_ITEM"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"visible": [
|
||||||
|
"app.selection.isNodeLink",
|
||||||
|
"app.selection.notEmpty",
|
||||||
|
"!app.navigation.isTrashcan",
|
||||||
|
"!app.navigation.isLibraries",
|
||||||
|
"!isMultiSelection"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.toolbar.editFolder",
|
"id": "app.toolbar.editFolder",
|
||||||
"order": 300,
|
"order": 300,
|
||||||
@@ -611,7 +629,8 @@
|
|||||||
"!app.navigation.isRecentFiles",
|
"!app.navigation.isRecentFiles",
|
||||||
"!app.navigation.isSharedFiles",
|
"!app.navigation.isSharedFiles",
|
||||||
"!app.navigation.isSearchResults",
|
"!app.navigation.isSearchResults",
|
||||||
"!app.navigation.isFavorites"
|
"!app.navigation.isFavorites",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -631,7 +650,8 @@
|
|||||||
"!app.navigation.isRecentFiles",
|
"!app.navigation.isRecentFiles",
|
||||||
"!app.navigation.isSharedFiles",
|
"!app.navigation.isSharedFiles",
|
||||||
"!app.navigation.isSearchResults",
|
"!app.navigation.isSearchResults",
|
||||||
"!app.navigation.isFavorites"
|
"!app.navigation.isFavorites",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -673,6 +693,23 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.toolbar.link",
|
||||||
|
"title": "APP.ACTIONS.CREATE_LINK",
|
||||||
|
"order": 750,
|
||||||
|
"icon": "link",
|
||||||
|
"actions": {
|
||||||
|
"click": "CREATE_LINK"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"visible": [
|
||||||
|
"app.selection.notEmpty",
|
||||||
|
"!app.navigation.isTrashcan",
|
||||||
|
"!app.navigation.isLibraries",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.toolbar.delete",
|
"id": "app.toolbar.delete",
|
||||||
"order": 800,
|
"order": 800,
|
||||||
@@ -725,7 +762,8 @@
|
|||||||
"visible": [
|
"visible": [
|
||||||
"app.selection.file",
|
"app.selection.file",
|
||||||
"!app.navigation.isTrashcan",
|
"!app.navigation.isTrashcan",
|
||||||
"!app.selection.file.isLocked"
|
"!app.selection.file.isLocked",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -775,7 +813,8 @@
|
|||||||
"!app.navigation.isTrashcan",
|
"!app.navigation.isTrashcan",
|
||||||
"app.selection.folder",
|
"app.selection.folder",
|
||||||
"!app.navigation.isFavorites",
|
"!app.navigation.isFavorites",
|
||||||
"!isSmartFolder"
|
"!isSmartFolder",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -813,6 +852,24 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"id": "app.context.menu.locate",
|
||||||
|
"title": "APP.ACTIONS.LOCATE_LINKED_ITEM",
|
||||||
|
"order": 50,
|
||||||
|
"icon": "my_location",
|
||||||
|
"actions": {
|
||||||
|
"click": "LOCATE_LINKED_ITEM"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"visible": [
|
||||||
|
"app.selection.isNodeLink",
|
||||||
|
"app.selection.notEmpty",
|
||||||
|
"!app.navigation.isTrashcan",
|
||||||
|
"!app.navigation.isLibraries",
|
||||||
|
"!isMultiSelection"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.context.menu.share",
|
"id": "app.context.menu.share",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
@@ -923,7 +980,8 @@
|
|||||||
"!app.navigation.isRecentFiles",
|
"!app.navigation.isRecentFiles",
|
||||||
"!app.navigation.isSharedFiles",
|
"!app.navigation.isSharedFiles",
|
||||||
"!app.navigation.isSearchResults",
|
"!app.navigation.isSearchResults",
|
||||||
"!app.navigation.isFavorites"
|
"!app.navigation.isFavorites",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -943,7 +1001,8 @@
|
|||||||
"!app.navigation.isRecentFiles",
|
"!app.navigation.isRecentFiles",
|
||||||
"!app.navigation.isSharedFiles",
|
"!app.navigation.isSharedFiles",
|
||||||
"!app.navigation.isSearchResults",
|
"!app.navigation.isSearchResults",
|
||||||
"!app.navigation.isFavorites"
|
"!app.navigation.isFavorites",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1016,6 +1075,23 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.context.menu.link",
|
||||||
|
"title": "APP.ACTIONS.CREATE_LINK",
|
||||||
|
"order": 1150,
|
||||||
|
"icon": "link",
|
||||||
|
"actions": {
|
||||||
|
"click": "CREATE_LINK"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"visible": [
|
||||||
|
"app.selection.notEmpty",
|
||||||
|
"!app.navigation.isTrashcan",
|
||||||
|
"!app.navigation.isLibraries",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.context.menu.delete",
|
"id": "app.context.menu.delete",
|
||||||
"title": "APP.ACTIONS.DELETE",
|
"title": "APP.ACTIONS.DELETE",
|
||||||
@@ -1068,7 +1144,8 @@
|
|||||||
"visible": [
|
"visible": [
|
||||||
"app.selection.file",
|
"app.selection.file",
|
||||||
"!app.navigation.isTrashcan",
|
"!app.navigation.isTrashcan",
|
||||||
"!app.selection.file.isLocked"
|
"!app.selection.file.isLocked",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1118,7 +1195,8 @@
|
|||||||
"!app.navigation.isTrashcan",
|
"!app.navigation.isTrashcan",
|
||||||
"app.selection.folder",
|
"app.selection.folder",
|
||||||
"!app.navigation.isFavorites",
|
"!app.navigation.isFavorites",
|
||||||
"!isSmartFolder"
|
"!isSmartFolder",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1442,7 +1520,8 @@
|
|||||||
"visible": [
|
"visible": [
|
||||||
"app.selection.file",
|
"app.selection.file",
|
||||||
"!app.navigation.isTrashcan",
|
"!app.navigation.isTrashcan",
|
||||||
"!app.selection.file.isLocked"
|
"!app.selection.file.isLocked",
|
||||||
|
"!app.selection.isNodeLink"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -320,7 +320,9 @@
|
|||||||
"EDIT_OFFLINE_CANCEL": "Cancel Editing",
|
"EDIT_OFFLINE_CANCEL": "Cancel Editing",
|
||||||
"CHANGE_ASPECT": "Edit Aspects",
|
"CHANGE_ASPECT": "Edit Aspects",
|
||||||
"ADD_ASPECTS": "Add Aspects",
|
"ADD_ASPECTS": "Add Aspects",
|
||||||
"NODE_INFO": "Information"
|
"NODE_INFO": "Information",
|
||||||
|
"LOCATE_LINKED_ITEM": "Locate Linked Item",
|
||||||
|
"CREATE_LINK": "Create Link"
|
||||||
},
|
},
|
||||||
"DIALOGS": {
|
"DIALOGS": {
|
||||||
"CONFIRM_PURGE": {
|
"CONFIRM_PURGE": {
|
||||||
@@ -366,6 +368,7 @@
|
|||||||
"GENERIC": "The action was unsuccessful. Try again or contact your IT Team.",
|
"GENERIC": "The action was unsuccessful. Try again or contact your IT Team.",
|
||||||
"CONFLICT": "This name is already in use, try a different name.",
|
"CONFLICT": "This name is already in use, try a different name.",
|
||||||
"NODE_MOVE": "Move unsuccessful, a file with the same name already exists.",
|
"NODE_MOVE": "Move unsuccessful, a file with the same name already exists.",
|
||||||
|
"NODE_LINK_DUPLICATE": "Can't create a link, a link with the same name already exists in this location.",
|
||||||
"EXISTENT_FOLDER": "There's already a folder with this name. Try a different name.",
|
"EXISTENT_FOLDER": "There's already a folder with this name. Try a different name.",
|
||||||
"NODE_DELETION": "{{ name }} couldn't be deleted",
|
"NODE_DELETION": "{{ name }} couldn't be deleted",
|
||||||
"NODE_DELETION_PLURAL": "{{ number }} items couldn't be deleted",
|
"NODE_DELETION_PLURAL": "{{ number }} items couldn't be deleted",
|
||||||
@@ -426,6 +429,14 @@
|
|||||||
"FAIL_SINGULAR": "{{ failed }} item couldn't be copied.",
|
"FAIL_SINGULAR": "{{ failed }} item couldn't be copied.",
|
||||||
"FAIL_PLURAL": "{{ failed }} items couldn't be copied."
|
"FAIL_PLURAL": "{{ failed }} items couldn't be copied."
|
||||||
},
|
},
|
||||||
|
"NODE_LINK": {
|
||||||
|
"SINGULAR": "Link created for {{ success }} item",
|
||||||
|
"PLURAL": "Links created for {{ success }} items",
|
||||||
|
"PARTIAL_SINGULAR": "Link created for {{ success }} item, couldn't create a link for {{ failed }}.",
|
||||||
|
"PARTIAL_PLURAL": "Links created for {{ success }} items, couldn't create links for {{ failed }}.",
|
||||||
|
"FAIL_SINGULAR": "Couldn't create a link for {{ failed }} item.",
|
||||||
|
"FAIL_PLURAL": "Couldn't create links for {{ failed }} items."
|
||||||
|
},
|
||||||
"NODE_MOVE": {
|
"NODE_MOVE": {
|
||||||
"SINGULAR": "Moved {{ success }} item.",
|
"SINGULAR": "Moved {{ success }} item.",
|
||||||
"PLURAL": "Moved {{ success }} items.",
|
"PLURAL": "Moved {{ success }} items.",
|
||||||
@@ -513,10 +524,13 @@
|
|||||||
"COPY_ITEMS": "Copy {{ number }} items to...",
|
"COPY_ITEMS": "Copy {{ number }} items to...",
|
||||||
"MOVE_ITEM": "Move '{{ name }}' to...",
|
"MOVE_ITEM": "Move '{{ name }}' to...",
|
||||||
"MOVE_ITEMS": "Move {{ number }} items to...",
|
"MOVE_ITEMS": "Move {{ number }} items to...",
|
||||||
|
"LINK_ITEM": "Create link for '{{ name }}' in...",
|
||||||
|
"LINK_ITEMS": "Create links for {{ number }} items in...",
|
||||||
"SEARCH": "Search",
|
"SEARCH": "Search",
|
||||||
"NEXT": "Next",
|
"NEXT": "Next",
|
||||||
"SELECT_FILE_TEMPLATE_TITLE": "Select a document template",
|
"SELECT_FILE_TEMPLATE_TITLE": "Select a document template",
|
||||||
"SELECT_FOLDER_TEMPLATE_TITLE": "Select a folder template"
|
"SELECT_FOLDER_TEMPLATE_TITLE": "Select a folder template",
|
||||||
|
"LINK_NAME": "Link to {{ name }}"
|
||||||
},
|
},
|
||||||
"NODE_FROM_TEMPLATE": {
|
"NODE_FROM_TEMPLATE": {
|
||||||
"CANCEL": "CANCEL",
|
"CANCEL": "CANCEL",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ import { IsFeatureSupportedInCurrentAcsPipe } from './pipes/is-feature-supported
|
|||||||
'app.selection.folder.canUpdate': rules.canUpdateSelectedFolder,
|
'app.selection.folder.canUpdate': rules.canUpdateSelectedFolder,
|
||||||
'app.selection.displayedKnowledgeRetrievalButton': rules.canDisplayKnowledgeRetrievalButton,
|
'app.selection.displayedKnowledgeRetrievalButton': rules.canDisplayKnowledgeRetrievalButton,
|
||||||
'app.selection.isCheckedOut': rules.isCheckedOut,
|
'app.selection.isCheckedOut': rules.isCheckedOut,
|
||||||
|
'app.selection.isNodeLink': rules.isNodeLink,
|
||||||
|
|
||||||
'app.navigation.folder.canCreate': rules.canCreateFolder,
|
'app.navigation.folder.canCreate': rules.canCreateFolder,
|
||||||
'app.navigation.isTrashcan': rules.isTrashcan,
|
'app.navigation.isTrashcan': rules.isTrashcan,
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ describe('FilesComponent', () => {
|
|||||||
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
|
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh onContentCopied event if parent is the same', () => {
|
it('should call refresh onContentAdded event if parent is the same', () => {
|
||||||
const nodes: any[] = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }];
|
const nodes: any[] = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }];
|
||||||
|
|
||||||
component.node = { id: '1' } as any;
|
component.node = { id: '1' } as any;
|
||||||
@@ -258,7 +258,7 @@ describe('FilesComponent', () => {
|
|||||||
expect(component.reload).toHaveBeenCalled();
|
expect(component.reload).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call refresh onContentCopied event when parent mismatch', () => {
|
it('should not call refresh onContentAdded event when parent mismatch', () => {
|
||||||
const nodes: any[] = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }];
|
const nodes: any[] = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }];
|
||||||
|
|
||||||
component.node = { id: '3' } as any;
|
component.node = { id: '3' } as any;
|
||||||
@@ -268,6 +268,26 @@ describe('FilesComponent', () => {
|
|||||||
expect(component.reload).not.toHaveBeenCalled();
|
expect(component.reload).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call reload when contentLinked emits a node in the current folder', () => {
|
||||||
|
const nodes = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
component.node = { id: '1' } as Node;
|
||||||
|
|
||||||
|
nodeActionsService.contentLinked.next({ succeeded: nodes, failed: [] });
|
||||||
|
|
||||||
|
expect(component.reload).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call reload when contentLinked emits nodes outside the current folder', () => {
|
||||||
|
const nodes = [{ entry: { parentId: '1' } }, { entry: { parentId: '2' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
component.node = { id: '3' } as Node;
|
||||||
|
|
||||||
|
nodeActionsService.contentLinked.next({ succeeded: nodes, failed: [] });
|
||||||
|
|
||||||
|
expect(component.reload).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should call refresh on fileUploadComplete event if parent node match', fakeAsync(() => {
|
it('should call refresh on fileUploadComplete event if parent node match', fakeAsync(() => {
|
||||||
const file: any = { file: { options: { parentId: 'parentId' } } };
|
const file: any = { file: { options: { parentId: 'parentId' } } };
|
||||||
component.node = { id: 'parentId' } as any;
|
component.node = { id: 'parentId' } as any;
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.subscriptions = this.subscriptions.concat([
|
this.subscriptions = this.subscriptions.concat([
|
||||||
this.nodeActionsService.contentCopied.subscribe((nodes) => this.onContentCopied(nodes)),
|
this.nodeActionsService.contentCopied.subscribe((nodes) => this.onContentAdded(nodes)),
|
||||||
|
this.nodeActionsService.contentLinked.subscribe(({ succeeded }) => this.onContentAdded(succeeded)),
|
||||||
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe((file) => this.onFileUploadedEvent(file)),
|
this.uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe((file) => this.onFileUploadedEvent(file)),
|
||||||
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe((file) => this.onFileUploadedEvent(file))
|
this.uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe((file) => this.onFileUploadedEvent(file))
|
||||||
]);
|
]);
|
||||||
@@ -305,7 +306,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
|||||||
this.reload(this.selectedNode);
|
this.reload(this.selectedNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentCopied(nodes: NodeEntry[]) {
|
onContentAdded(nodes: NodeEntry[]) {
|
||||||
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
|
const newNode = nodes.find((node) => node?.entry?.parentId === this.getParentNodeId());
|
||||||
if (newNode) {
|
if (newNode) {
|
||||||
this.reload(this.selectedNode);
|
this.reload(this.selectedNode);
|
||||||
|
|||||||
+55
@@ -138,4 +138,59 @@ describe('CommentsTabComponent', () => {
|
|||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(component.canUpdateNode).toBe(true);
|
expect(component.canUpdateNode).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('link node (isLink)', () => {
|
||||||
|
it('should set destination to cm:destination property value', async () => {
|
||||||
|
component.node = {
|
||||||
|
id: 'link-node-id',
|
||||||
|
isFile: false,
|
||||||
|
isFolder: false,
|
||||||
|
isLink: true,
|
||||||
|
properties: { 'cm:destination': 'original-node-id' }
|
||||||
|
} as Node;
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(component.destination).toBe('original-node-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set canUpdateNode based on permission check for link nodes', async () => {
|
||||||
|
component.node = {
|
||||||
|
id: 'link-node-id',
|
||||||
|
isFile: true,
|
||||||
|
isFolder: false,
|
||||||
|
isLink: true,
|
||||||
|
properties: { 'cm:destination': 'original-node-id' }
|
||||||
|
} as Node;
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(component.canUpdateNode).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check permissions for link nodes', async () => {
|
||||||
|
component.node = {
|
||||||
|
id: 'link-node-id',
|
||||||
|
isFile: false,
|
||||||
|
isFolder: true,
|
||||||
|
isLink: true,
|
||||||
|
properties: { 'cm:destination': 'original-node-id' }
|
||||||
|
} as Node;
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(nodePermissionService.check).toHaveBeenCalled();
|
||||||
|
expect(checked).toContain('update');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave destination null if cm:destination property is missing', async () => {
|
||||||
|
component.node = {
|
||||||
|
id: 'link-node-id',
|
||||||
|
isFile: false,
|
||||||
|
isFolder: false,
|
||||||
|
isLink: true,
|
||||||
|
properties: {}
|
||||||
|
} as Node;
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(component.destination).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-1
@@ -33,7 +33,7 @@ import { ExternalNodePermissionCommentsTabService } from './external-node-permis
|
|||||||
imports: [MatCardModule, NodeCommentsComponent],
|
imports: [MatCardModule, NodeCommentsComponent],
|
||||||
selector: 'app-comments-tab',
|
selector: 'app-comments-tab',
|
||||||
template: ` <mat-card class="adf-comments-tab-container" appearance="raised"
|
template: ` <mat-card class="adf-comments-tab-container" appearance="raised"
|
||||||
><adf-node-comments [readOnly]="!canUpdateNode" [nodeId]="node?.id"
|
><adf-node-comments [readOnly]="!canUpdateNode" [nodeId]="destination ?? node?.id"
|
||||||
/></mat-card>`,
|
/></mat-card>`,
|
||||||
styles: ['app-comments-tab mat-card { padding: 16px }'],
|
styles: ['app-comments-tab mat-card { padding: 16px }'],
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
@@ -47,11 +47,17 @@ export class CommentsTabComponent implements OnInit {
|
|||||||
node: Node;
|
node: Node;
|
||||||
|
|
||||||
canUpdateNode = false;
|
canUpdateNode = false;
|
||||||
|
destination: string | null = null;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.node) {
|
if (!this.node) {
|
||||||
this.canUpdateNode = false;
|
this.canUpdateNode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.node.isLink) {
|
||||||
|
this.destination = this.node.properties?.['cm:destination'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.node.isFolder || (this.node.isFile && !isLocked({ entry: this.node }))) {
|
if (this.node.isFolder || (this.node.isFile && !isLocked({ entry: this.node }))) {
|
||||||
this.canUpdateNode = this.permission.check(this.node, ['update']);
|
this.canUpdateNode = this.permission.check(this.node, ['update']);
|
||||||
if (this.externalPermissionNodeService) {
|
if (this.externalPermissionNodeService) {
|
||||||
|
|||||||
+18
@@ -394,6 +394,15 @@ describe('MetadataTabComponent', () => {
|
|||||||
expect(getContentMetadata().displayCategories).toBeFalse();
|
expect(getContentMetadata().displayCategories).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have assigned displayCategories to false if node is a link even if areCategoriesEnabled returns true', () => {
|
||||||
|
const categoryService = TestBed.inject(CategoryService);
|
||||||
|
spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(true);
|
||||||
|
component.node = { isLink: true, allowableOperations: [] } as Node;
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
expect(component.displayCategories).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
it('should have assigned displayTags to true if tagService.areTagsEnabled returns true', () => {
|
it('should have assigned displayTags to true if tagService.areTagsEnabled returns true', () => {
|
||||||
const tagService = TestBed.inject(TagService);
|
const tagService = TestBed.inject(TagService);
|
||||||
spyOn(tagService, 'areTagsEnabled').and.returnValue(true);
|
spyOn(tagService, 'areTagsEnabled').and.returnValue(true);
|
||||||
@@ -411,6 +420,15 @@ describe('MetadataTabComponent', () => {
|
|||||||
expect(tagService.areTagsEnabled).toHaveBeenCalled();
|
expect(tagService.areTagsEnabled).toHaveBeenCalled();
|
||||||
expect(getContentMetadata().displayTags).toBeFalse();
|
expect(getContentMetadata().displayTags).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have assigned displayTags to false if node is a link even if areTagsEnabled returns true', () => {
|
||||||
|
const tagService = TestBed.inject(TagService);
|
||||||
|
spyOn(tagService, 'areTagsEnabled').and.returnValue(true);
|
||||||
|
component.node = { isLink: true, allowableOperations: [] } as Node;
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
expect(component.displayTags).toBeFalse();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -98,8 +98,8 @@ export class MetadataTabComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._displayTags = this.tagService.areTagsEnabled();
|
this._displayTags = !this.node?.isLink && this.tagService.areTagsEnabled();
|
||||||
this._displayCategories = this.categoryService.areCategoriesEnabled();
|
this._displayCategories = !this.node?.isLink && this.categoryService.areCategoriesEnabled();
|
||||||
|
|
||||||
this.contentMetadataService.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((err: { message: string }) => {
|
this.contentMetadataService.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((err: { message: string }) => {
|
||||||
this.notificationService.showError(err.message);
|
this.notificationService.showError(err.message);
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class NodeInformationComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.nodeDetails.size = this.fileSizePipe.transform(this.node.content.sizeInBytes);
|
this.nodeDetails.size = this.fileSizePipe.transform(this.node.content?.sizeInBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DocumentListDirective } from './document-list.directive';
|
import { DocumentListDirective } from './document-list.directive';
|
||||||
import { Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
|
import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { DocumentListComponent, DocumentListService } from '@alfresco/adf-content-services';
|
import { DocumentListComponent, DocumentListService } from '@alfresco/adf-content-services';
|
||||||
import { UserPreferencesService } from '@alfresco/adf-core';
|
import { UserPreferencesService } from '@alfresco/adf-core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ElementRef } from '@angular/core';
|
||||||
|
import { AppHookService } from '@alfresco/aca-shared';
|
||||||
|
import { NodeEntry } from '@alfresco/js-api';
|
||||||
|
|
||||||
describe('DocumentListDirective', () => {
|
describe('DocumentListDirective', () => {
|
||||||
let documentListDirective: DocumentListDirective;
|
let documentListDirective: DocumentListDirective;
|
||||||
@@ -45,6 +48,7 @@ describe('DocumentListDirective', () => {
|
|||||||
setSorting: jasmine.createSpy('setSorting')
|
setSorting: jasmine.createSpy('setSorting')
|
||||||
},
|
},
|
||||||
selection: [],
|
selection: [],
|
||||||
|
preselectNodes: [],
|
||||||
reload: jasmine.createSpy('reload'),
|
reload: jasmine.createSpy('reload'),
|
||||||
resetSelection: jasmine.createSpy('resetSelection'),
|
resetSelection: jasmine.createSpy('resetSelection'),
|
||||||
ready: new Subject<any>(),
|
ready: new Subject<any>(),
|
||||||
@@ -80,35 +84,29 @@ describe('DocumentListDirective', () => {
|
|||||||
hasItem: jasmine.createSpy('hasItem')
|
hasItem: jasmine.createSpy('hasItem')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nodeToSelect$ = new BehaviorSubject<NodeEntry | null>(null);
|
||||||
|
const appHookServiceMock = { nodeToSelect$ };
|
||||||
|
|
||||||
|
const mockSelectedElement = { focus: jasmine.createSpy('focus') };
|
||||||
|
const elementRefMock = {
|
||||||
|
nativeElement: {
|
||||||
|
querySelector: jasmine.createSpy('querySelector').and.returnValue(mockSelectedElement)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [DocumentListDirective],
|
imports: [DocumentListDirective],
|
||||||
providers: [
|
providers: [
|
||||||
DocumentListDirective,
|
DocumentListDirective,
|
||||||
{
|
{ provide: Store, useValue: storeMock },
|
||||||
provide: Store,
|
{ provide: DocumentListComponent, useValue: documentListMock },
|
||||||
useValue: storeMock
|
{ provide: UserPreferencesService, useValue: userPreferencesServiceMock },
|
||||||
},
|
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||||
{
|
{ provide: Router, useValue: mockRouter },
|
||||||
provide: DocumentListComponent,
|
{ provide: DocumentListService, useValue: documentListServiceMock },
|
||||||
useValue: documentListMock
|
{ provide: AppHookService, useValue: appHookServiceMock },
|
||||||
},
|
{ provide: ElementRef, useValue: elementRefMock }
|
||||||
{
|
|
||||||
provide: UserPreferencesService,
|
|
||||||
useValue: userPreferencesServiceMock
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: mockRoute
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: Router,
|
|
||||||
useValue: mockRouter
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: DocumentListService,
|
|
||||||
useValue: documentListServiceMock
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
documentListDirective = TestBed.inject(DocumentListDirective);
|
documentListDirective = TestBed.inject(DocumentListDirective);
|
||||||
@@ -116,6 +114,11 @@ describe('DocumentListDirective', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
storeMock.dispatch.calls.reset();
|
storeMock.dispatch.calls.reset();
|
||||||
|
nodeToSelect$.next(null);
|
||||||
|
documentListMock.preselectNodes = [];
|
||||||
|
documentListMock.selection = [];
|
||||||
|
elementRefMock.nativeElement.querySelector.calls.reset();
|
||||||
|
mockSelectedElement.focus.calls.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update store selection on `documentList.ready` if route includes `viewer:view`', () => {
|
it('should not update store selection on `documentList.ready` if route includes `viewer:view`', () => {
|
||||||
@@ -278,4 +281,75 @@ describe('DocumentListDirective', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('nodeToSelect$', () => {
|
||||||
|
const pendingNode = { entry: { id: 'node-id', parentId: 'parent-id' } } as NodeEntry;
|
||||||
|
|
||||||
|
it('should set preselectNodes when a non-null node is emitted', () => {
|
||||||
|
documentListDirective.ngOnInit();
|
||||||
|
nodeToSelect$.next(pendingNode);
|
||||||
|
|
||||||
|
expect(documentListMock.preselectNodes).toEqual([pendingNode]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set preselectNodes when null is emitted (initial BehaviorSubject value)', () => {
|
||||||
|
documentListDirective.ngOnInit();
|
||||||
|
|
||||||
|
expect(documentListMock.preselectNodes).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onReady with pending node', () => {
|
||||||
|
const pendingNode = { entry: { id: 'node-id', parentId: 'parent-id' } } as NodeEntry;
|
||||||
|
let latestNodeToSelect: NodeEntry | null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
latestNodeToSelect = null;
|
||||||
|
nodeToSelect$.subscribe((value) => (latestNodeToSelect = value));
|
||||||
|
documentListDirective.ngOnInit();
|
||||||
|
nodeToSelect$.next(pendingNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus the selected row when the pending node is in the current selection', fakeAsync(() => {
|
||||||
|
documentListMock.selection = [{ entry: { id: 'node-id' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
documentListDirective.onReady();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(elementRefMock.nativeElement.querySelector).toHaveBeenCalledWith('.adf-is-selected');
|
||||||
|
expect(mockSelectedElement.focus).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should clear preselectNodes and reset nodeToSelect$ when node was selected', fakeAsync(() => {
|
||||||
|
documentListMock.selection = [{ entry: { id: 'node-id' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
documentListDirective.onReady();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(documentListMock.preselectNodes).toEqual([]);
|
||||||
|
expect(latestNodeToSelect).toBeNull();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not focus or clear state when pending node is not in the current selection', fakeAsync(() => {
|
||||||
|
documentListMock.selection = [];
|
||||||
|
|
||||||
|
documentListDirective.onReady();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(elementRefMock.nativeElement.querySelector).not.toHaveBeenCalled();
|
||||||
|
expect(mockSelectedElement.focus).not.toHaveBeenCalled();
|
||||||
|
expect(documentListMock.preselectNodes).toEqual([pendingNode]);
|
||||||
|
expect(latestNodeToSelect).toBe(pendingNode);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not attempt to focus or clear preselectNodes when no node is pending', fakeAsync(() => {
|
||||||
|
documentListDirective.ngOnInit();
|
||||||
|
|
||||||
|
documentListDirective.onReady();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(elementRefMock.nativeElement.querySelector).not.toHaveBeenCalled();
|
||||||
|
expect(documentListMock.preselectNodes).toEqual([]);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DestroyRef, Directive, HostListener, inject, OnInit } from '@angular/core';
|
import { DestroyRef, Directive, ElementRef, HostListener, inject, OnInit } from '@angular/core';
|
||||||
import { DocumentListComponent, DocumentListService } from '@alfresco/adf-content-services';
|
import { DocumentListComponent, DocumentListService } from '@alfresco/adf-content-services';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { UserPreferencesService } from '@alfresco/adf-core';
|
import { UserPreferencesService } from '@alfresco/adf-core';
|
||||||
@@ -31,6 +31,7 @@ import { SetSelectedNodesAction } from '@alfresco/aca-shared/store';
|
|||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { NodeEntry } from '@alfresco/js-api';
|
import { NodeEntry } from '@alfresco/js-api';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { AppHookService } from '@alfresco/aca-shared';
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -43,8 +44,12 @@ export class DocumentListDirective implements OnInit {
|
|||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly documentListService = inject(DocumentListService);
|
private readonly documentListService = inject(DocumentListService);
|
||||||
|
private readonly appHookService = inject(AppHookService);
|
||||||
|
private readonly elementRef = inject(ElementRef);
|
||||||
|
|
||||||
private isLibrary = false;
|
private isLibrary = false;
|
||||||
|
private pendingNode: NodeEntry | null = null;
|
||||||
|
|
||||||
selectedNode: NodeEntry;
|
selectedNode: NodeEntry;
|
||||||
|
|
||||||
get sortingPreferenceKey(): string {
|
get sortingPreferenceKey(): string {
|
||||||
@@ -55,7 +60,7 @@ export class DocumentListDirective implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.documentList.stickyHeader = true;
|
this.documentList.stickyHeader = true;
|
||||||
this.documentList.includeFields = ['isFavorite', 'aspectNames', 'definition'];
|
this.documentList.includeFields = ['isFavorite', 'aspectNames', 'definition', 'isLink'];
|
||||||
this.isLibrary =
|
this.isLibrary =
|
||||||
this.documentList.currentFolderId === '-mysites-' ||
|
this.documentList.currentFolderId === '-mysites-' ||
|
||||||
// workaround for custom node list
|
// workaround for custom node list
|
||||||
@@ -96,6 +101,16 @@ export class DocumentListDirective implements OnInit {
|
|||||||
this.documentListService.resetSelection$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
this.documentListService.resetSelection$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||||
this.reset();
|
this.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.appHookService.nodeToSelect$
|
||||||
|
.pipe(
|
||||||
|
filter((node): node is NodeEntry => node !== null),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe((node) => {
|
||||||
|
this.pendingNode = node;
|
||||||
|
this.documentList.preselectNodes = [node];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('sorting-changed', ['$event'])
|
@HostListener('sorting-changed', ['$event'])
|
||||||
@@ -147,6 +162,15 @@ export class DocumentListDirective implements OnInit {
|
|||||||
onReady() {
|
onReady() {
|
||||||
this.updateSelection();
|
this.updateSelection();
|
||||||
this.restoreSorting();
|
this.restoreSorting();
|
||||||
|
if (this.pendingNode) {
|
||||||
|
const wasSelected = this.documentList.selection.some((node) => node.entry?.id === this.pendingNode.entry.id);
|
||||||
|
if (wasSelected) {
|
||||||
|
setTimeout(() => this.elementRef.nativeElement.querySelector('.adf-is-selected')?.focus());
|
||||||
|
this.pendingNode = null;
|
||||||
|
this.documentList.preselectNodes = [];
|
||||||
|
this.appHookService.nodeToSelect$.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSelection() {
|
private updateSelection() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
AppStore,
|
AppStore,
|
||||||
CopyNodesAction,
|
CopyNodesAction,
|
||||||
DeleteNodesAction,
|
DeleteNodesAction,
|
||||||
|
LinkNodesAction,
|
||||||
MoveNodesAction,
|
MoveNodesAction,
|
||||||
NavigateRouteAction,
|
NavigateRouteAction,
|
||||||
NavigateToParentFolder,
|
NavigateToParentFolder,
|
||||||
@@ -44,7 +45,7 @@ import { AppTestingModule } from '../testing/app-testing.module';
|
|||||||
import { AppHookService, AppSettingsService, ContentApiService } from '@alfresco/aca-shared';
|
import { AppHookService, AppSettingsService, ContentApiService } from '@alfresco/aca-shared';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ContentManagementService } from './content-management.service';
|
import { ContentManagementService } from './content-management.service';
|
||||||
import { NodeActionsService } from './node-actions.service';
|
import { LinkOperationResult, NodeActionsService } from './node-actions.service';
|
||||||
import { ConfirmDialogComponent, DialogComponent, DialogSize, NotificationService, TranslationService } from '@alfresco/adf-core';
|
import { ConfirmDialogComponent, DialogComponent, DialogSize, NotificationService, TranslationService } from '@alfresco/adf-core';
|
||||||
import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatSnackBarModule, MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBarModule, MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
|
||||||
@@ -2101,4 +2102,224 @@ describe('ContentManagementService', () => {
|
|||||||
expect(mockElement.focus).toHaveBeenCalled();
|
expect(mockElement.focus).toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Link node action', () => {
|
||||||
|
let subject: Subject<string>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = new Subject<string>();
|
||||||
|
openSnackMessageActionSpy.and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => subject.complete());
|
||||||
|
|
||||||
|
it('notifies successful link of a single node', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
const linkedItems = [{ entry: { id: 'link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: linkedItems, failed: [] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_LINK.SINGULAR');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies successful link of multiple nodes', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-1', name: 'name1' } }, { entry: { id: 'node-to-link-2', name: 'name2' } }] as NodeEntry[];
|
||||||
|
const linkedItems = [{ entry: { id: 'link-of-node-1', name: 'name1' } }, { entry: { id: 'link-of-node-2', name: 'name2' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: linkedItems, failed: [] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_LINK.PLURAL');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies partial link of one node out of multiple selection', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-1', name: 'name1' } }, { entry: { id: 'node-to-link-2', name: 'name2' } }] as NodeEntry[];
|
||||||
|
const linkedItems = [{ entry: { id: 'link-of-node-1', name: 'name1' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: linkedItems, failed: [new Error('Server error')] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_LINK.PARTIAL_SINGULAR');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies failed link of a single node with a generic error', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: [], failed: [new Error('Server error')] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_LINK.FAIL_SINGULAR');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies duplicate link error when single node link fails with 409', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = {
|
||||||
|
succeeded: [],
|
||||||
|
failed: [new Error(JSON.stringify({ error: { statusCode: 409 } }))]
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.NODE_LINK_DUPLICATE');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies failed link of multiple nodes', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-0', name: 'name0' } }, { entry: { id: 'node-to-link-1', name: 'name1' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: [], failed: [new Error('Server error'), new Error('Server error')] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_LINK.FAIL_PLURAL');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies permission error on link failure', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
subject.error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(nodeActions.createLinkNodes).toHaveBeenCalled();
|
||||||
|
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
|
||||||
|
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should never show Undo action for link notifications', () => {
|
||||||
|
spyOn(nodeActions, 'createLinkNodes').and.returnValue(subject);
|
||||||
|
const selection = [{ entry: { id: 'node-to-link', name: 'name' } }] as NodeEntry[];
|
||||||
|
const linkedItems = [{ entry: { id: 'link-id', name: 'name' } }] as NodeEntry[];
|
||||||
|
const result: LinkOperationResult = { succeeded: linkedItems, failed: [] };
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction(selection));
|
||||||
|
nodeActions.contentLinked.next(result);
|
||||||
|
subject.next('OPERATION.SUCCESS.CONTENT.LINK');
|
||||||
|
|
||||||
|
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
|
||||||
|
expect(snackMessageCall[1]).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigateToLinkTarget', () => {
|
||||||
|
it('should dispatch NavigateToParentFolder when destination node is found', () => {
|
||||||
|
const originalNodeEntry: NodeEntry = {
|
||||||
|
entry: {
|
||||||
|
id: 'original-node-id',
|
||||||
|
name: 'original-file.txt',
|
||||||
|
nodeType: 'cm:content',
|
||||||
|
isFolder: false,
|
||||||
|
isFile: true,
|
||||||
|
modifiedAt: new Date(),
|
||||||
|
modifiedByUser: new UserInfo(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUser: new UserInfo(),
|
||||||
|
parentId: 'parent-folder-id'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkNode: NodeEntry = {
|
||||||
|
entry: {
|
||||||
|
id: 'link-node-id',
|
||||||
|
name: 'Link to original-file.txt.url',
|
||||||
|
nodeType: 'app:filelink',
|
||||||
|
isFolder: false,
|
||||||
|
isFile: false,
|
||||||
|
modifiedAt: new Date(),
|
||||||
|
modifiedByUser: new UserInfo(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUser: new UserInfo(),
|
||||||
|
properties: { 'cm:destination': 'original-node-id' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(contentApi, 'getNode').and.returnValue(of(originalNodeEntry));
|
||||||
|
spyOn(store, 'dispatch').and.callThrough();
|
||||||
|
|
||||||
|
contentManagementService.navigateToLinkTarget(linkNode);
|
||||||
|
|
||||||
|
expect(contentApi.getNode).toHaveBeenCalledWith('original-node-id');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new NavigateToParentFolder(originalNodeEntry) }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error when cm:destination property is missing', () => {
|
||||||
|
const linkNode: NodeEntry = {
|
||||||
|
entry: {
|
||||||
|
id: 'link-node-id',
|
||||||
|
name: 'Link to file.url',
|
||||||
|
nodeType: 'app:filelink',
|
||||||
|
isFolder: false,
|
||||||
|
isFile: false,
|
||||||
|
modifiedAt: new Date(),
|
||||||
|
modifiedByUser: new UserInfo(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUser: new UserInfo(),
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
contentManagementService.navigateToLinkTarget(linkNode);
|
||||||
|
|
||||||
|
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error when getNode API call fails', () => {
|
||||||
|
const linkNode: NodeEntry = {
|
||||||
|
entry: {
|
||||||
|
id: 'link-node-id',
|
||||||
|
name: 'Link to file.url',
|
||||||
|
nodeType: 'app:filelink',
|
||||||
|
isFolder: false,
|
||||||
|
isFile: false,
|
||||||
|
modifiedAt: new Date(),
|
||||||
|
modifiedByUser: new UserInfo(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUser: new UserInfo(),
|
||||||
|
properties: { 'cm:destination': 'original-node-id' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(contentApi, 'getNode').and.returnValue(throwError(() => new Error('Not found')));
|
||||||
|
|
||||||
|
contentManagementService.navigateToLinkTarget(linkNode);
|
||||||
|
|
||||||
|
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.GENERIC');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { forkJoin, Observable, of, zip } from 'rxjs';
|
import { forkJoin, Observable, of, zip } from 'rxjs';
|
||||||
import { catchError, map, mergeMap, take, tap } from 'rxjs/operators';
|
import { catchError, map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
import { NodeActionsService } from './node-actions.service';
|
import { LinkOperationResult, NodeActionsService } from './node-actions.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NodeInformationComponent } from '../dialogs/node-details/node-information.component';
|
import { NodeInformationComponent } from '../dialogs/node-details/node-information.component';
|
||||||
|
|
||||||
@@ -475,6 +475,34 @@ export class ContentManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkNodes(nodes: NodeEntry[], focusedElementOnCloseSelector?: string) {
|
||||||
|
zip(this.nodeActionsService.createLinkNodes(nodes, focusedElementOnCloseSelector), this.nodeActionsService.contentLinked.pipe(take(1))).subscribe(
|
||||||
|
{
|
||||||
|
next: ([, linkResponse]) => this.showLinkMessage(nodes, linkResponse),
|
||||||
|
error: (error) => this.showLinkMessage(nodes, { succeeded: [], failed: [error] })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLinkTarget(linkNode: NodeEntry): void {
|
||||||
|
const destinationId = linkNode.entry?.properties?.['cm:destination'];
|
||||||
|
if (!destinationId) {
|
||||||
|
this.notificationService.showError('APP.MESSAGES.ERRORS.GENERIC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contentApi.getNode(destinationId).subscribe({
|
||||||
|
next: (originalNode) => {
|
||||||
|
this.store.dispatch(new NavigateToParentFolder(originalNode));
|
||||||
|
this.appHookService.nodeToSelect$.next(originalNode);
|
||||||
|
this.documentListService.reload();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.notificationService.showError('APP.MESSAGES.ERRORS.GENERIC');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getErrorMessage(errorObject: { message: any }): string {
|
getErrorMessage(errorObject: { message: any }): string {
|
||||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||||
|
|
||||||
@@ -516,63 +544,76 @@ export class ContentManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showCopyMessage(info: any, nodes: Array<NodeEntry>, newItems?: Array<NodeEntry>) {
|
private showCopyMessage(info: any, nodes: Array<NodeEntry>, newItems?: Array<NodeEntry>) {
|
||||||
const numberOfCopiedItems = newItems ? newItems.length : 0;
|
const succeeded = newItems?.length ?? 0;
|
||||||
const failedItems = nodes.length - numberOfCopiedItems;
|
const failed = nodes.length - succeeded;
|
||||||
|
|
||||||
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC';
|
||||||
|
|
||||||
if (typeof info === 'string') {
|
if (typeof info === 'string' && info.toLowerCase().includes('succes')) {
|
||||||
if (info.toLowerCase().indexOf('succes') !== -1) {
|
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${this.getOperationMessageSuffix(succeeded, failed)}`;
|
||||||
let i18MessageSuffix;
|
} else if (typeof info !== 'string') {
|
||||||
|
|
||||||
if (failedItems) {
|
|
||||||
if (numberOfCopiedItems) {
|
|
||||||
i18MessageSuffix = numberOfCopiedItems === 1 ? 'PARTIAL_SINGULAR' : 'PARTIAL_PLURAL';
|
|
||||||
} else {
|
|
||||||
i18MessageSuffix = failedItems === 1 ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
i18MessageSuffix = numberOfCopiedItems === 1 ? 'SINGULAR' : 'PLURAL';
|
|
||||||
}
|
|
||||||
|
|
||||||
i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
error: { statusCode }
|
error: { statusCode }
|
||||||
} = JSON.parse(info.message);
|
} = JSON.parse(info.message);
|
||||||
|
|
||||||
if (statusCode === 403) {
|
if (statusCode === 403) {
|
||||||
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const undo = numberOfCopiedItems > 0 ? this.translation.instant('APP.ACTIONS.UNDO') : '';
|
const undo = succeeded > 0 ? this.translation.instant('APP.ACTIONS.UNDO') : '';
|
||||||
|
const message = this.translation.instant(i18nMessageString, { success: succeeded, failed });
|
||||||
const message = this.translation.instant(i18nMessageString, {
|
|
||||||
success: numberOfCopiedItems,
|
|
||||||
failed: failedItems
|
|
||||||
});
|
|
||||||
|
|
||||||
let messageType: string;
|
|
||||||
if (numberOfCopiedItems === 0) {
|
|
||||||
messageType = 'adf-error-snackbar';
|
|
||||||
} else if (failedItems > 0) {
|
|
||||||
messageType = 'adf-warning-snackbar';
|
|
||||||
} else {
|
|
||||||
messageType = 'adf-info-snackbar';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notificationService
|
this.notificationService
|
||||||
.openSnackMessageAction(message, undo, {
|
.openSnackMessageAction(message, undo, { panelClass: this.getSnackbarPanelClass(succeeded, failed) })
|
||||||
panelClass: messageType
|
|
||||||
})
|
|
||||||
.onAction()
|
.onAction()
|
||||||
.subscribe(() => this.undoCopyNodes(newItems));
|
.subscribe(() => this.undoCopyNodes(newItems));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showLinkMessage(nodes: Array<NodeEntry>, { succeeded: newItems, failed }: LinkOperationResult) {
|
||||||
|
const succeeded = newItems.length;
|
||||||
|
const totalFailed = nodes.length - succeeded;
|
||||||
|
|
||||||
|
let isDuplicate = false;
|
||||||
|
let isPermissionError = false;
|
||||||
|
if (nodes.length === 1 && succeeded === 0) {
|
||||||
|
try {
|
||||||
|
const statusCode = JSON.parse(failed[0].message).error.statusCode;
|
||||||
|
isDuplicate = statusCode === 409;
|
||||||
|
isPermissionError = statusCode === 403;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i18nMessageString: string;
|
||||||
|
if (isDuplicate) {
|
||||||
|
i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_LINK_DUPLICATE';
|
||||||
|
} else if (isPermissionError) {
|
||||||
|
i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION';
|
||||||
|
} else {
|
||||||
|
i18nMessageString = `APP.MESSAGES.INFO.NODE_LINK.${this.getOperationMessageSuffix(succeeded, totalFailed)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = this.translation.instant(i18nMessageString, { success: succeeded, failed: totalFailed });
|
||||||
|
this.notificationService.openSnackMessageAction(message, null, { panelClass: this.getSnackbarPanelClass(succeeded, totalFailed) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOperationMessageSuffix(succeeded: number, failed: number): string {
|
||||||
|
if (failed > 0 && succeeded > 0) {
|
||||||
|
return succeeded === 1 ? 'PARTIAL_SINGULAR' : 'PARTIAL_PLURAL';
|
||||||
|
}
|
||||||
|
if (failed > 0) {
|
||||||
|
return failed === 1 ? 'FAIL_SINGULAR' : 'FAIL_PLURAL';
|
||||||
|
}
|
||||||
|
return succeeded === 1 ? 'SINGULAR' : 'PLURAL';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSnackbarPanelClass(succeeded: number, failed: number): string {
|
||||||
|
if (succeeded === 0) {
|
||||||
|
return 'adf-error-snackbar';
|
||||||
|
}
|
||||||
|
return failed > 0 ? 'adf-warning-snackbar' : 'adf-info-snackbar';
|
||||||
|
}
|
||||||
|
|
||||||
private undoCopyNodes(nodes: NodeEntry[]) {
|
private undoCopyNodes(nodes: NodeEntry[]) {
|
||||||
const batch = this.nodeActionsService
|
const batch = this.nodeActionsService
|
||||||
.flatten(nodes)
|
.flatten(nodes)
|
||||||
@@ -697,6 +738,7 @@ export class ContentManagementService {
|
|||||||
|
|
||||||
deleteNodes(items: NodeEntry[], allowUndo = true, focusedElementOnCloseSelector?: string): void {
|
deleteNodes(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 batch: Observable<DeletedNodeInfo>[] = [];
|
const batch: Observable<DeletedNodeInfo>[] = [];
|
||||||
|
|
||||||
items.forEach((node) => {
|
items.forEach((node) => {
|
||||||
@@ -709,7 +751,7 @@ export class ContentManagementService {
|
|||||||
|
|
||||||
if (messageData && status.someSucceeded) {
|
if (messageData && status.someSucceeded) {
|
||||||
const translatedMessage: string = this.translation.instant(messageData.key, messageData.params);
|
const translatedMessage: string = this.translation.instant(messageData.key, messageData.params);
|
||||||
const action: string | null = allowUndo ? this.translation.instant('APP.ACTIONS.UNDO') : null;
|
const action: string | null = canUndo ? this.translation.instant('APP.ACTIONS.UNDO') : null;
|
||||||
|
|
||||||
const snackBarRef = this.notificationService.openSnackMessageAction(
|
const snackBarRef = this.notificationService.openSnackMessageAction(
|
||||||
translatedMessage,
|
translatedMessage,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dial
|
|||||||
import { of, throwError, Subject, Observable } from 'rxjs';
|
import { of, throwError, Subject, Observable } from 'rxjs';
|
||||||
import { TranslationService } from '@alfresco/adf-core';
|
import { TranslationService } from '@alfresco/adf-core';
|
||||||
import { AlfrescoApiService, DocumentListService, NodeAction } from '@alfresco/adf-content-services';
|
import { AlfrescoApiService, DocumentListService, NodeAction } from '@alfresco/adf-content-services';
|
||||||
import { NodeActionsService } from './node-actions.service';
|
import { LinkOperationResult, NodeActionsService } from './node-actions.service';
|
||||||
import { Node, NodeChildAssociationEntry, NodeEntry } from '@alfresco/js-api';
|
import { Node, NodeChildAssociationEntry, NodeEntry } from '@alfresco/js-api';
|
||||||
import { AppTestingModule } from '../testing/app-testing.module';
|
import { AppTestingModule } from '../testing/app-testing.module';
|
||||||
import { ContentApiService } from '@alfresco/aca-shared';
|
import { ContentApiService } from '@alfresco/aca-shared';
|
||||||
@@ -1228,4 +1228,122 @@ describe('NodeActionsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createLinkNodes', () => {
|
||||||
|
let fileToLink: TestNode;
|
||||||
|
let destinationFolder: TestNode;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileToLink = new TestNode(fileId, isFile, 'file-name');
|
||||||
|
destinationFolder = new TestNode(folderDestinationId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call doBatchOperation with NodeAction.LINK', () => {
|
||||||
|
const subject = new Subject<Node[]>();
|
||||||
|
const spyOnBatchOperation = spyOn(service, 'doBatchOperation').and.callThrough();
|
||||||
|
spyOn(service, 'getContentNodeSelection').and.returnValue(subject);
|
||||||
|
spyOn(service, 'linkNodeAction').and.returnValue(of({}));
|
||||||
|
|
||||||
|
service.createLinkNodes([fileToLink]);
|
||||||
|
subject.next([destinationFolder.entry]);
|
||||||
|
|
||||||
|
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.LINK, [fileToLink], undefined, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit on contentLinked after successful link', (done) => {
|
||||||
|
const subject = new Subject<Node[]>();
|
||||||
|
spyOn(service, 'getContentNodeSelection').and.returnValue(subject);
|
||||||
|
spyOn(service, 'linkNodeAction').and.returnValue(of({ entry: { id: 'new-link-id' } }));
|
||||||
|
|
||||||
|
service.contentLinked.subscribe((result: LinkOperationResult) => {
|
||||||
|
expect(result.succeeded.length).toBe(1);
|
||||||
|
expect(result.succeeded[0].entry.id).toBe('new-link-id');
|
||||||
|
expect(result.failed.length).toBe(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
service.createLinkNodes([fileToLink]);
|
||||||
|
subject.next([destinationFolder.entry]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if contentEntities is missing', (done) => {
|
||||||
|
service.createLinkNodes(undefined).subscribe({
|
||||||
|
next: () => spyOnSuccess(),
|
||||||
|
error: (error: Error) => {
|
||||||
|
spyOnError(error);
|
||||||
|
expect(spyOnSuccess).not.toHaveBeenCalled();
|
||||||
|
expect(spyOnError).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('linkNodeAction', () => {
|
||||||
|
let translationService: TranslationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
translationService = TestBed.inject(TranslationService);
|
||||||
|
spyOn(translationService, 'instant').and.callFake((key) => key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call nodesApi.createNode with app:filelink type for a file', (done) => {
|
||||||
|
const fileEntry = new TestNode(fileId, isFile, 'my-file.txt').entry;
|
||||||
|
spyOn(nodesApi, 'createNode').and.returnValue(Promise.resolve({ entry: { id: 'link-id' } }));
|
||||||
|
|
||||||
|
service.linkNodeAction(fileEntry, folderDestinationId).subscribe(() => {
|
||||||
|
expect(nodesApi.createNode).toHaveBeenCalledWith(
|
||||||
|
folderDestinationId,
|
||||||
|
jasmine.objectContaining({
|
||||||
|
nodeType: 'app:filelink',
|
||||||
|
properties: jasmine.objectContaining({ 'cm:destination': fileId })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call nodesApi.createNode with app:folderlink type for a folder', (done) => {
|
||||||
|
const folderEntry = new TestNode('folder-id', !isFile, 'my-folder').entry;
|
||||||
|
spyOn(nodesApi, 'createNode').and.returnValue(Promise.resolve({ entry: { id: 'link-id' } } as NodeEntry));
|
||||||
|
|
||||||
|
service.linkNodeAction(folderEntry, folderDestinationId).subscribe(() => {
|
||||||
|
expect(nodesApi.createNode).toHaveBeenCalledWith(folderDestinationId, jasmine.objectContaining({ nodeType: 'app:folderlink' }));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append .url suffix to link name if not already present', (done) => {
|
||||||
|
const fileEntry = new TestNode(fileId, isFile, 'report').entry;
|
||||||
|
spyOn(nodesApi, 'createNode').and.returnValue(Promise.resolve({ entry: { id: 'link-id' } } as NodeEntry));
|
||||||
|
|
||||||
|
service.linkNodeAction(fileEntry, folderDestinationId).subscribe(() => {
|
||||||
|
const body = (nodesApi.createNode as jasmine.Spy).calls.mostRecent().args[1];
|
||||||
|
expect(body.name.endsWith('.url')).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not double-append .url suffix if name already ends with .url', (done) => {
|
||||||
|
const fileEntry = new TestNode(fileId, isFile, 'report').entry;
|
||||||
|
spyOn(nodesApi, 'createNode').and.returnValue(Promise.resolve({ entry: { id: 'link-id' } } as NodeEntry));
|
||||||
|
|
||||||
|
service.linkNodeAction(fileEntry, folderDestinationId).subscribe(() => {
|
||||||
|
const body = (nodesApi.createNode as jasmine.Spy).calls.mostRecent().args[1];
|
||||||
|
expect(body.name).toBe('NODE_SELECTOR.LINK_NAME.url');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use nodeId over id when building the link body', (done) => {
|
||||||
|
const sharedFileEntry: any = { id: 'original-id', nodeId: 'shared-node-id', name: 'shared.txt', isFile: true, isFolder: false };
|
||||||
|
spyOn(nodesApi, 'createNode').and.returnValue(Promise.resolve({ entry: { id: 'link-id' } } as NodeEntry));
|
||||||
|
|
||||||
|
service.linkNodeAction(sharedFileEntry, folderDestinationId).subscribe(() => {
|
||||||
|
const body = (nodesApi.createNode as jasmine.Spy).calls.mostRecent().args[1];
|
||||||
|
expect(body.properties['cm:destination']).toBe('shared-node-id');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
NodeEntry,
|
NodeEntry,
|
||||||
Node,
|
Node,
|
||||||
|
SharedLink,
|
||||||
SitePaging,
|
SitePaging,
|
||||||
NodeChildAssociationPaging,
|
NodeChildAssociationPaging,
|
||||||
NodeChildAssociationEntry,
|
NodeChildAssociationEntry,
|
||||||
@@ -50,7 +51,12 @@ import {
|
|||||||
import { ContentApiService } from '@alfresco/aca-shared';
|
import { ContentApiService } from '@alfresco/aca-shared';
|
||||||
import { catchError, map, mergeMap } from 'rxjs/operators';
|
import { catchError, map, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
type BatchOperationType = Extract<NodeAction, 'COPY' | 'MOVE'>;
|
type BatchOperationType = Extract<NodeAction, 'COPY' | 'MOVE' | 'LINK'>;
|
||||||
|
|
||||||
|
export interface LinkOperationResult {
|
||||||
|
succeeded: NodeEntry[];
|
||||||
|
failed: Error[];
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -66,6 +72,7 @@ export class NodeActionsService {
|
|||||||
|
|
||||||
contentCopied: Subject<NodeEntry[]> = new Subject<NodeEntry[]>();
|
contentCopied: Subject<NodeEntry[]> = new Subject<NodeEntry[]>();
|
||||||
contentMoved: Subject<any> = new Subject<any>();
|
contentMoved: Subject<any> = new Subject<any>();
|
||||||
|
contentLinked = new Subject<LinkOperationResult>();
|
||||||
moveDeletedEntries: any[] = [];
|
moveDeletedEntries: any[] = [];
|
||||||
isSitesDestinationAvailable = false;
|
isSitesDestinationAvailable = false;
|
||||||
|
|
||||||
@@ -83,6 +90,16 @@ export class NodeActionsService {
|
|||||||
return this.doBatchOperation(NodeAction.COPY, contentEntities, permission, focusedElementOnCloseSelector);
|
return this.doBatchOperation(NodeAction.COPY, contentEntities, permission, focusedElementOnCloseSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create links for node list
|
||||||
|
*
|
||||||
|
* @param contentEntities nodes to create links for
|
||||||
|
* @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal
|
||||||
|
*/
|
||||||
|
createLinkNodes(contentEntities: any[], focusedElementOnCloseSelector?: string): Subject<string> {
|
||||||
|
return this.doBatchOperation(NodeAction.LINK, contentEntities, undefined, focusedElementOnCloseSelector);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move node list
|
* Move node list
|
||||||
*
|
*
|
||||||
@@ -138,7 +155,11 @@ export class NodeActionsService {
|
|||||||
this.contentCopied.next(processedData.succeeded);
|
this.contentCopied.next(processedData.succeeded);
|
||||||
} else if (action === NodeAction.MOVE) {
|
} else if (action === NodeAction.MOVE) {
|
||||||
this.contentMoved.next(processedData);
|
this.contentMoved.next(processedData);
|
||||||
|
} else if (action === NodeAction.LINK) {
|
||||||
|
this.contentLinked.next(processedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observable.complete();
|
||||||
}, observable.error.bind(observable));
|
}, observable.error.bind(observable));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -339,6 +360,19 @@ export class NodeActionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkNodeAction(nodeEntry: Node | SharedLink, destinationFolderId: string): Observable<NodeEntry> {
|
||||||
|
const sourceNodeId = (nodeEntry as SharedLink).nodeId || nodeEntry.id;
|
||||||
|
const isFolder = (nodeEntry as Node).isFolder ?? false;
|
||||||
|
const baseName = this.translation.instant('NODE_SELECTOR.LINK_NAME', { name: nodeEntry.name });
|
||||||
|
const linkName = baseName.endsWith('.url') ? baseName : `${baseName}.url`;
|
||||||
|
const nodeBody = {
|
||||||
|
name: linkName,
|
||||||
|
nodeType: isFolder ? 'app:folderlink' : 'app:filelink',
|
||||||
|
properties: { 'cm:destination': sourceNodeId, 'cm:description': linkName, 'cm:title': linkName }
|
||||||
|
};
|
||||||
|
return from(this.nodesApi.createNode(destinationFolderId, nodeBody)).pipe(catchError((err) => of(err)));
|
||||||
|
}
|
||||||
|
|
||||||
copyContentAction(contentEntry: any, selectionId: string, oldName?: string): Observable<any> {
|
copyContentAction(contentEntry: any, selectionId: string, oldName?: string): Observable<any> {
|
||||||
const _oldName = oldName || contentEntry.name;
|
const _oldName = oldName || contentEntry.name;
|
||||||
// Check if there's nodeId for Shared Files
|
// Check if there's nodeId for Shared Files
|
||||||
@@ -549,7 +583,7 @@ export class NodeActionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isActionAllowed(action: BatchOperationType, node: Node, permission?: string): boolean {
|
private isActionAllowed(action: BatchOperationType, node: Node, permission?: string): boolean {
|
||||||
if (action === NodeAction.COPY) {
|
if (action === NodeAction.COPY || action === NodeAction.LINK) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.contentService.hasAllowableOperations(node, permission);
|
return this.contentService.hasAllowableOperations(node, permission);
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ import {
|
|||||||
ShowLoaderAction,
|
ShowLoaderAction,
|
||||||
UndoDeleteNodesAction,
|
UndoDeleteNodesAction,
|
||||||
UnlockWriteAction,
|
UnlockWriteAction,
|
||||||
UnshareNodesAction
|
UnshareNodesAction,
|
||||||
|
LinkNodesAction,
|
||||||
|
LocateLinkedItemAction
|
||||||
} from '@alfresco/aca-shared/store';
|
} from '@alfresco/aca-shared/store';
|
||||||
import { RenditionService } from '@alfresco/adf-content-services';
|
import { RenditionService } from '@alfresco/adf-content-services';
|
||||||
import { ViewerEffects } from './viewer.effects';
|
import { ViewerEffects } from './viewer.effects';
|
||||||
@@ -390,6 +392,70 @@ describe('NodeEffects', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createLink$', () => {
|
||||||
|
it('should link nodes from the payload', () => {
|
||||||
|
spyOn(contentService, 'linkNodes').and.stub();
|
||||||
|
|
||||||
|
const node = { entry: { isFile: true } } as NodeEntry;
|
||||||
|
store.dispatch(new LinkNodesAction([node]));
|
||||||
|
|
||||||
|
expect(contentService.linkNodes).toHaveBeenCalledWith([node]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link nodes from the active selection', fakeAsync(() => {
|
||||||
|
spyOn(contentService, 'linkNodes').and.stub();
|
||||||
|
|
||||||
|
const node = { entry: { isFile: true } } as NodeEntry;
|
||||||
|
store.dispatch(new SetSelectedNodesAction([node]));
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction([]));
|
||||||
|
|
||||||
|
expect(contentService.linkNodes).toHaveBeenCalledWith([node], undefined);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should do nothing if invoking link with no data', () => {
|
||||||
|
spyOn(contentService, 'linkNodes').and.stub();
|
||||||
|
|
||||||
|
store.dispatch(new LinkNodesAction([]));
|
||||||
|
|
||||||
|
expect(contentService.linkNodes).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('locateLinkedItem$', () => {
|
||||||
|
it('should navigate to link target from the payload', () => {
|
||||||
|
spyOn(contentService, 'navigateToLinkTarget').and.stub();
|
||||||
|
|
||||||
|
const node = { entry: { isFile: true, id: 'link-node-id' } } as NodeEntry;
|
||||||
|
store.dispatch(new LocateLinkedItemAction([node]));
|
||||||
|
|
||||||
|
expect(contentService.navigateToLinkTarget).toHaveBeenCalledWith(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to link target from the active selection', fakeAsync(() => {
|
||||||
|
spyOn(contentService, 'navigateToLinkTarget').and.stub();
|
||||||
|
|
||||||
|
const node = { entry: { isFile: true, id: 'link-node-id' } } as NodeEntry;
|
||||||
|
store.dispatch(new SetSelectedNodesAction([node]));
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
store.dispatch(new LocateLinkedItemAction(null));
|
||||||
|
|
||||||
|
expect(contentService.navigateToLinkTarget).toHaveBeenCalledWith(node);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should do nothing if invoking locate with no data', () => {
|
||||||
|
spyOn(contentService, 'navigateToLinkTarget').and.stub();
|
||||||
|
|
||||||
|
store.dispatch(new LocateLinkedItemAction(null));
|
||||||
|
|
||||||
|
expect(contentService.navigateToLinkTarget).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('managePermissions$', () => {
|
describe('managePermissions$', () => {
|
||||||
it('should manage permissions from the payload', () => {
|
it('should manage permissions from the payload', () => {
|
||||||
spyOn(router, 'navigateByUrl').and.stub();
|
spyOn(router, 'navigateByUrl').and.stub();
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ import {
|
|||||||
UndoDeleteNodesAction,
|
UndoDeleteNodesAction,
|
||||||
UnlockWriteAction,
|
UnlockWriteAction,
|
||||||
UnshareNodesAction,
|
UnshareNodesAction,
|
||||||
NodeInformationAction
|
NodeInformationAction,
|
||||||
|
LinkNodesAction,
|
||||||
|
LocateLinkedItemAction
|
||||||
} from '@alfresco/aca-shared/store';
|
} from '@alfresco/aca-shared/store';
|
||||||
import { ContentManagementService } from '../../services/content-management.service';
|
import { ContentManagementService } from '../../services/content-management.service';
|
||||||
import { RenditionService } from '@alfresco/adf-content-services';
|
import { RenditionService } from '@alfresco/adf-content-services';
|
||||||
@@ -281,6 +283,50 @@ export class NodeEffects {
|
|||||||
{ dispatch: false }
|
{ dispatch: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
createLink$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType<LinkNodesAction>(NodeActionTypes.Link),
|
||||||
|
map((action) => {
|
||||||
|
if (action.payload?.length > 0) {
|
||||||
|
this.contentService.linkNodes(action.payload);
|
||||||
|
} else {
|
||||||
|
this.store
|
||||||
|
.select(getAppSelection)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((selection) => {
|
||||||
|
if (selection && !selection.isEmpty) {
|
||||||
|
this.contentService.linkNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
locateLinkedItem$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType<LocateLinkedItemAction>(NodeActionTypes.Locate),
|
||||||
|
map((action) => {
|
||||||
|
if (action.payload?.length > 0) {
|
||||||
|
this.contentService.navigateToLinkTarget(action.payload[0]);
|
||||||
|
} else {
|
||||||
|
this.store
|
||||||
|
.select(getAppSelection)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((selection) => {
|
||||||
|
if (selection && !selection.isEmpty) {
|
||||||
|
this.contentService.navigateToLinkTarget(selection.nodes[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
|
|
||||||
managePermissions$ = createEffect(
|
managePermissions$ = createEffect(
|
||||||
() =>
|
() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
|
|||||||
@@ -71,6 +71,27 @@ describe('app.evaluators', () => {
|
|||||||
|
|
||||||
expect(app.canDownloadSelection(context)).toBe(false);
|
expect(app.canDownloadSelection(context)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not allow downloading when selection contains a file link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink', isFile: true } }] as NodeEntry[];
|
||||||
|
|
||||||
|
expect(app.canDownloadSelection(context)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow downloading when selection contains a folder link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink', isFolder: true } }] as NodeEntry[];
|
||||||
|
|
||||||
|
expect(app.canDownloadSelection(context)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow downloading when any node in multi-selection is a link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { isFile: true } }, { entry: { nodeType: 'app:filelink', isFile: true } }] as NodeEntry[];
|
||||||
|
|
||||||
|
expect(app.canDownloadSelection(context)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isWriteLocked', () => {
|
describe('isWriteLocked', () => {
|
||||||
@@ -212,6 +233,22 @@ describe('app.evaluators', () => {
|
|||||||
|
|
||||||
expect(app.canUploadVersion(context)).toBe(true);
|
expect(app.canUploadVersion(context)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return [false] when any selected node is a file link', () => {
|
||||||
|
context.navigation.url = '/personal-files';
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
expect(app.canUploadVersion(context)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return [false] when any selected node is a folder link', () => {
|
||||||
|
context.navigation.url = '/personal-files';
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink' } }] as NodeEntry[];
|
||||||
|
|
||||||
|
expect(app.canUploadVersion(context)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isShared', () => {
|
describe('isShared', () => {
|
||||||
@@ -397,6 +434,26 @@ describe('app.evaluators', () => {
|
|||||||
|
|
||||||
expect(app.canOpenWithOffice(context)).toBeTruthy();
|
expect(app.canOpenWithOffice(context)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return [false] when any selected node is a file link', () => {
|
||||||
|
context.appConfig = { get: () => true } as any;
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
context.selection.file = { entry: { name: 'document.docx', isLocked: false, properties: {} } } as NodeEntry;
|
||||||
|
context.permissions = { check: () => true };
|
||||||
|
|
||||||
|
expect(app.canOpenWithOffice(context)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return [false] when any selected node is a folder link', () => {
|
||||||
|
context.appConfig = { get: () => true } as any;
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink' } }] as NodeEntry[];
|
||||||
|
context.selection.file = { entry: { name: 'document.docx', isLocked: false, properties: {} } } as NodeEntry;
|
||||||
|
context.permissions = { check: () => true };
|
||||||
|
|
||||||
|
expect(app.canOpenWithOffice(context)).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canEditAspects', () => {
|
describe('canEditAspects', () => {
|
||||||
@@ -736,6 +793,22 @@ describe('app.evaluators', () => {
|
|||||||
context.repository.status.isQuickShareEnabled = true;
|
context.repository.status.isQuickShareEnabled = true;
|
||||||
expect(app.canShareFile(context)).toBeTrue();
|
expect(app.canShareFile(context)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false when selection contains a file link node', () => {
|
||||||
|
context.selection.file = { entry: { properties: {} } } as NodeEntry;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
context.navigation.url = '/personal-files';
|
||||||
|
context.repository.status.isQuickShareEnabled = true;
|
||||||
|
expect(app.canShareFile(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when selection contains a folder link node', () => {
|
||||||
|
context.selection.file = { entry: { properties: {} } } as NodeEntry;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink' } }] as NodeEntry[];
|
||||||
|
context.navigation.url = '/personal-files';
|
||||||
|
context.repository.status.isQuickShareEnabled = true;
|
||||||
|
expect(app.canShareFile(context)).toBeFalse();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canToggleJoinLibrary', () => {
|
describe('canToggleJoinLibrary', () => {
|
||||||
@@ -1133,6 +1206,22 @@ describe('app.evaluators', () => {
|
|||||||
context.permissions = { check: () => false };
|
context.permissions = { check: () => false };
|
||||||
expect(app.canToggleFileLock(context)).toBeTrue();
|
expect(app.canToggleFileLock(context)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false when any selected node is a file link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
context.selection.file = { entry: { properties: {} } } as NodeEntry;
|
||||||
|
context.permissions = { check: () => true };
|
||||||
|
expect(app.canToggleFileLock(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when any selected node is a folder link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink' } }] as NodeEntry[];
|
||||||
|
context.selection.file = { entry: { properties: {} } } as NodeEntry;
|
||||||
|
context.permissions = { check: () => true };
|
||||||
|
expect(app.canToggleFileLock(context)).toBeFalse();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canPrintFile', () => {
|
describe('canPrintFile', () => {
|
||||||
@@ -1223,6 +1312,49 @@ describe('app.evaluators', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isNodeLink', () => {
|
||||||
|
it('should return false when selection is empty', () => {
|
||||||
|
context.selection.isEmpty = true;
|
||||||
|
expect(app.isNodeLink(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when selected node has no nodeType', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { isFile: true } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when selected node has an unrelated nodeType', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'cm:content' } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when selected node has nodeType app:filelink', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when selected node has nodeType app:folderlink', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'app:folderlink' } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when any node in multi-selection is a link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'cm:content' } }, { entry: { nodeType: 'app:filelink' } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no node in multi-selection is a link', () => {
|
||||||
|
context.selection.isEmpty = false;
|
||||||
|
context.selection.nodes = [{ entry: { nodeType: 'cm:content' } }, { entry: { nodeType: 'cm:folder' } }] as NodeEntry[];
|
||||||
|
expect(app.isNodeLink(context)).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isSSOEnabled', () => {
|
describe('isSSOEnabled', () => {
|
||||||
it('should return true if sso is enabled', () => {
|
it('should return true if sso is enabled', () => {
|
||||||
context.appConfig = { get: () => 'OAUTH' } as any;
|
context.appConfig = { get: () => 'OAUTH' } as any;
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export function canRemoveFavorite(context: RuleContext): boolean {
|
|||||||
* JSON ref: `app.selection.file.canShare`
|
* JSON ref: `app.selection.file.canShare`
|
||||||
*/
|
*/
|
||||||
export const canShareFile = (context: RuleContext): boolean =>
|
export const canShareFile = (context: RuleContext): boolean =>
|
||||||
[context.selection.file, !navigation.isTrashcan(context), repository.hasQuickShareEnabled(context), !isShared(context)].every(Boolean);
|
[context.selection.file, !navigation.isTrashcan(context), repository.hasQuickShareEnabled(context), !isShared(context), !isNodeLink(context)].every(
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if user can perform "Join" or "Cancel Join Request" on a library.
|
* Checks if user can perform "Join" or "Cancel Join Request" on a library.
|
||||||
@@ -208,6 +210,9 @@ export function canCreateFolder(context: AcaRuleContext): boolean {
|
|||||||
* JSON ref: `app.selection.canDownload`
|
* JSON ref: `app.selection.canDownload`
|
||||||
*/
|
*/
|
||||||
export function canDownloadSelection(context: RuleContext): boolean {
|
export function canDownloadSelection(context: RuleContext): boolean {
|
||||||
|
if (isNodeLink(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return context.selection.nodes.every((node: any) => node.entry && (node.entry.isFile || node.entry.isFolder || !!node.entry.nodeId));
|
return context.selection.nodes.every((node: any) => node.entry && (node.entry.isFile || node.entry.isFolder || !!node.entry.nodeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +349,10 @@ export function canUnlockFile(context: RuleContext): boolean {
|
|||||||
* JSON ref: `app.selection.file.canUploadVersion`
|
* JSON ref: `app.selection.file.canUploadVersion`
|
||||||
*/
|
*/
|
||||||
export function canUploadVersion(context: RuleContext): boolean {
|
export function canUploadVersion(context: RuleContext): boolean {
|
||||||
|
if (isNodeLink(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (navigation.isFavorites(context) || navigation.isSharedFiles(context)) {
|
if (navigation.isFavorites(context) || navigation.isSharedFiles(context)) {
|
||||||
return hasFileSelected(context);
|
return hasFileSelected(context);
|
||||||
}
|
}
|
||||||
@@ -363,6 +372,9 @@ export function canUploadVersion(context: RuleContext): boolean {
|
|||||||
*/
|
*/
|
||||||
export const canPrintFile = (context: RuleContext): boolean => {
|
export const canPrintFile = (context: RuleContext): boolean => {
|
||||||
const nodeEntry = context.selection.file.entry;
|
const nodeEntry = context.selection.file.entry;
|
||||||
|
if (!nodeEntry?.content?.mimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const mediaMimeTypes = ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav'];
|
const mediaMimeTypes = ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav'];
|
||||||
return !mediaMimeTypes.includes(nodeEntry.content.mimeType);
|
return !mediaMimeTypes.includes(nodeEntry.content.mimeType);
|
||||||
};
|
};
|
||||||
@@ -390,7 +402,8 @@ export const canEditAspects = (context: RuleContext): boolean =>
|
|||||||
repository.isMajorVersionAvailable(context, '7')
|
repository.isMajorVersionAvailable(context, '7')
|
||||||
].every(Boolean);
|
].every(Boolean);
|
||||||
|
|
||||||
export const canToggleFileLock = (context: RuleContext): boolean => [canLockFile(context) || canUnlockFile(context)].some(Boolean);
|
export const canToggleFileLock = (context: RuleContext): boolean =>
|
||||||
|
!isNodeLink(context) && [canLockFile(context) || canUnlockFile(context)].some(Boolean);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Uses workarounds for for recent files and search api issues.
|
* @deprecated Uses workarounds for for recent files and search api issues.
|
||||||
@@ -434,6 +447,10 @@ export function canOpenWithOffice(context: AcaRuleContext): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNodeLink(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.navigation?.url?.startsWith('/trashcan')) {
|
if (context.navigation?.url?.startsWith('/trashcan')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -577,3 +594,13 @@ export const isCheckedOut = (context: RuleContext): boolean => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any of the selected nodes is a link node (app:filelink or app:folderlink).
|
||||||
|
* JSON ref: `app.selection.isNodeLink`
|
||||||
|
*
|
||||||
|
* @param context Rule execution context
|
||||||
|
*/
|
||||||
|
export const isNodeLink = (context: RuleContext): boolean =>
|
||||||
|
!context.selection?.isEmpty &&
|
||||||
|
context.selection.nodes.some((node) => node.entry?.nodeType === 'app:filelink' || node.entry?.nodeType === 'app:folderlink');
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
import { SiteEntry } from '@alfresco/js-api';
|
import { NodeEntry, SiteEntry } from '@alfresco/js-api';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -84,4 +84,9 @@ export class AppHookService {
|
|||||||
* Gets emitted when user mark the favorite library
|
* Gets emitted when user mark the favorite library
|
||||||
*/
|
*/
|
||||||
favoriteLibraryToggle = new Subject<void>();
|
favoriteLibraryToggle = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the node that should be pre-selected in the document list after navigation.
|
||||||
|
*/
|
||||||
|
nodeToSelect$ = new BehaviorSubject<NodeEntry | null>(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class ContentApiService {
|
|||||||
*/
|
*/
|
||||||
getNode(nodeId: string, options: any = {}): Observable<NodeEntry> {
|
getNode(nodeId: string, options: any = {}): Observable<NodeEntry> {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
include: ['path', 'properties', 'allowableOperations', 'permissions', 'definition']
|
include: ['path', 'properties', 'allowableOperations', 'permissions', 'definition', 'isLink']
|
||||||
};
|
};
|
||||||
const queryOptions = Object.assign(defaults, options);
|
const queryOptions = Object.assign(defaults, options);
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ export class ContentApiService {
|
|||||||
|
|
||||||
getNodeInfo(nodeId: string, options?: any): Observable<Node> {
|
getNodeInfo(nodeId: string, options?: any): Observable<Node> {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
include: ['isFavorite', 'allowableOperations', 'path', 'definition']
|
include: ['isFavorite', 'allowableOperations', 'path', 'definition', 'isLink']
|
||||||
};
|
};
|
||||||
const queryOptions = Object.assign(defaults, options || {});
|
const queryOptions = Object.assign(defaults, options || {});
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export enum NodeActionTypes {
|
|||||||
Unshare = 'UNSHARE_NODES',
|
Unshare = 'UNSHARE_NODES',
|
||||||
Copy = 'COPY_NODES',
|
Copy = 'COPY_NODES',
|
||||||
Move = 'MOVE_NODES',
|
Move = 'MOVE_NODES',
|
||||||
|
Link = 'CREATE_LINK',
|
||||||
|
Locate = 'LOCATE_LINKED_ITEM',
|
||||||
NodeInformation = 'NODE_INFORMATION',
|
NodeInformation = 'NODE_INFORMATION',
|
||||||
ManagePermissions = 'MANAGE_PERMISSIONS',
|
ManagePermissions = 'MANAGE_PERMISSIONS',
|
||||||
PrintFile = 'PRINT_FILE',
|
PrintFile = 'PRINT_FILE',
|
||||||
@@ -149,6 +151,21 @@ export class MoveNodesAction implements Action {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class LinkNodesAction implements Action {
|
||||||
|
readonly type = NodeActionTypes.Link;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public payload: Array<NodeEntry>,
|
||||||
|
public configuration?: ModalConfiguration
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LocateLinkedItemAction implements Action {
|
||||||
|
readonly type = NodeActionTypes.Locate;
|
||||||
|
|
||||||
|
constructor(public payload: Array<NodeEntry>) {}
|
||||||
|
}
|
||||||
|
|
||||||
export class ManagePermissionsAction implements Action {
|
export class ManagePermissionsAction implements Action {
|
||||||
readonly type = NodeActionTypes.ManagePermissions;
|
readonly type = NodeActionTypes.ManagePermissions;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user