feat(files): add batch support to copy-move

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
pull/42124/head
John Molakvoæ 2 years ago
parent ab1856085a
commit 6882ec5898
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
  1. 65
      apps/files/src/actions/moveOrCopyAction.ts
  2. 7
      apps/files/src/actions/moveOrCopyActionUtils.ts

@ -22,6 +22,7 @@
import '@nextcloud/dialogs/style.css' import '@nextcloud/dialogs/style.css'
import type { Folder, Node, View } from '@nextcloud/files' import type { Folder, Node, View } from '@nextcloud/files'
import type { IFilePickerButton } from '@nextcloud/dialogs' import type { IFilePickerButton } from '@nextcloud/dialogs'
import type { MoveCopyResult } from './moveOrCopyActionUtils'
// eslint-disable-next-line n/no-extraneous-import // eslint-disable-next-line n/no-extraneous-import
import { AxiosError } from 'axios' 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 relativePath = join(destination.path, node.basename)
const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`) const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`)
logger.debug(`${method} ${node.basename} to ${destinationUrl}`)
// Set loading state // Set loading state
Vue.set(node, 'status', NodeStatus.LOADING) 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 * Open a file picker for the given action
* @param {MoveCopyAction} action The action to open the file picker for * @param {MoveCopyAction} action The action to open the file picker for
* @param {string} dir The directory to start the file picker in * @param {string} dir The directory to start the file picker in
* @param {Node} node The node to move/copy * @param {Node[]} nodes The nodes to move/copy
* @return {Promise<boolean>} A promise that resolves to true if the action was successful * @return {Promise<MoveCopyResult>} The picked destination
*/ */
const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: Node): Promise<boolean> => { const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: Node[]): Promise<MoveCopyResult> => {
const fileIDs = nodes.map(node => node.fileid).filter(Boolean)
const filePicker = getFilePickerBuilder(t('files', 'Chose destination')) const filePicker = getFilePickerBuilder(t('files', 'Chose destination'))
.allowDirectories(true) .allowDirectories(true)
.setFilter((n: Node) => { .setFilter((n: Node) => {
// We only want to show folders that we can create nodes in // We only want to show folders that we can create nodes in
return (n.permissions & Permission.CREATE) !== 0 return (n.permissions & Permission.CREATE) !== 0
// We don't want to show the current node in the file picker // We don't want to show the current nodes in the file picker
&& node.fileid !== n.fileid && !fileIDs.includes(n.fileid)
}) })
.setMimeTypeFilter([]) .setMimeTypeFilter([])
.setMultiSelect(false) .setMultiSelect(false)
.startAt(dir) .startAt(dir)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
filePicker.setButtonFactory((nodes: Node[], path: string) => { filePicker.setButtonFactory((_selection, path: string) => {
const buttons: IFilePickerButton[] = [] const buttons: IFilePickerButton[] = []
const target = basename(path) 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 // This file/folder is already in that directory
return buttons return buttons
} }
if (node.path === path) { if (paths.includes(path)) {
// You cannot move a file/folder onto itself // You cannot move a file/folder onto itself
return buttons return buttons
} }
@ -177,12 +181,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node:
type: 'primary', type: 'primary',
icon: CopyIconSvg, icon: CopyIconSvg,
async callback(destination: Node[]) { async callback(destination: Node[]) {
try { resolve({
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.COPY) destination: destination[0] as Folder,
resolve(true) action: MoveCopyAction.COPY,
} catch (error) { } as MoveCopyResult)
reject(error)
}
}, },
}) })
} }
@ -193,13 +195,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node:
type: action === MoveCopyAction.MOVE ? 'primary' : 'secondary', type: action === MoveCopyAction.MOVE ? 'primary' : 'secondary',
icon: FolderMoveSvg, icon: FolderMoveSvg,
async callback(destination: Node[]) { async callback(destination: Node[]) {
try { resolve({
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.MOVE) destination: destination[0] as Folder,
resolve(true) action: MoveCopyAction.MOVE,
} catch (error) { } as MoveCopyResult)
console.warn('got error', error)
reject(error)
}
}, },
}) })
} }
@ -237,8 +236,9 @@ export const action = new FileAction({
async exec(node: Node, view: View, dir: string) { async exec(node: Node, view: View, dir: string) {
const action = getActionForNodes([node]) const action = getActionForNodes([node])
const result = await openFilePickerForAction(action, dir, [node])
try { try {
await openFilePickerForAction(action, dir, node) await handleCopyMoveNodeTo(node, result.destination, result.action)
return true return true
} catch (error) { } catch (error) {
if (error instanceof Error && !!error.message) { 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, order: 15,
}) })

@ -22,7 +22,7 @@
import '@nextcloud/dialogs/style.css' import '@nextcloud/dialogs/style.css'
import type { Node } from '@nextcloud/files' import type { Folder, Node } from '@nextcloud/files'
import { Permission } from '@nextcloud/files' import { Permission } from '@nextcloud/files'
import PQueue from 'p-queue' import PQueue from 'p-queue'
@ -51,6 +51,11 @@ export enum MoveCopyAction {
MOVE_OR_COPY = 'move-or-copy', MOVE_OR_COPY = 'move-or-copy',
} }
export type MoveCopyResult = {
destination: Folder
action: MoveCopyAction.COPY | MoveCopyAction.MOVE
}
export const canMove = (nodes: Node[]) => { export const canMove = (nodes: Node[]) => {
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL) const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
return (minPermission & Permission.UPDATE) !== 0 return (minPermission & Permission.UPDATE) !== 0

Loading…
Cancel
Save