Files
spacedrive/interface/app/$libraryId/Explorer/useExplorerDnd.tsx
nikec d007b55763 [ENG-1619] Add spacedrop to the context menu (#2119)
add spacedrop to context menu
2024-02-22 12:53:02 +00:00

210 lines
5.5 KiB
TypeScript

import { useDndMonitor } from '@dnd-kit/core';
import {
ExplorerItem,
getIndexedItemFilePath,
getItemFilePath,
libraryClient,
useLibraryMutation
} from '@sd/client';
import { isNonEmptyObject } from '~/util';
import { useAssignItemsToTag } from '../settings/library/tags/CreateDialog';
import { useExplorerContext } from './Context';
import { explorerStore } from './store';
import { explorerDroppableSchema } from './useExplorerDroppable';
import { useExplorerSearchParams } from './util';
export const getPaths = async (items: ExplorerItem[]) => {
const paths = items.map(async (item) => {
const filePath = getItemFilePath(item);
if (!filePath) return;
return 'path' in filePath
? filePath.path
: await libraryClient.query(['files.getPath', filePath.id]);
});
return (await Promise.all(paths)).filter((path): path is string => Boolean(path));
};
const getPathIdsPerLocation = (items: ExplorerItem[]) => {
return items.reduce(
(items, item) => {
const path = getIndexedItemFilePath(item);
if (!path || path.location_id === null) return items;
return {
...items,
[path.location_id]: [...(items[path.location_id] ?? []), path.id]
};
},
{} as Record<number, number[]>
);
};
export const useExplorerDnd = () => {
const explorer = useExplorerContext();
const [{ path }] = useExplorerSearchParams();
const cutFiles = useLibraryMutation('files.cutFiles');
const cutEphemeralFiles = useLibraryMutation('ephemeralFiles.cutFiles');
const assignItemsToTag = useAssignItemsToTag();
useDndMonitor({
onDragStart: () => {
if (explorer.selectedItems.size === 0) return;
explorerStore.drag = {
type: 'dragging',
items: [...explorer.selectedItems],
sourcePath: path ?? '/',
sourceLocationId:
explorer.parent?.type === 'Location' ? explorer.parent.location.id : undefined,
sourceTagId: explorer.parent?.type === 'Tag' ? explorer.parent.tag.id : undefined
};
},
onDragEnd: async ({ over }) => {
const drag = explorerStore.drag;
explorerStore.drag = null;
if (!over || !drag || drag.type === 'touched') return;
const drop = explorerDroppableSchema.parse(over.data.current);
switch (drop.type) {
case 'location': {
if (!drop.data) {
cutEphemeralFiles.mutate({
sources: await getPaths(drag.items),
target_dir: drop.path
});
return;
}
const paths = getPathIdsPerLocation(drag.items);
if (isNonEmptyObject(paths)) {
const locationId = drop.data.id;
Object.entries(paths).map(([sourceLocationId, paths]) => {
cutFiles.mutate({
source_location_id: Number(sourceLocationId),
sources_file_path_ids: paths,
target_location_id: locationId,
target_location_relative_directory_path: drop.path
});
});
return;
}
cutEphemeralFiles.mutate({
sources: await getPaths(drag.items),
target_dir: drop.data.path + drop.path
});
break;
}
case 'explorer-item': {
switch (drop.data.type) {
case 'Path':
case 'Object': {
const { item } = drop.data;
const filePath = 'file_paths' in item ? item.file_paths[0] : item;
if (!filePath) return;
const paths = getPathIdsPerLocation(drag.items);
if (isNonEmptyObject(paths)) {
const locationId = filePath.location_id;
const path = filePath.materialized_path + filePath.name + '/';
Object.entries(paths).map(([sourceLocationId, paths]) => {
cutFiles.mutate({
source_location_id: Number(sourceLocationId),
sources_file_path_ids: paths,
target_location_id: locationId,
target_location_relative_directory_path: path
});
});
return;
}
const path = await libraryClient.query(['files.getPath', filePath.id]);
if (!path) return;
cutEphemeralFiles.mutate({
sources: await getPaths(drag.items),
target_dir: path
});
break;
}
case 'Location':
case 'NonIndexedPath': {
cutEphemeralFiles.mutate({
sources: await getPaths(drag.items),
target_dir: drop.data.item.path
});
}
}
break;
}
case 'tag': {
const items = drag.items.flatMap((item) => {
if (item.type !== 'Object' && item.type !== 'Path') return [];
return [item];
});
await assignItemsToTag(drop.data.id, items);
}
}
},
onDragCancel: () => (explorerStore.drag = null)
});
};
// interface DndNoticeProps extends UseDialogProps {
// count: number;
// path: string;
// onConfirm: (val: { dismissNotice: boolean }) => void;
// }
// const DndNotice = (props: DndNoticeProps) => {
// const form = useZodForm();
// const [dismissNotice, setDismissNotice] = useState(false);
// const { t } = useLocale();
// return (
// <Dialog
// form={form}
// onSubmit={form.handleSubmit(() => props.onConfirm({ dismissNotice: dismissNotice }))}
// dialog={useDialog(props)}
// title={t('move_files')}
// icon={<Icon name="MoveLocation" size={28} />}
// description={
// <span className="break-all">
// Are you sure you want to move {props.count} file{props.count > 1 ? 's' : ''} to{' '}
// {props.path}?
// </span>
// }
// ctaDanger
// ctaLabel={t('continue')}
// closeLabel={t('cancel')}
// buttonsSideContent={
// <RadixCheckbox
// label={t('dont_show_again')}
// name="ephemeral-alert-notice"
// checked={dismissNotice}
// onCheckedChange={(val) => typeof val === 'boolean' && setDismissNotice(val)}
// />
// }
// />
// );
// };