diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index 2b241846..c4d75ee6 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -69,8 +69,8 @@ .block-frame-default-header { display: flex; - height: 26px; - padding-left: 6px; + height: 34px; + padding: 4px 0 4px 10px; align-items: center; gap: 8px; align-self: stretch; @@ -84,6 +84,10 @@ gap: 8px; color: var(--main-text-color); + .block-frame-back-button { + cursor: pointer; + } + .block-frame-view-icon { font-size: var(--header-icon-size); opacity: 0.5; @@ -103,6 +107,12 @@ } } + .block-frame-text { + font: var(--fixed-font); + font-size: 11px; + opacity: 0.7; + } + .block-frame-end-icons { display: flex; align-items: center; @@ -139,109 +149,9 @@ } } - &.block-frame-tech { - border: 2px solid var(--border-color); - border-radius: 7px; - margin: 8px 0 0 0; - padding: 10px 2px 2px 2px; - height: calc(100% - 8px); - width: 100%; - overflow: visible; - - .block-header-icon { - font-size: 14px; - padding: 0 0; - } - - &.block-preview { - background-color: var(--main-bg-color); - - .block-frame-tech-close { - display: none; - } - } - - &.block-focused { - border: 2px solid var(--accent-color); - - .block-frame-tech-header { - color: var(--main-text-color); - } - } - - .block-frame-tech-header { - position: absolute; - max-width: 85%; - left: 50%; - transform: translate(-50%, 0); - overflow: hidden; - text-overflow: ellipsis; - top: -11px; - padding: 4px 6px 4px 6px; - background-color: var(--main-bg-color); - font: var(--fixed-font); - color: var(--secondary-text-color); - white-space: nowrap; - cursor: grab; - } - - .block-frame-tech-close { - position: absolute; - top: 0; - right: -2px; - padding: 0 0 1px 1px; - cursor: pointer; - opacity: 0; - - &:hover { - color: var(--secondary-text-color); - opacity: 1; - } - } - } - .block-frame-preview { background-color: var(--main-bg-color); width: 100%; flex-grow: 1; } } - -.block-header { - display: flex; - flex-direction: row; - flex-shrink: 0; - height: 30px; - width: 100%; - align-items: center; - justify-content: center; - background-color: var(--panel-bg-color); - user-select: none; - -webkit-user-select: none; - cursor: default; - border-radius: 0 0 4px 4px; - cursor: grab; - - .block-header-icon { - font-size: 20px; - padding: 0 5px; - } - - .block-header-text { - padding: 0 5px; - flex-grow: 1; - white-space: nowrap; - } - - .close-button { - font-size: 12px; - padding: 5px 5px 5px 5px; - margin-right: 5px; - border-radius: 4px; - cursor: pointer; - - &:hover { - background-color: var(--highlight-bg-color); - } - } -} diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 9bd2dd73..7848cde0 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -1,12 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { CodeEdit } from "@/app/view/codeedit"; -import { PlotView } from "@/app/view/plotview"; -import { PreviewView } from "@/app/view/preview"; -import { TerminalView } from "@/app/view/term/term"; -import { WaveAi } from "@/app/view/waveai"; -import { WebView } from "@/app/view/webview"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; import { ContextMenuModel } from "@/store/contextmenu"; @@ -14,19 +8,43 @@ import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global" import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import * as util from "@/util/util"; +import { CodeEdit } from "@/view/codeedit"; +import { PlotView } from "@/view/plotview"; +import { PreviewView, makePreviewModel } from "@/view/preview"; +import { TerminalView } from "@/view/term/term"; +import { WaveAi } from "@/view/waveai"; +import { WebView } from "@/view/webview"; import clsx from "clsx"; import * as jotai from "jotai"; import * as React from "react"; import "./block.less"; -const HoverPixels = 15; -const HoverTimeoutMs = 100; +interface LayoutComponentModel { + onClose?: () => void; + dragHandleRef?: React.RefObject; +} interface BlockProps { blockId: string; - onClose?: () => void; - dragHandleRef?: React.RefObject; + preview: boolean; + layoutModel: LayoutComponentModel; +} + +interface BlockComponentModel { + onClick?: () => void; + onFocusCapture?: React.FocusEventHandler; + blockRef?: React.RefObject; +} + +interface BlockFrameProps { + blockId: string; + blockModel?: BlockComponentModel; + layoutModel?: LayoutComponentModel; + viewModel?: ViewModel; + preview: boolean; + numBlocksInTab?: number; + children?: React.ReactNode; } const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/; @@ -176,118 +194,13 @@ function handleHeaderContextMenu(e: React.MouseEvent, blockData: ContextMenuModel.showContextMenu(menu, e); } -interface FramelessBlockHeaderProps { - blockId: string; - onClose?: () => void; - dragHandleRef?: React.RefObject; -} - -const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); - - return ( -
handleHeaderContextMenu(e, blockData, onClose)} - > -
{getBlockHeaderText(null, blockData, settingsConfig)}
- {onClose && ( -
- -
- )} -
- ); -}); - -const hoverStateOff = "off"; -const hoverStatePending = "pending"; -const hoverStateOn = "on"; - -interface BlockFrameProps { - blockId: string; - onClose?: () => void; - onClick?: () => void; - onFocusCapture?: React.FocusEventHandler; - preview: boolean; - numBlocksInTab?: number; - children?: React.ReactNode; - blockRef?: React.RefObject; - dragHandleRef?: React.RefObject; -} - -const BlockFrame_Tech_Component = ({ - blockId, - onClose, - onClick, - onFocusCapture, - preview, - blockRef, - dragHandleRef, - children, -}: BlockFrameProps) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); - const isFocusedAtom = useBlockAtom(blockId, "isFocused", () => { - return jotai.atom((get) => { - const winData = get(atoms.waveWindow); - return winData?.activeblockid === blockId; - }); - }); - let isFocused = jotai.useAtomValue(isFocusedAtom); - const blockIcon = useBlockIcon(blockId); - if (preview) { - isFocused = true; - } - let style: React.CSSProperties = {}; - if (!isFocused && blockData?.meta?.["frame:bordercolor"]) { - style.borderColor = blockData.meta["frame:bordercolor"]; - } - if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) { - style.borderColor = blockData.meta["frame:bordercolor:focused"]; - } - return ( -
-
handleHeaderContextMenu(e, blockData, onClose)} - > - {getBlockHeaderText(blockIcon, blockData, settingsConfig)} -
-
- -
- {preview ?
: children} -
- ); -}; - -const BlockFrame_Tech = React.memo(BlockFrame_Tech_Component) as typeof BlockFrame_Tech_Component; - const BlockFrame_Default_Component = ({ blockId, - onClose, - onClick, + layoutModel, + viewModel, + blockModel, preview, - blockRef, - dragHandleRef, numBlocksInTab, - onFocusCapture, children, }: BlockFrameProps) => { const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); @@ -299,7 +212,10 @@ const BlockFrame_Default_Component = ({ }); }); let isFocused = jotai.useAtomValue(isFocusedAtom); - const blockIcon = useBlockIcon(blockId); + const viewIcon = jotai.useAtomValue(viewModel.viewIcon); + const viewText = jotai.useAtomValue(viewModel.viewText); + const hasBackButton = jotai.useAtomValue(viewModel.hasBackButton); + const hasForwardButton = jotai.useAtomValue(viewModel.hasForwardButton); if (preview) { isFocused = true; } @@ -339,7 +255,7 @@ const BlockFrame_Default_Component = ({ }, }); menuItems.push({ type: "separator" }); - menuItems.push({ label: "Close", click: onClose }); + menuItems.push({ label: "Close", click: layoutModel?.onClose }); ContextMenuModel.showContextMenu(menuItems, e); }; if (preview) { @@ -355,29 +271,35 @@ const BlockFrame_Default_Component = ({ preview ? "block-preview" : null, numBlocksInTab == 1 ? "block-no-highlight" : null )} - onClick={onClick} - onFocusCapture={onFocusCapture} - ref={blockRef} + onClick={blockModel?.onClick} + onFocusCapture={blockModel?.onFocusCapture} + ref={blockModel?.blockRef} style={style} >
handleHeaderContextMenu(e, blockData, onClose)} + ref={layoutModel?.dragHandleRef} + onContextMenu={(e) => handleHeaderContextMenu(e, blockData, layoutModel?.onClose)} >
-
{getBlockHeaderIcon(blockIcon, blockData)}
+ {hasBackButton && !hasForwardButton && ( +
+ +
+ )} +
{getBlockHeaderIcon(viewIcon, blockData)}
{blockViewToName(blockData?.view)}
{settingsConfig?.blockheader?.showblockids && (
[{blockId.substring(0, 8)}]
)}
+ {util.isBlank(viewText) ? null :
{viewText}
}
-
+
@@ -390,80 +312,6 @@ const BlockFrame_Default_Component = ({ const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; -const BlockFrame_Frameless = ({ - blockId, - onClose, - onClick, - preview, - blockRef, - dragHandleRef, - children, -}: BlockFrameProps) => { - const localBlockRef = React.useRef(null); - const [showHeader, setShowHeader] = React.useState(preview ? true : false); - const hoverState = React.useRef(hoverStateOff); - - // this forward the lcal ref to the blockRef - React.useImperativeHandle(blockRef, () => localBlockRef.current); - - React.useEffect(() => { - if (preview) { - return; - } - const block = localBlockRef.current; - let hoverTimeout: NodeJS.Timeout = null; - const handleMouseMove = (event) => { - const rect = block.getBoundingClientRect(); - if (event.clientY - rect.top <= HoverPixels) { - if (hoverState.current == hoverStateOff) { - hoverTimeout = setTimeout(() => { - if (hoverState.current == hoverStatePending) { - hoverState.current = hoverStateOn; - setShowHeader(true); - } - }, HoverTimeoutMs); - hoverState.current = hoverStatePending; - } - } else { - if (hoverTimeout) { - if (hoverState.current == hoverStatePending) { - hoverState.current = hoverStateOff; - } - clearTimeout(hoverTimeout); - hoverTimeout = null; - } - } - }; - block.addEventListener("mousemove", handleMouseMove); - return () => { - block.removeEventListener("mousemove", handleMouseMove); - }; - }); - let mouseLeaveHandler = () => { - if (preview) { - return; - } - setShowHeader(false); - hoverState.current = hoverStateOff; - }; - return ( -
-
- -
- {preview ?
: children} -
- ); -}; - const BlockFrame = React.memo((props: BlockFrameProps) => { const blockId = props.blockId; const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); @@ -474,18 +322,6 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { } let FrameElem = BlockFrame_Default; const numBlocks = tabData?.blockids?.length ?? 0; - // Preview should always render as the full tech frame - if (!props.preview) { - // if 0 or 1 blocks, use frameless, otherwise use tech - // if (numBlocks <= 1) { - // FrameElem = BlockFrame_Frameless; - // } - if (blockData?.meta?.["frame"] === "tech") { - FrameElem = BlockFrame_Tech; - } else if (blockData?.meta?.["frame"] === "frameless") { - FrameElem = BlockFrame_Frameless; - } - } return ; }); @@ -547,17 +383,78 @@ function useBlockIcon(blockId: string): string { return blockIcon; } -const wm = new WeakMap(); -let wmCounter = 0; -function getObjectId(obj: any): number { - if (!wm.has(obj)) { - wm.set(obj, wmCounter++); +function getViewElemAndModel( + blockId: string, + blockView: string, + blockRef: React.RefObject +): { viewModel: ViewModel; viewElem: JSX.Element } { + if (blockView == null) { + return { viewElem: null, viewModel: null }; } - return wm.get(obj); + let viewElem: JSX.Element = null; + let viewModel: ViewModel = null; + if (blockView === "term") { + viewElem = ; + } else if (blockView === "preview") { + const previewModel = makePreviewModel(blockId); + viewElem = ; + viewModel = previewModel; + } else if (blockView === "plot") { + viewElem = ; + } else if (blockView === "codeedit") { + viewElem = ; + } else if (blockView === "web") { + viewElem = ; + } else if (blockView === "waveai") { + viewElem = ; + } + if (viewModel == null) { + viewModel = makeDefaultViewModel(blockId); + } + return { viewElem, viewModel }; } -const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => { - let blockElem: JSX.Element = null; +function makeDefaultViewModel(blockId: string): ViewModel { + const blockDataAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", blockId)); + let viewModel: ViewModel = { + viewIcon: jotai.atom((get) => { + const blockData = get(blockDataAtom); + return blockViewToIcon(blockData?.view); + }), + viewName: jotai.atom((get) => { + const blockData = get(blockDataAtom); + return blockViewToName(blockData?.view); + }), + viewText: jotai.atom((get) => { + const blockData = get(blockDataAtom); + return blockData?.meta?.title; + }), + hasBackButton: jotai.atom(false), + hasForwardButton: jotai.atom(false), + hasSearch: jotai.atom(false), + }; + return viewModel; +} + +const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => { + const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + if (!blockData) { + return null; + } + let { viewModel } = getViewElemAndModel(blockId, blockData?.view, null); + return ( + + ); +}); + +const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => { const focusElemRef = React.useRef(null); const blockRef = React.useRef(null); const [blockClicked, setBlockClicked] = React.useState(false); @@ -626,33 +523,30 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => { } }, [focusElemRef.current, getFocusableChildren]); + let { viewElem, viewModel } = React.useMemo( + () => getViewElemAndModel(blockId, blockData?.view, blockRef), + [blockId, blockData?.view, blockRef] + ); + if (!blockId || !blockData) return null; + if (blockDataLoading) { - blockElem = Loading...; - } else if (blockData.view === "term") { - blockElem = ; - } else if (blockData.view === "preview") { - blockElem = ; - } else if (blockData.view === "plot") { - blockElem = ; - } else if (blockData.view === "codeedit") { - blockElem = ; - } else if (blockData.view === "web") { - blockElem = ; - } else if (blockData.view === "waveai") { - blockElem = ; + viewElem = Loading...; } + const blockModel: BlockComponentModel = { + onClick: setBlockClickedTrue, + onFocusCapture: determineFocusedChild, + blockRef: blockRef, + }; return ( determineFocusedChild(e)} + blockModel={blockModel} + viewModel={viewModel} >
{
- Loading...}>{blockElem} + Loading...}>{viewElem}
); }); -export { Block, BlockFrame }; +const Block = React.memo((props: BlockProps) => { + if (props.preview) { + return ; + } + return ; +}); + +export { Block }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 5ff18e06..85e2d0ce 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -353,6 +353,15 @@ function setBlockFocus(blockId: string) { WOS.setObjectValue(winData, globalStore.set, true); } +const objectIdWeakMap = new WeakMap(); +let objectIdCounter = 0; +function getObjectId(obj: any): number { + if (!objectIdWeakMap.has(obj)) { + objectIdWeakMap.set(obj, objectIdCounter++); + } + return objectIdWeakMap.get(obj); +} + export { WOS, atoms, @@ -363,6 +372,7 @@ export { getEventORefSubject, getEventSubject, getFileSubject, + getObjectId, globalStore, globalWS, initWS, diff --git a/frontend/app/tab/tabcontent.tsx b/frontend/app/tab/tabcontent.tsx index 87646da4..3ff42f34 100644 --- a/frontend/app/tab/tabcontent.tsx +++ b/frontend/app/tab/tabcontent.tsx @@ -1,7 +1,7 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Block, BlockFrame } from "@/app/block/block"; +import { Block } from "@/app/block/block"; import { getApi } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; @@ -32,18 +32,15 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => { if (!tabData.blockId || !ready) { return null; } - return ( - - ); + const layoutModel = { + onClose: onClose, + dragHandleRef: dragHandleRef, + }; + return ; } function renderPreview(tabData: TabLayoutData) { - return ; + return ; } function onNodeDelete(data: TabLayoutData) { diff --git a/frontend/app/view/directorypreview.tsx b/frontend/app/view/directorypreview.tsx index 76224b13..dd3967c2 100644 --- a/frontend/app/view/directorypreview.tsx +++ b/frontend/app/view/directorypreview.tsx @@ -466,6 +466,7 @@ interface DirectoryPreviewProps { } function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) { + console.log("DirectoryPreview render"); const [searchText, setSearchText] = React.useState(""); const [focusIndex, setFocusIndex] = React.useState(0); const [content, setContent] = React.useState([]); diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index 37f3e3d9..cf5db524 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { Markdown } from "@/element/markdown"; -import { getBackendHostPort, globalStore, useBlockAtom, useBlockCache } from "@/store/global"; +import { getBackendHostPort, getObjectId, globalStore, useBlockAtom } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; +import { loadable } from "jotai/utils"; import { useRef } from "react"; import { CenteredDiv } from "../element/quickelems"; import { CodeEdit } from "./codeedit"; @@ -17,6 +18,113 @@ import { DirectoryPreview } from "./directorypreview"; import "./view.less"; const MaxFileSize = 1024 * 1024 * 10; // 10MB +const MaxCSVSize = 1024 * 1024 * 1; // 1MB + +export class PreviewModel implements ViewModel { + blockId: string; + blockAtom: jotai.Atom; + viewIcon: jotai.Atom; + viewName: jotai.Atom; + viewText: jotai.Atom; + hasBackButton: jotai.Atom; + hasForwardButton: jotai.Atom; + hasSearch: jotai.Atom; + + fileName: jotai.WritableAtom; + statFile: jotai.Atom>; + fullFile: jotai.Atom>; + fileMimeType: jotai.Atom>; + fileMimeTypeLoadable: jotai.Atom>; + fileContent: jotai.Atom>; + + setPreviewFileName(fileName: string) { + services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName }); + } + + constructor(blockId: string) { + this.blockId = blockId; + this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); + this.viewIcon = jotai.atom((get) => { + let blockData = get(this.blockAtom); + if (blockData?.meta?.icon) { + return blockData.meta.icon; + } + const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); + const fileName = get(this.fileName); + return iconForFile(mimeType, fileName); + }); + this.viewName = jotai.atom("Preview"); + this.viewText = jotai.atom((get) => { + return get(this.fileName); + }); + this.hasBackButton = jotai.atom(true); + this.hasForwardButton = jotai.atom((get) => { + const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); + if (mimeType == "directory") { + return true; + } + return false; + }); + this.hasSearch = jotai.atom(false); + + this.fileName = jotai.atom( + (get) => { + return get(this.blockAtom)?.meta?.file; + }, + (get, set, update) => { + services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update }); + } + ); + this.statFile = jotai.atom>(async (get) => { + const fileName = get(this.fileName); + if (fileName == null) { + return null; + } + // const statFile = await FileService.StatFile(fileName); + console.log("PreviewModel calling StatFile", fileName); + const statFile = await services.FileService.StatFile(fileName); + return statFile; + }); + this.fullFile = jotai.atom>(async (get) => { + const fileName = get(this.fileName); + if (fileName == null) { + return null; + } + // const file = await FileService.ReadFile(fileName); + const file = await services.FileService.ReadFile(fileName); + return file; + }); + this.fileMimeType = jotai.atom>(async (get) => { + const fileInfo = await get(this.statFile); + return fileInfo?.mimetype; + }); + this.fileMimeTypeLoadable = loadable(this.fileMimeType); + this.fileContent = jotai.atom>(async (get) => { + const fullFile = await get(this.fullFile); + return util.base64ToString(fullFile?.data64); + }); + + this.onBack = this.onBack.bind(this); + } + + onBack() { + const fileName = globalStore.get(this.fileName); + if (fileName == null) { + return; + } + const splitPath = fileName.split("/"); + console.log("splitPath-1", splitPath); + splitPath.pop(); + console.log("splitPath-2", splitPath); + const newPath = splitPath.join("/"); + globalStore.set(this.fileName, newPath); + } +} + +function makePreviewModel(blockId: string): PreviewModel { + const previewModel = new PreviewModel(blockId); + return previewModel; +} function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom }) { const [cwd, setCwd] = jotai.useAtom(cwdAtom); @@ -139,6 +247,9 @@ function CSVViewPreview({ } function iconForFile(mimeType: string, fileName: string): string { + if (mimeType == null) { + mimeType = "unknown"; + } if (mimeType == "application/pdf") { return "file-pdf"; } else if (mimeType.startsWith("image/")) { @@ -153,6 +264,7 @@ function iconForFile(mimeType: string, fileName: string): string { return "file-csv"; } else if ( mimeType.startsWith("text/") || + mimeType == "application/sql" || (mimeType.startsWith("application/") && (mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml"))) ) { @@ -161,60 +273,21 @@ function iconForFile(mimeType: string, fileName: string): string { if (fileName == "~" || fileName == "~/") { return "home"; } - return "folder"; + return "folder-open"; } else { return "file"; } } -function PreviewView({ blockId }: { blockId: string }) { +function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) { + console.log("render previewview", getObjectId(model)); const ref = useRef(null); const blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); - const fileNameAtom: jotai.WritableAtom = useBlockCache(blockId, "preview:filename", () => - jotai.atom( - (get) => { - return get(blockAtom)?.meta?.file; - }, - (get, set, update) => { - const blockId = get(blockAtom)?.oid; - services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update }); - } - ) - ); - const statFileAtom = useBlockAtom(blockId, "preview:statfile", () => - jotai.atom>(async (get) => { - const fileName = get(fileNameAtom); - if (fileName == null) { - return null; - } - // const statFile = await FileService.StatFile(fileName); - const statFile = await services.FileService.StatFile(fileName); - return statFile; - }) - ); - const fullFileAtom = useBlockAtom(blockId, "preview:fullfile", () => - jotai.atom>(async (get) => { - const fileName = get(fileNameAtom); - if (fileName == null) { - return null; - } - // const file = await FileService.ReadFile(fileName); - const file = await services.FileService.ReadFile(fileName); - return file; - }) - ); - const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () => - jotai.atom>(async (get) => { - const fileInfo = await get(statFileAtom); - return fileInfo?.mimetype; - }) - ); - const fileContentAtom = useBlockAtom(blockId, "preview:filecontent", () => - jotai.atom>(async (get) => { - const fullFile = await get(fullFileAtom); - return util.base64ToString(fullFile?.data64); - }) - ); + const fileNameAtom = model.fileName; + const statFileAtom = model.statFile; + const fullFileAtom = model.fullFile; + const fileMimeTypeAtom = model.fileMimeType; + const fileContentAtom = model.fileContent; let mimeType = jotai.useAtomValue(fileMimeTypeAtom); if (mimeType == null) { mimeType = ""; @@ -241,11 +314,16 @@ function PreviewView({ blockId }: { blockId: string }) { } else if (mimeType === "text/markdown") { specializedView = ; } else if (mimeType === "text/csv") { - specializedView = ( - - ); + if (fileInfo.size > MaxCSVSize) { + specializedView = CSV File Too Large to Preview (1MB Max); + } else { + specializedView = ( + + ); + } } else if ( mimeType.startsWith("text/") || + mimeType == "application/sql" || (mimeType.startsWith("application/") && (mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml"))) ) { @@ -274,4 +352,4 @@ function PreviewView({ blockId }: { blockId: string }) { ); } -export { PreviewView }; +export { PreviewView, makePreviewModel }; diff --git a/frontend/app/view/webview.tsx b/frontend/app/view/webview.tsx index 31578952..ac0eb08a 100644 --- a/frontend/app/view/webview.tsx +++ b/frontend/app/view/webview.tsx @@ -4,17 +4,20 @@ import { Button } from "@/app/element/button"; import { useParentHeight } from "@/app/hook/useParentHeight"; import { getApi } from "@/app/store/global"; +import { WOS } from "@/store/global"; import { WebviewTag } from "electron"; -import React, { memo, useEffect, useRef, useState } from "react"; +import React, { memo, useEffect, useMemo, useRef, useState } from "react"; import "./webview.less"; interface WebViewProps { + blockId: string; parentRef: React.MutableRefObject; - initialUrl: string; } -const WebView = memo(({ parentRef, initialUrl }: WebViewProps) => { +const WebView = memo(({ blockId, parentRef }: WebViewProps) => { + const blockData = WOS.useWaveObjectValueWithSuspense(WOS.makeORef("block", blockId)); + const initialUrl = useMemo(() => blockData?.meta?.url, []); const [url, setUrl] = useState(initialUrl); const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field const [isLoading, setIsLoading] = useState(false); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 768b8c19..657328b0 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,6 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import type * as jotai from "jotai"; import type * as rxjs from "rxjs"; declare global { @@ -114,6 +115,24 @@ declare global { } type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; + + interface ViewModel { + viewIcon: jotai.Atom; + viewName: jotai.Atom; + viewText: jotai.Atom; + hasBackButton: jotai.Atom; + hasForwardButton: jotai.Atom; + hasSearch: jotai.Atom; + + onBack?: () => void; + onForward?: () => void; + onSearchChange?: (text: string) => void; + onSearch?: (text: string) => void; + getSettingsMenuItems?: () => ContextMenuItem[]; + } + + // jotai doesn't export this type :/ + type Loadable = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown }; } export {}; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 723f77e0..4f06f3f2 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -99,4 +99,68 @@ function fireAndForget(f: () => Promise) { }); } -export { base64ToArray, base64ToString, fireAndForget, isBlank, jsonDeepEqual, makeIconClass, stringToBase64 }; +const promiseWeakMap = new WeakMap, ResolvedValue>(); + +type ResolvedValue = { + pending: boolean; + error: any; + value: T; +}; + +// returns the value, pending state, and error of a promise +function getPromiseState(promise: Promise): [T, boolean, any] { + if (promise == null) { + return [null, false, null]; + } + if (promiseWeakMap.has(promise)) { + const value = promiseWeakMap.get(promise); + return [value.value, value.pending, value.error]; + } + const value: ResolvedValue = { + pending: true, + error: null, + value: null, + }; + promise.then( + (result) => { + value.pending = false; + value.error = null; + value.value = result; + }, + (error) => { + value.pending = false; + value.error = error; + } + ); + promiseWeakMap.set(promise, value); + return [value.value, value.pending, value.error]; +} + +// returns the value of a promise, or a default value if the promise is still pending (or had an error) +function getPromiseValue(promise: Promise, def: T): T { + const [value, pending, error] = getPromiseState(promise); + if (pending || error) { + return def; + } + return value; +} + +function jotaiLoadableValue(value: Loadable, def: T): T { + if (value.state === "hasData") { + return value.data; + } + return def; +} + +export { + base64ToArray, + base64ToString, + fireAndForget, + getPromiseState, + getPromiseValue, + isBlank, + jotaiLoadableValue, + jsonDeepEqual, + makeIconClass, + stringToBase64, +}; diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 627ade2a..08c5ddb2 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -56,7 +56,7 @@ func getSettingsConfigDefaults() SettingsConfigType { "text/css": {Icon: "css3-alt fa-brands"}, "text/javascript": {Icon: "js fa-brands"}, "text/typescript": {Icon: "js fa-brands"}, - "text/golang": {Icon: "go fa-brands"}, + "text/golang": {Icon: "golang fa-brands"}, "text/html": {Icon: "html5 fa-brands"}, "text/less": {Icon: "less fa-brands"}, "text/markdown": {Icon: "markdown fa-brands"}, diff --git a/tsconfig.json b/tsconfig.json index 3a32ff9d..09dd412d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "@/util/*": ["frontend/util/*"], "@/faraday/*": ["frontend/faraday/*"], "@/store/*": ["frontend/app/store/*"], + "@/view/*": ["frontend/app/view/*"], "@/element/*": ["frontend/app/element/*"], "@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"], "@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"],