Skip to content

Commit e018e7b

Browse files
Drag and Drop File Copy (#1910)
Let's you drag and drop to copy files between preview widgets, even if they use different connections. --------- Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
1 parent 9e79df0 commit e018e7b

File tree

4 files changed

+157
-38
lines changed

4 files changed

+157
-38
lines changed

frontend/app/view/preview/directorypreview.tsx

Lines changed: 142 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import dayjs from "dayjs";
2727
import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
2828
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
2929
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
30+
import { useDrag, useDrop } from "react-dnd";
3031
import { quote as shellQuote } from "shell-quote";
3132
import { debounce } from "throttle-debounce";
3233
import "./directorypreview.scss";
@@ -657,34 +658,6 @@ function TableBody({
657658
[setRefreshVersion, conn]
658659
);
659660

660-
const displayRow = useCallback(
661-
(row: Row<FileInfo>, idx: number) => (
662-
<div
663-
ref={(el) => (rowRefs.current[idx] = el)}
664-
className={clsx("dir-table-body-row", { focused: focusIndex === idx })}
665-
key={row.id}
666-
onDoubleClick={() => {
667-
const newFileName = row.getValue("path") as string;
668-
model.goHistory(newFileName);
669-
setSearch("");
670-
}}
671-
onClick={() => setFocusIndex(idx)}
672-
onContextMenu={(e) => handleFileContextMenu(e, row.original)}
673-
>
674-
{row.getVisibleCells().map((cell) => (
675-
<div
676-
className={clsx("dir-table-body-cell", "col-" + cell.column.id)}
677-
key={cell.id}
678-
style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)` }}
679-
>
680-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
681-
</div>
682-
))}
683-
</div>
684-
),
685-
[setSearch, handleFileContextMenu, setFocusIndex, focusIndex]
686-
);
687-
688661
return (
689662
<div className="dir-table-body" ref={bodyRef}>
690663
{search !== "" && (
@@ -700,13 +673,110 @@ function TableBody({
700673
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
701674
<div className="dir-table-body-cell">dummy-data</div>
702675
</div>
703-
{table.getTopRows().map(displayRow)}
704-
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
676+
{table.getTopRows().map((row, idx) => (
677+
<TableRow
678+
model={model}
679+
row={row}
680+
focusIndex={focusIndex}
681+
setFocusIndex={setFocusIndex}
682+
setSearch={setSearch}
683+
idx={idx}
684+
handleFileContextMenu={handleFileContextMenu}
685+
ref={(el) => (rowRefs.current[idx] = el)}
686+
key={idx}
687+
/>
688+
))}
689+
{table.getCenterRows().map((row, idx) => (
690+
<TableRow
691+
model={model}
692+
row={row}
693+
focusIndex={focusIndex}
694+
setFocusIndex={setFocusIndex}
695+
setSearch={setSearch}
696+
idx={idx + table.getTopRows().length}
697+
handleFileContextMenu={handleFileContextMenu}
698+
ref={(el) => (rowRefs.current[idx] = el)}
699+
key={idx}
700+
/>
701+
))}
705702
</div>
706703
</div>
707704
);
708705
}
709706

707+
type TableRowProps = {
708+
model: PreviewModel;
709+
row: Row<FileInfo>;
710+
focusIndex: number;
711+
setFocusIndex: (_: number) => void;
712+
setSearch: (_: string) => void;
713+
idx: number;
714+
handleFileContextMenu: (e: any, finfo: FileInfo) => Promise<void>;
715+
};
716+
717+
const TableRow = React.forwardRef(function (
718+
{ model, row, focusIndex, setFocusIndex, setSearch, idx, handleFileContextMenu }: TableRowProps,
719+
ref: React.RefObject<HTMLDivElement>
720+
) {
721+
const dirPath = useAtomValue(model.normFilePath);
722+
const connection = useAtomValue(model.connection);
723+
const formatRemoteUri = useCallback(
724+
(path: string) => {
725+
let conn: string;
726+
if (!connection) {
727+
conn = "local";
728+
} else {
729+
conn = connection;
730+
}
731+
return `wsh://${conn}/${path}`;
732+
},
733+
[connection]
734+
);
735+
736+
const dragItem: DraggedFile = {
737+
relName: row.getValue("name") as string,
738+
absParent: dirPath,
739+
uri: formatRemoteUri(row.getValue("path") as string),
740+
};
741+
const [{ isDragging }, drag, dragPreview] = useDrag(
742+
() => ({
743+
type: "FILE_ITEM",
744+
canDrag: true,
745+
item: () => dragItem,
746+
collect: (monitor) => {
747+
return {
748+
isDragging: monitor.isDragging(),
749+
};
750+
},
751+
}),
752+
[dragItem]
753+
);
754+
755+
return (
756+
<div
757+
className={clsx("dir-table-body-row", { focused: focusIndex === idx })}
758+
onDoubleClick={() => {
759+
const newFileName = row.getValue("path") as string;
760+
model.goHistory(newFileName);
761+
setSearch("");
762+
}}
763+
onClick={() => setFocusIndex(idx)}
764+
onContextMenu={(e) => handleFileContextMenu(e, row.original)}
765+
ref={drag}
766+
>
767+
{row.getVisibleCells().map((cell) => (
768+
<div
769+
className={clsx("dir-table-body-cell", "col-" + cell.column.id)}
770+
key={cell.id}
771+
style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)` }}
772+
>
773+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
774+
</div>
775+
))}
776+
</div>
777+
);
778+
});
779+
710780
const MemoizedTableBody = React.memo(
711781
TableBody,
712782
(prev, next) => prev.table.options.data == next.table.options.data
@@ -837,6 +907,48 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
837907
middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)],
838908
});
839909

910+
const [, drop] = useDrop(
911+
() => ({
912+
accept: "FILE_ITEM", //a name of file drop type
913+
canDrop: (_, monitor) => {
914+
const dragItem = monitor.getItem<DraggedFile>();
915+
// drop if not current dir is the parent directory of the dragged item
916+
// requires absolute path
917+
if (monitor.isOver({ shallow: false }) && dragItem.absParent !== dirPath) {
918+
return true;
919+
}
920+
return false;
921+
},
922+
drop: async (draggedFile: DraggedFile, monitor) => {
923+
if (!monitor.didDrop()) {
924+
const timeoutYear = 31536000000; // one year
925+
const opts: FileCopyOpts = {
926+
timeout: timeoutYear,
927+
recursive: true,
928+
};
929+
const desturi = await model.formatRemoteUri(dirPath, globalStore.get);
930+
const data: CommandFileCopyData = {
931+
srcuri: draggedFile.uri,
932+
desturi,
933+
opts,
934+
};
935+
try {
936+
await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: timeoutYear });
937+
} catch (e) {
938+
console.log("copy failed:", e);
939+
}
940+
model.refreshCallback();
941+
}
942+
},
943+
// TODO: mabe add a hover option?
944+
}),
945+
[dirPath, model.formatRemoteUri, model.refreshCallback]
946+
);
947+
948+
useEffect(() => {
949+
drop(refs.reference);
950+
}, [refs.reference]);
951+
840952
const dismiss = useDismiss(context);
841953
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
842954

frontend/layout/lib/TileLayout.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
} from "./types";
3232
import { determineDropDirection } from "./utils";
3333

34+
const tileItemType = "TILE_ITEM";
35+
3436
export interface TileLayoutProps {
3537
/**
3638
* The atom containing the layout tree state.
@@ -59,14 +61,16 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
5961
const setReady = useSetAtom(layoutModel.ready);
6062
const isResizing = useAtomValue(layoutModel.isResizing);
6163

62-
const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
64+
const { activeDrag, dragClientOffset, dragItemType } = useDragLayer((monitor) => ({
6365
activeDrag: monitor.isDragging(),
6466
dragClientOffset: monitor.getClientOffset(),
67+
dragItemType: monitor.getItemType(),
6568
}));
6669

6770
useEffect(() => {
68-
setActiveDrag(activeDrag);
69-
}, [setActiveDrag, activeDrag]);
71+
const activeTileDrag = activeDrag && dragItemType == tileItemType;
72+
setActiveDrag(activeTileDrag);
73+
}, [activeDrag, dragItemType]);
7074

7175
const checkForCursorBounds = useCallback(
7276
debounce(100, (dragClientOffset: XYCoord) => {
@@ -214,8 +218,6 @@ interface DisplayNodeProps {
214218
node: LayoutNode;
215219
}
216220

217-
const dragItemType = "TILE_ITEM";
218-
219221
/**
220222
* The draggable and displayable portion of a leaf node in a layout tree.
221223
*/
@@ -230,7 +232,7 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
230232

231233
const [{ isDragging }, drag, dragPreview] = useDrag(
232234
() => ({
233-
type: dragItemType,
235+
type: tileItemType,
234236
canDrag: () => !(isEphemeral || isMagnified),
235237
item: () => node,
236238
collect: (monitor) => ({
@@ -358,7 +360,7 @@ const OverlayNode = memo(({ node, layoutModel }: OverlayNodeProps) => {
358360

359361
const [, drop] = useDrop(
360362
() => ({
361-
accept: dragItemType,
363+
accept: tileItemType,
362364
canDrop: (_, monitor) => {
363365
const dragItem = monitor.getItem<LayoutNode>();
364366
if (monitor.isOver({ shallow: true }) && dragItem.id !== node.id) {

frontend/types/custom.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ declare global {
372372
miny?: string | number;
373373
decimalPlaces?: number;
374374
};
375+
376+
type DraggedFile = {
377+
uri: string;
378+
absParent: string;
379+
relName: string;
380+
};
375381
}
376382

377383
export {};

pkg/util/tarcopy/tarcopy.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func TarCopyDest(ctx context.Context, cancel context.CancelCauseFunc, ch <-chan
8282
pipeReader, pipeWriter := io.Pipe()
8383
iochan.WriterChan(ctx, pipeWriter, ch, func() {
8484
gracefulClose(pipeWriter, tarCopyDestName, pipeWriterName)
85-
cancel(nil)
8685
}, cancel)
8786
tarReader := tar.NewReader(pipeReader)
8887
defer func() {

0 commit comments

Comments
 (0)