From c98574f0e2e1f6f0ac48b9bb4b28188bec3fd1cb Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 7 Feb 2025 19:04:42 -0800 Subject: [PATCH 1/4] feat: add retry drag/drop with overwrite This allows a user to retry an attempted copy if it requires the overwrite option. --- .../app/view/preview/directorypreview.scss | 117 ++++++++++++++++ .../app/view/preview/directorypreview.tsx | 127 +++++++++++++++++- 2 files changed, 237 insertions(+), 7 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.scss b/frontend/app/view/preview/directorypreview.scss index b91e76c7a8..0c50266f17 100644 --- a/frontend/app/view/preview/directorypreview.scss +++ b/frontend/app/view/preview/directorypreview.scss @@ -200,6 +200,123 @@ pointer-events: none; } } + .copyerror-overlay { + position: absolute; + top: calc(var(--header-height) + 6px); + left: 6px; + right: 6px; + z-index: var(--zindex-block-mask-inner); + overflow: hidden; + background: var(--conn-status-overlay-bg-color); + backdrop-filter: blur(50px); + border-radius: 6px; + box-shadow: 0px 13px 16px 0px rgb(from var(--block-bg-color) r g b / 40%); + + .copyerror-content { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 8px 10px 12px; + font: var(--base-font); + color: var(--secondary-text-color); + + .copyerror-status-icon-wrapper { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + flex-grow: 1; + min-width: 0; + + &.has-error { + align-items: flex-start; + } + + > i { + color: #e6ba1e; + font-size: 16px; + } + + .copyerror-status { + @include mixins.ellipsis(); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + flex-grow: 1; + width: 100%; + + .copyerror-status-text { + max-width: 100%; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 16px; + letter-spacing: 0.11px; + color: white; + } + + .copyerror-error { + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 15px; + letter-spacing: 0.11px; + text-wrap: wrap; + max-height: 80px; + border-radius: 8px; + padding: 5px; + padding-left: 0; + position: relative; + width: 100%; + + .copy-button { + visibility: hidden; + display: flex; + position: sticky; + top: 0; + right: 4px; + float: right; + border-radius: 4px; + backdrop-filter: blur(8px); + padding: 0.286em; + align-items: center; + justify-content: flex-end; + gap: 0.286em; + } + + &:hover .copy-button { + visibility: visible; + } + } + + .copyerror-overwrite-btns { + display: flex; + flex-direction: row; + gap: 5px; + } + } + } + + .copyerror-actions { + display: flex; + align-items: flex-start; + justify-content: center; + gap: 6px; + + button { + i { + font-size: 11px; + opacity: 0.7; + } + } + + .wave-button:last-child { + margin-top: 1.5px; + } + } + } + } } .dir-table-button { diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 6e03d89983..af9244b9fa 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -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 { 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(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 && ( + + )} void; + handleDropCopy: (data: CommandFileCopyData) => Promise; + }) => { + const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); + const width = domRect?.width; + + const handleRetryCopy = React.useCallback(async () => { + if (!copyStatus) { + return; + } + //const retryOpts: FileCopyOpts = { ...copyStatus.copyData.opts, overwrite: true }; + //const newData: CommandFileCopyData = { ...copyStatus.copyData, opts: } + copyStatus.copyData.opts.overwrite = true; + + await handleDropCopy(copyStatus.copyData); + }, [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?"; + } + + let reconClassName = "outlined grey"; + if (width && width < 350) { + reconClassName = clsx(reconClassName, "font-size-12 vertical-padding-5 horizontal-padding-6"); + } else { + reconClassName = clsx(reconClassName, "font-size-11 vertical-padding-3 horizontal-padding-7"); + } + + const handleRemoveCopyError = React.useCallback(async () => { + setCopyStatus(null); + }, [setCopyStatus]); + + const handleCopyToClipboard = React.useCallback( + async (e: React.MouseEvent) => { + await navigator.clipboard.writeText(errorMsg); + }, + [errorMsg] + ); + + return ( +
+
+
+ +
+
{statusText}
+ + +
{errorMsg}
+
+ {copyStatus?.allowRetry && ( +
+ + +
+ )} +
+
+
+
+ ); + } +); + export { DirectoryPreview }; From c41e252cd6230f1dd269161358d9e91870c92f4a Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 7 Feb 2025 19:11:01 -0800 Subject: [PATCH 2/4] fix: add a dismiss button for regular copy errors --- frontend/app/view/preview/directorypreview.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index af9244b9fa..c138c5dd39 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -1125,8 +1125,6 @@ const CopyErrorOverlay = React.memo( if (!copyStatus) { return; } - //const retryOpts: FileCopyOpts = { ...copyStatus.copyData.opts, overwrite: true }; - //const newData: CommandFileCopyData = { ...copyStatus.copyData, opts: } copyStatus.copyData.opts.overwrite = true; await handleDropCopy(copyStatus.copyData); @@ -1182,6 +1180,14 @@ const CopyErrorOverlay = React.memo( )} + {copyStatus?.allowRetry || ( +
+
+ )} From 73fcb6564d3c22ee7d0e7acb2236fb2f19eae194 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 7 Feb 2025 19:22:52 -0800 Subject: [PATCH 3/4] refactor: create new data instead of mutating --- frontend/app/view/preview/directorypreview.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index c138c5dd39..ae954c7896 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -1125,9 +1125,11 @@ const CopyErrorOverlay = React.memo( if (!copyStatus) { return; } - copyStatus.copyData.opts.overwrite = true; - - await handleDropCopy(copyStatus.copyData); + const updatedData = { + ...copyStatus.copyData, + opts: { ...copyStatus.copyData.opts, overwrite: true }, + }; + await handleDropCopy(updatedData); }, [copyStatus.copyData]); let statusText = "Copy Error"; From 5950cdaeb8eddc7b6a6f95379112aca304ed778a Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Mon, 10 Feb 2025 15:16:56 -0800 Subject: [PATCH 4/4] refactor: use tailwind for copy error box --- .../app/view/preview/directorypreview.scss | 117 ------------------ .../app/view/preview/directorypreview.tsx | 65 +++++----- 2 files changed, 37 insertions(+), 145 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.scss b/frontend/app/view/preview/directorypreview.scss index 0c50266f17..b91e76c7a8 100644 --- a/frontend/app/view/preview/directorypreview.scss +++ b/frontend/app/view/preview/directorypreview.scss @@ -200,123 +200,6 @@ pointer-events: none; } } - .copyerror-overlay { - position: absolute; - top: calc(var(--header-height) + 6px); - left: 6px; - right: 6px; - z-index: var(--zindex-block-mask-inner); - overflow: hidden; - background: var(--conn-status-overlay-bg-color); - backdrop-filter: blur(50px); - border-radius: 6px; - box-shadow: 0px 13px 16px 0px rgb(from var(--block-bg-color) r g b / 40%); - - .copyerror-content { - display: flex; - flex-direction: row; - justify-content: space-between; - padding: 10px 8px 10px 12px; - font: var(--base-font); - color: var(--secondary-text-color); - - .copyerror-status-icon-wrapper { - display: flex; - flex-direction: row; - align-items: center; - gap: 12px; - flex-grow: 1; - min-width: 0; - - &.has-error { - align-items: flex-start; - } - - > i { - color: #e6ba1e; - font-size: 16px; - } - - .copyerror-status { - @include mixins.ellipsis(); - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - flex-grow: 1; - width: 100%; - - .copyerror-status-text { - max-width: 100%; - font-size: 11px; - font-style: normal; - font-weight: 600; - line-height: 16px; - letter-spacing: 0.11px; - color: white; - } - - .copyerror-error { - font-size: 11px; - font-style: normal; - font-weight: 400; - line-height: 15px; - letter-spacing: 0.11px; - text-wrap: wrap; - max-height: 80px; - border-radius: 8px; - padding: 5px; - padding-left: 0; - position: relative; - width: 100%; - - .copy-button { - visibility: hidden; - display: flex; - position: sticky; - top: 0; - right: 4px; - float: right; - border-radius: 4px; - backdrop-filter: blur(8px); - padding: 0.286em; - align-items: center; - justify-content: flex-end; - gap: 0.286em; - } - - &:hover .copy-button { - visibility: visible; - } - } - - .copyerror-overwrite-btns { - display: flex; - flex-direction: row; - gap: 5px; - } - } - } - - .copyerror-actions { - display: flex; - align-items: flex-start; - justify-content: center; - gap: 6px; - - button { - i { - font-size: 11px; - opacity: 0.7; - } - } - - .wave-button:last-child { - margin-top: 1.5px; - } - } - } - } } .dir-table-button { diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index ae954c7896..a5c600760e 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -1139,53 +1139,62 @@ const CopyErrorOverlay = React.memo( errorMsg = "This copy operation will overwrite an existing file. Would you like to continue?"; } - let reconClassName = "outlined grey"; - if (width && width < 350) { - reconClassName = clsx(reconClassName, "font-size-12 vertical-padding-5 horizontal-padding-6"); - } else { - reconClassName = clsx(reconClassName, "font-size-11 vertical-padding-3 horizontal-padding-7"); - } + 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 (e: React.MouseEvent) => { - await navigator.clipboard.writeText(errorMsg); - }, - [errorMsg] - ); + const handleCopyToClipboard = React.useCallback(async () => { + await navigator.clipboard.writeText(errorMsg); + }, [errorMsg]); return ( -
-
-
- -
-
{statusText}
+
+
+
+ + +
+
+ {statusText} +
+ - +
{errorMsg}
+ {copyStatus?.allowRetry && ( -
- -
)}
- {copyStatus?.allowRetry || ( -
+ + {!copyStatus?.allowRetry && ( +