Skip to content

Commit

Permalink
Drag and Drop File Copy (#1910)
Browse files Browse the repository at this point in the history
Let's you drag and drop to copy files between preview widgets, even if
they use different connections.

---------

Co-authored-by: Evan Simkowitz <[email protected]>
  • Loading branch information
oneirocosm and esimkowitz authored Feb 6, 2025
1 parent 9e79df0 commit e018e7b
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 38 deletions.
172 changes: 142 additions & 30 deletions frontend/app/view/preview/directorypreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import dayjs from "dayjs";
import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import { quote as shellQuote } from "shell-quote";
import { debounce } from "throttle-debounce";
import "./directorypreview.scss";
Expand Down Expand Up @@ -657,34 +658,6 @@ function TableBody({
[setRefreshVersion, conn]
);

const displayRow = useCallback(
(row: Row<FileInfo>, idx: number) => (
<div
ref={(el) => (rowRefs.current[idx] = el)}
className={clsx("dir-table-body-row", { focused: focusIndex === idx })}
key={row.id}
onDoubleClick={() => {
const newFileName = row.getValue("path") as string;
model.goHistory(newFileName);
setSearch("");
}}
onClick={() => setFocusIndex(idx)}
onContextMenu={(e) => handleFileContextMenu(e, row.original)}
>
{row.getVisibleCells().map((cell) => (
<div
className={clsx("dir-table-body-cell", "col-" + cell.column.id)}
key={cell.id}
style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)` }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
))}
</div>
),
[setSearch, handleFileContextMenu, setFocusIndex, focusIndex]
);

return (
<div className="dir-table-body" ref={bodyRef}>
{search !== "" && (
Expand All @@ -700,13 +673,110 @@ function TableBody({
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
<div className="dir-table-body-cell">dummy-data</div>
</div>
{table.getTopRows().map(displayRow)}
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
{table.getTopRows().map((row, idx) => (
<TableRow
model={model}
row={row}
focusIndex={focusIndex}
setFocusIndex={setFocusIndex}
setSearch={setSearch}
idx={idx}
handleFileContextMenu={handleFileContextMenu}
ref={(el) => (rowRefs.current[idx] = el)}
key={idx}
/>
))}
{table.getCenterRows().map((row, idx) => (
<TableRow
model={model}
row={row}
focusIndex={focusIndex}
setFocusIndex={setFocusIndex}
setSearch={setSearch}
idx={idx + table.getTopRows().length}
handleFileContextMenu={handleFileContextMenu}
ref={(el) => (rowRefs.current[idx] = el)}
key={idx}
/>
))}
</div>
</div>
);
}

type TableRowProps = {
model: PreviewModel;
row: Row<FileInfo>;
focusIndex: number;
setFocusIndex: (_: number) => void;
setSearch: (_: string) => void;
idx: number;
handleFileContextMenu: (e: any, finfo: FileInfo) => Promise<void>;
};

const TableRow = React.forwardRef(function (
{ model, row, focusIndex, setFocusIndex, setSearch, idx, handleFileContextMenu }: TableRowProps,
ref: React.RefObject<HTMLDivElement>
) {
const dirPath = useAtomValue(model.normFilePath);
const connection = useAtomValue(model.connection);
const formatRemoteUri = useCallback(
(path: string) => {
let conn: string;
if (!connection) {
conn = "local";
} else {
conn = connection;
}
return `wsh://${conn}/${path}`;
},
[connection]
);

const dragItem: DraggedFile = {
relName: row.getValue("name") as string,
absParent: dirPath,
uri: formatRemoteUri(row.getValue("path") as string),
};
const [{ isDragging }, drag, dragPreview] = useDrag(
() => ({
type: "FILE_ITEM",
canDrag: true,
item: () => dragItem,
collect: (monitor) => {
return {
isDragging: monitor.isDragging(),
};
},
}),
[dragItem]
);

return (
<div
className={clsx("dir-table-body-row", { focused: focusIndex === idx })}
onDoubleClick={() => {
const newFileName = row.getValue("path") as string;
model.goHistory(newFileName);
setSearch("");
}}
onClick={() => setFocusIndex(idx)}
onContextMenu={(e) => handleFileContextMenu(e, row.original)}
ref={drag}
>
{row.getVisibleCells().map((cell) => (
<div
className={clsx("dir-table-body-cell", "col-" + cell.column.id)}
key={cell.id}
style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)` }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
))}
</div>
);
});

const MemoizedTableBody = React.memo(
TableBody,
(prev, next) => prev.table.options.data == next.table.options.data
Expand Down Expand Up @@ -837,6 +907,48 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)],
});

const [, drop] = useDrop(
() => ({
accept: "FILE_ITEM", //a name of file drop type
canDrop: (_, monitor) => {
const dragItem = monitor.getItem<DraggedFile>();
// drop if not current dir is the parent directory of the dragged item
// requires absolute path
if (monitor.isOver({ shallow: false }) && dragItem.absParent !== dirPath) {
return true;
}
return false;
},
drop: async (draggedFile: DraggedFile, monitor) => {
if (!monitor.didDrop()) {
const timeoutYear = 31536000000; // one year
const opts: FileCopyOpts = {
timeout: timeoutYear,
recursive: true,
};
const desturi = await model.formatRemoteUri(dirPath, globalStore.get);
const data: CommandFileCopyData = {
srcuri: draggedFile.uri,
desturi,
opts,
};
try {
await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: timeoutYear });
} catch (e) {
console.log("copy failed:", e);
}
model.refreshCallback();
}
},
// TODO: mabe add a hover option?
}),
[dirPath, model.formatRemoteUri, model.refreshCallback]
);

useEffect(() => {
drop(refs.reference);
}, [refs.reference]);

const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);

Expand Down
16 changes: 9 additions & 7 deletions frontend/layout/lib/TileLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
} from "./types";
import { determineDropDirection } from "./utils";

const tileItemType = "TILE_ITEM";

export interface TileLayoutProps {
/**
* The atom containing the layout tree state.
Expand Down Expand Up @@ -59,14 +61,16 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
const setReady = useSetAtom(layoutModel.ready);
const isResizing = useAtomValue(layoutModel.isResizing);

const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
const { activeDrag, dragClientOffset, dragItemType } = useDragLayer((monitor) => ({
activeDrag: monitor.isDragging(),
dragClientOffset: monitor.getClientOffset(),
dragItemType: monitor.getItemType(),
}));

useEffect(() => {
setActiveDrag(activeDrag);
}, [setActiveDrag, activeDrag]);
const activeTileDrag = activeDrag && dragItemType == tileItemType;
setActiveDrag(activeTileDrag);
}, [activeDrag, dragItemType]);

const checkForCursorBounds = useCallback(
debounce(100, (dragClientOffset: XYCoord) => {
Expand Down Expand Up @@ -214,8 +218,6 @@ interface DisplayNodeProps {
node: LayoutNode;
}

const dragItemType = "TILE_ITEM";

/**
* The draggable and displayable portion of a leaf node in a layout tree.
*/
Expand All @@ -230,7 +232,7 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {

const [{ isDragging }, drag, dragPreview] = useDrag(
() => ({
type: dragItemType,
type: tileItemType,
canDrag: () => !(isEphemeral || isMagnified),
item: () => node,
collect: (monitor) => ({
Expand Down Expand Up @@ -358,7 +360,7 @@ const OverlayNode = memo(({ node, layoutModel }: OverlayNodeProps) => {

const [, drop] = useDrop(
() => ({
accept: dragItemType,
accept: tileItemType,
canDrop: (_, monitor) => {
const dragItem = monitor.getItem<LayoutNode>();
if (monitor.isOver({ shallow: true }) && dragItem.id !== node.id) {
Expand Down
6 changes: 6 additions & 0 deletions frontend/types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ declare global {
miny?: string | number;
decimalPlaces?: number;
};

type DraggedFile = {
uri: string;
absParent: string;
relName: string;
};
}

export {};
1 change: 0 additions & 1 deletion pkg/util/tarcopy/tarcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ func TarCopyDest(ctx context.Context, cancel context.CancelCauseFunc, ch <-chan
pipeReader, pipeWriter := io.Pipe()
iochan.WriterChan(ctx, pipeWriter, ch, func() {
gracefulClose(pipeWriter, tarCopyDestName, pipeWriterName)
cancel(nil)
}, cancel)
tarReader := tar.NewReader(pipeReader)
defer func() {
Expand Down

0 comments on commit e018e7b

Please sign in to comment.