From e3183d9620c413521722a2d34a3b76b5eedfe2ed Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Mon, 13 Jul 2026 14:51:24 +0200 Subject: [PATCH 1/3] feat: skip item option in name collision --- .../NameCollisionContainer.tsx | 255 ++++++++++++------ .../components/NameCollisionDialog/index.tsx | 81 ++++-- src/app/i18n/locales/de.json | 10 +- src/app/i18n/locales/en.json | 10 +- src/app/i18n/locales/es.json | 10 +- src/app/i18n/locales/fr.json | 10 +- src/app/i18n/locales/it.json | 10 +- src/app/i18n/locales/ru.json | 10 +- src/app/i18n/locales/tw.json | 10 +- src/app/i18n/locales/zh.json | 10 +- 10 files changed, 263 insertions(+), 153 deletions(-) diff --git a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx index f20ebff415..b630152c59 100644 --- a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx +++ b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx @@ -31,6 +31,7 @@ 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 = useMemo(() => existingItems.length, [existingItems]); const selectedWorkspace = useAppSelector(workspacesSelectors.getSelectedWorkspace); const limits = useAppSelector(fileVersionsSelectors.getLimits); @@ -41,40 +42,57 @@ const NameCollisionContainer: FC = () => { dispatch(uiActions.setIsNameCollisionDialogOpen({ open: false, info: undefined })); }; + const replaceAndMoveSingleItem = async ( + itemToUpload: DriveItemData, + itemToReplace: DriveItemData, + destinationUuid: string, + ) => { + await moveItemsToTrash([itemToReplace]); + await dispatch( + storageThunks.moveItemsThunk({ + items: [itemToUpload], + destinationFolderId: destinationUuid, + }), + ); + }; + const replaceAndMoveItem = async (group: CollisionGroup) => { - await moveItemsToTrash(group.existingItems); + const itemsToUpload = group.duplicatedItems as DriveItemData[]; + const itemsToReplace = group.existingItems; + + for (let i = 0; i < itemsToUpload.length; i++) { + await replaceAndMoveSingleItem(itemsToUpload[i], itemsToReplace[i], group.destinationUuid); + } + }; + + const keepAndMoveSingleItem = async (item: DriveItemData, destinationUuid: string) => { + let itemParsed: MoveItemPayload; + + if (item.isFolder) { + const { duplicatedFoldersResponse } = await checkFolderDuplicated([item], destinationUuid); + const finalName = await getUniqueFolderName( + item.plainName ?? item.name, + duplicatedFoldersResponse as DriveItemData[], + destinationUuid, + ); + itemParsed = { ...item, name: finalName, plain_name: finalName, newItemName: finalName }; + } else { + const { duplicatedFilesResponse } = await checkDuplicatedFiles([item], destinationUuid); + const finalName = await getUniqueFilename(item.name, item.type, duplicatedFilesResponse, destinationUuid); + itemParsed = { ...item, name: finalName, plainName: finalName, plain_name: finalName, newItemName: finalName }; + } + await dispatch( storageThunks.moveItemsThunk({ - items: group.duplicatedItems as DriveItemData[], - destinationFolderId: group.destinationUuid, + items: [itemParsed], + destinationFolderId: 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, - }), - ); + await keepAndMoveSingleItem(item, group.destinationUuid); } }; @@ -96,84 +114,146 @@ const NameCollisionContainer: FC = () => { dispatch(fileVersionsActions.invalidateCache(itemToReplace.uuid)); }; + const replaceAndUploadSingleItem = async ( + itemToUpload: IRoot | File, + itemToReplace: DriveItemData, + destinationUuid: string, + ) => { + if ((itemToUpload as IRoot).fullPathEdited) { + await moveItemsToTrash([itemToReplace]); + await uploadFoldersWithTracking({ + payload: [{ root: { ...(itemToUpload as IRoot) }, currentFolderId: 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: destinationUuid, + options: { disableDuplicatedNamesCheck: true }, + }), + ); + } + } + + dispatch(fetchSortedFolderContentThunk(destinationUuid)); + }; + 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 }, - }), - ); - } - } + await replaceAndUploadSingleItem(itemsToUpload[i], itemsToReplace[i], group.destinationUuid); + } + }; - dispatch(fetchSortedFolderContentThunk(group.destinationUuid)); + const keepAndUploadSingleItem = async (itemToUpload: IRoot | File, destinationUuid: string) => { + if ((itemToUpload as IRoot).fullPathEdited) { + await uploadFoldersWithTracking({ + payload: [{ root: { ...(itemToUpload as IRoot) }, currentFolderId: destinationUuid }], + selectedWorkspace, + dispatch, + maxUploadFileSize, + }); + } else { + await dispatch( + storageThunks.uploadItemsThunk({ + files: [itemToUpload as File], + parentFolderId: destinationUuid, + }), + ); } + dispatch(fetchSortedFolderContentThunk(destinationUuid)); }; 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)); + await keepAndUploadSingleItem(itemToUpload, group.destinationUuid); } }; - 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 triggerSelectedOptionsOnSubmit = async ({ operationType, operation, applyToAll }: OnSubmitPressed) => { + if (applyToAll) { + 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; + case 'upload' + 'skip': + break; + } } + closeDialog(); + return; + } + + const groupIndex = collisionGroups.findIndex((g) => g.duplicatedItems.length > 0); + if (groupIndex === -1) { + closeDialog(); + return; + } + + const group = collisionGroups[groupIndex]; + const itemToUpload = group.duplicatedItems[0]; + const itemToReplace = group.existingItems[0]; + + switch (operationType + operation) { + case 'move' + 'keep': + await keepAndMoveSingleItem(itemToUpload as DriveItemData, group.destinationUuid); + dispatch(storageActions.popItemsToDelete([itemToUpload as DriveItemData])); + break; + case 'move' + 'replace': + await replaceAndMoveSingleItem(itemToUpload as DriveItemData, itemToReplace, group.destinationUuid); + dispatch(storageActions.popItemsToDelete([itemToUpload as DriveItemData])); + break; + case 'upload' + 'keep': + await keepAndUploadSingleItem(itemToUpload as IRoot | File, group.destinationUuid); + break; + case 'upload' + 'replace': + await replaceAndUploadSingleItem(itemToUpload as IRoot | File, itemToReplace, group.destinationUuid); + break; + case 'upload' + 'skip': + break; + } + + const remainingGroups = collisionGroups + .map((g, idx) => + idx === groupIndex + ? { ...g, duplicatedItems: g.duplicatedItems.slice(1), existingItems: g.existingItems.slice(1) } + : g, + ) + .filter((g) => g.duplicatedItems.length > 0); + + if (remainingGroups.length > 0) { + dispatch( + uiActions.setIsNameCollisionDialogOpen({ + open: true, + info: { groups: remainingGroups, operation: operationType }, + }), + ); + } else { + closeDialog(); } - closeDialog(); }; if (!collisionDialogInfo) return null; @@ -187,6 +267,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..0d95c6b7d5 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,45 @@ 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(() => { + const collisionOptions: { operation: 'replace' | 'keep' | 'skip'; name: string }[] = [ + { + operation: 'replace', + name: translate('modals.alreadyExistsModal.replaceItem'), + }, + { + operation: 'keep', + name: translate('modals.alreadyExistsModal.keepBoth'), + }, + ]; + + if (operationType === OPERATION_TYPE.UPLOAD) { + collisionOptions.push({ + operation: 'skip', + name: translate('modals.alreadyExistsModal.skipFile'), + }); + } + + return collisionOptions; + }, [operationType, 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 +87,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 +108,10 @@ const NameCollisionDialog: FC = ({ operation: selectedOption.operation, itemsToUpload: newItems, itemsToReplace: driveItems, + applyToAll, }); - onClose(); + setIsLoading(false); }; return ( @@ -100,7 +121,7 @@ const NameCollisionDialog: FC = ({

{description}

- +
{options.map((option) => ( @@ -109,12 +130,12 @@ 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 +149,14 @@ const NameCollisionDialog: FC = ({
+ {remainingItemsCount > 1 && ( +
+ setApplyToAll((prev) => !prev)} /> +

+ {translate('modals.alreadyExistsModal.applyToAll')} +

+
+ )}