mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-21 02:33:34 +01:00
File Drag and Drop Overwrite Prompt (#1932)
If a file drag and drop file fails because the file already exists, this adds a popup to allow the operation to be retried with the overwrite flag set. Additionally, it will make a similar dismissible popup to cover other copy errors.
This commit is contained in:
parent
8d1e649b7c
commit
2b060ebc98
@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Button } from "@/app/element/button";
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { Input } from "@/app/element/input";
|
||||
import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
@ -34,6 +36,12 @@ import "./directorypreview.scss";
|
||||
|
||||
const PageJumpSize = 20;
|
||||
|
||||
type FileCopyStatus = {
|
||||
copyData: CommandFileCopyData;
|
||||
copyError: string;
|
||||
allowRetry: boolean;
|
||||
};
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
updateName: (path: string) => void;
|
||||
@ -790,6 +798,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||
const conn = useAtomValue(model.connection);
|
||||
const blockData = useAtomValue(model.blockAtom);
|
||||
const dirPath = useAtomValue(model.normFilePath);
|
||||
const [copyStatus, setCopyStatus] = useState<FileCopyStatus>(null);
|
||||
|
||||
useEffect(() => {
|
||||
model.refreshCallback = () => {
|
||||
@ -900,6 +909,27 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||
middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)],
|
||||
});
|
||||
|
||||
const handleDropCopy = useCallback(
|
||||
async (data: CommandFileCopyData) => {
|
||||
try {
|
||||
await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: data.opts.timeout });
|
||||
setCopyStatus(null);
|
||||
} catch (e) {
|
||||
console.log("copy failed:", e);
|
||||
const copyError = `${e}`;
|
||||
const allowRetry = copyError.endsWith("overwrite not specified");
|
||||
const copyStatus: FileCopyStatus = {
|
||||
copyError,
|
||||
copyData: data,
|
||||
allowRetry,
|
||||
};
|
||||
setCopyStatus(copyStatus);
|
||||
}
|
||||
model.refreshCallback();
|
||||
},
|
||||
[setCopyStatus, model.refreshCallback]
|
||||
);
|
||||
|
||||
const [, drop] = useDrop(
|
||||
() => ({
|
||||
accept: "FILE_ITEM", //a name of file drop type
|
||||
@ -925,17 +955,12 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||
desturi,
|
||||
opts,
|
||||
};
|
||||
try {
|
||||
await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: timeoutYear });
|
||||
} catch (e) {
|
||||
console.log("copy failed:", e);
|
||||
}
|
||||
model.refreshCallback();
|
||||
await handleDropCopy(data);
|
||||
}
|
||||
},
|
||||
// TODO: mabe add a hover option?
|
||||
}),
|
||||
[dirPath, model.formatRemoteUri, model.refreshCallback]
|
||||
[dirPath, model.formatRemoteUri, model.refreshCallback, setCopyStatus]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -1049,6 +1074,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||
onContextMenu={(e) => handleFileContextMenu(e)}
|
||||
onClick={() => setEntryManagerProps(undefined)}
|
||||
>
|
||||
{copyStatus != null && (
|
||||
<CopyErrorOverlay
|
||||
copyStatus={copyStatus}
|
||||
setCopyStatus={setCopyStatus}
|
||||
handleDropCopy={handleDropCopy}
|
||||
/>
|
||||
)}
|
||||
<DirectoryTable
|
||||
model={model}
|
||||
data={filteredData}
|
||||
@ -1076,4 +1108,102 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const CopyErrorOverlay = React.memo(
|
||||
({
|
||||
copyStatus,
|
||||
setCopyStatus,
|
||||
handleDropCopy,
|
||||
}: {
|
||||
copyStatus: FileCopyStatus;
|
||||
setCopyStatus: (_: FileCopyStatus) => void;
|
||||
handleDropCopy: (data: CommandFileCopyData) => Promise<void>;
|
||||
}) => {
|
||||
const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
|
||||
const width = domRect?.width;
|
||||
|
||||
const handleRetryCopy = React.useCallback(async () => {
|
||||
if (!copyStatus) {
|
||||
return;
|
||||
}
|
||||
const updatedData = {
|
||||
...copyStatus.copyData,
|
||||
opts: { ...copyStatus.copyData.opts, overwrite: true },
|
||||
};
|
||||
await handleDropCopy(updatedData);
|
||||
}, [copyStatus.copyData]);
|
||||
|
||||
let statusText = "Copy Error";
|
||||
let errorMsg = `error: ${copyStatus?.copyError}`;
|
||||
if (copyStatus?.allowRetry) {
|
||||
statusText = "Confirm Overwrite File(s)";
|
||||
errorMsg = "This copy operation will overwrite an existing file. Would you like to continue?";
|
||||
}
|
||||
|
||||
const buttonClassName = "outlined grey font-size-11 vertical-padding-3 horizontal-padding-7";
|
||||
|
||||
const handleRemoveCopyError = React.useCallback(async () => {
|
||||
setCopyStatus(null);
|
||||
}, [setCopyStatus]);
|
||||
|
||||
const handleCopyToClipboard = React.useCallback(async () => {
|
||||
await navigator.clipboard.writeText(errorMsg);
|
||||
}, [errorMsg]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRefCallback}
|
||||
className="absolute top-[0] left-1.5 right-1.5 z-[var(--zindex-block-mask-inner)] overflow-hidden bg-[var(--conn-status-overlay-bg-color)] backdrop-blur-[50px] rounded-md shadow-lg"
|
||||
>
|
||||
<div className="flex flex-row justify-between p-2.5 pl-3 font-[var(--base-font)] text-[var(--secondary-text-color)]">
|
||||
<div
|
||||
className={clsx("flex flex-row items-center gap-3 grow min-w-0", {
|
||||
"items-start": true,
|
||||
})}
|
||||
>
|
||||
<i className="fa-solid fa-triangle-exclamation text-[#e6ba1e] text-base"></i>
|
||||
|
||||
<div className="flex flex-col items-start gap-1 grow w-full">
|
||||
<div className="max-w-full text-xs font-semibold leading-4 tracking-[0.11px] text-white">
|
||||
{statusText}
|
||||
</div>
|
||||
|
||||
<OverlayScrollbarsComponent
|
||||
className="group text-xs font-normal leading-[15px] tracking-[0.11px] text-wrap max-h-20 rounded-lg py-1.5 pl-0 relative w-full"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
>
|
||||
<CopyButton
|
||||
className="invisible group-hover:visible flex absolute top-0 right-1 rounded backdrop-blur-lg p-1 items-center justify-end gap-1"
|
||||
onClick={handleCopyToClipboard}
|
||||
title="Copy"
|
||||
/>
|
||||
<div>{errorMsg}</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
|
||||
{copyStatus?.allowRetry && (
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<Button className={buttonClassName} onClick={handleRetryCopy}>
|
||||
Override
|
||||
</Button>
|
||||
<Button className={buttonClassName} onClick={handleRemoveCopyError}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!copyStatus?.allowRetry && (
|
||||
<div className="flex items-start">
|
||||
<Button
|
||||
className={clsx(buttonClassName, "fa-xmark fa-solid")}
|
||||
onClick={handleRemoveCopyError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { DirectoryPreview };
|
||||
|
Loading…
Reference in New Issue
Block a user