Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d1bcfd6
refactor: extract name collision move actions out of the container
terrerox Sep 11, 2026
6f45d60
refactor: update mocks and dispatch actions in name collision tests
terrerox Sep 15, 2026
dbd2971
fix: match name collisions by name and type and batch their resolution
terrerox Sep 11, 2026
71f6dcc
fix: tag colliding existing items with isFolder so folders and files …
terrerox Sep 11, 2026
5a3ca29
fix: rename asExistingItems to addIsFolderField for clarity and consi…
terrerox Sep 20, 2026
a73a26b
refactor: extract name collision upload actions out of the container
terrerox Sep 11, 2026
4b0a43e
test: add e2e coverage for the skip option in name collisions
terrerox Sep 11, 2026
c947fa0
feat: skip item option in name collision
terrerox Sep 11, 2026
858fda9
feat: merge folder uploads into the existing folder on skip
terrerox Aug 26, 2026
8c4722c
test: add e2e coverage for name collision resolutions
terrerox Sep 11, 2026
b37601a
test: ensure collision dialog closes after resolving file upload
terrerox Sep 11, 2026
c66e58e
test: add e2e coverage for same-name restore collisions
terrerox Sep 20, 2026
8322c32
fix: resolve same-name items within a move collision batch
terrerox Sep 20, 2026
f9e798b
test: add e2e coverage for same-name restore collisions resolved one …
terrerox Sep 21, 2026
846a9a6
fix: restore same-name items resolved one at a time with replace
terrerox Sep 21, 2026
b733417
feat: add versioning max file size to name collision context and upda…
terrerox Oct 7, 2026
f549f92
feat: enhance unique name handling for file uploads to avoid collisions
terrerox Oct 7, 2026
8b4af77
Merge pull request #2160 from internxt/test/name-collision-same-name-…
terrerox Oct 7, 2026
c0be480
Merge pull request #2159 from internxt/fix/name-collision-same-name-b…
terrerox Oct 7, 2026
d6191a4
Merge pull request #2129 from internxt/feat/name-collision-folder-mer…
terrerox Oct 7, 2026
53ac5f2
Merge pull request #2150 from internxt/test/name-collision-skip-e2e
terrerox Oct 7, 2026
afbfe83
Merge pull request #2039 from internxt/feat/skip-item-option-in-name-…
terrerox Oct 7, 2026
bef2cfe
Merge pull request #2148 from internxt/fix/name-collision-matching-an…
terrerox Oct 7, 2026
3ccc291
Merge pull request #2147 from internxt/refactor/extract-name-collisio…
terrerox Oct 7, 2026
c6b054e
Merge branch 'master' into refactor/extract-name-collision-move-actions
terrerox Oct 7, 2026
d5f6c25
await dispatch fetchSortedFolderContentThunk in move actions
terrerox Oct 7, 2026
531d94c
fix: Add versioning max file size to bootstrap responses
terrerox Oct 7, 2026
4025a7e
Merge branch 'master' into refactor/extract-name-collision-move-actions
terrerox Oct 8, 2026
13eedd2
Merge branch 'master' into refactor/extract-name-collision-move-actions
terrerox Oct 8, 2026
1598ef4
refactor: Optimize folder upload handling with Promise.all for concur…
terrerox Oct 8, 2026
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
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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;
Expand All @@ -187,6 +107,7 @@ const NameCollisionContainer: FC = () => {
onSubmitButtonPressed={triggerSelectedOptionsOnSubmit}
onCloseDialog={closeDialog}
operationType={operationType as 'move' | 'upload'}
remainingItemsCount={remainingItemsCount}
/>
);
};
Expand Down
Loading
Loading