diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index a5bb77b20a..badead2691 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -9,9 +9,8 @@ import { FullSubBlockProps, SubBlockProps, } from "@/app/block/blocktypes"; -import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview"; -import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo"; -import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom"; +import { PreviewModel } from "@/app/view/preview/preview"; +import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo"; import { VDomModel } from "@/app/view/vdom/vdom-model"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; @@ -25,40 +24,31 @@ import { import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { isBlank, useAtomValueSafe } from "@/util/util"; -import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview"; -import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview"; -import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term"; -import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai"; -import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview"; +import { HelpViewModel } from "@/view/helpview/helpview"; +import { TermViewModel } from "@/view/term/term"; +import { WaveAiModel } from "@/view/waveai/waveai"; +import { WebViewModel } from "@/view/webview/webview"; import clsx from "clsx"; import { atom, useAtomValue } from "jotai"; -import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import "./block.scss"; import { BlockFrame } from "./blockframe"; import { blockViewToIcon, blockViewToName } from "./blockutil"; +const BlockRegistry: Map = new Map(); +BlockRegistry.set("term", TermViewModel); +BlockRegistry.set("preview", PreviewModel); +BlockRegistry.set("web", WebViewModel); +BlockRegistry.set("waveai", WaveAiModel); +BlockRegistry.set("cpuplot", SysinfoViewModel); +BlockRegistry.set("sysinfo", SysinfoViewModel); +BlockRegistry.set("vdom", VDomModel); +BlockRegistry.set("help", HelpViewModel); + function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel { - if (blockView === "term") { - return makeTerminalModel(blockId, nodeModel); - } - if (blockView === "preview") { - return makePreviewModel(blockId, nodeModel); - } - if (blockView === "web") { - return makeWebViewModel(blockId, nodeModel); - } - if (blockView === "waveai") { - return makeWaveAiViewModel(blockId); - } - if (blockView === "cpuplot" || blockView == "sysinfo") { - // "cpuplot" is for backwards compatibility with already-opened widgets - return makeSysinfoViewModel(blockId, blockView); - } - if (blockView == "vdom") { - return makeVDomModel(blockId, nodeModel); - } - if (blockView === "help") { - return makeHelpViewModel(blockId, nodeModel); + const ctor = BlockRegistry.get(blockView); + if (ctor != null) { + return new ctor(blockId, nodeModel); } return makeDefaultViewModel(blockId, blockView); } @@ -73,40 +63,11 @@ function getViewElem( if (isBlank(blockView)) { return No View; } - if (blockView === "term") { - return ; - } - if (blockView === "preview") { - return ( - - ); - } - if (blockView === "web") { - return ; - } - if (blockView === "waveai") { - return ; - } - if (blockView === "cpuplot" || blockView === "sysinfo") { - // "cpuplot" is for backwards compatibility with already opened widgets - return ; - } - if (blockView == "help") { - return ; - } - if (blockView == "tips") { - return ; - } - if (blockView == "vdom") { - return ; + if (viewModel.viewComponent == null) { + return No View Component; } - return Invalid View "{blockView}"; + const VC = viewModel.viewComponent; + return ; } function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { @@ -123,6 +84,7 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { }), preIconButton: atom(null), endIconButtons: atom(null), + viewComponent: null, }; return viewModel; } diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 33c33c1263..6a9e4185dd 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -607,7 +607,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { "--magnified-block-blur": `${magnifiedBlockBlur}px`, } as React.CSSProperties } - inert={preview ? "1" : undefined} // this does exist in the DOM, just not in react + {...({ inert: preview ? "1" : undefined } as any)} // sets insert="1" ... but tricks TS into accepting it > {preview || viewModel == null ? null : ( diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index 34ce9ba924..51069240c9 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -141,7 +141,8 @@ function makeHelpViewModel(blockId: string, nodeModel: BlockNodeModel) { return new HelpViewModel(blockId, nodeModel); } -function HelpView({ model }: { model: HelpViewModel }) { +function HelpView(props: ViewComponentProps) { + const model = props.model; const homepageUrl = useAtomValue(model.homepageUrl); // Effect to update the docsite base url when the app restarts, since the webserver port is dynamic @@ -166,7 +167,7 @@ function HelpView({ model }: { model: HelpViewModel }) { ); return (
- +
); } diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 01675288a5..5d2dc55157 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -482,6 +482,10 @@ export class PreviewModel implements ViewModel { globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc)); } + get viewComponent(): ViewComponent { + return PreviewView; + } + async getSpecializedView(getFn: Getter): Promise<{ specializedView?: string; errorStr?: string }> { const mimeType = await getFn(this.fileMimeType); const fileInfo = await getFn(this.statFile); diff --git a/frontend/app/view/quicktipsview/quicktipsview.tsx b/frontend/app/view/quicktipsview/quicktipsview.tsx index a9b5a177ec..bdb55dba8a 100644 --- a/frontend/app/view/quicktipsview/quicktipsview.tsx +++ b/frontend/app/view/quicktipsview/quicktipsview.tsx @@ -16,6 +16,10 @@ class QuickTipsViewModel implements ViewModel { this.showTocAtom = atom(false); } + get viewComponent(): ViewComponent { + return QuickTipsView; + } + showTocToggle() { globalStore.set(this.showTocAtom, !globalStore.get(this.showTocAtom)); } diff --git a/frontend/app/view/sysinfo/sysinfo.tsx b/frontend/app/view/sysinfo/sysinfo.tsx index f3f451ee88..c18c883772 100644 --- a/frontend/app/view/sysinfo/sysinfo.tsx +++ b/frontend/app/view/sysinfo/sysinfo.tsx @@ -240,6 +240,10 @@ class SysinfoViewModel implements ViewModel { }); } + get viewComponent(): ViewComponent { + return SysinfoView; + } + async loadInitialData() { globalStore.set(this.loadingAtom, true); try { diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index e15618c071..47a0362a92 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -318,6 +318,10 @@ class TermViewModel implements ViewModel { }); } + get viewComponent(): ViewComponent { + return TerminalView; + } + isBasicTerm(getFn: jotai.Getter): boolean { // needs to match "const isBasicTerm" in TerminalView() const termMode = getFn(this.termMode); @@ -873,7 +877,7 @@ const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => { ); }; -const TerminalView = ({ blockId, model }: TerminalViewProps) => { +const TerminalView = ({ blockId, model }: ViewComponentProps) => { const viewRef = React.useRef(null); const connectElemRef = React.useRef(null); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index 6831e8e53d..5c07799f30 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -9,6 +9,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; +import { VDomView } from "@/app/view/vdom/vdom"; import { applyCanvasOp, mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils"; import { getWebServerEndpoint } from "@/util/endpoints"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; @@ -183,6 +184,10 @@ export class VDomModel { ); } + get viewComponent(): ViewComponent { + return VDomView; + } + dispose() { DefaultRouter.unregisterRoute(this.wshClient.routeId); this.routeGoneUnsub?.(); diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 8ba1a08526..b4d2589095 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -284,6 +284,10 @@ export class WaveAiModel implements ViewModel { }); } + get viewComponent(): ViewComponent { + return WaveAi; + } + dispose() { DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId)); } diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 8b96a674ef..0aeae49a5d 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -176,6 +176,10 @@ export class WebViewModel implements ViewModel { }); } + get viewComponent(): ViewComponent { + return WebView; + } + /** * Whether the back button in the header should be disabled. * @returns True if the WebView cannot go back or if the WebView call fails. False otherwise. @@ -595,13 +599,6 @@ function makeWebViewModel(blockId: string, nodeModel: BlockNodeModel): WebViewMo return webviewModel; } -interface WebViewProps { - blockId: string; - model: WebViewModel; - onFailLoad?: (url: string) => void; - blockRef: React.RefObject; -} - const BookmarkTypeahead = memo( ({ model, blockRef }: { model: WebViewModel; blockRef: React.RefObject }) => { const openBookmarksJson = () => { @@ -662,6 +659,14 @@ const BookmarkTypeahead = memo( } ); +interface WebViewProps { + blockId: string; + model: WebViewModel; + onFailLoad?: (url: string) => void; + blockRef: React.RefObject; + contentRef: React.RefObject; +} + const WebView = memo(({ model, onFailLoad, blockRef }: WebViewProps) => { const blockData = useAtomValue(model.blockAtom); const defaultUrl = useAtomValue(model.homepageUrl); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 1811632c50..d0e8fbc92b 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -249,27 +249,67 @@ declare global { wholeWord?: PrimitiveAtom; }; + declare type ViewComponentProps = { + blockId: string; + blockRef: React.RefObject; + contentRef: React.RefObject; + model: T; + }; + + declare type ViewComponent = React.FC; + + type ViewModelClass = new (blockId: string, nodeModel: BlockNodeModel) => ViewModel; + interface ViewModel { + // The type of view, used for identifying and rendering the appropriate component. viewType: string; + + // Icon representing the view, can be a string or an IconButton declaration. viewIcon?: jotai.Atom; + + // Display name for the view, used in UI headers. viewName?: jotai.Atom; + + // Optional header text or elements for the view. viewText?: jotai.Atom; + + // Icon button displayed before the title in the header. preIconButton?: jotai.Atom; + + // Icon buttons displayed at the end of the block header. endIconButtons?: jotai.Atom; + + // Background styling metadata for the block. blockBg?: jotai.Atom; + + // Whether the block manages its own connection (e.g., for remote access). manageConnection?: jotai.Atom; - noPadding?: jotai.Atom; + + // If true, filters out 'nowsh' connections (when managing connections) filterOutNowsh?: jotai.Atom; + + // If true, removes padding inside the block content area. + noPadding?: jotai.Atom; + + // Atoms used for managing search functionality within the block. searchAtoms?: SearchAtoms; - // just for terminal + // The main view component associated with this ViewModel. + viewComponent: ViewComponent; + + // Function to determine if this is a basic terminal block. isBasicTerm?: (getFn: jotai.Getter) => boolean; - onBack?: () => void; - onForward?: () => void; + // Returns menu items for the settings dropdown. getSettingsMenuItems?: () => ContextMenuItem[]; + + // Attempts to give focus to the block, returning true if successful. giveFocus?: () => boolean; + + // Handles keydown events within the block. keyDownHandler?: (e: WaveKeyboardEvent) => boolean; + + // Cleans up resources when the block is disposed. dispose?: () => void; }