Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 25 additions & 63 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string, ViewModelClass> = 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);
}
Expand All @@ -73,40 +63,11 @@ function getViewElem(
if (isBlank(blockView)) {
return <CenteredDiv>No View</CenteredDiv>;
}
if (blockView === "term") {
return <TerminalView key={blockId} blockId={blockId} model={viewModel as TermViewModel} />;
}
if (blockView === "preview") {
return (
<PreviewView
key={blockId}
blockId={blockId}
blockRef={blockRef}
contentRef={contentRef}
model={viewModel as PreviewModel}
/>
);
}
if (blockView === "web") {
return <WebView key={blockId} blockId={blockId} model={viewModel as WebViewModel} blockRef={blockRef} />;
}
if (blockView === "waveai") {
return <WaveAi key={blockId} blockId={blockId} model={viewModel as WaveAiModel} />;
}
if (blockView === "cpuplot" || blockView === "sysinfo") {
// "cpuplot" is for backwards compatibility with already opened widgets
return <SysinfoView key={blockId} blockId={blockId} model={viewModel as SysinfoViewModel} />;
}
if (blockView == "help") {
return <HelpView key={blockId} model={viewModel as HelpViewModel} />;
}
if (blockView == "tips") {
return <QuickTipsView key={blockId} model={viewModel as QuickTipsViewModel} />;
}
if (blockView == "vdom") {
return <VDomView key={blockId} blockId={blockId} model={viewModel as VDomModel} />;
if (viewModel.viewComponent == null) {
return <CenteredDiv>No View Component</CenteredDiv>;
}
return <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
const VC = viewModel.viewComponent;
return <VC key={blockId} blockId={blockId} blockRef={blockRef} contentRef={contentRef} model={viewModel} />;
}

function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
Expand All @@ -123,6 +84,7 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
}),
preIconButton: atom(null),
endIconButtons: atom(null),
viewComponent: null,
};
return viewModel;
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
>
<BlockMask nodeModel={nodeModel} />
{preview || viewModel == null ? null : (
Expand Down
5 changes: 3 additions & 2 deletions frontend/app/view/helpview/helpview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ function makeHelpViewModel(blockId: string, nodeModel: BlockNodeModel) {
return new HelpViewModel(blockId, nodeModel);
}

function HelpView({ model }: { model: HelpViewModel }) {
function HelpView(props: ViewComponentProps<HelpViewModel>) {
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
Expand All @@ -166,7 +167,7 @@ function HelpView({ model }: { model: HelpViewModel }) {
);
return (
<div className="help-view">
<WebView blockId={model.blockId} model={model} onFailLoad={onFailLoad} />
<WebView {...props} onFailLoad={onFailLoad} />
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/view/preview/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/view/quicktipsview/quicktipsview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/view/sysinfo/sysinfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ class SysinfoViewModel implements ViewModel {
});
}

get viewComponent(): ViewComponent {
return SysinfoView;
}

async loadInitialData() {
globalStore.set(this.loadingAtom, true);
try {
Expand Down
6 changes: 5 additions & 1 deletion frontend/app/view/term/term.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -873,7 +877,7 @@ const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => {
);
};

const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) => {
const viewRef = React.useRef<HTMLDivElement>(null);
const connectElemRef = React.useRef<HTMLDivElement>(null);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/view/vdom/vdom-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -183,6 +184,10 @@ export class VDomModel {
);
}

get viewComponent(): ViewComponent {
return VDomView;
}

dispose() {
DefaultRouter.unregisterRoute(this.wshClient.routeId);
this.routeGoneUnsub?.();
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/view/waveai/waveai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ export class WaveAiModel implements ViewModel {
});
}

get viewComponent(): ViewComponent {
return WaveAi;
}

dispose() {
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
}
Expand Down
19 changes: 12 additions & 7 deletions frontend/app/view/webview/webview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<HTMLDivElement>;
}

const BookmarkTypeahead = memo(
({ model, blockRef }: { model: WebViewModel; blockRef: React.RefObject<HTMLDivElement> }) => {
const openBookmarksJson = () => {
Expand Down Expand Up @@ -662,6 +659,14 @@ const BookmarkTypeahead = memo(
}
);

interface WebViewProps {
blockId: string;
model: WebViewModel;
onFailLoad?: (url: string) => void;
blockRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement>;
}
Comment on lines +662 to +668
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the usage of contentRef prop.

The contentRef prop is added to the interface but appears to be unused in the WebView component implementation.


🏁 Script executed:

#!/bin/bash
# Check if contentRef is used in the component
rg -A 5 "contentRef" frontend/app/view/webview/webview.tsx

Length of output: 293


Content Reference Prop Issue in WebView Component

  • The contentRef property is declared in the WebViewProps interface but is not deconstructed or used in the WebView component.
  • The component only destructures { model, onFailLoad, blockRef }, confirming that contentRef is currently unused.


const WebView = memo(({ model, onFailLoad, blockRef }: WebViewProps) => {
const blockData = useAtomValue(model.blockAtom);
const defaultUrl = useAtomValue(model.homepageUrl);
Expand Down
48 changes: 44 additions & 4 deletions frontend/types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,27 +249,67 @@ declare global {
wholeWord?: PrimitiveAtom<boolean>;
};

declare type ViewComponentProps<T extends ViewModel> = {
blockId: string;
blockRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement>;
model: T;
};

declare type ViewComponent = React.FC<ViewComponentProps>;

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<string | IconButtonDecl>;

// Display name for the view, used in UI headers.
viewName?: jotai.Atom<string>;

// Optional header text or elements for the view.
viewText?: jotai.Atom<string | HeaderElem[]>;

// Icon button displayed before the title in the header.
preIconButton?: jotai.Atom<IconButtonDecl>;

// Icon buttons displayed at the end of the block header.
endIconButtons?: jotai.Atom<IconButtonDecl[]>;

// Background styling metadata for the block.
blockBg?: jotai.Atom<MetaType>;

// Whether the block manages its own connection (e.g., for remote access).
manageConnection?: jotai.Atom<boolean>;
noPadding?: jotai.Atom<boolean>;

// If true, filters out 'nowsh' connections (when managing connections)
filterOutNowsh?: jotai.Atom<boolean>;

// If true, removes padding inside the block content area.
noPadding?: jotai.Atom<boolean>;

// Atoms used for managing search functionality within the block.
searchAtoms?: SearchAtoms;

// just for terminal
// The main view component associated with this ViewModel.
viewComponent: ViewComponent<ViewModel>;

Comment on lines +297 to +299
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix the type of viewComponent property.

The viewComponent property should use the generic type parameter of the interface for better type safety.

-        viewComponent: ViewComponent<ViewModel>;
+        viewComponent: ViewComponent<this>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The main view component associated with this ViewModel.
viewComponent: ViewComponent<ViewModel>;
// The main view component associated with this ViewModel.
viewComponent: ViewComponent<this>;

// 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;
}

Expand Down
Loading