mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 08:16:43 -04:00
Merge pull request #42279 from nextcloud/backport/42124/stable28
[stable28] feat(files): add batch support to copy-move
This commit is contained in:
commit
7fa0c9d410
7 changed files with 55 additions and 31 deletions
|
|
@ -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<boolean>} A promise that resolves to true if the action was successful
|
||||
* @param {Node[]} nodes The nodes to move/copy
|
||||
* @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'))
|
||||
.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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ document.body.appendChild(TemplatePickerRoot)
|
|||
// Retrieve and init templates
|
||||
let templates = loadState('files', 'templates', [])
|
||||
let templatesPath = loadState('files', 'templates_path', false)
|
||||
logger.debug('Templates providers', templates)
|
||||
logger.debug('Templates providers', { templates })
|
||||
logger.debug('Templates folder', { templatesPath })
|
||||
|
||||
// Init vue app
|
||||
|
|
|
|||
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue