diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts index 22afb79c4d6f4..9a9f8ba805f7b 100644 --- a/apps/files/src/actions/moveOrCopyAction.ts +++ b/apps/files/src/actions/moveOrCopyAction.ts @@ -22,6 +22,7 @@ import '@nextcloud/dialogs/style.css' import type { Folder, Node, View } from '@nextcloud/files' import type { IFilePickerButton } from '@nextcloud/dialogs' +import type { MoveCopyResult } from './moveOrCopyActionUtils' // eslint-disable-next-line n/no-extraneous-import import { AxiosError } from 'axios' @@ -92,7 +93,6 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth const relativePath = join(destination.path, node.basename) const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`) - logger.debug(`${method} ${node.basename} to ${destinationUrl}`) // Set loading state Vue.set(node, 'status', NodeStatus.LOADING) @@ -140,33 +140,37 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth * Open a file picker for the given action * @param {MoveCopyAction} action The action to open the file picker for * @param {string} dir The directory to start the file picker in - * @param {Node} node The node to move/copy - * @return {Promise} A promise that resolves to true if the action was successful + * @param {Node[]} nodes The nodes to move/copy + * @return {Promise} The picked destination */ -const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: Node): Promise => { +const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: Node[]): Promise => { + const fileIDs = nodes.map(node => node.fileid).filter(Boolean) const filePicker = getFilePickerBuilder(t('files', 'Chose destination')) .allowDirectories(true) .setFilter((n: Node) => { // We only want to show folders that we can create nodes in return (n.permissions & Permission.CREATE) !== 0 - // We don't want to show the current node in the file picker - && node.fileid !== n.fileid + // We don't want to show the current nodes in the file picker + && !fileIDs.includes(n.fileid) }) .setMimeTypeFilter([]) .setMultiSelect(false) .startAt(dir) return new Promise((resolve, reject) => { - filePicker.setButtonFactory((nodes: Node[], path: string) => { + filePicker.setButtonFactory((_selection, path: string) => { const buttons: IFilePickerButton[] = [] const target = basename(path) - if (node.dirname === path) { + const dirnames = nodes.map(node => node.dirname) + const paths = nodes.map(node => node.path) + + if (dirnames.includes(path)) { // This file/folder is already in that directory return buttons } - if (node.path === path) { + if (paths.includes(path)) { // You cannot move a file/folder onto itself return buttons } @@ -177,12 +181,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: type: 'primary', icon: CopyIconSvg, async callback(destination: Node[]) { - try { - await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.COPY) - resolve(true) - } catch (error) { - reject(error) - } + resolve({ + destination: destination[0] as Folder, + action: MoveCopyAction.COPY, + } as MoveCopyResult) }, }) } @@ -193,13 +195,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: type: action === MoveCopyAction.MOVE ? 'primary' : 'secondary', icon: FolderMoveSvg, async callback(destination: Node[]) { - try { - await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.MOVE) - resolve(true) - } catch (error) { - console.warn('got error', error) - reject(error) - } + resolve({ + destination: destination[0] as Folder, + action: MoveCopyAction.MOVE, + } as MoveCopyResult) }, }) } @@ -237,8 +236,9 @@ export const action = new FileAction({ async exec(node: Node, view: View, dir: string) { const action = getActionForNodes([node]) + const result = await openFilePickerForAction(action, dir, [node]) try { - await openFilePickerForAction(action, dir, node) + await handleCopyMoveNodeTo(node, result.destination, result.action) return true } catch (error) { if (error instanceof Error && !!error.message) { @@ -250,5 +250,24 @@ export const action = new FileAction({ } }, + async execBatch(nodes: Node[], view: View, dir: string) { + const action = getActionForNodes(nodes) + const result = await openFilePickerForAction(action, dir, nodes) + const promises = nodes.map(async node => { + try { + await handleCopyMoveNodeTo(node, result.destination, result.action) + return true + } catch (error) { + logger.error(`Failed to ${result.action} node`, { node, error }) + return false + } + }) + + // We need to keep the selection on error! + // So we do not return null, and for batch action + // we let the front handle the error. + return await Promise.all(promises) + }, + order: 15, }) diff --git a/apps/files/src/actions/moveOrCopyActionUtils.ts b/apps/files/src/actions/moveOrCopyActionUtils.ts index 82aaa02f9ed41..01614b14d8ac4 100644 --- a/apps/files/src/actions/moveOrCopyActionUtils.ts +++ b/apps/files/src/actions/moveOrCopyActionUtils.ts @@ -22,7 +22,7 @@ import '@nextcloud/dialogs/style.css' -import type { Node } from '@nextcloud/files' +import type { Folder, Node } from '@nextcloud/files' import { Permission } from '@nextcloud/files' import PQueue from 'p-queue' @@ -51,6 +51,11 @@ export enum MoveCopyAction { MOVE_OR_COPY = 'move-or-copy', } +export type MoveCopyResult = { + destination: Folder + action: MoveCopyAction.COPY | MoveCopyAction.MOVE +} + export const canMove = (nodes: Node[]) => { const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL) return (minPermission & Permission.UPDATE) !== 0