diff --git a/frontend/app/element/markdown-util.ts b/frontend/app/element/markdown-util.ts index 01cb0241fe..ae860648d1 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 da826dad8d..7539aa3130 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 d351aa9bcf..3f278a6825 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 64522df95c..4ce45bc3a6 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 0000000000..da77fb96aa --- /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 0000000000..c00840c60c --- /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 d671d7137c..8d25b97980 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 3915424828..f249f7ee7f 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 0000000000..1b70a23efe --- /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 87e8a336df..6eda00d3c4 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 d76bef4289..f16babe7f8 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 ( -
+