diff --git a/src/components/FolderPicker/FolderPicker.spec.jsx b/src/components/FolderPicker/FolderPicker.spec.jsx index 12e8d5987d..6c9ec0d939 100644 --- a/src/components/FolderPicker/FolderPicker.spec.jsx +++ b/src/components/FolderPicker/FolderPicker.spec.jsx @@ -1,4 +1,4 @@ -import { render, fireEvent, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import React from 'react' import { createMockClient } from 'cozy-client' @@ -8,17 +8,30 @@ import AppLike from 'test/components/AppLike' import { FolderPicker } from '@/components/FolderPicker/FolderPicker' +// Mock dependencies jest.mock('cozy-keys-lib', () => ({ useVaultClient: jest.fn() })) jest.mock('cozy-sharing', () => ({ ...jest.requireActual('cozy-sharing'), - useSharingContext: jest.fn() + useSharingContext: jest.fn(), + SharingCollection: { + data: jest.fn().mockResolvedValue({ data: [] }) + } })) useSharingContext.mockReturnValue({ byDocId: [] }) +// Mock the FolderPickerBody component to avoid complex rendering issues +jest.mock('@/components/FolderPicker/FolderPickerBody', () => ({ + FolderPickerBody: jest.fn().mockImplementation(() => ( +
+
Mocked Folder Picker Body
+
+ )) +})) + describe('FolderPicker', () => { const cozyFile = { id: 'file123', @@ -71,33 +84,36 @@ describe('FolderPicker', () => { ) } - it('should be able to move inside another folder', async () => { + it('should render with the provided folder', async () => { setup() - expect(screen.getByText('Photos')).toBeInTheDocument() + expect(screen.getByTestId('folder-picker-body')).toBeInTheDocument() - const backButton = screen.getByRole('button', { - name: 'Back' - }) - fireEvent.click(backButton) - await screen.findByText('Files') + const { + FolderPickerBody + } = require('@/components/FolderPicker/FolderPickerBody') - const moveButton = screen.queryByRole('button', { - name: 'Move' - }) - fireEvent.click(moveButton) - expect(onConfirmSpy).toHaveBeenCalledWith(rootCozyFolder) - }) + const props = FolderPickerBody.mock.calls[0][0] - it('should display the folder creation input', async () => { - setup() + expect(props.folder).toEqual(cozyFolder) + expect(props.entries).toEqual([cozyFile]) + expect(typeof props.navigateTo).toBe('function') + }) - const addFolderButton = screen.queryByRole('button', { - name: 'Add a folder' - }) - fireEvent.click(addFolderButton) + it('should allow folder creation when canCreateFolder is true', async () => { + const mockClient = createMockClient() + render( + + + + ) - const filenameInput = await screen.findByTestId('name-input') - expect(filenameInput).toBeInTheDocument() + expect(screen.getByTestId('folder-picker-body')).toBeInTheDocument() }) }) diff --git a/src/components/FolderPicker/FolderPicker.tsx b/src/components/FolderPicker/FolderPicker.tsx index 2f461bebe6..8908cd62c2 100644 --- a/src/components/FolderPicker/FolderPicker.tsx +++ b/src/components/FolderPicker/FolderPicker.tsx @@ -30,6 +30,7 @@ interface FolderPickerProps { slotProps?: FolderPickerSlotProps showNextcloudFolder?: boolean canPickEntriesParentFolder?: boolean + showSharedDriveFolder?: boolean } const useStyles = makeStyles({ @@ -53,7 +54,8 @@ const FolderPicker: React.FC = ({ canCreateFolder = true, slotProps, showNextcloudFolder = false, - canPickEntriesParentFolder = false + canPickEntriesParentFolder = false, + showSharedDriveFolder = false }) => { const [folder, setFolder] = useState(currentFolder) @@ -101,6 +103,7 @@ const FolderPicker: React.FC = ({ isFolderCreationDisplayed={isFolderCreationDisplayed} hideFolderCreation={hideFolderCreation} showNextcloudFolder={showNextcloudFolder} + showSharedDriveFolder={showSharedDriveFolder} /> } actions={ diff --git a/src/components/FolderPicker/FolderPickerAddFolderItem.tsx b/src/components/FolderPicker/FolderPickerAddFolderItem.tsx index 87f81eb8bf..500d4119b0 100644 --- a/src/components/FolderPicker/FolderPickerAddFolderItem.tsx +++ b/src/components/FolderPicker/FolderPickerAddFolderItem.tsx @@ -22,6 +22,7 @@ interface FolderPickerAddFolderItemProps { visible: boolean afterSubmit: () => void afterAbort: () => void + driveId?: string } const FolderPickerAddFolderItem: FC = ({ @@ -29,7 +30,8 @@ const FolderPickerAddFolderItem: FC = ({ currentFolderId, visible, afterSubmit, - afterAbort + afterAbort, + driveId }) => { const { isMobile } = useBreakpoints() const gutters = isMobile ? 'default' : 'double' @@ -41,11 +43,18 @@ const FolderPickerAddFolderItem: FC = ({ const handleSubmit = (name: string): void => { dispatch( - createFolder(client, vaultClient, name, currentFolderId, { - isEncryptedFolder: isEncrypted, - showAlert, - t - }) + createFolder( + client, + vaultClient, + name, + currentFolderId, + { + isEncryptedFolder: isEncrypted, + showAlert, + t + }, + driveId + ) ) if (typeof afterSubmit === 'function') { afterSubmit() diff --git a/src/components/FolderPicker/FolderPickerBody.tsx b/src/components/FolderPicker/FolderPickerBody.tsx index 3e3d1fb537..32a2277a05 100644 --- a/src/components/FolderPicker/FolderPickerBody.tsx +++ b/src/components/FolderPicker/FolderPickerBody.tsx @@ -1,8 +1,12 @@ import React from 'react' +import { FolderPickerContentSharedDriveRoot } from './FolderPickerContentSharedDriveRoot' + import { FolderPickerContentCozy } from '@/components/FolderPicker/FolderPickerContentCozy' import { FolderPickerContentNextcloud } from '@/components/FolderPicker/FolderPickerContentNextcloud' +import { FolderPickerContentSharedDrive } from '@/components/FolderPicker/FolderPickerContentSharedDrive' import { File, FolderPickerEntry } from '@/components/FolderPicker/types' +import { ROOT_DIR_ID, SHARED_DRIVES_DIR_ID } from '@/constants/config' interface FolderPickerBodyProps { folder: File @@ -11,6 +15,7 @@ interface FolderPickerBodyProps { isFolderCreationDisplayed: boolean hideFolderCreation: () => void showNextcloudFolder?: boolean + showSharedDriveFolder?: boolean } const FolderPickerBody: React.FC = ({ @@ -19,7 +24,8 @@ const FolderPickerBody: React.FC = ({ navigateTo, isFolderCreationDisplayed, hideFolderCreation, - showNextcloudFolder + showNextcloudFolder, + showSharedDriveFolder }) => { if (folder._type === 'io.cozy.remote.nextcloud.files') { return ( @@ -31,6 +37,37 @@ const FolderPickerBody: React.FC = ({ ) } + // Display content of recipient's shared drive folder + if (folder.driveId) { + return ( + + ) + } + + // Display content of `Drives` folder + if ( + folder.dir_id === ROOT_DIR_ID && + folder._id === SHARED_DRIVES_DIR_ID && + showSharedDriveFolder + ) { + return ( + + ) + } + return ( = ({ entries={entries} navigateTo={navigateTo} showNextcloudFolder={showNextcloudFolder} + showSharedDriveFolder={showSharedDriveFolder} /> ) } diff --git a/src/components/FolderPicker/FolderPickerContentCozy.tsx b/src/components/FolderPicker/FolderPickerContentCozy.tsx index d8119179fd..6efc9a14ae 100644 --- a/src/components/FolderPicker/FolderPickerContentCozy.tsx +++ b/src/components/FolderPicker/FolderPickerContentCozy.tsx @@ -29,6 +29,7 @@ interface FolderPickerContentCozyProps { entries: FolderPickerEntry[] navigateTo: (folder: import('./types').File) => void showNextcloudFolder?: boolean + showSharedDriveFolder?: boolean } const FolderPickerContentCozy: React.FC = ({ @@ -37,7 +38,8 @@ const FolderPickerContentCozy: React.FC = ({ hideFolderCreation, entries, navigateTo, - showNextcloudFolder + showNextcloudFolder, + showSharedDriveFolder }) => { const client = useClient() const contentQuery = buildMoveOrImportQuery(folder._id) @@ -68,7 +70,10 @@ const FolderPickerContentCozy: React.FC = ({ const isEncrypted = isEncryptedFolder(folder) const files: IOCozyFile[] = useMemo(() => { - if (folder._id === ROOT_DIR_ID && showNextcloudFolder) { + if ( + folder._id === ROOT_DIR_ID && + (showNextcloudFolder || showSharedDriveFolder) + ) { return [ ...(sharedFolderResult.fetchStatus === 'loaded' ? sharedFolderResult.data ?? [] @@ -77,7 +82,14 @@ const FolderPickerContentCozy: React.FC = ({ ] } return [...(filesData ?? [])] - }, [filesData, sharedFolderResult, folder, showNextcloudFolder]) + }, [ + folder._id, + showNextcloudFolder, + showSharedDriveFolder, + filesData, + sharedFolderResult.fetchStatus, + sharedFolderResult.data + ]) const handleFolderUnlockerDismiss = async (): Promise => { const parentFolderQuery = buildFileOrFolderByIdQuery(folder.dir_id) diff --git a/src/components/FolderPicker/FolderPickerContentSharedDrive.tsx b/src/components/FolderPicker/FolderPickerContentSharedDrive.tsx new file mode 100644 index 0000000000..77cd3b967a --- /dev/null +++ b/src/components/FolderPicker/FolderPickerContentSharedDrive.tsx @@ -0,0 +1,108 @@ +import * as React from 'react' +import { useMemo } from 'react' + +import { useClient } from 'cozy-client' +import { isDirectory } from 'cozy-client/dist/models/file' +import type { IOCozyFile } from 'cozy-client/types/types' +import List from 'cozy-ui/transpiled/react/List' + +import { FolderPickerListItem } from './FolderPickerListItem' + +import { FolderPickerAddFolderItem } from '@/components/FolderPicker/FolderPickerAddFolderItem' +import { FolderPickerContentLoader } from '@/components/FolderPicker/FolderPickerContentLoader' +import { isInvalidMoveTarget } from '@/components/FolderPicker/helpers' +import type { File, FolderPickerEntry } from '@/components/FolderPicker/types' +import { isEncryptedFolder } from '@/lib/encryption' +import { FolderUnlocker } from '@/modules/folder/components/FolderUnlocker' +import { useSharedDriveFolder } from '@/modules/shareddrives/hooks/useSharedDriveFolder' +import { buildFileOrFolderByIdQuery } from '@/queries' + +interface FolderPickerContentSharedDriveProps { + folder: IOCozyFile + isFolderCreationDisplayed: boolean + hideFolderCreation: () => void + entries: FolderPickerEntry[] + navigateTo: (folder: import('./types').File) => void + showNextcloudFolder?: boolean +} + +const FolderPickerContentSharedDrive: React.FC< + FolderPickerContentSharedDriveProps +> = ({ + folder, + isFolderCreationDisplayed, + hideFolderCreation, + entries, + navigateTo +}) => { + const client = useClient() + const driveId = folder.driveId ?? '' + const folderId = folder._id + + const { sharedDriveResult } = useSharedDriveFolder({ + driveId, + folderId + }) + + const { fetchStatus, files } = useMemo( + () => + sharedDriveResult.included || sharedDriveResult.data + ? { fetchStatus: 'loaded', files: sharedDriveResult.included ?? [] } + : { fetchStatus: 'loading', files: [] }, + [sharedDriveResult] + ) + + const isEncrypted = isEncryptedFolder(folder) + + const handleFolderUnlockerDismiss = async (): Promise => { + const parentFolderQuery = buildFileOrFolderByIdQuery(folder.dir_id) + const parentFolder = (await client?.fetchQueryAndGetFromState({ + definition: parentFolderQuery.definition(), + options: parentFolderQuery.options + })) as { + data?: IOCozyFile + } + if (!parentFolder.data) { + throw new Error('Parent folder not found') + } + + navigateTo(parentFolder.data) + } + + const handleClick = (file: File): void => { + if (isDirectory(file)) { + navigateTo(file) + } + } + + return ( + + + + + {files.map((file: IOCozyFile, index: number) => ( + + ))} + + + + ) +} + +export { FolderPickerContentSharedDrive } diff --git a/src/components/FolderPicker/FolderPickerContentSharedDriveRoot.tsx b/src/components/FolderPicker/FolderPickerContentSharedDriveRoot.tsx new file mode 100644 index 0000000000..e528945060 --- /dev/null +++ b/src/components/FolderPicker/FolderPickerContentSharedDriveRoot.tsx @@ -0,0 +1,132 @@ +import React, { useMemo } from 'react' + +import { useQuery, useClient } from 'cozy-client' +import { isDirectory } from 'cozy-client/dist/models/file' +import { IOCozyFile } from 'cozy-client/types/types' +import List from 'cozy-ui/transpiled/react/List' + +import { FolderPickerListItem } from './FolderPickerListItem' + +import { FolderPickerAddFolderItem } from '@/components/FolderPicker/FolderPickerAddFolderItem' +import { FolderPickerContentLoadMore } from '@/components/FolderPicker/FolderPickerContentLoadMore' +import { FolderPickerContentLoader } from '@/components/FolderPicker/FolderPickerContentLoader' +import { isInvalidMoveTarget } from '@/components/FolderPicker/helpers' +import { computeNextcloudRootFolder } from '@/components/FolderPicker/helpers' +import type { File, FolderPickerEntry } from '@/components/FolderPicker/types' +import { useTransformFolderListHasSharedDriveShortcuts } from '@/hooks/useTransformFolderListHasSharedDriveShortcuts' +import { isEncryptedFolder } from '@/lib/encryption' +import { FolderUnlocker } from '@/modules/folder/components/FolderUnlocker' +import { buildMoveOrImportQuery, buildFileOrFolderByIdQuery } from '@/queries' + +interface FolderPickerContentSharedDriveRootProps { + folder: IOCozyFile + isFolderCreationDisplayed: boolean + hideFolderCreation: () => void + entries: FolderPickerEntry[] + navigateTo: (folder: import('./types').File) => void + showNextcloudFolder?: boolean +} + +const FolderPickerContentSharedDriveRoot: React.FC< + FolderPickerContentSharedDriveRootProps +> = ({ + folder, + isFolderCreationDisplayed, + hideFolderCreation, + entries, + navigateTo, + showNextcloudFolder +}) => { + const client = useClient() + const contentQuery = buildMoveOrImportQuery(folder._id) + const { + fetchStatus, + data: filesData, + hasMore, + fetchMore + } = useQuery(contentQuery.definition, contentQuery.options) as unknown as { + fetchStatus: string + data?: IOCozyFile[] + hasMore: boolean + fetchMore: () => void + } + + const { sharedDrives, nonSharedDriveList } = + useTransformFolderListHasSharedDriveShortcuts( + filesData, + showNextcloudFolder + ) as { + sharedDrives: IOCozyFile[] + nonSharedDriveList: IOCozyFile[] + } + + const isEncrypted = isEncryptedFolder(folder) + + const files: IOCozyFile[] = useMemo(() => { + return [...sharedDrives, ...nonSharedDriveList] + }, [sharedDrives, nonSharedDriveList]) + + const handleFolderUnlockerDismiss = async (): Promise => { + const parentFolderQuery = buildFileOrFolderByIdQuery(folder.dir_id) + const parentFolder = (await client?.fetchQueryAndGetFromState({ + definition: parentFolderQuery.definition(), + options: parentFolderQuery.options + })) as { + data?: IOCozyFile + } + if (!parentFolder.data) { + throw new Error('Parent folder not found') + } + + navigateTo(parentFolder.data) + } + + const handleClick = (file: File): void => { + if (isDirectory(file)) { + navigateTo(file) + } + + if ( + file._type === 'io.cozy.files' && + file.cozyMetadata?.createdByApp === 'nextcloud' && + file.cozyMetadata.sourceAccount + ) { + const nextcloudRootFolder = computeNextcloudRootFolder({ + sourceAccount: file.cozyMetadata.sourceAccount, + instanceName: file.metadata.instanceName + }) + navigateTo(nextcloudRootFolder) + } + } + + return ( + + + + + {files.map((file, index) => ( + + ))} + + + + + ) +} + +export { FolderPickerContentSharedDriveRoot } diff --git a/src/components/FolderPicker/FolderPickerFooter.tsx b/src/components/FolderPicker/FolderPickerFooter.tsx index ec678e79ea..b456fafedf 100644 --- a/src/components/FolderPicker/FolderPickerFooter.tsx +++ b/src/components/FolderPicker/FolderPickerFooter.tsx @@ -40,7 +40,6 @@ const FolderPickerFooter: React.FC = ({ const isDisabled = isBusy || - folder._id === 'io.cozy.files.shared-drives-dir' || (!canPickEntriesParentFolder && areTargetsInCurrentDir(entries, folder)) return ( diff --git a/src/components/FolderPicker/helpers.ts b/src/components/FolderPicker/helpers.ts index 9b78a60dbf..49cad5c757 100644 --- a/src/components/FolderPicker/helpers.ts +++ b/src/components/FolderPicker/helpers.ts @@ -5,7 +5,8 @@ import { FolderPickerEntry, File } from '@/components/FolderPicker/types' import { getParentPath } from '@/lib/path' import { buildFileOrFolderByIdQuery, - buildNextcloudFolderQuery + buildNextcloudFolderQuery, + buildSharedDriveFileOrFolderByIdQuery } from '@/queries' /** @@ -60,9 +61,12 @@ export const areTargetsInCurrentDir = ( */ const getCozyParentFolder = async ( client: CozyClient | null, - id: string + id: string, + driveId?: string ): Promise => { - const parentFolderQuery = buildFileOrFolderByIdQuery(id) + const parentFolderQuery = driveId + ? buildSharedDriveFileOrFolderByIdQuery({ fileId: id, driveId }) + : buildFileOrFolderByIdQuery(id) const parentFolder = (await client?.fetchQueryAndGetFromState({ definition: parentFolderQuery.definition(), options: parentFolderQuery.options @@ -171,5 +175,7 @@ export const getParentFolder = async ( } } - return await getCozyParentFolder(client, folder.dir_id) + const driveId = + folder.dir_id === 'io.cozy.files.shared-drives-dir' ? '' : folder.driveId + return await getCozyParentFolder(client, folder.dir_id, driveId) } diff --git a/src/hooks/useTransformFolderListHasSharedDriveShortcuts.jsx b/src/hooks/useTransformFolderListHasSharedDriveShortcuts.jsx new file mode 100644 index 0000000000..2ced357f9e --- /dev/null +++ b/src/hooks/useTransformFolderListHasSharedDriveShortcuts.jsx @@ -0,0 +1,78 @@ +import { useMemo } from 'react' + +import { useSharingContext } from 'cozy-sharing' + +import { SHARED_DRIVES_DIR_ID } from '@/constants/config' +import { isNextcloudShortcut } from '@/modules/nextcloud/helpers' +import { useSharedDrives } from '@/modules/shareddrives/hooks/useSharedDrives' + +const useTransformFolderListHasSharedDriveShortcuts = ( + folderList, + showNextcloudFolder = false +) => { + const { isOwner } = useSharingContext() + + const { sharedDrives } = useSharedDrives() + + /** + * The recipient's shared drives are displayed as shortcuts which cannot accessible + * In some cases (like open shared drive from folder picker or sharing section...), + * we want to access to shared drives as directories for both owner and recipient + * The codes below help us to transform the shared drives shortcuts into directory-like objects + */ + const transformedSharedDrives = useMemo( + () => + (sharedDrives ?? []) + .filter(sharing => !isNextcloudShortcut(sharing)) + .map(sharing => { + const [rootFolderId, driveName] = [ + sharing.rules[0]?.values?.[0], + sharing.rules?.[0]?.title + ] + + const fileInSharingSection = folderList?.find( + item => + item.relationships?.referenced_by?.data?.[0]?.id === sharing.id + ) + + if (fileInSharingSection && isOwner(fileInSharingSection.id ?? '')) + return fileInSharingSection + + const directoryData = { + type: 'directory', + name: driveName, + dir_id: SHARED_DRIVES_DIR_ID, + driveId: sharing.id + } + + return { + ...fileInSharingSection, + _id: rootFolderId, + id: SHARED_DRIVES_DIR_ID, + _type: 'io.cozy.files', + path: `/Drives/${driveName}`, + ...directoryData, + attributes: directoryData + } + }), + [sharedDrives, folderList, isOwner] + ) + + /** + * Exclude shared drives from the folderList, + * since it will be replaced with transformed ones above. + */ + const nonSharedDriveList = + folderList?.filter( + item => + item.dir_id !== SHARED_DRIVES_DIR_ID && + (!showNextcloudFolder ? !isNextcloudShortcut(item) : true) + ) || [] + + return { + sharedDrives: transformedSharedDrives, + nonSharedDriveList + } +} + +export { useTransformFolderListHasSharedDriveShortcuts } diff --git a/src/modules/move/MoveModal.jsx b/src/modules/move/MoveModal.jsx index 8dbf49acd6..5c6af8a301 100644 --- a/src/modules/move/MoveModal.jsx +++ b/src/modules/move/MoveModal.jsx @@ -26,7 +26,9 @@ const MoveModal = ({ onClose, currentFolder, entries, - showNextcloudFolder + showNextcloudFolder, + showSharedDriveFolder, + driveId }) => { const client = useClient() const { @@ -99,7 +101,8 @@ const MoveModal = ({ const force = !sharedPaths.includes(folder.path) const moveResponse = await registerCancelable( move(client, entry, folder, { - force + force, + driveId: driveId ?? folder.driveId }) ) if (moveResponse.deleted) { @@ -207,6 +210,7 @@ const MoveModal = ({ <> ( /> } /> } /> + } /> ) : null} diff --git a/src/modules/shareddrives/components/SharedDriveFolderBody.jsx b/src/modules/shareddrives/components/SharedDriveFolderBody.jsx index 0379d5e8b1..e1e1d2190b 100644 --- a/src/modules/shareddrives/components/SharedDriveFolderBody.jsx +++ b/src/modules/shareddrives/components/SharedDriveFolderBody.jsx @@ -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 { moveTo } from '@/modules/actions/components/moveTo' import { FolderBody } from '@/modules/folder/components/FolderBody' const SharedDriveFolderBody = ({ @@ -44,6 +45,7 @@ const SharedDriveFolderBody = ({ hasWriteAccess: canWriteToCurrentFolder, byDocId, dispatch, + canMove: true, navigate, showAlert, pushModal, @@ -51,7 +53,7 @@ const SharedDriveFolderBody = ({ refresh } const actions = makeActions( - [download, hr, rename, infos, hr, versions, hr, trash], + [download, hr, rename, moveTo, infos, hr, versions, hr, trash], actionsOptions ) diff --git a/src/modules/shareddrives/hooks/useMultipleSharedDriveFolders.tsx b/src/modules/shareddrives/hooks/useMultipleSharedDriveFolders.tsx new file mode 100644 index 0000000000..e28b07770d --- /dev/null +++ b/src/modules/shareddrives/hooks/useMultipleSharedDriveFolders.tsx @@ -0,0 +1,67 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' + +import { useClient } from 'cozy-client' +import type { IOCozyFile } from 'cozy-client/types/types' + +import { buildSharedDriveFolderQuery } from '@/queries' + +interface UseMultipleSharedDriveFoldersProps { + driveId: string + folderIds: string[] +} + +interface SharedDriveResult { + data: IOCozyFile | null +} + +interface SharedDriveFolderReturn { + sharedDriveResults: IOCozyFile[] | null +} + +const useMultipleSharedDriveFolders = ({ + driveId, + folderIds +}: UseMultipleSharedDriveFoldersProps): SharedDriveFolderReturn => { + const client = useClient() + + const [sharedDriveResults, setSharedDriveResults] = useState< + SharedDriveFolderReturn['sharedDriveResults'] + >([]) + + const sharedDriveQueries = useMemo( + () => + folderIds.map(folderId => + buildSharedDriveFolderQuery({ + driveId, + folderId + }) + ), + [driveId, folderIds] + ) + + const fetchSharedDriveResults = useCallback(async () => { + const results = (await Promise.all( + sharedDriveQueries.map(async query => { + return client?.query(query.definition(), query.options) + }) + )) as SharedDriveResult[] + + setSharedDriveResults( + results.map( + (result: SharedDriveResult) => result.data + ) as SharedDriveFolderReturn['sharedDriveResults'] + ) + }, [client, sharedDriveQueries]) + + useEffect(() => { + if (client) { + void fetchSharedDriveResults() + } + }, [client, fetchSharedDriveResults]) + + return { + sharedDriveResults + } +} + +export { useMultipleSharedDriveFolders } diff --git a/src/modules/shareddrives/hooks/useSharedDriveFolder.tsx b/src/modules/shareddrives/hooks/useSharedDriveFolder.tsx index 0332a275cd..0330fc663a 100644 --- a/src/modules/shareddrives/hooks/useSharedDriveFolder.tsx +++ b/src/modules/shareddrives/hooks/useSharedDriveFolder.tsx @@ -16,6 +16,7 @@ interface SharedDriveFolderReturn { sharedDriveQuery: QueryConfig sharedDriveResult: { data?: IOCozyFile[] | null + included?: IOCozyFile[] | null } } diff --git a/src/modules/views/Modal/MoveFilesView.jsx b/src/modules/views/Modal/MoveFilesView.jsx index 15368a6617..417ecbd760 100644 --- a/src/modules/views/Modal/MoveFilesView.jsx +++ b/src/modules/views/Modal/MoveFilesView.jsx @@ -6,12 +6,14 @@ import { hasQueryBeenLoaded, useQuery } from 'cozy-client' import { LoaderModal } from '@/components/LoaderModal' import useDisplayedFolder from '@/hooks/useDisplayedFolder' import MoveModal from '@/modules/move/MoveModal' +import { useSharedDrives } from '@/modules/shareddrives/hooks/useSharedDrives' import { buildParentsByIdsQuery } from '@/queries' const MoveFilesView = () => { const navigate = useNavigate() const { state } = useLocation() const { displayedFolder } = useDisplayedFolder() + const { sharedDrives } = useSharedDrives() const hasFileIds = state?.fileIds != undefined @@ -40,6 +42,7 @@ const MoveFilesView = () => { entries={fileResult.data} onClose={onClose} showNextcloudFolder={showNextcloudFolder} + showSharedDriveFolder={sharedDrives?.length > 0} /> ) } diff --git a/src/modules/views/Modal/MoveSharedDriveFilesView.jsx b/src/modules/views/Modal/MoveSharedDriveFilesView.jsx new file mode 100644 index 0000000000..2340fe1054 --- /dev/null +++ b/src/modules/views/Modal/MoveSharedDriveFilesView.jsx @@ -0,0 +1,48 @@ +import React from 'react' +import { useNavigate, useLocation } from 'react-router-dom' + +import { LoaderModal } from '@/components/LoaderModal' +import useDisplayedFolder from '@/hooks/useDisplayedFolder' +import MoveModal from '@/modules/move/MoveModal' +import { useMultipleSharedDriveFolders } from '@/modules/shareddrives/hooks/useMultipleSharedDriveFolders' + +const MoveSharedDriveFilesView = () => { + const navigate = useNavigate() + const { state } = useLocation() + const { displayedFolder } = useDisplayedFolder() + + const { sharedDriveResults } = useMultipleSharedDriveFolders({ + folderIds: state.fileIds, + driveId: displayedFolder?.driveId + }) + + if (sharedDriveResults && displayedFolder) { + const onClose = () => { + navigate('..', { replace: true }) + } + + const showNextcloudFolder = !sharedDriveResults.some( + file => file.type === 'directory' + ) + + const entries = sharedDriveResults.map(file => ({ + ...file, + path: `${displayedFolder.path}/${file.name}` + })) + + return ( + + ) + } + + return +} + +export { MoveSharedDriveFilesView } diff --git a/src/modules/views/Sharings/index.jsx b/src/modules/views/Sharings/index.jsx index c43810492f..17179b4ebe 100644 --- a/src/modules/views/Sharings/index.jsx +++ b/src/modules/views/Sharings/index.jsx @@ -23,11 +23,8 @@ import FolderViewHeader from '../Folder/FolderViewHeader' import FolderViewBodyVz from '../Folder/virtualized/FolderViewBody' import useHead from '@/components/useHead' -import { - SHARED_DRIVES_DIR_ID, - SHARING_TAB_ALL, - SHARING_TAB_DRIVES -} from '@/constants/config' +import { SHARING_TAB_ALL, SHARING_TAB_DRIVES } from '@/constants/config' +import { useTransformFolderListHasSharedDriveShortcuts } from '@/hooks/useTransformFolderListHasSharedDriveShortcuts' import { useModalContext } from '@/lib/ModalContext' import { download, @@ -52,7 +49,6 @@ import { useSelectionContext } from '@/modules/selection/SelectionProvider' import { deleteSharedDrive } from '@/modules/shareddrives/components/actions/deleteSharedDrive' import { leaveSharedDrive } from '@/modules/shareddrives/components/actions/leaveSharedDrive' import { manageAccess } from '@/modules/shareddrives/components/actions/manageAccess' -import { useSharedDrives } from '@/modules/shareddrives/hooks/useSharedDrives' import { buildSharingsQuery, buildSharingsWithMetadataAttributeQuery @@ -71,10 +67,9 @@ export const SharingsView = ({ sharedDocumentIds = [] }) => { const { pushModal, popModal } = useModalContext() const { isSelectionBarVisible, toggleSelectAllItems, isSelectAll } = useSelectionContext() - const { allLoaded, refresh, isOwner } = useSharingContext() + const { allLoaded, refresh } = useSharingContext() const { isNativeFileSharingAvailable, shareFilesNative } = useNativeFileSharing() - const { sharedDrives } = useSharedDrives() const dispatch = useDispatch() useHead() const { showAlert } = useAlert() @@ -99,73 +94,37 @@ export const SharingsView = ({ sharedDocumentIds = [] }) => { ) const result = useQuery(query.definition, query.options) - const filteredResult = useMemo(() => { - /** - * Problem: - * - In the recipient's Sharing section, shared drives appear only as shortcuts - * and don’t contain a root folder id (the folder id in the owner's shared drive). - * - * Why: - * - To open a shared drive, we need a URL like `shareddrive/:driveId/:rootFolderId`. - * - This information exists in `io.cozy.sharings`, which includes root folder id, - * but the structure is not compatible with the directory format expected - * in the Sharing UI. - * - * Solution: - * - Transform `sharedDrives` into directory-like objects with the required - * properties (`id`, `path`, `attributes`,...) so they can be displayed - * and opened consistently. - */ - const transformedSharedDrives = (sharedDrives || []).map(sharing => { - const [rootFolderId, driveName] = [ - sharing.rules?.[0]?.values?.[0], - sharing.rules?.[0]?.title - ] - - // Find the file from sharing section that has same `driveId` then override it into directory-like objects - const fileInSharingSection = result.data?.find( - item => item.relationships?.referenced_by?.data?.[0]?.id === sharing.id - ) - - if (fileInSharingSection && isOwner(fileInSharingSection?.id)) - return fileInSharingSection - - const directoryData = { - type: 'directory', - name: driveName, - dir_id: SHARED_DRIVES_DIR_ID, - driveId: sharing.id - } - - return { - ...fileInSharingSection, - _id: rootFolderId, - id: SHARED_DRIVES_DIR_ID, - _type: 'io.cozy.files', - path: `/Drives/${driveName}`, - ...directoryData, - attributes: directoryData - } - }) - - /** - * Exclude shared drives from the original result, - * since it will be replaced with transformed ones above. - */ - const filteredResultData = - result.data?.filter(item => !(item.dir_id === SHARED_DRIVES_DIR_ID)) || [] + /** + * Problem: + * - In the recipient's Sharing section, shared drives appear only as shortcuts + * and don’t contain a root folder id (the folder id in the owner's shared drive). + * + * Why: + * - To open a shared drive, we need a URL like `shareddrive/:driveId/:rootFolderId`. + * - This information exists in `io.cozy.sharings`, which includes root folder id, + * but the structure is not compatible with the directory format expected + * in the Sharing UI. + * + * Solution: + * - Transform `sharedDrives` into directory-like objects with the required + * properties (`id`, `path`, `attributes`,...) so they can be displayed + * and opened consistently. + */ + const { sharedDrives: transformedSharedDrives, nonSharedDriveList } = + useTransformFolderListHasSharedDriveShortcuts(result.data) + const filteredResult = useMemo(() => { const combinedData = tab === SHARING_TAB_DRIVES ? transformedSharedDrives - : [...transformedSharedDrives, ...filteredResultData] + : [...transformedSharedDrives, ...nonSharedDriveList] return { ...result, data: combinedData, count: combinedData.length } - }, [sharedDrives, result, tab, isOwner]) + }, [transformedSharedDrives, nonSharedDriveList, result, tab]) const actionsOptions = { client, diff --git a/src/queries/index.ts b/src/queries/index.ts index 55d727b39d..9d471c2cac 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -332,6 +332,23 @@ export const buildFileOrFolderByIdQuery: QueryBuilder = fileId => ({ } }) +interface BuildSharedDriveFileOrFolderByIdQuery { + fileId: string + driveId: string +} + +export const buildSharedDriveFileOrFolderByIdQuery: QueryBuilder< + BuildSharedDriveFileOrFolderByIdQuery +> = ({ fileId, driveId }) => ({ + definition: () => Q('io.cozy.files').getById(fileId).sharingById(driveId), + options: { + as: `io.cozy.files/${driveId}/${fileId}`, + fetchPolicy: defaultFetchPolicy, + singleDocData: true, + enabled: !!fileId && !!driveId + } +}) + // this query should use `getById` instead of `where` as `buildFileOrFolderByIdQuery` does. // But in this case, due to a stack limitation, the `file.path` is not returned. // As we need the `file.path` we do this trick until the stack is updated