[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:
Mykyta Maliarchuk
2026-05-22 17:10:40 +02:00
committed by GitHub
parent c4ff14f359
commit 286bf1b9b4
23 changed files with 1100 additions and 100 deletions
@@ -71,6 +71,27 @@ describe('app.evaluators', () => {
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', () => {
@@ -212,6 +233,22 @@ describe('app.evaluators', () => {
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', () => {
@@ -397,6 +434,26 @@ describe('app.evaluators', () => {
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', () => {
@@ -736,6 +793,22 @@ describe('app.evaluators', () => {
context.repository.status.isQuickShareEnabled = true;
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', () => {
@@ -1133,6 +1206,22 @@ describe('app.evaluators', () => {
context.permissions = { check: () => false };
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', () => {
@@ -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', () => {
it('should return true if sso is enabled', () => {
context.appConfig = { get: () => 'OAUTH' } as any;
+29 -2
View File
@@ -121,7 +121,9 @@ export function canRemoveFavorite(context: RuleContext): boolean {
* JSON ref: `app.selection.file.canShare`
*/
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.
@@ -208,6 +210,9 @@ export function canCreateFolder(context: AcaRuleContext): boolean {
* JSON ref: `app.selection.canDownload`
*/
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));
}
@@ -344,6 +349,10 @@ export function canUnlockFile(context: RuleContext): boolean {
* JSON ref: `app.selection.file.canUploadVersion`
*/
export function canUploadVersion(context: RuleContext): boolean {
if (isNodeLink(context)) {
return false;
}
if (navigation.isFavorites(context) || navigation.isSharedFiles(context)) {
return hasFileSelected(context);
}
@@ -363,6 +372,9 @@ export function canUploadVersion(context: RuleContext): boolean {
*/
export const canPrintFile = (context: RuleContext): boolean => {
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'];
return !mediaMimeTypes.includes(nodeEntry.content.mimeType);
};
@@ -390,7 +402,8 @@ export const canEditAspects = (context: RuleContext): boolean =>
repository.isMajorVersionAvailable(context, '7')
].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.
@@ -434,6 +447,10 @@ export function canOpenWithOffice(context: AcaRuleContext): boolean {
return false;
}
if (isNodeLink(context)) {
return false;
}
if (context.navigation?.url?.startsWith('/trashcan')) {
return false;
}
@@ -577,3 +594,13 @@ export const isCheckedOut = (context: RuleContext): boolean => {
}
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 { Subject } from 'rxjs';
import { SiteEntry } from '@alfresco/js-api';
import { BehaviorSubject, Subject } from 'rxjs';
import { NodeEntry, SiteEntry } from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
@@ -84,4 +84,9 @@ export class AppHookService {
* Gets emitted when user mark the favorite library
*/
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> {
const defaults = {
include: ['path', 'properties', 'allowableOperations', 'permissions', 'definition']
include: ['path', 'properties', 'allowableOperations', 'permissions', 'definition', 'isLink']
};
const queryOptions = Object.assign(defaults, options);
@@ -123,7 +123,7 @@ export class ContentApiService {
getNodeInfo(nodeId: string, options?: any): Observable<Node> {
const defaults = {
include: ['isFavorite', 'allowableOperations', 'path', 'definition']
include: ['isFavorite', 'allowableOperations', 'path', 'definition', 'isLink']
};
const queryOptions = Object.assign(defaults, options || {});
@@ -39,6 +39,8 @@ export enum NodeActionTypes {
Unshare = 'UNSHARE_NODES',
Copy = 'COPY_NODES',
Move = 'MOVE_NODES',
Link = 'CREATE_LINK',
Locate = 'LOCATE_LINKED_ITEM',
NodeInformation = 'NODE_INFORMATION',
ManagePermissions = 'MANAGE_PERMISSIONS',
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 {
readonly type = NodeActionTypes.ManagePermissions;