Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/components/FolderPicker/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { IOCozyFile, NextcloudFile } from 'cozy-client/types/types'

export type File = IOCozyFile | NextcloudFile
export type File = (IOCozyFile | NextcloudFile) & {
cozyMetadata?: {
createdOn?: string
}
attributes?: {
cozyMetadata?: {
createdOn?: string
}
}
}

export interface FolderPickerEntry {
_id?: string
Expand All @@ -11,4 +20,5 @@ export interface FolderPickerEntry {
dir_id?: string
class?: string
path?: string
driveId?: string
}
14 changes: 11 additions & 3 deletions src/contexts/ClipboardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ interface ClipboardState {

interface ClipboardContextValue {
clipboardData: ClipboardState
copyFiles: (files: IOCozyFile[], sourceFolderIds?: Set<string>) => void
copyFiles: (
files: IOCozyFile[],
sourceFolderIds?: Set<string>,
sourceDirectory?: IOCozyFile
) => void
cutFiles: (
files: IOCozyFile[],
sourceFolderIds?: Set<string>,
Expand Down Expand Up @@ -170,10 +174,14 @@ const ClipboardProvider: React.FC<ClipboardProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(clipboardReducer, initialState)

const copyFiles = useCallback(
(files: IOCozyFile[], sourceFolderIds?: Set<string>) => {
(
files: IOCozyFile[],
sourceFolderIds?: Set<string>,
sourceDirectory?: IOCozyFile
) => {
dispatch({
type: COPY_FILES,
payload: { files, sourceFolderIds }
payload: { files, sourceFolderIds, sourceDirectory }
})
},
[]
Expand Down
15 changes: 15 additions & 0 deletions src/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ declare module 'cozy-client/dist/models/file' {
filename: string,
driveId: string
) => Promise<string>
export const moveRelateToSharedDrive: (
client: import('cozy-client/types/CozyClient').CozyClient,
source: {
instance?: string
sharing_id?: string
file_id?: string
dir_id?: string
},
dest: {
instance?: string
sharing_id?: string
dir_id: string
},
isCopy?: boolean
) => Promise<void>
}

declare module 'cozy-client/dist/models/note' {
Expand Down
9 changes: 6 additions & 3 deletions src/hooks/useKeyboardShortcuts.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ describe('useKeyboardShortcuts', () => {

expect(mockCopyFiles).toHaveBeenCalledWith(
mockSelectedItems,
new Set(['parent-folder-1', 'parent-folder-2'])
new Set(['parent-folder-1', 'parent-folder-2']),
mockCurrentFolder
)
expect(mockShowAlert).toHaveBeenCalledWith({
message: 'alert.items_copied_2',
Expand Down Expand Up @@ -311,7 +312,8 @@ describe('useKeyboardShortcuts', () => {

expect(mockCopyFiles).toHaveBeenCalledWith(
mockSelectedItems.filter(item => item.type === 'file'),
new Set(['parent-folder-1', 'parent-folder-2'])
new Set(['parent-folder-1', 'parent-folder-2']),
mockCurrentFolder
)
})
})
Expand Down Expand Up @@ -761,7 +763,8 @@ describe('useKeyboardShortcuts', () => {

expect(mockCopyFiles).toHaveBeenCalledWith(
sharedDriveFiles,
new Set(['shared-folder-1'])
new Set(['shared-folder-1']),
sharedDriveFolder
)
expect(mockShowAlert).toHaveBeenCalledWith({
message: 'alert.item_copied',
Expand Down
16 changes: 14 additions & 2 deletions src/hooks/useKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,26 @@ export const useKeyboardShortcuts = ({
return
}

copyFiles(filesToCopy, new Set(parentFolderIds))
copyFiles(
filesToCopy,
new Set(parentFolderIds),
currentFolder as IOCozyFile
)
const message =
filesToCopy.length === 1
? t('alert.item_copied')
: t('alert.items_copied', { count: filesToCopy.length })
showAlert({ message, severity: 'success' })
clearSelection()
}, [allowCopy, selectedItems, copyFiles, showAlert, t, clearSelection])
}, [
allowCopy,
selectedItems,
currentFolder,
copyFiles,
showAlert,
t,
clearSelection
])

const handleCut = useCallback(() => {
if (!selectedItems.length) return
Expand Down
16 changes: 12 additions & 4 deletions src/modules/duplicate/components/DuplicateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { FC, useState } from 'react'
import { useNavigate } from 'react-router-dom'

import { useClient } from 'cozy-client'
import { copy } from 'cozy-client/dist/models/file'
import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

Expand All @@ -12,21 +11,24 @@ import { File, FolderPickerEntry } from '@/components/FolderPicker/types'
import { ROOT_DIR_ID } from '@/constants/config'
import { useCancelable } from '@/modules/move/hooks/useCancelable'
import { computeNextcloudFolderQueryId } from '@/modules/nextcloud/helpers'
import { executeDuplicate } from '@/modules/paste'

interface DuplicateModalProps {
entries: FolderPickerEntry[]
currentFolder: File
onClose: () => void | Promise<void>
showNextcloudFolder?: boolean
isPublic?: boolean
showSharedDriveFolder?: boolean
}

const DuplicateModal: FC<DuplicateModalProps> = ({
entries,
currentFolder,
onClose,
showNextcloudFolder,
isPublic
isPublic,
showSharedDriveFolder
}) => {
const { t } = useI18n()
const { showAlert } = useAlert()
Expand All @@ -41,7 +43,9 @@ const DuplicateModal: FC<DuplicateModalProps> = ({
setBusy(true)
await Promise.all(
entries.map(async entry => {
await registerCancelable(copy(client, entry as Partial<File>, folder))
await registerCancelable(
executeDuplicate(client, entry as File, currentFolder, folder)
)
})
)

Expand Down Expand Up @@ -80,8 +84,11 @@ const DuplicateModal: FC<DuplicateModalProps> = ({
* This is only a proxy to Nextcloud queries so we don't have real-time or mutations updates
*/
const refreshNextcloudQueries = (folder: File): void => {
const sourceAccount = folder.cozyMetadata?.sourceAccount
if (!sourceAccount || !folder.path) return

const queryId = computeNextcloudFolderQueryId({
sourceAccount: folder.cozyMetadata?.sourceAccount,
sourceAccount,
path: folder.path
})
void client?.resetQuery(queryId)
Expand All @@ -90,6 +97,7 @@ const DuplicateModal: FC<DuplicateModalProps> = ({
return (
<FolderPicker
showNextcloudFolder={showNextcloudFolder}
showSharedDriveFolder={showSharedDriveFolder}
currentFolder={currentFolder}
entries={entries}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
Expand Down
2 changes: 2 additions & 0 deletions src/modules/navigation/AppRoute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import FileOpenerExternal from '@/modules/viewer/FileOpenerExternal'
import { KonnectorRoutes } from '@/modules/views/Drive/KonnectorRoutes'
import { FavoritesView } from '@/modules/views/Favorites/FavoritesView'
import { FolderDuplicateView } from '@/modules/views/Folder/FolderDuplicateView'
import { SharedDriveDuplicateView } from '@/modules/views/Folder/SharedDriveDuplicateView'
import { MoveFilesView } from '@/modules/views/Modal/MoveFilesView'
import { MoveSharedDriveFilesView } from '@/modules/views/Modal/MoveSharedDriveFilesView'
import { QualifyFileView } from '@/modules/views/Modal/QualifyFileView'
Expand Down Expand Up @@ -144,6 +145,7 @@ const AppRoute = () => (
<Route path="file/:fileId/v/revision" element={<FileHistory />} />
<Route path="share" element={<ShareDisplayedFolderView />} />
<Route path="move" element={<MoveSharedDriveFilesView />} />
<Route path="duplicate" element={<SharedDriveDuplicateView />} />
</Route>
</>
) : null}
Expand Down
85 changes: 85 additions & 0 deletions src/modules/paste/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { CozyClient } from 'cozy-client'

import { File } from '@/components/FolderPicker/types'

export interface PasteOperationOptions {
showAlert?: (alert: { message: string; severity: string }) => void
t?: (key: string, params?: Record<string, unknown>) => string
sharingContext?: {
getSharedParentPath?: (path: string) => string
hasSharedParent?: (path: string) => boolean
byDocId?: Record<string, unknown>
}
showMoveValidationModal?: (
modalType: string,
file: File,
targetFolder: File,
onConfirm: () => void,
onCancel: () => void
) => void
isPublic?: boolean
}

export interface PasteOperationResult {
success: boolean
file: File
error?: Error
operation: string
}

/**
* Executes a move operation for files or folders.
* Automatically detects if it's a shared drive operation and uses the appropriate API.
*
* @param client - The cozy client instance
* @param entry - The file or folder to move
* @param sourceDirectory - The source directory containing the entry
* @param destDirectory - The destination directory
* @param force - Whether to force the move operation
* @returns The result of the move operation
*/
export function executeMove(
client: CozyClient,
entry: File,
sourceDirectory: File,
destDirectory: File,
force?: boolean
): Promise<unknown>

/**
* Executes a duplicate operation for files or folders.
* Automatically detects if it's a shared drive operation and uses the appropriate API.
*
* @param client - The cozy client instance
* @param entry - The file or folder to duplicate
* @param sourceDirectory - The source directory containing the entry
* @param destDirectory - The destination directory
* @returns The result of the duplicate operation
*/
export function executeDuplicate(
client: CozyClient,
entry: File,
sourceDirectory: File,
destDirectory: File
): Promise<unknown>

/**
* Handles paste operations (copy or cut) for multiple files/folders.
* Processes each file individually and handles validation, conflicts, and sharing permissions.
*
* @param client - The cozy client instance
* @param files - Array of files/folders to paste
* @param operation - The paste operation ('copy' or 'cut')
* @param sourceDirectory - The source directory containing the files
* @param targetFolder - The target folder for the paste operation
* @param options - Additional options
* @returns Array of operation results with success/failure status
*/
export function handlePasteOperation(
client: CozyClient,
files: File[],
operation: string | null,
sourceDirectory: File,
targetFolder: File,
options?: PasteOperationOptions
): Promise<PasteOperationResult[]>
39 changes: 38 additions & 1 deletion src/modules/paste/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,36 @@ export const executeMove = async (
})
}

/**
* Executes a duplicate operation for files or folders.
* Automatically detects if it's a shared drive operation and uses the appropriate API.
*
* @param {CozyClient} client - The cozy client instance
* @param {import('@/components/FolderPicker/types').File} entry - The file or folder to move
* @param {import('@/components/FolderPicker/types').File} sourceDirectory - The source directory containing the entry
* @param {import('@/components/FolderPicker/types').File} destDirectory - The destination directory
* @param {boolean} [force=false] - Whether to force the move operation
* @returns {Promise<Object>} The result of the move operation
*/
export const executeDuplicate = async (
client,
entry,
sourceDirectory,
destDirectory
) => {
const isSharedDriveOperation = entry.driveId || destDirectory.driveId
if (isSharedDriveOperation) {
return await executeSharedDriveMoveOrCopy(
client,
entry,
sourceDirectory,
destDirectory,
'copy'
)
}
return await copy(client, entry, destDirectory)
}

/**
* Handles paste operations (copy or cut) for multiple files/folders.
* Processes each file individually and handles validation, conflicts, and sharing permissions.
Expand Down Expand Up @@ -137,6 +167,7 @@ export const handlePasteOperation = async (
const result = await handleDuplicateWithValidation(
client,
file,
sourceDirectory,
targetFolder,
{ showAlert, t }
)
Expand Down Expand Up @@ -184,12 +215,18 @@ export const handlePasteOperation = async (
const handleDuplicateWithValidation = async (
client,
file,
sourceDirectory,
targetFolder,
options = {}
) => {
const { showAlert, t } = options

const result = await copy(client, file, targetFolder)
const result = await executeDuplicate(
client,
file,
sourceDirectory,
targetFolder
)

const isCopyingInsideNextcloud = targetFolder._type === NEXTCLOUD_FILE_ID
if (isCopyingInsideNextcloud) {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/paste/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ describe('handlePasteOperation', () => {
expect(result[1].success).toBe(false)
expect(result[0].error).toBeInstanceOf(Error)
expect(result[1].error).toBeInstanceOf(Error)
expect(copy).toHaveBeenCalledTimes(2)
expect(copy).not.toHaveBeenCalled()
})

it('should handle missing options', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

import { useModalContext } from '@/lib/ModalContext'
import { download, infos, versions, rename, trash, hr } from '@/modules/actions'
import { duplicateTo } from '@/modules/actions/components/duplicateTo'
import { moveTo } from '@/modules/actions/components/moveTo'
import { FolderBody } from '@/modules/folder/components/FolderBody'

Expand Down Expand Up @@ -53,7 +54,7 @@ const SharedDriveFolderBody = ({
refresh
}
const actions = makeActions(
[download, hr, rename, moveTo, infos, hr, versions, hr, trash],
[download, hr, rename, moveTo, duplicateTo, infos, hr, versions, hr, trash],
actionsOptions
)

Expand Down
Loading
Loading