From 77bbf74ef95be2122db8e1ac9481a14013bf60e2 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Mon, 15 Sep 2025 16:01:29 -0700 Subject: [PATCH] fix bug with CodeEditor/monaco model, more preview refactoring (#2353) the primary purpose of this PR is to fix a showstopper bug in the CodeEditor component by setting "path" to a stable UUID. the bug was that it started as empty string, so it created a shared model between all of the codeeditor components. now each will get their own monaco model. also took this opportunity to do more more preview view refactoring, splitting up code, and more tailwind migrations. --- frontend/app/element/markdown-util.ts | 2 +- frontend/app/element/markdown.tsx | 43 +- frontend/app/view/codeeditor/codeeditor.tsx | 37 +- .../app/view/preview/directorypreview.scss | 56 --- frontend/app/view/preview/entry-manager.tsx | 65 +++ .../view/preview/preview-directory-utils.tsx | 175 ++++++++ .../app/view/preview/preview-directory.tsx | 376 +++++------------- frontend/app/view/preview/preview-edit.tsx | 6 +- .../view/preview/preview-error-overlay.tsx | 84 ++++ .../app/view/preview/preview-markdown.tsx | 3 +- .../app/view/preview/preview-streaming.tsx | 24 +- frontend/app/view/preview/preview.scss | 106 ----- frontend/app/view/preview/preview.tsx | 88 +--- 13 files changed, 468 insertions(+), 597 deletions(-) create mode 100644 frontend/app/view/preview/entry-manager.tsx create mode 100644 frontend/app/view/preview/preview-directory-utils.tsx create mode 100644 frontend/app/view/preview/preview-error-overlay.tsx delete mode 100644 frontend/app/view/preview/preview.scss diff --git a/frontend/app/element/markdown-util.ts b/frontend/app/element/markdown-util.ts index 01cb0241f..ae860648d 100644 --- a/frontend/app/element/markdown-util.ts +++ b/frontend/app/element/markdown-util.ts @@ -162,7 +162,7 @@ export const resolveRemoteFile = async (filepath: string, resolveOpts: MarkdownR const baseDirUri = formatRemoteUri(resolveOpts.baseDir, resolveOpts.connName); const fileInfo = await RpcApi.FileJoinCommand(TabRpcClient, [baseDirUri, filepath]); const remoteUri = formatRemoteUri(fileInfo.path, resolveOpts.connName); - console.log("markdown resolve", resolveOpts, filepath, "=>", baseDirUri, remoteUri); + // console.log("markdown resolve", resolveOpts, filepath, "=>", baseDirUri, remoteUri); const usp = new URLSearchParams(); usp.set("path", remoteUri); return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString(); diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index da826dad8..7539aa313 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -10,7 +10,7 @@ import { transformBlocks, } from "@/app/element/markdown-util"; import remarkMermaidToTag from "@/app/element/remark-mermaid-to-tag"; -import { boundNumber, useAtomValueSafe } from "@/util/util"; +import { boundNumber, useAtomValueSafe, cn } from "@/util/util"; import clsx from "clsx"; import { Atom } from "jotai"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; @@ -297,6 +297,7 @@ type MarkdownProps = { showTocAtom?: Atom; style?: React.CSSProperties; className?: string; + contentClassName?: string; onClickExecute?: (cmd: string) => void; resolveOpts?: MarkdownResolveOpts; scrollable?: boolean; @@ -311,6 +312,7 @@ const Markdown = ({ showTocAtom, style, className, + contentClassName, resolveOpts, fontSizeOverride, fixedFontSizeOverride, @@ -383,19 +385,30 @@ const Markdown = ({ }; const toc = useMemo(() => { - if (showToc && tocRef.current.length > 0) { - return tocRef.current.map((item) => { + if (showToc) { + if (tocRef.current.length > 0) { + return tocRef.current.map((item) => { + return ( + setFocusedHeading(item.href)} + > + {item.value} + + ); + }); + } else { return ( - setFocusedHeading(item.href)} +
- {item.value} - + No sub-headings found +
); - }); + } } }, [showToc, tocRef]); @@ -444,7 +457,7 @@ const Markdown = ({ return ( { return ( -
+
{scrollable ? : } {toc && ( - +
-

Table of Contents

+

Table of Contents

{toc}
diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index d351aa9bc..3f278a682 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -8,9 +8,7 @@ import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; import { configureMonacoYaml } from "monaco-yaml"; import React, { useMemo, useRef } from "react"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { boundNumber, makeConnRoute } from "@/util/util"; +import { boundNumber } from "@/util/util"; import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; @@ -19,7 +17,6 @@ import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker" import { SchemaEndpoints, getSchemaEndpointInfo } from "./schemaendpoints"; import ymlWorker from "./yamlworker?worker"; - // there is a global monaco variable (TODO get the correct TS type) declare var monaco: Monaco; @@ -109,15 +106,13 @@ function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions { interface CodeEditorProps { blockId: string; text: string; - filename: string; - fileinfo: FileInfo; + readonly: boolean; language?: string; - meta?: MetaType; onChange?: (text: string) => void; onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => () => void; } -export function CodeEditor({ blockId, text, language, filename, fileinfo, meta, onChange, onMount }: CodeEditorProps) { +export function CodeEditor({ blockId, text, language, readonly, onChange, onMount }: CodeEditorProps) { const divRef = useRef(null); const unmountRef = useRef<() => void>(null); const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false; @@ -125,7 +120,7 @@ export function CodeEditor({ blockId, text, language, filename, fileinfo, meta, const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false; const fontSize = boundNumber(useOverrideConfigAtom(blockId, "editor:fontsize"), 6, 64); const theme = "wave-theme-dark"; - const [absPath, setAbsPath] = React.useState(""); + const editorPath = useRef(crypto.randomUUID()).current; React.useEffect(() => { return () => { @@ -136,24 +131,6 @@ export function CodeEditor({ blockId, text, language, filename, fileinfo, meta, }; }, []); - React.useEffect(() => { - const inner = async () => { - try { - const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [filename], { - route: makeConnRoute(meta.connection ?? ""), - }); - setAbsPath(fileInfo.path); - } catch (e) { - setAbsPath(filename); - } - }; - inner(); - }, [filename]); - - React.useEffect(() => { - console.log("abspath is", absPath); - }, [absPath]); - function handleEditorChange(text: string, ev: MonacoTypes.editor.IModelContentChangedEvent) { if (onChange) { onChange(text); @@ -168,13 +145,13 @@ export function CodeEditor({ blockId, text, language, filename, fileinfo, meta, const editorOpts = useMemo(() => { const opts = defaultEditorOptions(); - opts.readOnly = fileinfo.readonly; + opts.readOnly = readonly; opts.minimap.enabled = minimapEnabled; opts.stickyScroll.enabled = stickyScrollEnabled; opts.wordWrap = wordWrap ? "on" : "off"; opts.fontSize = fontSize; return opts; - }, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize, fileinfo.readonly]); + }, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize, readonly]); return (
@@ -185,7 +162,7 @@ export function CodeEditor({ blockId, text, language, filename, fileinfo, meta, options={editorOpts} onChange={handleEditorChange} onMount={handleEditorOnMount} - path={absPath} + path={editorPath} language={language} />
diff --git a/frontend/app/view/preview/directorypreview.scss b/frontend/app/view/preview/directorypreview.scss index 64522df95..4ce45bc3a 100644 --- a/frontend/app/view/preview/directorypreview.scss +++ b/frontend/app/view/preview/directorypreview.scss @@ -90,16 +90,6 @@ display: flex; flex-direction: column; padding: 0 5px 5px 5px; - .dir-table-body-search-display { - display: flex; - border-radius: 3px; - padding: 0.25rem 0.5rem; - background-color: var(--warning-color); - - .search-display-close-button { - margin-left: auto; - } - } .dir-table-body-scroll-box { position: relative; @@ -185,52 +175,6 @@ } } } - - .dir-table-search-line { - display: flex; - justify-content: flex-end; - gap: 0.7rem; - - .dir-table-search-box { - width: 0; - height: 0; - opacity: 0; - padding: 0; - border: none; - pointer-events: none; - } - } -} - -.dir-table-button { - background-color: transparent; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - padding: 0.2rem; - border-radius: 6px; - - input { - width: 0; - height: 0; - opacity: 0; - padding: 0; - border: none; - pointer-events: none; - } - - &:hover { - background-color: var(--highlight-bg-color); - } - - &:focus { - background-color: var(--highlight-bg-color); - } - - &:focus-within { - background-color: var(--highlight-bg-color); - } } .entry-manager-overlay { diff --git a/frontend/app/view/preview/entry-manager.tsx b/frontend/app/view/preview/entry-manager.tsx new file mode 100644 index 000000000..da77fb96a --- /dev/null +++ b/frontend/app/view/preview/entry-manager.tsx @@ -0,0 +1,65 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Button } from "@/app/element/button"; +import { Input } from "@/app/element/input"; +import React, { memo, useState } from "react"; + +export enum EntryManagerType { + NewFile = "New File", + NewDirectory = "New Folder", + EditName = "Rename", +} + +export type EntryManagerOverlayProps = { + forwardRef?: React.Ref; + entryManagerType: EntryManagerType; + startingValue?: string; + onSave: (newValue: string) => void; + onCancel?: () => void; + style?: React.CSSProperties; + getReferenceProps?: () => any; +}; + +export const EntryManagerOverlay = memo( + ({ + entryManagerType, + startingValue, + onSave, + onCancel, + forwardRef, + style, + getReferenceProps, + }: EntryManagerOverlayProps) => { + const [value, setValue] = useState(startingValue); + return ( +
+
{entryManagerType}
+
+ { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + onSave(value); + } + }} + /> +
+
+ + +
+
+ ); + } +); + +EntryManagerOverlay.displayName = "EntryManagerOverlay"; diff --git a/frontend/app/view/preview/preview-directory-utils.tsx b/frontend/app/view/preview/preview-directory-utils.tsx new file mode 100644 index 000000000..c00840c60 --- /dev/null +++ b/frontend/app/view/preview/preview-directory-utils.tsx @@ -0,0 +1,175 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { globalStore } from "@/app/store/global"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { fireAndForget, isBlank } from "@/util/util"; +import { Column } from "@tanstack/react-table"; +import dayjs from "dayjs"; +import React from "react"; +import { type PreviewModel } from "./preview-model"; + +export const recursiveError = "recursive flag must be set for directory operations"; +export const overwriteError = "set overwrite flag to delete the existing file"; +export const mergeError = "set overwrite flag to delete the existing contents or set merge flag to merge the contents"; + +export const displaySuffixes = { + B: "b", + kB: "k", + MB: "m", + GB: "g", + TB: "t", + KiB: "k", + MiB: "m", + GiB: "g", + TiB: "t", +}; + +export function getBestUnit(bytes: number, si = false, sigfig = 3): string { + if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "-"; + if (bytes === 0) return "0B"; + + const units = si ? ["kB", "MB", "GB", "TB"] : ["KiB", "MiB", "GiB", "TiB"]; + const divisor = si ? 1000 : 1024; + + const idx = Math.min(Math.floor(Math.log(bytes) / Math.log(divisor)), units.length); + const unit = idx === 0 ? "B" : units[idx - 1]; + const value = bytes / Math.pow(divisor, idx); + + return `${parseFloat(value.toPrecision(sigfig))}${displaySuffixes[unit] ?? unit}`; +} + +export function getLastModifiedTime(unixMillis: number, column: Column): string { + const fileDatetime = dayjs(new Date(unixMillis)); + const nowDatetime = dayjs(new Date()); + + let datePortion: string; + if (nowDatetime.isSame(fileDatetime, "date")) { + datePortion = "Today"; + } else if (nowDatetime.subtract(1, "day").isSame(fileDatetime, "date")) { + datePortion = "Yesterday"; + } else { + datePortion = dayjs(fileDatetime).format("M/D/YY"); + } + + if (column.getSize() > 120) { + return `${datePortion}, ${dayjs(fileDatetime).format("h:mm A")}`; + } + return datePortion; +} + +const iconRegex = /^[a-z0-9- ]+$/; + +export function isIconValid(icon: string): boolean { + if (isBlank(icon)) { + return false; + } + return icon.match(iconRegex) != null; +} + +export function getSortIcon(sortType: string | boolean): React.ReactNode { + switch (sortType) { + case "asc": + return ; + case "desc": + return ; + default: + return null; + } +} + +export function cleanMimetype(input: string): string { + const truncated = input.split(";")[0]; + return truncated.trim(); +} + +export function handleRename( + model: PreviewModel, + path: string, + newPath: string, + isDir: boolean, + recursive: boolean, + setErrorMsg: (msg: ErrorMsg) => void +) { + fireAndForget(async () => { + try { + let srcuri = await model.formatRemoteUri(path, globalStore.get); + if (isDir) { + srcuri += "/"; + } + await RpcApi.FileMoveCommand(TabRpcClient, { + srcuri, + desturi: await model.formatRemoteUri(newPath, globalStore.get), + opts: { + recursive, + }, + }); + } catch (e) { + const errorText = `${e}`; + console.warn(`Rename failed: ${errorText}`); + let errorMsg: ErrorMsg; + if (errorText.includes(recursiveError) && !recursive) { + errorMsg = { + status: "Confirm Rename Directory", + text: "Renaming a directory requires the recursive flag. Proceed?", + level: "warning", + buttons: [ + { + text: "Rename Recursively", + onClick: () => handleRename(model, path, newPath, isDir, true, setErrorMsg), + }, + ], + }; + } else { + errorMsg = { + status: "Rename Failed", + text: `${e}`, + }; + } + setErrorMsg(errorMsg); + } + model.refreshCallback(); + }); +} + +export function handleFileDelete( + model: PreviewModel, + path: string, + recursive: boolean, + setErrorMsg: (msg: ErrorMsg) => void +) { + fireAndForget(async () => { + const formattedPath = await model.formatRemoteUri(path, globalStore.get); + try { + await RpcApi.FileDeleteCommand(TabRpcClient, { + path: formattedPath, + recursive, + }); + } catch (e) { + const errorText = `${e}`; + console.warn(`Delete failed: ${errorText}`); + let errorMsg: ErrorMsg; + if (errorText.includes(recursiveError) && !recursive) { + errorMsg = { + status: "Confirm Delete Directory", + text: "Deleting a directory requires the recursive flag. Proceed?", + level: "warning", + buttons: [ + { + text: "Delete Recursively", + onClick: () => handleFileDelete(model, path, true, setErrorMsg), + }, + ], + }; + } else { + errorMsg = { + status: "Delete Failed", + text: `${e}`, + }; + } + setErrorMsg(errorMsg); + } + model.refreshCallback(); + }); +} diff --git a/frontend/app/view/preview/preview-directory.tsx b/frontend/app/view/preview/preview-directory.tsx index d671d7137..8d25b9798 100644 --- a/frontend/app/view/preview/preview-directory.tsx +++ b/frontend/app/view/preview/preview-directory.tsx @@ -1,8 +1,6 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Button } from "@/app/element/button"; -import { Input } from "@/app/element/input"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, getApi, globalStore } from "@/app/store/global"; import { RpcApi } from "@/app/store/wshclientapi"; @@ -10,11 +8,11 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; import { PLATFORM, PlatformMacOS } from "@/util/platformutil"; import { addOpenMenuItems } from "@/util/previewutil"; -import { fireAndForget, isBlank } from "@/util/util"; +import { fireAndForget } from "@/util/util"; import { formatRemoteUri } from "@/util/waveutil"; import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react"; import { - Column, + Header, Row, RowData, Table, @@ -25,21 +23,54 @@ import { useReactTable, } from "@tanstack/react-table"; import clsx from "clsx"; -import dayjs from "dayjs"; import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; -import React, { Fragment, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { Fragment, useCallback, useEffect, useLayoutEffect, 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"; +import { EntryManagerOverlay, EntryManagerOverlayProps, EntryManagerType } from "./entry-manager"; +import { + cleanMimetype, + getBestUnit, + getLastModifiedTime, + getSortIcon, + handleFileDelete, + handleRename, + isIconValid, + mergeError, + overwriteError, +} from "./preview-directory-utils"; import { type PreviewModel } from "./preview-model"; const PageJumpSize = 20; -const recursiveError = "recursive flag must be set for directory operations"; -const overwriteError = "set overwrite flag to delete the existing file"; -const mergeError = "set overwrite flag to delete the existing contents or set merge flag to merge the contents"; +interface DirectoryTableHeaderCellProps { + header: Header; +} + +function DirectoryTableHeaderCell({ header }: DirectoryTableHeaderCellProps) { + return ( +
+
header.column.toggleSorting()}> + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {getSortIcon(header.column.getIsSorted())} +
+
+
+
+
+ ); +} declare module "@tanstack/react-table" { interface TableMeta { @@ -65,138 +96,6 @@ interface DirectoryTableProps { const columnHelper = createColumnHelper(); -const displaySuffixes = { - B: "b", - kB: "k", - MB: "m", - GB: "g", - TB: "t", - KiB: "k", - MiB: "m", - GiB: "g", - TiB: "t", -}; - -function getBestUnit(bytes: number, si: boolean = false, sigfig: number = 3): string { - if (bytes === undefined || bytes < 0) { - return "-"; - } - const units = si ? ["kB", "MB", "GB", "TB"] : ["KiB", "MiB", "GiB", "TiB"]; - const divisor = si ? 1000 : 1024; - - let currentUnit = "B"; - let currentValue = bytes; - let idx = 0; - while (currentValue > divisor && idx < units.length - 1) { - currentUnit = units[idx]; - currentValue /= divisor; - idx += 1; - } - - return `${parseFloat(currentValue.toPrecision(sigfig))}${displaySuffixes[currentUnit]}`; -} - -function getLastModifiedTime(unixMillis: number, column: Column): string { - const fileDatetime = dayjs(new Date(unixMillis)); - const nowDatetime = dayjs(new Date()); - - let datePortion: string; - if (nowDatetime.isSame(fileDatetime, "date")) { - datePortion = "Today"; - } else if (nowDatetime.subtract(1, "day").isSame(fileDatetime, "date")) { - datePortion = "Yesterday"; - } else { - datePortion = dayjs(fileDatetime).format("M/D/YY"); - } - - if (column.getSize() > 120) { - return `${datePortion}, ${dayjs(fileDatetime).format("h:mm A")}`; - } - return datePortion; -} - -const iconRegex = /^[a-z0-9- ]+$/; - -function isIconValid(icon: string): boolean { - if (isBlank(icon)) { - return false; - } - return icon.match(iconRegex) != null; -} - -function getSortIcon(sortType: string | boolean): React.ReactNode { - switch (sortType) { - case "asc": - return ; - case "desc": - return ; - default: - return null; - } -} - -function cleanMimetype(input: string): string { - const truncated = input.split(";")[0]; - return truncated.trim(); -} - -enum EntryManagerType { - NewFile = "New File", - NewDirectory = "New Folder", - EditName = "Rename", -} - -type EntryManagerOverlayProps = { - forwardRef?: React.Ref; - entryManagerType: EntryManagerType; - startingValue?: string; - onSave: (newValue: string) => void; - onCancel?: () => void; - style?: React.CSSProperties; - getReferenceProps?: () => any; -}; - -const EntryManagerOverlay = memo( - ({ - entryManagerType, - startingValue, - onSave, - onCancel, - forwardRef, - style, - getReferenceProps, - }: EntryManagerOverlayProps) => { - const [value, setValue] = useState(startingValue); - return ( -
-
{entryManagerType}
-
- { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - onSave(value); - } - }} - /> -
-
- - -
-
- ); - } -); - function DirectoryTable({ model, data, @@ -287,63 +186,26 @@ function DirectoryTable({ const setEntryManagerProps = useSetAtom(entryManagerOverlayPropsAtom); - const updateName = useCallback((path: string, isDir: boolean) => { - const fileName = path.split("/").at(-1); - setEntryManagerProps({ - entryManagerType: EntryManagerType.EditName, - startingValue: fileName, - onSave: (newName: string) => { - let newPath: string; - if (newName !== fileName) { - const lastInstance = path.lastIndexOf(fileName); - newPath = path.substring(0, lastInstance) + newName; - console.log(`replacing ${fileName} with ${newName}: ${path}`); - const handleRename = (recursive: boolean) => - fireAndForget(async () => { - try { - let srcuri = await model.formatRemoteUri(path, globalStore.get); - if (isDir) { - srcuri += "/"; - } - await RpcApi.FileMoveCommand(TabRpcClient, { - srcuri, - desturi: await model.formatRemoteUri(newPath, globalStore.get), - opts: { - recursive, - }, - }); - } catch (e) { - const errorText = `${e}`; - console.warn(`Rename failed: ${errorText}`); - let errorMsg: ErrorMsg; - if (errorText.includes(recursiveError)) { - errorMsg = { - status: "Confirm Rename Directory", - text: "Renaming a directory requires the recursive flag. Proceed?", - level: "warning", - buttons: [ - { - text: "Rename Recursively", - onClick: () => handleRename(true), - }, - ], - }; - } else { - errorMsg = { - status: "Rename Failed", - text: `${e}`, - }; - } - setErrorMsg(errorMsg); - } - model.refreshCallback(); - }); - handleRename(false); - } - setEntryManagerProps(undefined); - }, - }); - }, []); + const updateName = useCallback( + (path: string, isDir: boolean) => { + const fileName = path.split("/").at(-1); + setEntryManagerProps({ + entryManagerType: EntryManagerType.EditName, + startingValue: fileName, + onSave: (newName: string) => { + let newPath: string; + if (newName !== fileName) { + const lastInstance = path.lastIndexOf(fileName); + newPath = path.substring(0, lastInstance) + newName; + console.log(`replacing ${fileName} with ${newName}: ${path}`); + handleRename(model, path, newPath, isDir, false, setErrorMsg); + } + setEntryManagerProps(undefined); + }, + }); + }, + [model, setErrorMsg] + ); const table = useReactTable({ data, @@ -421,6 +283,9 @@ function DirectoryTable({ }), [] ); + + const TableComponent = table.getState().columnSizingInfo.isResizingColumn ? MemoizedTableBody : TableBody; + return ( (
{headerGroup.headers.map((header) => ( -
-
header.column.toggleSorting()} - > - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - {getSortIcon(header.column.getIsSorted())} -
-
-
-
-
+ ))}
))}
- {table.getState().columnSizingInfo.isResizingColumn ? ( - - ) : ( - - )} +
); } @@ -563,40 +391,6 @@ function TableBody({ return; } const fileName = finfo.path.split("/").pop(); - const handleFileDelete = (recursive: boolean) => - fireAndForget(async () => { - const path = await model.formatRemoteUri(finfo.path, globalStore.get); - try { - await RpcApi.FileDeleteCommand(TabRpcClient, { - path, - recursive, - }); - } catch (e) { - const errorText = `${e}`; - console.warn(`Delete failed: ${errorText}`); - let errorMsg: ErrorMsg; - if (errorText.includes(recursiveError)) { - errorMsg = { - status: "Confirm Delete Directory", - text: "Deleting a directory requires the recursive flag. Proceed?", - level: "warning", - buttons: [ - { - text: "Delete Recursively", - onClick: () => handleFileDelete(true), - }, - ], - }; - } else { - errorMsg = { - status: "Delete Failed", - text: `${e}`, - }; - } - setErrorMsg(errorMsg); - } - setRefreshVersion((current) => current + 1); - }); const menu: ContextMenuItem[] = [ { label: "New File", @@ -643,7 +437,7 @@ function TableBody({ }, { label: "Delete", - click: () => handleFileDelete(false), + click: () => handleFileDelete(model, finfo.path, false, setErrorMsg), } ); ContextMenuModel.showContextMenu(menu, e); @@ -654,11 +448,19 @@ function TableBody({ return (
{search !== "" && ( -
+
Searching for "{search}" -
setSearch("")}> +
setSearch("")} + > - {}} /> + {}} + className="w-0 h-0 opacity-0 p-0 border-none pointer-events-none" + />
)} diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index 391542482..f249f7ee7 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -16,8 +16,6 @@ function CodeEditPreview({ model }: SpecializedViewProps) { const fileContent = useAtomValue(model.fileContent); const setNewFileContent = useSetAtom(model.newFileContent); const fileInfo = useAtomValue(model.statFile); - const fileName = fileInfo?.name; - const blockMeta = useAtomValue(model.blockAtom)?.meta; function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean { if (checkKeyPressed(e, "Cmd:e")) { @@ -67,9 +65,7 @@ function CodeEditPreview({ model }: SpecializedViewProps) { setNewFileContent(text)} onMount={onMount} /> diff --git a/frontend/app/view/preview/preview-error-overlay.tsx b/frontend/app/view/preview/preview-error-overlay.tsx new file mode 100644 index 000000000..1b70a23ef --- /dev/null +++ b/frontend/app/view/preview/preview-error-overlay.tsx @@ -0,0 +1,84 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Button } from "@/app/element/button"; +import { CopyButton } from "@/app/element/copybutton"; +import clsx from "clsx"; +import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; +import { memo, useCallback } from "react"; + +export const ErrorOverlay = memo(({ errorMsg, resetOverlay }: { errorMsg: ErrorMsg; resetOverlay: () => void }) => { + const showDismiss = errorMsg.showDismiss ?? true; + const buttonClassName = "outlined grey font-size-11 vertical-padding-3 horizontal-padding-7"; + + let iconClass = "fa-solid fa-circle-exclamation text-error text-base"; + if (errorMsg.level == "warning") { + iconClass = "fa-solid fa-triangle-exclamation text-warning text-base"; + } + + const handleCopyToClipboard = useCallback(async () => { + await navigator.clipboard.writeText(errorMsg.text); + }, [errorMsg.text]); + + return ( +
+
+
+ + +
+
+ {errorMsg.status} +
+ + + +
{errorMsg.text}
+
+ {!!errorMsg.buttons && ( +
+ {errorMsg.buttons?.map((buttonDef) => ( + + ))} +
+ )} +
+ + {showDismiss && ( +
+
+ )} +
+
+
+ ); +}); diff --git a/frontend/app/view/preview/preview-markdown.tsx b/frontend/app/view/preview/preview-markdown.tsx index 87e8a336d..6eda00d3c 100644 --- a/frontend/app/view/preview/preview-markdown.tsx +++ b/frontend/app/view/preview/preview-markdown.tsx @@ -19,13 +19,14 @@ function MarkdownPreview({ model }: SpecializedViewProps) { }; }, [connName, fileInfo.dir]); return ( -
+
); diff --git a/frontend/app/view/preview/preview-streaming.tsx b/frontend/app/view/preview/preview-streaming.tsx index d76bef428..f16babe7f 100644 --- a/frontend/app/view/preview/preview-streaming.tsx +++ b/frontend/app/view/preview/preview-streaming.tsx @@ -13,14 +13,14 @@ function ImageZoomControls() { const { zoomIn, zoomOut, resetTransform } = useControls(); return ( -
- - -
@@ -29,13 +29,13 @@ function ImageZoomControls() { function StreamingImagePreview({ url }: { url: string }) { return ( -
+
{({ zoomIn, zoomOut, resetTransform, ...rest }) => ( <> - - + + )} @@ -57,15 +57,15 @@ function StreamingPreview({ model }: SpecializedViewProps) { const streamingUrl = `${getWebServerEndpoint()}/wave/stream-file?${usp.toString()}`; if (fileInfo.mimetype === "application/pdf") { return ( -
+