Skip to content

Commit fbc3e34

Browse files
committed
feat(files): add batch support to copy-move
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
1 parent 70c2f57 commit fbc3e34

File tree

2 files changed

+48
-24
lines changed

2 files changed

+48
-24
lines changed

apps/files/src/actions/moveOrCopyAction.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import '@nextcloud/dialogs/style.css'
2323
import type { Folder, Node, View } from '@nextcloud/files'
2424
import type { IFilePickerButton } from '@nextcloud/dialogs'
25+
import type { MoveCopyResult } from './moveOrCopyActionUtils'
2526

2627
// eslint-disable-next-line n/no-extraneous-import
2728
import { AxiosError } from 'axios'
@@ -92,7 +93,6 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
9293

9394
const relativePath = join(destination.path, node.basename)
9495
const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`)
95-
logger.debug(`${method} ${node.basename} to ${destinationUrl}`)
9696

9797
// Set loading state
9898
Vue.set(node, 'status', NodeStatus.LOADING)
@@ -140,33 +140,37 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
140140
* Open a file picker for the given action
141141
* @param {MoveCopyAction} action The action to open the file picker for
142142
* @param {string} dir The directory to start the file picker in
143-
* @param {Node} node The node to move/copy
144-
* @return {Promise<boolean>} A promise that resolves to true if the action was successful
143+
* @param {Node[]} nodes The nodes to move/copy
144+
* @return {Promise<MoveCopyResult>} The picked destination
145145
*/
146-
const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node: Node): Promise<boolean> => {
146+
const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: Node[]): Promise<MoveCopyResult> => {
147+
const fileIDs = nodes.map(node => node.fileid).filter(Boolean)
147148
const filePicker = getFilePickerBuilder(t('files', 'Chose destination'))
148149
.allowDirectories(true)
149150
.setFilter((n: Node) => {
150151
// We only want to show folders that we can create nodes in
151152
return (n.permissions & Permission.CREATE) !== 0
152-
// We don't want to show the current node in the file picker
153-
&& node.fileid !== n.fileid
153+
// We don't want to show the current nodes in the file picker
154+
&& !fileIDs.includes(n.fileid)
154155
})
155156
.setMimeTypeFilter([])
156157
.setMultiSelect(false)
157158
.startAt(dir)
158159

159160
return new Promise((resolve, reject) => {
160-
filePicker.setButtonFactory((nodes: Node[], path: string) => {
161+
filePicker.setButtonFactory((_selection, path: string) => {
161162
const buttons: IFilePickerButton[] = []
162163
const target = basename(path)
163164

164-
if (node.dirname === path) {
165+
const dirnames = nodes.map(node => node.dirname)
166+
const paths = nodes.map(node => node.path)
167+
168+
if (dirnames.includes(path)) {
165169
// This file/folder is already in that directory
166170
return buttons
167171
}
168172

169-
if (node.path === path) {
173+
if (paths.includes(path)) {
170174
// You cannot move a file/folder onto itself
171175
return buttons
172176
}
@@ -177,12 +181,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node:
177181
type: 'primary',
178182
icon: CopyIconSvg,
179183
async callback(destination: Node[]) {
180-
try {
181-
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.COPY)
182-
resolve(true)
183-
} catch (error) {
184-
reject(error)
185-
}
184+
resolve({
185+
destination: destination[0] as Folder,
186+
action: MoveCopyAction.COPY,
187+
} as MoveCopyResult)
186188
},
187189
})
188190
}
@@ -193,13 +195,10 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', node:
193195
type: action === MoveCopyAction.MOVE ? 'primary' : 'secondary',
194196
icon: FolderMoveSvg,
195197
async callback(destination: Node[]) {
196-
try {
197-
await handleCopyMoveNodeTo(node, destination[0], MoveCopyAction.MOVE)
198-
resolve(true)
199-
} catch (error) {
200-
console.warn('got error', error)
201-
reject(error)
202-
}
198+
resolve({
199+
destination: destination[0] as Folder,
200+
action: MoveCopyAction.MOVE,
201+
} as MoveCopyResult)
203202
},
204203
})
205204
}
@@ -237,8 +236,9 @@ export const action = new FileAction({
237236

238237
async exec(node: Node, view: View, dir: string) {
239238
const action = getActionForNodes([node])
239+
const result = await openFilePickerForAction(action, dir, [node])
240240
try {
241-
await openFilePickerForAction(action, dir, node)
241+
await handleCopyMoveNodeTo(node, result.destination, result.action)
242242
return true
243243
} catch (error) {
244244
if (error instanceof Error && !!error.message) {
@@ -250,5 +250,24 @@ export const action = new FileAction({
250250
}
251251
},
252252

253+
async execBatch(nodes: Node[], view: View, dir: string) {
254+
const action = getActionForNodes(nodes)
255+
const result = await openFilePickerForAction(action, dir, nodes)
256+
const promises = nodes.map(async node => {
257+
try {
258+
await handleCopyMoveNodeTo(node, result.destination, result.action)
259+
return true
260+
} catch (error) {
261+
logger.error(`Failed to ${result.action} node`, { node, error })
262+
return false
263+
}
264+
})
265+
266+
// We need to keep the selection on error!
267+
// So we do not return null, and for batch action
268+
// we let the front handle the error.
269+
return await Promise.all(promises)
270+
},
271+
253272
order: 15,
254273
})

apps/files/src/actions/moveOrCopyActionUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import '@nextcloud/dialogs/style.css'
2424

25-
import type { Node } from '@nextcloud/files'
25+
import type { Folder, Node } from '@nextcloud/files'
2626
import { Permission } from '@nextcloud/files'
2727
import PQueue from 'p-queue'
2828

@@ -51,6 +51,11 @@ export enum MoveCopyAction {
5151
MOVE_OR_COPY = 'move-or-copy',
5252
}
5353

54+
export type MoveCopyResult = {
55+
destination: Folder
56+
action: MoveCopyAction.COPY | MoveCopyAction.MOVE
57+
}
58+
5459
export const canMove = (nodes: Node[]) => {
5560
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
5661
return (minPermission & Permission.UPDATE) !== 0

0 commit comments

Comments
 (0)