Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IRoot } from 'app/store/slices/storage/types';
import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors';
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 @@ -17,32 +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 };
const context: NameCollisionContext = {
dispatch,
selectedWorkspace,
maxUploadFileSize,
isVersioningEnabled,
versioningMaxFileSize,
};

const closeDialog = () => {
dispatch(uiActions.setIsNameCollisionDialogOpen({ open: false, info: undefined }));
};

const triggerSelectedOptionsOnSubmit = async ({ operationType, operation }: OnSubmitPressed) => {
for (const group of collisionGroups) {
await resolveCollision(
{
operationType,
operation,
items: group.duplicatedItems,
existingItems: group.existingItems,
destinationUuid: group.destinationUuid,
},
context,
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 groupIndex = findPendingGroupIndex(collisionGroups);
const hasPendingGroup = groupIndex !== -1;
if (!hasPendingGroup) {
closeDialog();
return;
}

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 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 @@ -56,6 +107,7 @@ const NameCollisionContainer: FC = () => {
onSubmitButtonPressed={triggerSelectedOptionsOnSubmit}
onCloseDialog={closeDialog}
operationType={operationType as 'move' | 'upload'}
remainingItemsCount={remainingItemsCount}
/>
);
};
Expand Down
82 changes: 56 additions & 26 deletions src/app/drive/components/NameCollisionDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,69 +13,85 @@ 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 {
operationType: 'upload' | 'move';
isOpen: boolean;
driveItems: (DriveItemData | IRoot)[];
newItems: (File | IRoot)[];
remainingItemsCount: number;
onCloseDialog(): void;
onCancelButtonPressed(): void;
onSubmitButtonPressed({ operationType, operation, itemsToUpload, itemsToReplace }: OnSubmitPressed): Promise<void>;
onSubmitButtonPressed({
operationType,
operation,
itemsToUpload,
itemsToReplace,
applyToAll,
}: OnSubmitPressed): Promise<void>;
}

const NameCollisionDialog: FC<NameCollisionDialogProps> = ({
isOpen,
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],
);

useEffect(() => {
if (isOpen) {
setIsLoading(false);
setSelectedOption(options[0]);
setApplyToAll(false);
}
}, [isOpen]);
}, [isOpen, options]);

const onClose = (): void => {
onCloseDialog();
onCancelButtonPressed();
setSelectedOption(options[0]);
setApplyToAll(false);
setIsLoading(false);
};

Expand All @@ -88,9 +104,10 @@ const NameCollisionDialog: FC<NameCollisionDialogProps> = ({
operation: selectedOption.operation,
itemsToUpload: newItems,
itemsToReplace: driveItems,
applyToAll,
});

onClose();
setIsLoading(false);
};

return (
Expand All @@ -100,7 +117,7 @@ const NameCollisionDialog: FC<NameCollisionDialogProps> = ({
<p className="text-base text-gray-80">{description}</p>

<RadioGroup value={selectedOption} onChange={setSelectedOption} disabled={isLoading}>
<Label className="sr-only">{translate('modals.renameModal.selectOption')}</Label>
<Label className="sr-only">{translate('modals.alreadyExistsModal.selectOption')}</Label>
<div className="flex flex-col items-start space-y-3">
{options.map((option) => (
<Radio value={option} className="rounded-md outline-none" key={option.operation}>
Expand All @@ -109,12 +126,14 @@ const NameCollisionDialog: FC<NameCollisionDialogProps> = ({
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'
}`}
>
<div
className={`h-2.5 w-2.5 rounded-full ${
option.operation === selectedOption.operation ? 'bg-white' : 'bg-white group-hover:bg-gray-5'
option.operation === selectedOption.operation
? 'bg-white'
: 'bg-white dark:bg-transparent group-hover:bg-gray-5'
}`}
/>
</div>
Expand All @@ -128,6 +147,17 @@ const NameCollisionDialog: FC<NameCollisionDialogProps> = ({
</div>
</RadioGroup>

{remainingItemsCount > 1 && (
<div className="flex items-center">
<Checkbox id="apply-to-all" checked={applyToAll} onClick={() => setApplyToAll((prev) => !prev)} />
<label
htmlFor="apply-to-all"
className="ml-2 cursor-pointer select-none text-base font-medium text-gray-80"
>
{translate('modals.alreadyExistsModal.applyToAll')}
</label>
</div>
)}
Comment on lines +150 to +160

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to keep element consistency maybe we should do that pressint the text also select/unselect the checkbox

<div className="flex flex-row items-center justify-end space-x-2">
<Button disabled={isLoading} variant="secondary" onClick={onClose}>
{translate('actions.cancel')}
Expand Down
Loading
Loading