|
|
@ -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, |
|
|
|
}) |
|
|
|
}) |
|
|
|