diff --git a/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts b/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts index 7894afab214ec..b2937024f5a5f 100644 --- a/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts +++ b/apps/comments/src/actions/inlineUnreadCommentsAction.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { View } from '@nextcloud/files' +import type { Folder, View } from '@nextcloud/files' import { File, FileAction, Permission } from '@nextcloud/files' import { describe, expect, test, vi } from 'vitest' @@ -26,15 +26,41 @@ describe('Inline unread comments action display name tests', () => { attributes: { 'comments-unread': 1, }, + root: '/files/admin', }) expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('comments-unread') - expect(action.displayName([file], view)).toBe('') - expect(action.title!([file], view)).toBe('1 new comment') - expect(action.iconSvgInline([], view)).toMatch(//) - expect(action.enabled!([file], view)).toBe(true) - expect(action.inline!(file, view)).toBe(true) + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('') + expect(action.title!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('1 new comment') + expect(action.iconSvgInline({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) + expect(action.inline!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) expect(action.default).toBeUndefined() expect(action.order).toBe(-140) }) @@ -49,10 +75,21 @@ describe('Inline unread comments action display name tests', () => { attributes: { 'comments-unread': 2, }, + root: '/files/admin', }) - expect(action.displayName([file], view)).toBe('') - expect(action.title!([file], view)).toBe('2 new comments') + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('') + expect(action.title!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('2 new comments') }) }) @@ -64,10 +101,16 @@ describe('Inline unread comments action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, - attributes: { }, + attributes: {}, + root: '/files/admin', }) - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Action is disabled when file does not have unread comments', () => { @@ -80,9 +123,15 @@ describe('Inline unread comments action enabled tests', () => { attributes: { 'comments-unread': 0, }, + root: '/files/admin', }) - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Action is enabled when file has a single unread comment', () => { @@ -95,9 +144,15 @@ describe('Inline unread comments action enabled tests', () => { attributes: { 'comments-unread': 1, }, + root: '/files/admin', }) - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Action is enabled when file has a two unread comments', () => { @@ -110,9 +165,15 @@ describe('Inline unread comments action enabled tests', () => { attributes: { 'comments-unread': 2, }, + root: '/files/admin', }) - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) }) @@ -139,9 +200,15 @@ describe('Inline unread comments action execute tests', () => { attributes: { 'comments-unread': 1, }, + root: '/files/admin', }) - const result = await action.exec!(file, view, '/') + const result = await action.exec!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(result).toBe(null) expect(setActiveTabMock).toBeCalledWith('comments') @@ -173,9 +240,15 @@ describe('Inline unread comments action execute tests', () => { attributes: { 'comments-unread': 1, }, + root: '/files/admin', }) - const result = await action.exec!(file, view, '/') + const result = await action.exec!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(result).toBe(false) expect(setActiveTabMock).toBeCalledWith('comments') diff --git a/apps/comments/src/actions/inlineUnreadCommentsAction.ts b/apps/comments/src/actions/inlineUnreadCommentsAction.ts index 302107604bad0..0891d6fd01e82 100644 --- a/apps/comments/src/actions/inlineUnreadCommentsAction.ts +++ b/apps/comments/src/actions/inlineUnreadCommentsAction.ts @@ -2,9 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { Node } from '@nextcloud/files' - import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw' import { FileAction } from '@nextcloud/files' import { n, t } from '@nextcloud/l10n' @@ -13,9 +10,9 @@ import logger from '../logger.js' export const action = new FileAction({ id: 'comments-unread', - title(nodes: Node[]) { - const unread = nodes[0].attributes['comments-unread'] as number - if (unread >= 0) { + title({ nodes }) { + const unread = nodes[0]?.attributes['comments-unread'] as number | undefined + if (typeof unread === 'number' && unread >= 0) { return n('comments', '1 new comment', '{unread} new comments', unread, { unread }) } return t('comments', 'Comment') @@ -26,15 +23,19 @@ export const action = new FileAction({ iconSvgInline: () => CommentProcessingSvg, - enabled(nodes: Node[]) { - const unread = nodes[0].attributes['comments-unread'] as number | undefined + enabled({ nodes }) { + const unread = nodes[0]?.attributes?.['comments-unread'] as number | undefined return typeof unread === 'number' && unread > 0 }, - async exec(node: Node) { + async exec({ nodes }) { + if (nodes.length !== 1 || !nodes[0]) { + return false + } + try { window.OCA.Files.Sidebar.setActiveTab('comments') - await window.OCA.Files.Sidebar.open(node.path) + await window.OCA.Files.Sidebar.open(nodes[0].path) return null } catch (error) { logger.error('Error while opening sidebar', { error }) diff --git a/apps/files/src/actions/convertAction.ts b/apps/files/src/actions/convertAction.ts index 1a632d37dbb34..0feae7e87dd50 100644 --- a/apps/files/src/actions/convertAction.ts +++ b/apps/files/src/actions/convertAction.ts @@ -2,9 +2,6 @@ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { Node, View } from '@nextcloud/files' - import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw' import { getCapabilities } from '@nextcloud/capabilities' import { FileAction, registerFileAction } from '@nextcloud/files' @@ -31,20 +28,24 @@ export function registerConvertActions() { id: `convert-${from}-${to}`, displayName: () => t('files', 'Save as {displayName}', { displayName }), iconSvgInline: () => generateIconSvg(to), - enabled: (nodes: Node[]) => { + enabled: ({ nodes }) => { // Check that all nodes have the same mime type return nodes.every((node) => from === node.mime) }, - async exec(node: Node) { + async exec({ nodes }) { + if (!nodes[0]) { + return false + } + // If we're here, we know that the node has a fileid - convertFile(node.fileid as number, to) + convertFile(nodes[0].fileid as number, to) // Silently terminate, we'll handle the UI in the background return null }, - async execBatch(nodes: Node[]) { + async execBatch({ nodes }) { const fileIds = nodes.map((node) => node.fileid).filter(Boolean) as number[] convertFiles(fileIds, to) @@ -61,8 +62,8 @@ export function registerConvertActions() { id: ACTION_CONVERT, displayName: () => t('files', 'Save as …'), iconSvgInline: () => AutoRenewSvg, - enabled: (nodes: Node[], view: View) => { - return actions.some((action) => action.enabled!(nodes, view)) + enabled: (context) => { + return actions.some((action) => action.enabled!(context)) }, async exec() { return null diff --git a/apps/files/src/actions/deleteAction.spec.ts b/apps/files/src/actions/deleteAction.spec.ts index 738f2876e2af9..54f8734380735 100644 --- a/apps/files/src/actions/deleteAction.spec.ts +++ b/apps/files/src/actions/deleteAction.spec.ts @@ -38,6 +38,7 @@ describe('Delete action conditions tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/test', }) const file2 = new File({ @@ -50,6 +51,7 @@ describe('Delete action conditions tests', () => { 'is-mount-root': true, 'mount-type': 'shared', }, + root: '/files/admin', }) const folder = new Folder({ @@ -58,6 +60,7 @@ describe('Delete action conditions tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) const folder2 = new Folder({ @@ -70,6 +73,7 @@ describe('Delete action conditions tests', () => { 'is-mount-root': true, 'mount-type': 'shared', }, + root: '/files/admin', }) const folder3 = new Folder({ @@ -82,23 +86,44 @@ describe('Delete action conditions tests', () => { 'is-mount-root': true, 'mount-type': 'external', }, + root: '/files/admin', }) test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('delete') - expect(action.displayName([file], view)).toBe('Delete file') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('Delete file') + expect(action.iconSvgInline({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(100) }) test('Default folder displayName', () => { - expect(action.displayName([folder], view)).toBe('Delete folder') + expect(action.displayName({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe('Delete folder') }) test('Default trashbin view displayName', () => { - expect(action.displayName([file], trashbinView)).toBe('Delete permanently') + expect(action.displayName({ + nodes: [file], + view: trashbinView, + folder: {} as Folder, + contents: [], + })).toBe('Delete permanently') }) test('Trashbin disabled displayName', () => { @@ -107,23 +132,58 @@ describe('Delete action conditions tests', () => { files: {}, } }) - expect(action.displayName([file], view)).toBe('Delete permanently') + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('Delete permanently') expect(capabilities.getCapabilities).toBeCalledTimes(1) }) test('Shared root node displayName', () => { - expect(action.displayName([file2], view)).toBe('Leave this share') - expect(action.displayName([folder2], view)).toBe('Leave this share') - expect(action.displayName([file2, folder2], view)).toBe('Leave these shares') + expect(action.displayName({ + nodes: [file2], + view, + folder: {} as Folder, + contents: [], + })).toBe('Leave this share') + expect(action.displayName({ + nodes: [folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe('Leave this share') + expect(action.displayName({ + nodes: [file2, folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe('Leave these shares') }) test('External storage root node displayName', () => { - expect(action.displayName([folder3], view)).toBe('Disconnect storage') - expect(action.displayName([folder3, folder3], view)).toBe('Disconnect storages') + expect(action.displayName({ + nodes: [folder3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Disconnect storage') + expect(action.displayName({ + nodes: [folder3, folder3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Disconnect storages') }) test('Shared and owned nodes displayName', () => { - expect(action.displayName([file, file2], view)).toBe('Delete and unshare') + expect(action.displayName({ + nodes: [file, file2], + view, + folder: {} as Folder, + contents: [], + })).toBe('Delete and unshare') }) }) @@ -151,10 +211,16 @@ describe('Delete action enabled tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/test', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled without DELETE permissions', () => { @@ -164,15 +230,26 @@ describe('Delete action enabled tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ, + root: '/files/test', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled without nodes', () => { expect(action.enabled).toBeDefined() - expect(action.enabled!([], view)).toBe(false) + expect(action.enabled!({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if not all nodes can be deleted', () => { @@ -181,18 +258,35 @@ describe('Delete action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/test/Foo/', owner: 'test', permissions: Permission.DELETE, + root: '/files/test', }) const folder2 = new Folder({ id: 2, source: 'https://cloud.domain.com/remote.php/dav/files/test/Bar/', owner: 'test', permissions: Permission.READ, + root: '/files/test', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder1], view)).toBe(true) - expect(action.enabled!([folder2], view)).toBe(false) - expect(action.enabled!([folder1, folder2], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder1], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) + expect(action.enabled!({ + nodes: [folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) + expect(action.enabled!({ + nodes: [folder1, folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if not allowed', () => { @@ -201,7 +295,12 @@ describe('Delete action enabled tests', () => { }))) expect(action.enabled).toBeDefined() - expect(action.enabled!([], view)).toBe(false) + expect(action.enabled!({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -219,9 +318,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(true) expect(axios.delete).toBeCalledTimes(1) @@ -244,6 +349,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file2 = new File({ @@ -252,9 +358,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.execBatch!([file1, file2], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) // Not enough nodes to trigger a confirmation dialog expect(confirmMock).toBeCalledTimes(0) @@ -283,6 +395,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file2 = new File({ @@ -291,6 +404,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file3 = new File({ @@ -299,6 +413,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file4 = new File({ @@ -307,6 +422,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file5 = new File({ @@ -315,9 +431,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.execBatch!([file1, file2, file3, file4, file5], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2, file3, file4, file5], + view, + folder: {} as Folder, + contents: [], + }) // Enough nodes to trigger a confirmation dialog expect(confirmMock).toBeCalledTimes(1) @@ -361,6 +483,7 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) const file2 = new File({ @@ -369,9 +492,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.execBatch!([file1, file2], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) // Will trigger a confirmation dialog because trashbin app is disabled expect(confirmMock).toBeCalledTimes(1) @@ -401,9 +530,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) expect(axios.delete).toBeCalledTimes(1) @@ -435,9 +570,15 @@ describe('Delete action execute tests', () => { owner: 'test', mime: 'text/plain', permissions: Permission.READ | Permission.UPDATE | Permission.DELETE, + root: '/files/test', }) - const exec = await action.execBatch!([file1], view, '/') + const exec = await action.execBatch!({ + nodes: [file1], + view, + folder: {} as Folder, + contents: [], + }) expect(confirmMock).toBeCalledTimes(1) diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 7e30df91cada3..013d954b1ddd4 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -2,9 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { Node, View } from '@nextcloud/files' - import CloseSvg from '@mdi/svg/svg/close.svg?raw' import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw' import TrashCanSvg from '@mdi/svg/svg/trash-can-outline.svg?raw' @@ -26,7 +23,7 @@ export const ACTION_DELETE = 'delete' export const action = new FileAction({ id: ACTION_DELETE, displayName, - iconSvgInline: (nodes: Node[]) => { + iconSvgInline: ({ nodes }) => { if (canUnshareOnly(nodes)) { return CloseSvg } @@ -38,7 +35,7 @@ export const action = new FileAction({ return TrashCanSvg }, - enabled(nodes: Node[], view: View): boolean { + enabled({ nodes, view }) { if (view.id === TRASHBIN_VIEW_ID) { const config = loadState('files_trashbin', 'config', { allow_delete: true }) if (config.allow_delete === false) { @@ -51,7 +48,7 @@ export const action = new FileAction({ .every((permission) => (permission & Permission.DELETE) !== 0) }, - async exec(node: Node, view: View) { + async exec({ nodes, view }) { try { let confirm = true @@ -62,7 +59,7 @@ export const action = new FileAction({ const isCalledFromEventListener = callStack.toLocaleLowerCase().includes('keydown') if (shouldAskForConfirmation() || isCalledFromEventListener) { - confirm = await askConfirmation([node], view) + confirm = await askConfirmation([nodes[0]], view) } // If the user cancels the deletion, we don't want to do anything @@ -70,16 +67,16 @@ export const action = new FileAction({ return null } - await deleteNode(node) + await deleteNode(nodes[0]) return true } catch (error) { - logger.error('Error while deleting a file', { error, source: node.source, node }) + logger.error('Error while deleting a file', { error, source: nodes[0].source, node: nodes[0] }) return false } }, - async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> { + async execBatch({ nodes, view }) { let confirm = true if (shouldAskForConfirmation()) { diff --git a/apps/files/src/actions/deleteUtils.ts b/apps/files/src/actions/deleteUtils.ts index 7bed716321da0..6a32dc1bccea9 100644 --- a/apps/files/src/actions/deleteUtils.ts +++ b/apps/files/src/actions/deleteUtils.ts @@ -66,10 +66,11 @@ export function isAllFolders(nodes: Node[]) { /** * - * @param nodes - * @param view + * @param root0 + * @param root0.nodes + * @param root0.view */ -export function displayName(nodes: Node[], view: View) { +export function displayName({ nodes, view }: { nodes: Node[], view: View }) { /** * If those nodes are all the root node of a * share, we can only unshare them. @@ -154,7 +155,7 @@ export async function askConfirmation(nodes: Node[], view: View) { t('files', 'Confirm deletion'), { type: window.OC.dialogs.YES_NO_BUTTONS, - confirm: displayName(nodes, view), + confirm: displayName({ nodes, view }), confirmClasses: 'error', cancel: t('files', 'Cancel'), }, diff --git a/apps/files/src/actions/downloadAction.spec.ts b/apps/files/src/actions/downloadAction.spec.ts index 65c431d6f06f2..880053ccf145a 100644 --- a/apps/files/src/actions/downloadAction.spec.ts +++ b/apps/files/src/actions/downloadAction.spec.ts @@ -30,8 +30,18 @@ describe('Download action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('download') - expect(action.displayName([], view)).toBe('Download') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe('Download') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBe(DefaultType.DEFAULT) expect(action.order).toBe(30) }) @@ -45,10 +55,16 @@ describe('Download action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled without READ permissions', () => { @@ -58,10 +74,16 @@ describe('Download action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.NONE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if not all nodes have READ permissions', () => { @@ -70,23 +92,45 @@ describe('Download action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) const folder2 = new Folder({ id: 2, source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/', owner: 'admin', permissions: Permission.NONE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder1], view)).toBe(true) - expect(action.enabled!([folder2], view)).toBe(false) - expect(action.enabled!([folder1, folder2], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder1], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) + expect(action.enabled!({ + nodes: [folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) + expect(action.enabled!({ + nodes: [folder1, folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled without nodes', () => { expect(action.enabled).toBeDefined() - expect(action.enabled!([], view)).toBe(false) + expect(action.enabled!({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -107,9 +151,15 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) @@ -125,9 +175,15 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.execBatch!([file], view, '/') + const exec = await action.execBatch!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toStrictEqual([null]) @@ -144,9 +200,15 @@ describe('Download action execute tests', () => { mime: 'text/plain', displayname: 'baz.txt', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.execBatch!([file], view, '/') + const exec = await action.execBatch!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toStrictEqual([null]) @@ -161,9 +223,15 @@ describe('Download action execute tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.exec(folder, view, '/') + const exec = await action.exec({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) @@ -179,6 +247,7 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) const file2 = new File({ id: 1, @@ -186,9 +255,15 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.execBatch!([file1, file2], view, '/Dir') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toStrictEqual([null, null]) @@ -204,11 +279,17 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) vi.spyOn(axios, 'head').mockRejectedValue(new Error('File not found')) const errorSpy = vi.spyOn(dialogs, 'showError') - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(null) expect(errorSpy).toHaveBeenCalledWith('The requested file is not available.') expect(link.click).not.toHaveBeenCalled() @@ -221,6 +302,7 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) const file2 = new File({ id: 2, @@ -228,12 +310,18 @@ describe('Download action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) vi.spyOn(axios, 'head').mockRejectedValue(new Error('File not found')) vi.spyOn(eventBus, 'emit').mockImplementation(() => {}) const errorSpy = vi.spyOn(dialogs, 'showError') - const exec = await action.execBatch!([file1, file2], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toStrictEqual([null, null]) expect(errorSpy).toHaveBeenCalledWith('The requested files are not available.') expect(link.click).not.toHaveBeenCalled() diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts index c2b1a39e4baf5..17d01303f44df 100644 --- a/apps/files/src/actions/downloadAction.ts +++ b/apps/files/src/actions/downloadAction.ts @@ -67,6 +67,10 @@ function longestCommonPath(first: string, second: string): string { async function downloadNodes(nodes: Node[]) { let url: URL + if (!nodes[0]) { + throw new Error('No nodes to download') + } + if (nodes.length === 1) { if (nodes[0].type === FileType.File) { await triggerDownload(nodes[0].encodedSource, nodes[0].displayname) @@ -125,7 +129,7 @@ export const action = new FileAction({ displayName: () => t('files', 'Download'), iconSvgInline: () => ArrowDownSvg, - enabled(nodes: Node[], view: View) { + enabled({ nodes, view }): boolean { if (nodes.length === 0) { return false } @@ -143,25 +147,25 @@ export const action = new FileAction({ return nodes.every(isDownloadable) }, - async exec(node: Node) { + async exec({ nodes }) { try { - await downloadNodes([node]) + await downloadNodes(nodes) } catch (error) { showError(t('files', 'The requested file is not available.')) logger.error('The requested file is not available.', { error }) - emit('files:node:deleted', node) + emit('files:node:deleted', nodes[0]) } return null }, - async execBatch(nodes: Node[], view: View, dir: string) { + async execBatch({ nodes, view, folder }) { try { await downloadNodes(nodes) } catch (error) { showError(t('files', 'The requested files are not available.')) logger.error('The requested files are not available.', { error }) // Try to reload the current directory to update the view - const directory = getCurrentDirectory(view, dir)! + const directory = getCurrentDirectory(view, folder.path)! emit('files:node:updated', directory) } return new Array(nodes.length).fill(null) diff --git a/apps/files/src/actions/favoriteAction.spec.ts b/apps/files/src/actions/favoriteAction.spec.ts index d1dba186202ac..3c0d93bcb1827 100644 --- a/apps/files/src/actions/favoriteAction.spec.ts +++ b/apps/files/src/actions/favoriteAction.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { View } from '@nextcloud/files' +import type { Folder, View } from '@nextcloud/files' import axios from '@nextcloud/axios' import * as eventBus from '@nextcloud/event-bus' @@ -43,12 +43,23 @@ describe('Favorite action conditions tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('favorite') - expect(action.displayName([file], view)).toBe('Add to favorites') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('Add to favorites') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(-50) }) @@ -62,9 +73,15 @@ describe('Favorite action conditions tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) - expect(action.displayName([file], view)).toBe('Remove from favorites') + expect(action.displayName({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe('Remove from favorites') }) test('Display name for multiple state files', () => { @@ -77,6 +94,7 @@ describe('Favorite action conditions tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) const file2 = new File({ id: 1, @@ -87,6 +105,7 @@ describe('Favorite action conditions tests', () => { attributes: { favorite: 0, }, + root: '/files/admin', }) const file3 = new File({ id: 1, @@ -97,12 +116,33 @@ describe('Favorite action conditions tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) - expect(action.displayName([file1, file2, file3], view)).toBe('Add to favorites') - expect(action.displayName([file1, file2], view)).toBe('Add to favorites') - expect(action.displayName([file2, file3], view)).toBe('Add to favorites') - expect(action.displayName([file1, file3], view)).toBe('Remove from favorites') + expect(action.displayName({ + nodes: [file1, file2, file3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Add to favorites') + expect(action.displayName({ + nodes: [file2, file3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Add to favorites') + expect(action.displayName({ + nodes: [file2, file3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Add to favorites') + expect(action.displayName({ + nodes: [file1, file3], + view, + folder: {} as Folder, + contents: [], + })).toBe('Remove from favorites') }) }) @@ -114,10 +154,16 @@ describe('Favorite action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled for non-dav ressources', () => { @@ -126,10 +172,16 @@ describe('Favorite action enabled tests', () => { source: 'https://domain.com/data/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -147,9 +199,15 @@ describe('Favorite action execute tests', () => { source: 'http://localhost/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(true) @@ -175,9 +233,15 @@ describe('Favorite action execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(true) @@ -203,9 +267,15 @@ describe('Favorite action execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) - const exec = await action.exec(file, favoriteView, '/') + const exec = await action.exec({ + nodes: [file], + view: favoriteView, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(true) @@ -235,7 +305,12 @@ describe('Favorite action execute tests', () => { }, }) - const exec = await action.exec(file, favoriteView, '/') + const exec = await action.exec({ + nodes: [file], + view: favoriteView, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(true) @@ -264,9 +339,15 @@ describe('Favorite action execute tests', () => { attributes: { favorite: 0, }, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) @@ -296,9 +377,15 @@ describe('Favorite action execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) @@ -332,6 +419,7 @@ describe('Favorite action batch execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) const file2 = new File({ id: 1, @@ -342,10 +430,16 @@ describe('Favorite action batch execute tests', () => { attributes: { favorite: 0, }, + root: '/files/admin', }) // Mixed states triggers favorite action - const exec = await action.execBatch!([file1, file2], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toStrictEqual([true, true]) expect([file1, file2].every((file) => file.attributes.favorite === 1)).toBe(true) @@ -367,6 +461,7 @@ describe('Favorite action batch execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) const file2 = new File({ id: 1, @@ -377,10 +472,16 @@ describe('Favorite action batch execute tests', () => { attributes: { favorite: 1, }, + root: '/files/admin', }) // Mixed states triggers favorite action - const exec = await action.execBatch!([file1, file2], view, '/') + const exec = await action.execBatch!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toStrictEqual([true, true]) expect([file1, file2].every((file) => file.attributes.favorite === 0)).toBe(true) diff --git a/apps/files/src/actions/favoriteAction.ts b/apps/files/src/actions/favoriteAction.ts index b571a547e852a..1d213a2081f57 100644 --- a/apps/files/src/actions/favoriteAction.ts +++ b/apps/files/src/actions/favoriteAction.ts @@ -73,18 +73,18 @@ export async function favoriteNode(node: Node, view: View, willFavorite: boolean export const action = new FileAction({ id: ACTION_FAVORITE, - displayName(nodes: Node[]) { + displayName({ nodes }) { return shouldFavorite(nodes) ? t('files', 'Add to favorites') : t('files', 'Remove from favorites') }, - iconSvgInline: (nodes: Node[]) => { + iconSvgInline: ({ nodes }) => { return shouldFavorite(nodes) ? StarOutlineSvg : StarSvg }, - enabled(nodes: Node[]) { + enabled({ nodes }) { // Not enabled for public shares if (isPublicShare()) { return false @@ -96,11 +96,11 @@ export const action = new FileAction({ && nodes.every((node) => node.permissions !== Permission.NONE) }, - async exec(node: Node, view: View) { - const willFavorite = shouldFavorite([node]) - return await favoriteNode(node, view, willFavorite) + async exec({ nodes, view }): Promise { + const willFavorite = shouldFavorite([nodes[0]]) + return await favoriteNode(nodes[0], view, willFavorite) }, - async execBatch(nodes: Node[], view: View) { + async execBatch({ nodes, view }): Promise { const willFavorite = shouldFavorite(nodes) // Map each node to a promise that resolves with the result of exec(node) diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts index 3945736eae358..12c3155aeec05 100644 --- a/apps/files/src/actions/moveOrCopyAction.ts +++ b/apps/files/src/actions/moveOrCopyAction.ts @@ -2,9 +2,8 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - import type { IFilePickerButton } from '@nextcloud/dialogs' -import type { Folder, Node, View } from '@nextcloud/files' +import type { Folder, Node } from '@nextcloud/files' import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav' import type { MoveCopyResult } from './moveOrCopyActionUtils.ts' @@ -306,7 +305,7 @@ async function openFilePickerForAction( export const ACTION_COPY_MOVE = 'move-copy' export const action = new FileAction({ id: ACTION_COPY_MOVE, - displayName(nodes: Node[]) { + displayName({ nodes }) { switch (getActionForNodes(nodes)) { case MoveCopyAction.MOVE: return t('files', 'Move') @@ -317,7 +316,7 @@ export const action = new FileAction({ } }, iconSvgInline: () => FolderMoveSvg, - enabled(nodes: Node[], view: View) { + enabled({ nodes, view }): boolean { // We can not copy or move in single file shares if (view.id === 'public-file-share') { return false @@ -329,11 +328,11 @@ export const action = new FileAction({ return nodes.length > 0 && (canMove(nodes) || canCopy(nodes)) }, - async exec(node: Node, view: View, dir: string) { - const action = getActionForNodes([node]) + async exec({ nodes, folder }) { + const action = getActionForNodes([nodes[0]]) let result try { - result = await openFilePickerForAction(action, dir, [node]) + result = await openFilePickerForAction(action, folder.path, [nodes[0]]) } catch (e) { logger.error(e as Error) return false @@ -343,7 +342,7 @@ export const action = new FileAction({ } try { - await handleCopyMoveNodeTo(node, result.destination, result.action) + await handleCopyMoveNodeTo(nodes[0], result.destination, result.action) return true } catch (error) { if (error instanceof Error && !!error.message) { @@ -355,9 +354,9 @@ export const action = new FileAction({ } }, - async execBatch(nodes: Node[], view: View, dir: string) { + async execBatch({ nodes, folder }) { const action = getActionForNodes(nodes) - const result = await openFilePickerForAction(action, dir, nodes) + const result = await openFilePickerForAction(action, folder.path, nodes) // Handle cancellation silently if (result === false) { return nodes.map(() => null) diff --git a/apps/files/src/actions/openFolderAction.spec.ts b/apps/files/src/actions/openFolderAction.spec.ts index b04a4a7054b7f..17c6fd2e945b8 100644 --- a/apps/files/src/actions/openFolderAction.spec.ts +++ b/apps/files/src/actions/openFolderAction.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node, View } from '@nextcloud/files' +import type { View } from '@nextcloud/files' import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files' import { describe, expect, test, vi } from 'vitest' @@ -21,12 +21,23 @@ describe('Open folder action conditions tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('open-folder') - expect(action.displayName([folder], view)).toBe('Open folder FooBar') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe('Open folder FooBar') + expect(action.iconSvgInline({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBe(DefaultType.HIDDEN) expect(action.order).toBe(-100) }) @@ -39,10 +50,16 @@ describe('Open folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder], view)).toBe(true) + expect(action.enabled!({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled for non-dav ressources', () => { @@ -51,10 +68,16 @@ describe('Open folder action enabled tests', () => { source: 'https://domain.com/data/FooBar/', owner: 'admin', permissions: Permission.NONE, + root: '/', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if more than one node', () => { @@ -63,16 +86,23 @@ describe('Open folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) const folder2 = new Folder({ id: 2, source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder1, folder2], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder1, folder2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled for files', () => { @@ -81,10 +111,16 @@ describe('Open folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled without READ permissions', () => { @@ -93,17 +129,22 @@ describe('Open folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', permissions: Permission.NONE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) describe('Open folder action execute tests', () => { test('Open folder', async () => { const goToRouteMock = vi.fn() - // @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } const folder = new Folder({ @@ -111,9 +152,22 @@ describe('Open folder action execute tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.exec(folder, view, '/') + const root = new Folder({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/', + owner: 'admin', + root: '/files/admin', + }) + + const exec = await action.exec({ + nodes: [folder], + view, + folder: root, + contents: [], + }) // Silent action expect(exec).toBe(null) expect(goToRouteMock).toBeCalledTimes(1) @@ -122,17 +176,21 @@ describe('Open folder action execute tests', () => { test('Open folder fails without node', async () => { const goToRouteMock = vi.fn() - // @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } - const exec = await action.exec(null as unknown as Node, view, '/') + const exec = await action.exec({ + // @ts-expect-error We want to test without node + nodes: [], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) expect(goToRouteMock).toBeCalledTimes(0) }) test('Open folder fails without Folder', async () => { const goToRouteMock = vi.fn() - // @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } const file = new File({ @@ -140,9 +198,22 @@ describe('Open folder action execute tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const root = new Folder({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/', + owner: 'admin', + root: '/files/admin', + }) + + const exec = await action.exec({ + nodes: [file], + view, + folder: root, + contents: [], + }) expect(exec).toBe(false) expect(goToRouteMock).toBeCalledTimes(0) }) diff --git a/apps/files/src/actions/openFolderAction.ts b/apps/files/src/actions/openFolderAction.ts index a7bbb6c40c2c2..801c68aad6728 100644 --- a/apps/files/src/actions/openFolderAction.ts +++ b/apps/files/src/actions/openFolderAction.ts @@ -2,30 +2,31 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node, View } from '@nextcloud/files' - import FolderSvg from '@mdi/svg/svg/folder.svg?raw' import { DefaultType, FileAction, FileType, Permission } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' export const action = new FileAction({ id: 'open-folder', - displayName(files: Node[]) { + displayName({ nodes }) { + if (nodes.length !== 1 || !nodes[0]) { + return t('files', 'Open folder') + } + // Only works on single node - const displayName = files[0].displayname + const displayName = nodes[0].displayname return t('files', 'Open folder {displayName}', { displayName }) }, iconSvgInline: () => FolderSvg, - enabled(nodes: Node[]) { + enabled({ nodes }) { // Only works on single node - if (nodes.length !== 1) { + if (nodes.length !== 1 || !nodes[0]) { return false } const node = nodes[0] - - if (!node.isDavRessource) { + if (!node.isDavResource) { return false } @@ -33,7 +34,8 @@ export const action = new FileAction({ && (node.permissions & Permission.READ) !== 0 }, - async exec(node: Node, view: View) { + async exec({ nodes, view }) { + const node = nodes[0] if (!node || node.type !== FileType.Folder) { return false } diff --git a/apps/files/src/actions/openInFilesAction.spec.ts b/apps/files/src/actions/openInFilesAction.spec.ts index 2689672804e92..9950eef6883db 100644 --- a/apps/files/src/actions/openInFilesAction.spec.ts +++ b/apps/files/src/actions/openInFilesAction.spec.ts @@ -23,8 +23,18 @@ describe('Open in files action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('open-in-files') - expect(action.displayName([], recentView)).toBe('Open in Files') - expect(action.iconSvgInline([], recentView)).toBe('') + expect(action.displayName({ + nodes: [], + view: recentView, + folder: {} as Folder, + contents: [], + })).toBe('Open in Files') + expect(action.iconSvgInline({ + nodes: [], + view: recentView, + folder: {} as Folder, + contents: [], + })).toBe('') expect(action.default).toBe(DefaultType.HIDDEN) expect(action.order).toBe(-1000) expect(action.inline).toBeUndefined() @@ -34,12 +44,22 @@ describe('Open in files action conditions tests', () => { describe('Open in files action enabled tests', () => { test('Enabled with on valid view', () => { expect(action.enabled).toBeDefined() - expect(action.enabled!([], recentView)).toBe(true) + expect(action.enabled!({ + nodes: [], + view: recentView, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled on wrong view', () => { expect(action.enabled).toBeDefined() - expect(action.enabled!([], view)).toBe(false) + expect(action.enabled!({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -57,7 +77,12 @@ describe('Open in files action execute tests', () => { permissions: Permission.ALL, }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) @@ -77,7 +102,12 @@ describe('Open in files action execute tests', () => { permissions: Permission.ALL, }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) diff --git a/apps/files/src/actions/openInFilesAction.ts b/apps/files/src/actions/openInFilesAction.ts index a4f90fe3148a3..38229bff7f416 100644 --- a/apps/files/src/actions/openInFilesAction.ts +++ b/apps/files/src/actions/openInFilesAction.ts @@ -2,9 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { Node } from '@nextcloud/files' - import { DefaultType, FileAction, FileType } from '@nextcloud/files' import { t } from '@nextcloud/l10n' import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search.ts' @@ -14,19 +11,23 @@ export const action = new FileAction({ displayName: () => t('files', 'Open in Files'), iconSvgInline: () => '', - enabled(nodes, view) { + enabled({ view }) { return view.id === 'recent' || view.id === SEARCH_VIEW_ID }, - async exec(node: Node) { - let dir = node.dirname - if (node.type === FileType.Folder) { - dir = dir + '/' + node.basename + async exec({ nodes }) { + if (!nodes[0]) { + return false + } + + let dir = nodes[0].dirname + if (nodes[0].type === FileType.Folder) { + dir = dir + '/' + nodes[0].basename } window.OCP.Files.Router.goToRoute( null, // use default route - { view: 'files', fileid: String(node.fileid) }, + { view: 'files', fileid: String(nodes[0].fileid) }, { dir, openfile: 'true' }, ) return null diff --git a/apps/files/src/actions/openLocallyAction.spec.ts b/apps/files/src/actions/openLocallyAction.spec.ts index ba2f3c6dd84d4..aa72788d79e02 100644 --- a/apps/files/src/actions/openLocallyAction.spec.ts +++ b/apps/files/src/actions/openLocallyAction.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { View } from '@nextcloud/files' +import type { Folder, View } from '@nextcloud/files' import axios from '@nextcloud/axios' import * as nextcloudDialogs from '@nextcloud/dialogs' @@ -30,8 +30,18 @@ describe('Open locally action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('edit-locally') - expect(action.displayName([], view)).toBe('Open locally') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [], + view, + folder: {} as any, + contents: [], + })).toBe('Open locally') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as any, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(25) }) @@ -45,10 +55,16 @@ describe('Open locally action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as any, + contents: [], + })).toBe(true) }) test('Disabled for non-dav resources', () => { @@ -57,10 +73,16 @@ describe('Open locally action enabled tests', () => { source: 'https://domain.com/data/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as any, + contents: [], + })).toBe(false) }) test('Disabled if more than one node', () => { @@ -70,6 +92,7 @@ describe('Open locally action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) const file2 = new File({ id: 1, @@ -77,10 +100,16 @@ describe('Open locally action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file1, file2], view)).toBe(false) + expect(action.enabled!({ + nodes: [file1, file2], + view, + folder: {} as any, + contents: [], + })).toBe(false) }) test('Disabled for files', () => { @@ -89,10 +118,16 @@ describe('Open locally action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as any, + contents: [], + })).toBe(false) }) test('Disabled without UPDATE permissions', () => { @@ -102,10 +137,16 @@ describe('Open locally action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as any, + contents: [], + })).toBe(false) }) }) @@ -130,9 +171,15 @@ describe('Open locally action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.UPDATE, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(spyShowDialog).toBeCalled() @@ -154,9 +201,15 @@ describe('Open locally action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.UPDATE, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(spyShowDialog).toBeCalled() diff --git a/apps/files/src/actions/openLocallyAction.ts b/apps/files/src/actions/openLocallyAction.ts index 8b4c0df11dd53..b0b4c34aaa73a 100644 --- a/apps/files/src/actions/openLocallyAction.ts +++ b/apps/files/src/actions/openLocallyAction.ts @@ -2,9 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { Node } from '@nextcloud/files' - import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw' import IconWeb from '@mdi/svg/svg/web.svg?raw' import { getCurrentUser } from '@nextcloud/auth' @@ -24,9 +21,9 @@ export const action = new FileAction({ iconSvgInline: () => LaptopSvg, // Only works on single files - enabled(nodes: Node[]) { + enabled({ nodes }) { // Only works on single node - if (nodes.length !== 1) { + if (nodes.length !== 1 || !nodes[0]) { return false } @@ -38,8 +35,8 @@ export const action = new FileAction({ return isSyncable(nodes[0]) }, - async exec(node: Node) { - await attemptOpenLocalClient(node.path) + async exec({ nodes }) { + await attemptOpenLocalClient(nodes[0].path) return null }, @@ -68,7 +65,7 @@ async function attemptOpenLocalClient(path: string) { /** * Try to open a file in the Nextcloud client. - * There is no way to get notified if this action was successfull. + * There is no way to get notified if this action was successful. * * @param path - Path to open */ diff --git a/apps/files/src/actions/renameAction.spec.ts b/apps/files/src/actions/renameAction.spec.ts index ba14e30d77ca5..ba2d66fd90d25 100644 --- a/apps/files/src/actions/renameAction.spec.ts +++ b/apps/files/src/actions/renameAction.spec.ts @@ -18,7 +18,13 @@ const view = { } as View beforeEach(() => { - const root = new Folder({ owner: 'test', source: 'https://cloud.domain.com/remote.php/dav/files/admin/', id: 1, permissions: Permission.CREATE }) + const root = new Folder({ + owner: 'test', + source: 'https://cloud.domain.com/remote.php/dav/files/admin/', + id: 1, + permissions: Permission.CREATE, + root: '/files/admin', + }) const files = useFilesStore(getPinia()) files.setRoot({ service: 'files', root }) }) @@ -27,8 +33,18 @@ describe('Rename action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('rename') - expect(action.displayName([], view)).toBe('Rename') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe('Rename') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(10) }) @@ -42,10 +58,16 @@ describe('Rename action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.UPDATE | Permission.DELETE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled for node without DELETE permission', () => { @@ -55,10 +77,16 @@ describe('Rename action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if more than one node', () => { @@ -70,16 +98,23 @@ describe('Rename action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) const file2 = new File({ id: 2, source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file1, file2], view)).toBe(false) + expect(action.enabled!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -92,9 +127,15 @@ describe('Rename action exec tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) diff --git a/apps/files/src/actions/renameAction.ts b/apps/files/src/actions/renameAction.ts index 55d004d080349..7bf6b1e6b5e7c 100644 --- a/apps/files/src/actions/renameAction.ts +++ b/apps/files/src/actions/renameAction.ts @@ -2,10 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - -import type { View } from '@nextcloud/files' -import type { Node } from '@nextcloud/files' - import PencilSvg from '@mdi/svg/svg/pencil-outline.svg?raw' import { emit } from '@nextcloud/event-bus' import { FileAction, Permission } from '@nextcloud/files' @@ -21,8 +17,8 @@ export const action = new FileAction({ displayName: () => t('files', 'Rename'), iconSvgInline: () => PencilSvg, - enabled: (nodes: Node[], view: View) => { - if (nodes.length === 0) { + enabled: ({ nodes, view }) => { + if (nodes.length === 0 || !nodes[0]) { return false } @@ -44,9 +40,9 @@ export const action = new FileAction({ && Boolean(parentPermissions & Permission.CREATE) }, - async exec(node: Node) { + async exec({ nodes }) { // Renaming is a built-in feature of the files app - emit('files:node:rename', node) + emit('files:node:rename', nodes[0]) return null }, diff --git a/apps/files/src/actions/sidebarAction.spec.ts b/apps/files/src/actions/sidebarAction.spec.ts index dede555ef503f..bc89297734b70 100644 --- a/apps/files/src/actions/sidebarAction.spec.ts +++ b/apps/files/src/actions/sidebarAction.spec.ts @@ -19,8 +19,18 @@ describe('Open sidebar action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('details') - expect(action.displayName([], view)).toBe('Details') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe('Details') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(-50) }) @@ -37,10 +47,16 @@ describe('Open sidebar action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled without permissions', () => { @@ -53,10 +69,16 @@ describe('Open sidebar action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.NONE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if more than one node', () => { @@ -68,16 +90,23 @@ describe('Open sidebar action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) const file2 = new File({ id: 1, source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file1, file2], view)).toBe(false) + expect(action.enabled!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if no Sidebar', () => { @@ -89,10 +118,16 @@ describe('Open sidebar action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled for non-dav ressources', () => { @@ -104,10 +139,16 @@ describe('Open sidebar action enabled tests', () => { source: 'https://domain.com/documents/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/documents/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -126,9 +167,22 @@ describe('Open sidebar action exec tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const folder = new Folder({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/', + owner: 'admin', + root: '/files/admin', + }) + + const exec = await action.exec({ + nodes: [file], + view, + folder, + contents: [], + }) // Silent action expect(exec).toBe(null) expect(openMock).toBeCalledWith('/foobar.txt') @@ -155,9 +209,22 @@ describe('Open sidebar action exec tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar', owner: 'admin', mime: 'httpd/unix-directory', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const folder = new Folder({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/', + owner: 'admin', + root: '/files/admin', + }) + + const exec = await action.exec({ + nodes: [file], + view, + folder, + contents: [], + }) // Silent action expect(exec).toBe(null) expect(openMock).toBeCalledWith('/foobar') @@ -184,9 +251,15 @@ describe('Open sidebar action exec tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) expect(openMock).toBeCalledTimes(1) expect(logger.error).toBeCalledTimes(1) diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts index 7fba44802a16f..d967c2434ef9d 100644 --- a/apps/files/src/actions/sidebarAction.ts +++ b/apps/files/src/actions/sidebarAction.ts @@ -2,8 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node, View } from '@nextcloud/files' - import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw' import { FileAction, Permission } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' @@ -18,7 +16,7 @@ export const action = new FileAction({ iconSvgInline: () => InformationSvg, // Sidebar currently supports user folder only, /files/USER - enabled: (nodes: Node[]) => { + enabled: ({ nodes }) => { if (isPublicShare()) { return false } @@ -40,7 +38,8 @@ export const action = new FileAction({ return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false }, - async exec(node: Node, view: View, dir: string) { + async exec({ nodes, view, folder }) { + const node = nodes[0] try { // If the sidebar is already open for the current file, do nothing if (window.OCA.Files?.Sidebar?.file === node.path) { @@ -57,7 +56,7 @@ export const action = new FileAction({ window.OCP?.Files?.Router?.goToRoute( null, { view: view.id, fileid: String(node.fileid) }, - { ...window.OCP.Files.Router.query, dir, opendetails: 'true' }, + { ...window.OCP.Files.Router.query, dir: folder.path, opendetails: 'true' }, true, ) diff --git a/apps/files/src/actions/viewInFolderAction.spec.ts b/apps/files/src/actions/viewInFolderAction.spec.ts index 2becec7103d48..8650f042afd9c 100644 --- a/apps/files/src/actions/viewInFolderAction.spec.ts +++ b/apps/files/src/actions/viewInFolderAction.spec.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node, View } from '@nextcloud/files' +import type { View } from '@nextcloud/files' import { File, FileAction, Folder, Permission } from '@nextcloud/files' import { describe, expect, test, vi } from 'vitest' @@ -22,8 +22,18 @@ describe('View in folder action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('view-in-folder') - expect(action.displayName([], view)).toBe('View in folder') - expect(action.iconSvgInline([], view)).toMatch(//) + expect(action.displayName({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toBe('View in folder') + expect(action.iconSvgInline({ + nodes: [], + view, + folder: {} as Folder, + contents: [], + })).toMatch(//) expect(action.default).toBeUndefined() expect(action.order).toBe(80) expect(action.enabled).toBeDefined() @@ -38,10 +48,16 @@ describe('View in folder action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(true) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(true) }) test('Disabled for files', () => { @@ -51,10 +67,16 @@ describe('View in folder action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.ALL, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], viewFiles)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view: viewFiles, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled without permissions', () => { @@ -64,10 +86,16 @@ describe('View in folder action enabled tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.NONE, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled for non-dav ressources', () => { @@ -76,10 +104,16 @@ describe('View in folder action enabled tests', () => { source: 'https://domain.com/foobar.txt', owner: 'admin', mime: 'text/plain', + root: '/', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled if more than one node', () => { @@ -88,16 +122,23 @@ describe('View in folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) const file2 = new File({ id: 1, source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', owner: 'admin', mime: 'text/plain', + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file1, file2], view)).toBe(false) + expect(action.enabled!({ + nodes: [file1, file2], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled for folders', () => { @@ -106,10 +147,16 @@ describe('View in folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', owner: 'admin', permissions: Permission.READ, + root: '/files/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([folder], view)).toBe(false) + expect(action.enabled!({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) test('Disabled for files outside the user root folder', () => { @@ -118,10 +165,16 @@ describe('View in folder action enabled tests', () => { source: 'https://cloud.domain.com/remote.php/dav/trashbin/admin/trash/image.jpg.d1731053878', owner: 'admin', permissions: Permission.READ, + root: '/trashbin/admin', }) expect(action.enabled).toBeDefined() - expect(action.enabled!([file], view)).toBe(false) + expect(action.enabled!({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + })).toBe(false) }) }) @@ -136,9 +189,15 @@ describe('View in folder action execute tests', () => { owner: 'admin', mime: 'text/plain', permissions: Permission.READ, + root: '/files/admin', }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) expect(goToRouteMock).toBeCalledTimes(1) @@ -158,7 +217,12 @@ describe('View in folder action execute tests', () => { permissions: Permission.READ, }) - const exec = await action.exec(file, view, '/') + const exec = await action.exec({ + nodes: [file], + view, + folder: {} as Folder, + contents: [], + }) // Silent action expect(exec).toBe(null) expect(goToRouteMock).toBeCalledTimes(1) @@ -169,7 +233,13 @@ describe('View in folder action execute tests', () => { const goToRouteMock = vi.fn() window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } - const exec = await action.exec(null as unknown as Node, view, '/') + const exec = await action.exec({ + // @ts-expect-error We want to test without node + nodes: [], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) expect(goToRouteMock).toBeCalledTimes(0) }) @@ -182,9 +252,15 @@ describe('View in folder action execute tests', () => { id: 1, source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', owner: 'admin', + root: '/files/admin', }) - const exec = await action.exec(folder, view, '/') + const exec = await action.exec({ + nodes: [folder], + view, + folder: {} as Folder, + contents: [], + }) expect(exec).toBe(false) expect(goToRouteMock).toBeCalledTimes(0) }) diff --git a/apps/files/src/actions/viewInFolderAction.ts b/apps/files/src/actions/viewInFolderAction.ts index e08c28c3bc264..9d78b876e302e 100644 --- a/apps/files/src/actions/viewInFolderAction.ts +++ b/apps/files/src/actions/viewInFolderAction.ts @@ -2,8 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node, View } from '@nextcloud/files' - import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw' import { FileAction, FileType, Permission } from '@nextcloud/files' import { t } from '@nextcloud/l10n' @@ -16,7 +14,7 @@ export const action = new FileAction({ }, iconSvgInline: () => FolderMoveSvg, - enabled(nodes: Node[], view: View) { + enabled({ nodes, view }) { // Not enabled for public shares if (isPublicShare()) { return false @@ -28,13 +26,12 @@ export const action = new FileAction({ } // Only works on single node - if (nodes.length !== 1) { + if (nodes.length !== 1 || !nodes[0]) { return false } const node = nodes[0] - - if (!node.isDavRessource) { + if (!node.isDavResource) { return false } @@ -50,15 +47,15 @@ export const action = new FileAction({ return node.type === FileType.File }, - async exec(node: Node) { - if (!node || node.type !== FileType.File) { + async exec({ nodes }) { + if (!nodes[0] || nodes[0].type !== FileType.File) { return false } window.OCP.Files.Router.goToRoute( null, - { view: 'files', fileid: String(node.fileid) }, - { dir: node.dirname }, + { view: 'files', fileid: String(nodes[0].fileid) }, + { dir: nodes[0].dirname }, ) return null }, diff --git a/apps/files/src/components/CustomElementRender.vue b/apps/files/src/components/CustomElementRender.vue index df630125f84ad..3eb8ff1664437 100644 --- a/apps/files/src/components/CustomElementRender.vue +++ b/apps/files/src/components/CustomElementRender.vue @@ -7,6 +7,11 @@