2222import '@nextcloud/dialogs/style.css'
2323import type { Folder , Node , View } from '@nextcloud/files'
2424import type { IFilePickerButton } from '@nextcloud/dialogs'
25+ import type { MoveCopyResult } from './moveOrCopyActionUtils'
2526
2627// eslint-disable-next-line n/no-extraneous-import
2728import { 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} )
0 commit comments