diff --git a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx index f20ebff415..d919461527 100644 --- a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx +++ b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx @@ -1,26 +1,13 @@ import { FC, useMemo } from 'react'; import NameCollisionDialog, { OnSubmitPressed } from '.'; -import { moveItemsToTrash } from 'views/Trash/services'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -import { storageActions } from 'app/store/slices/storage'; -import storageThunks from 'app/store/slices/storage/storage.thunks'; -import { fetchSortedFolderContentThunk } from 'app/store/slices/storage/storage.thunks/fetchSortedFolderContentThunk'; import { uiActions } from 'app/store/slices/ui'; -import { DriveItemData } from 'app/drive/types'; import { IRoot } from 'app/store/slices/storage/types'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; -import { uploadFoldersWithTracking } from 'app/drive/services/folder.service/uploadFoldersWithTracking'; -import replaceFileService from 'views/Drive/services/replaceFile.service'; -import { Network, getEnvironmentConfig } from 'app/drive/services/network.service'; -import { fileVersionsActions, fileVersionsSelectors } from 'app/store/slices/fileVersions'; -import { isVersioningExtensionAllowed } from 'views/Drive/components/VersionHistory/utils'; -import { checkFolderDuplicated } from 'app/store/slices/storage/folderUtils/checkFolderDuplicated'; -import { getUniqueFolderName } from 'app/store/slices/storage/folderUtils/getUniqueFolderName'; -import { getUniqueFilename } from 'app/store/slices/storage/fileUtils/getUniqueFilename'; -import { checkDuplicatedFiles } from 'app/store/slices/storage/fileUtils/checkDuplicatedFiles'; -import { CollisionGroup } from 'app/store/slices/storage/storage.model'; -import { MoveItemPayload } from 'app/store/slices/storage/storage.thunks/moveItemsThunk'; +import { fileVersionsSelectors } from 'app/store/slices/fileVersions'; +import { NameCollisionContext, resolveCollision } from './nameCollision.actions'; +import { findExistingItemFor, findPendingGroupIndex, getRemainingGroups } from './nameCollision.utils'; const NameCollisionContainer: FC = () => { const dispatch = useAppDispatch(); @@ -31,149 +18,82 @@ const NameCollisionContainer: FC = () => { const operationType = collisionDialogInfo?.operation; const newItems = useMemo(() => collisionGroups.flatMap((g) => g.duplicatedItems), [collisionGroups]); const existingItems = useMemo(() => collisionGroups.flatMap((g) => g.existingItems), [collisionGroups]); + const remainingItemsCount = newItems.length; const selectedWorkspace = useAppSelector(workspacesSelectors.getSelectedWorkspace); const limits = useAppSelector(fileVersionsSelectors.getLimits); const maxUploadFileSize = useAppSelector(fileVersionsSelectors.getMaxFileSizeLimit); const isVersioningEnabled = limits?.versioning?.enabled ?? false; + const versioningMaxFileSize = limits?.versioning?.maxFileSize ?? 0; + + const context: NameCollisionContext = { + dispatch, + selectedWorkspace, + maxUploadFileSize, + isVersioningEnabled, + versioningMaxFileSize, + }; const closeDialog = () => { dispatch(uiActions.setIsNameCollisionDialogOpen({ open: false, info: undefined })); }; - const replaceAndMoveItem = async (group: CollisionGroup) => { - await moveItemsToTrash(group.existingItems); - await dispatch( - storageThunks.moveItemsThunk({ - items: group.duplicatedItems as DriveItemData[], - destinationFolderId: group.destinationUuid, - }), - ); - }; - - const keepAndMoveItem = async (group: CollisionGroup) => { - for (const item of group.duplicatedItems as DriveItemData[]) { - let itemParsed: MoveItemPayload; - - if (item.isFolder) { - const { duplicatedFoldersResponse } = await checkFolderDuplicated([item], group.destinationUuid); - const finalName = await getUniqueFolderName( - item.plainName ?? item.name, - duplicatedFoldersResponse as DriveItemData[], - group.destinationUuid, - ); - itemParsed = { ...item, name: finalName, plain_name: finalName, newItemName: finalName }; - } else { - const { duplicatedFilesResponse } = await checkDuplicatedFiles([item], group.destinationUuid); - const finalName = await getUniqueFilename(item.name, item.type, duplicatedFilesResponse, group.destinationUuid); - itemParsed = { ...item, name: finalName, plainName: finalName, plain_name: finalName, newItemName: finalName }; - } - - await dispatch( - storageThunks.moveItemsThunk({ - items: [itemParsed], - destinationFolderId: group.destinationUuid, - }), + const triggerSelectedOptionsOnSubmit = async ({ operationType, operation, applyToAll }: OnSubmitPressed) => { + if (applyToAll) { + closeDialog(); + await Promise.all( + collisionGroups.map((group) => + resolveCollision( + { + operationType, + operation, + items: group.duplicatedItems, + existingItems: group.existingItems, + destinationUuid: group.destinationUuid, + }, + context, + ), + ), ); + return; } - }; - - const uploadFileAndGetFileId = async (file: File, itemToReplace: DriveItemData) => { - const { bridgeUser, bridgePass, encryptionKey, bucketId } = await getEnvironmentConfig(!!selectedWorkspace); - const network = new Network(bridgeUser, bridgePass, encryptionKey); - const taskId = `replace-${itemToReplace.uuid}-${Date.now()}`; - const [uploadPromise] = network.uploadFile( - bucketId, - { filecontent: file, filesize: file.size, progressCallback: () => {} }, - { taskId }, - ); - return uploadPromise; - }; - const replaceFileVersion = async (file: File, itemToReplace: DriveItemData) => { - const newFileId = await uploadFileAndGetFileId(file, itemToReplace); - await replaceFileService.replaceFile(itemToReplace.uuid, { fileId: newFileId, size: file.size }); - dispatch(fileVersionsActions.invalidateCache(itemToReplace.uuid)); - }; - - const replaceAndUploadItem = async (group: CollisionGroup) => { - const itemsToUpload = group.duplicatedItems as (IRoot | File)[]; - const itemsToReplace = group.existingItems; - - for (let i = 0; i < itemsToUpload.length; i++) { - const itemToUpload = itemsToUpload[i]; - const itemToReplace = itemsToReplace[i]; - - if ((itemToUpload as IRoot).fullPathEdited) { - await moveItemsToTrash([itemToReplace]); - await uploadFoldersWithTracking({ - payload: [{ root: { ...(itemToUpload as IRoot) }, currentFolderId: group.destinationUuid }], - selectedWorkspace, - dispatch, - maxUploadFileSize, - }); - } else { - const file = itemToUpload as File; - const canReplaceVersion = isVersioningEnabled && isVersioningExtensionAllowed(itemToReplace); - if (canReplaceVersion) { - await replaceFileVersion(file, itemToReplace); - } else { - await moveItemsToTrash([itemToReplace]); - await dispatch( - storageThunks.uploadItemsThunk({ - files: [file], - parentFolderId: group.destinationUuid, - options: { disableDuplicatedNamesCheck: true }, - }), - ); - } - } - - dispatch(fetchSortedFolderContentThunk(group.destinationUuid)); + const groupIndex = findPendingGroupIndex(collisionGroups); + const hasPendingGroup = groupIndex !== -1; + if (!hasPendingGroup) { + closeDialog(); + return; } - }; - const keepAndUploadItem = async (group: CollisionGroup) => { - for (const itemToUpload of group.duplicatedItems as (IRoot | File)[]) { - if ((itemToUpload as IRoot).fullPathEdited) { - await uploadFoldersWithTracking({ - payload: [{ root: { ...(itemToUpload as IRoot) }, currentFolderId: group.destinationUuid }], - selectedWorkspace, - dispatch, - maxUploadFileSize, - }); - } else { - await dispatch( - storageThunks.uploadItemsThunk({ - files: [itemToUpload as File], - parentFolderId: group.destinationUuid, - }), - ); - } - dispatch(fetchSortedFolderContentThunk(group.destinationUuid)); - } - }; + const group = collisionGroups[groupIndex]; + const itemToUpload = group.duplicatedItems[0]; + const collidingExistingItem = findExistingItemFor(itemToUpload, group.existingItems); + const isReplacing = operation === 'replace'; + const replacedExistingItem = isReplacing ? collidingExistingItem : undefined; + + await resolveCollision( + { + operationType, + operation, + items: [itemToUpload], + existingItems: group.existingItems, + destinationUuid: group.destinationUuid, + }, + context, + ); - const triggerSelectedOptionsOnSubmit = async ({ operationType, operation }: OnSubmitPressed) => { - for (const group of collisionGroups) { - switch (operationType + operation) { - case 'move' + 'keep': - await keepAndMoveItem(group); - dispatch(storageActions.popItemsToDelete(group.duplicatedItems as DriveItemData[])); - break; - case 'move' + 'replace': - await replaceAndMoveItem(group); - dispatch(storageActions.popItemsToDelete(group.duplicatedItems as DriveItemData[])); - break; - case 'upload' + 'keep': - await keepAndUploadItem(group); - break; - case 'upload' + 'replace': - await replaceAndUploadItem(group); - break; - } + const remainingGroups = getRemainingGroups(collisionGroups, groupIndex, replacedExistingItem); + const hasRemainingGroups = remainingGroups.length > 0; + if (hasRemainingGroups) { + dispatch( + uiActions.setIsNameCollisionDialogOpen({ + open: true, + info: { groups: remainingGroups, operation: operationType }, + }), + ); + } else { + closeDialog(); } - closeDialog(); }; if (!collisionDialogInfo) return null; @@ -187,6 +107,7 @@ const NameCollisionContainer: FC = () => { onSubmitButtonPressed={triggerSelectedOptionsOnSubmit} onCloseDialog={closeDialog} operationType={operationType as 'move' | 'upload'} + remainingItemsCount={remainingItemsCount} /> ); }; diff --git a/src/app/drive/components/NameCollisionDialog/index.tsx b/src/app/drive/components/NameCollisionDialog/index.tsx index 7db0c245bb..f6419a0d9d 100644 --- a/src/app/drive/components/NameCollisionDialog/index.tsx +++ b/src/app/drive/components/NameCollisionDialog/index.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { Label, Radio, RadioGroup } from '@headlessui/react'; -import { Button, Modal } from '@internxt/ui'; +import { Button, Checkbox, Modal } from '@internxt/ui'; import { DriveItemData } from 'app/drive/types'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { IRoot } from 'app/store/slices/storage/types'; @@ -13,9 +13,10 @@ export const OPERATION_TYPE = { export type OnSubmitPressed = { operationType: 'move' | 'upload'; - operation: 'keep' | 'replace'; + operation: 'keep' | 'replace' | 'skip'; itemsToUpload: (File | IRoot | DriveItemData)[]; itemsToReplace: (DriveItemData | IRoot)[]; + applyToAll: boolean; }; export interface NameCollisionDialogProps { @@ -23,9 +24,16 @@ export interface NameCollisionDialogProps { isOpen: boolean; driveItems: (DriveItemData | IRoot)[]; newItems: (File | IRoot)[]; + remainingItemsCount: number; onCloseDialog(): void; onCancelButtonPressed(): void; - onSubmitButtonPressed({ operationType, operation, itemsToUpload, itemsToReplace }: OnSubmitPressed): Promise; + onSubmitButtonPressed({ + operationType, + operation, + itemsToUpload, + itemsToReplace, + applyToAll, + }: OnSubmitPressed): Promise; } const NameCollisionDialog: FC = ({ @@ -33,35 +41,41 @@ const NameCollisionDialog: FC = ({ operationType, newItems, driveItems, + remainingItemsCount, onCancelButtonPressed, onCloseDialog, onSubmitButtonPressed, }: NameCollisionDialogProps) => { const { translate } = useTranslationContext(); - const options = [ - { - operation: 'replace' as const, - name: translate('modals.renameModal.replaceItem'), - }, - { - operation: 'keep' as const, - name: translate('modals.renameModal.keepBoth'), - }, - ]; + const options = useMemo( + () => [ + { + operation: 'replace' as const, + name: translate('modals.alreadyExistsModal.replaceItem'), + }, + { + operation: 'keep' as const, + name: translate('modals.alreadyExistsModal.keepBoth'), + }, + { + operation: 'skip' as const, + name: translate('modals.alreadyExistsModal.skipItem'), + }, + ], + [translate], + ); const [isLoading, setIsLoading] = useState(false); const [selectedOption, setSelectedOption] = useState(options[0]); + const [applyToAll, setApplyToAll] = useState(false); + + const title = translate('modals.alreadyExistsModal.title'); + const description = translate('modals.alreadyExistsModal.description', { itemName: newItems?.[0]?.name }); - const title = - newItems?.length > 1 ? translate('modals.renameModal.titleMultipleItems') : translate('modals.renameModal.title'); - const description = - newItems?.length > 1 - ? translate('modals.renameModal.multipleDescription') - : translate('modals.renameModal.description', { itemName: newItems?.[0]?.name }); const primaryButtonText = useMemo( () => operationType === OPERATION_TYPE.MOVE - ? translate('modals.renameModal.move') - : translate('modals.renameModal.upload'), + ? translate('modals.alreadyExistsModal.move') + : translate('modals.alreadyExistsModal.upload'), [operationType], ); @@ -69,13 +83,15 @@ const NameCollisionDialog: FC = ({ if (isOpen) { setIsLoading(false); setSelectedOption(options[0]); + setApplyToAll(false); } - }, [isOpen]); + }, [isOpen, options]); const onClose = (): void => { onCloseDialog(); onCancelButtonPressed(); setSelectedOption(options[0]); + setApplyToAll(false); setIsLoading(false); }; @@ -88,9 +104,10 @@ const NameCollisionDialog: FC = ({ operation: selectedOption.operation, itemsToUpload: newItems, itemsToReplace: driveItems, + applyToAll, }); - onClose(); + setIsLoading(false); }; return ( @@ -100,7 +117,7 @@ const NameCollisionDialog: FC = ({

{description}

- +
{options.map((option) => ( @@ -109,12 +126,14 @@ const NameCollisionDialog: FC = ({ className={`flex h-5 w-5 flex-col items-center justify-center rounded-full ${ option.operation === selectedOption.operation ? 'bg-primary active:bg-primary-dark' - : 'border border-gray-40 bg-white group-hover:border-gray-50' + : 'border border-gray-40 bg-white dark:bg-transparent group-hover:border-gray-50' }`} >
@@ -128,6 +147,17 @@ const NameCollisionDialog: FC = ({
+ {remainingItemsCount > 1 && ( +
+ setApplyToAll((prev) => !prev)} /> + +
+ )}