diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 314ca7606..3b5129039 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -13,17 +13,17 @@ import { registerBlockComponentModel, unregisterBlockComponentModel, } from "@/store/global"; -import * as WOS from "@/store/wos"; +import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; -import * as util from "@/util/util"; +import { isBlank } from "@/util/util"; import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot"; 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 * as jotai from "jotai"; -import * as React from "react"; -import { QuickTipsView, QuickTipsViewModel } from "../view/quicktipsview/quicktipsview"; +import { atom, useAtomValue } from "jotai"; +import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import "./block.less"; import { BlockFrame } from "./blockframe"; import { blockViewToIcon, blockViewToName } from "./blockutil"; @@ -51,7 +51,7 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel) return makeCpuPlotViewModel(blockId); } if (blockView === "help") { - return makeHelpViewModel(blockId); + return makeHelpViewModel(blockId, nodeModel); } return makeDefaultViewModel(blockId, blockView); } @@ -63,7 +63,7 @@ function getViewElem( blockView: string, viewModel: ViewModel ): JSX.Element { - if (util.isBlank(blockView)) { + if (isBlank(blockView)) { return No View; } if (blockView === "term") { @@ -102,25 +102,25 @@ function getViewElem( } function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { - const blockDataAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", blockId)); + const blockDataAtom = getWaveObjectAtom(makeORef("block", blockId)); let viewModel: ViewModel = { viewType: viewType, - viewIcon: jotai.atom((get) => { + viewIcon: atom((get) => { const blockData = get(blockDataAtom); return blockViewToIcon(blockData?.meta?.view); }), - viewName: jotai.atom((get) => { + viewName: atom((get) => { const blockData = get(blockDataAtom); return blockViewToName(blockData?.meta?.view); }), - preIconButton: jotai.atom(null), - endIconButtons: jotai.atom(null), + preIconButton: atom(null), + endIconButtons: atom(null), }; return viewModel; } -const BlockPreview = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); +const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { + const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); if (!blockData) { return null; } @@ -135,22 +135,22 @@ const BlockPreview = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { ); }); -const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { +const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { counterInc("render-BlockFull"); - const focusElemRef = React.useRef(null); - const blockRef = React.useRef(null); - const contentRef = React.useRef(null); - const [blockClicked, setBlockClicked] = React.useState(false); - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); - const isFocused = jotai.useAtomValue(nodeModel.isFocused); - const disablePointerEvents = jotai.useAtomValue(nodeModel.disablePointerEvents); + const focusElemRef = useRef(null); + const blockRef = useRef(null); + const contentRef = useRef(null); + const [blockClicked, setBlockClicked] = useState(false); + const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const isFocused = useAtomValue(nodeModel.isFocused); + const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents); const innerRect = useDebouncedNodeInnerRect(nodeModel); - React.useLayoutEffect(() => { + useLayoutEffect(() => { setBlockClicked(isFocused); }, [isFocused]); - React.useLayoutEffect(() => { + useLayoutEffect(() => { if (!blockClicked) { return; } @@ -164,13 +164,13 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { } }, [blockClicked, isFocused]); - const setBlockClickedTrue = React.useCallback(() => { + const setBlockClickedTrue = useCallback(() => { setBlockClicked(true); }, []); - const [blockContentOffset, setBlockContentOffset] = React.useState(); + const [blockContentOffset, setBlockContentOffset] = useState(); - React.useEffect(() => { + useEffect(() => { if (blockRef.current && contentRef.current) { const blockRect = blockRef.current.getBoundingClientRect(); const contentRect = contentRef.current.getBoundingClientRect(); @@ -183,7 +183,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { } }, [blockRef, contentRef]); - const blockContentStyle = React.useMemo(() => { + const blockContentStyle = useMemo(() => { const retVal: React.CSSProperties = { pointerEvents: disablePointerEvents ? "none" : undefined, }; @@ -194,12 +194,12 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { return retVal; }, [innerRect, disablePointerEvents, blockContentOffset]); - const viewElem = React.useMemo( + const viewElem = useMemo( () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel), [nodeModel.blockId, blockData?.meta?.view, viewModel] ); - const handleChildFocus = React.useCallback( + const handleChildFocus = useCallback( (event: React.FocusEvent) => { console.log("setFocusedChild", nodeModel.blockId, getElemAsStr(event.target)); if (!isFocused) { @@ -210,7 +210,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { [isFocused] ); - const setFocusTarget = React.useCallback(() => { + const setFocusTarget = useCallback(() => { const ok = viewModel?.giveFocus?.(); if (ok) { return; @@ -244,29 +244,29 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
- Loading...}>{viewElem} + Loading...}>{viewElem}
); }); -const Block = React.memo((props: BlockProps) => { +const Block = memo((props: BlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8)); - const [blockData, loading] = WOS.useWaveObjectValue(WOS.makeORef("block", props.nodeModel.blockId)); + const [blockData, loading] = useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } - React.useEffect(() => { + useEffect(() => { return () => { unregisterBlockComponentModel(props.nodeModel.blockId); }; }, []); - if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) { + if (loading || isBlank(props.nodeModel.blockId) || blockData == null) { return null; } if (props.preview) { diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index 8fb2e2750..85a5a67e2 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -2,63 +2,46 @@ // SPDX-License-Identifier: Apache-2.0 import { getApi } from "@/app/store/global"; +import { WebView, WebViewModel } from "@/app/view/webview/webview"; +import { NodeModel } from "@/layout/index"; import { WebviewTag } from "electron"; -import { createRef, useEffect, useState } from "react"; +import { atom } from "jotai"; +import { createRef } from "react"; import "./helpview.less"; -class HelpViewModel implements ViewModel { +class HelpViewModel extends WebViewModel { viewType: string; blockId: string; webviewRef: React.RefObject; - constructor(blockId: string) { + constructor(blockId: string, nodeModel: NodeModel) { + super(blockId, nodeModel); + this.getSettingsMenuItems = undefined; + this.viewText = atom([ + { + elemtype: "iconbutton", + icon: "house", + click: this.handleHome.bind(this), + disabled: this.shouldDisabledHomeButton(), + }, + ]); + this.homepageUrl = atom(getApi().getDocsiteUrl()); this.viewType = "help"; this.blockId = blockId; + this.viewIcon = atom("circle-question"); + this.viewName = atom("Help"); this.webviewRef = createRef(); } } -function makeHelpViewModel(blockId: string) { - return new HelpViewModel(blockId); +function makeHelpViewModel(blockId: string, nodeModel: NodeModel) { + return new HelpViewModel(blockId, nodeModel); } function HelpView({ model }: { model: HelpViewModel }) { - const [url] = useState(() => getApi().getDocsiteUrl()); - const [webContentsId, setWebContentsId] = useState(null); - const [domReady, setDomReady] = useState(false); - - useEffect(() => { - if (model.webviewRef.current && domReady) { - const wcId = model.webviewRef.current.getWebContentsId?.(); - if (wcId) { - setWebContentsId(wcId); - } - } - }, [model.webviewRef.current, domReady]); - - useEffect(() => { - const webview = model.webviewRef.current; - if (!webview) { - return; - } - const handleDomReady = () => { - setDomReady(true); - }; - webview.addEventListener("dom-ready", handleDomReady); - return () => { - webview.removeEventListener("dom-ready", handleDomReady); - }; - }); - return (
- +
); } diff --git a/frontend/app/view/quicktipsview/quicktipsview.tsx b/frontend/app/view/quicktipsview/quicktipsview.tsx index 270466a60..a017ff3ab 100644 --- a/frontend/app/view/quicktipsview/quicktipsview.tsx +++ b/frontend/app/view/quicktipsview/quicktipsview.tsx @@ -21,7 +21,7 @@ class QuickTipsViewModel implements ViewModel { } } -function makeHelpViewModel() { +function makeQuickTipsViewModel() { return new QuickTipsViewModel(); } @@ -33,4 +33,4 @@ function QuickTipsView({ model }: { model: QuickTipsViewModel }) { ); } -export { makeHelpViewModel, QuickTipsView, QuickTipsViewModel }; +export { makeQuickTipsViewModel, QuickTipsView, QuickTipsViewModel }; diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 1d088347e..a72df73fd 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -3,16 +3,16 @@ import { getApi, getSettingsKeyAtom, openLink } from "@/app/store/global"; import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; +import { ObjectService } from "@/app/store/services"; import { RpcApi } from "@/app/store/wshclientapi"; import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { NodeModel } from "@/layout/index"; import { WOS, globalStore } from "@/store/global"; -import * as services from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget } from "@/util/util"; import clsx from "clsx"; import { WebviewTag } from "electron"; -import * as jotai from "jotai"; +import { Atom, PrimitiveAtom, atom, useAtomValue } from "jotai"; import React, { memo, useEffect, useState } from "react"; import "./webview.less"; @@ -32,19 +32,20 @@ function getWebviewPreloadUrl() { export class WebViewModel implements ViewModel { viewType: string; blockId: string; - blockAtom: jotai.Atom; - viewIcon: jotai.Atom; - viewName: jotai.Atom; - viewText: jotai.Atom; - url: jotai.PrimitiveAtom; - urlInputFocused: jotai.PrimitiveAtom; - isLoading: jotai.PrimitiveAtom; - urlWrapperClassName: jotai.PrimitiveAtom; - refreshIcon: jotai.PrimitiveAtom; + blockAtom: Atom; + viewIcon: Atom; + viewName: Atom; + viewText: Atom; + url: PrimitiveAtom; + homepageUrl: Atom; + urlInputFocused: PrimitiveAtom; + isLoading: PrimitiveAtom; + urlWrapperClassName: PrimitiveAtom; + refreshIcon: PrimitiveAtom; webviewRef: React.RefObject; urlInputRef: React.RefObject; nodeModel: NodeModel; - endIconButtons?: jotai.Atom; + endIconButtons?: Atom; constructor(blockId: string, nodeModel: NodeModel) { this.nodeModel = nodeModel; @@ -52,19 +53,25 @@ export class WebViewModel implements ViewModel { this.blockId = blockId; this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); - this.url = jotai.atom(); - this.urlWrapperClassName = jotai.atom(""); - this.urlInputFocused = jotai.atom(false); - this.isLoading = jotai.atom(false); - this.refreshIcon = jotai.atom("rotate-right"); - this.viewIcon = jotai.atom("globe"); - this.viewName = jotai.atom("Web"); + this.url = atom(); + const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl"); + this.homepageUrl = atom((get) => { + const defaultUrl = get(defaultUrlAtom); + const pinnedUrl = get(this.blockAtom).meta.pinnedurl; + console.log("homepageUrl", pinnedUrl, defaultUrl); + return pinnedUrl ?? defaultUrl; + }); + this.urlWrapperClassName = atom(""); + this.urlInputFocused = atom(false); + this.isLoading = atom(false); + this.refreshIcon = atom("rotate-right"); + this.viewIcon = atom("globe"); + this.viewName = atom("Web"); this.urlInputRef = React.createRef(); this.webviewRef = React.createRef(); - this.viewText = jotai.atom((get) => { - const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl"); - let url = get(this.blockAtom)?.meta?.url || get(defaultUrlAtom); + this.viewText = atom((get) => { + let url = get(this.blockAtom)?.meta?.url || get(this.homepageUrl); const currUrl = get(this.url); if (currUrl !== undefined) { url = currUrl; @@ -82,6 +89,12 @@ export class WebViewModel implements ViewModel { click: this.handleForward.bind(this), disabled: this.shouldDisabledForwardButton(), }, + { + elemtype: "iconbutton", + icon: "house", + click: this.handleHome.bind(this), + disabled: this.shouldDisabledHomeButton(), + }, { elemtype: "div", className: clsx("block-frame-div-url", get(this.urlWrapperClassName)), @@ -108,7 +121,7 @@ export class WebViewModel implements ViewModel { ] as HeaderElem[]; }); - this.endIconButtons = jotai.atom((get) => { + this.endIconButtons = atom((get) => { return [ { elemtype: "iconbutton", @@ -147,6 +160,26 @@ export class WebViewModel implements ViewModel { return true; } + /** + * Whether the home button in the header should be disabled. + * @returns True if the current url is the pinned url or the pinned url is not set. False otherwise. + */ + shouldDisabledHomeButton() { + try { + const homepageUrl = globalStore.get(this.homepageUrl); + return !homepageUrl || this.getUrl() === homepageUrl; + } catch (_) {} + return true; + } + + handleHome(e?: React.MouseEvent) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + this.loadUrl(globalStore.get(this.homepageUrl), "home"); + } + handleUrlWrapperMouseOver(e: React.MouseEvent) { const urlInputFocused = globalStore.get(this.urlInputFocused); if (e.type === "mouseover" && !urlInputFocused) { @@ -227,7 +260,7 @@ export class WebViewModel implements ViewModel { * @param url The URL that has been navigated to. */ handleNavigate(url: string) { - services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url }); + ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url }); globalStore.set(this.url, url); } @@ -349,7 +382,10 @@ export class WebViewModel implements ViewModel { click: async () => { const url = this.getUrl(); if (url != null && url != "") { - RpcApi.SetConfigCommand(WindowRpcClient, { "web:defaulturl": url }); + await RpcApi.SetMetaCommand(WindowRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { pinnedurl: url }, + }); } }, }, @@ -383,11 +419,10 @@ interface WebViewProps { } const WebView = memo(({ model }: WebViewProps) => { - const blockData = jotai.useAtomValue(model.blockAtom); - const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl"); - const defaultUrl = jotai.useAtomValue(defaultUrlAtom); + const blockData = useAtomValue(model.blockAtom); + const defaultUrl = useAtomValue(model.homepageUrl); const defaultSearchAtom = getSettingsKeyAtom("web:defaultsearch"); - const defaultSearch = jotai.useAtomValue(defaultSearchAtom); + const defaultSearch = useAtomValue(defaultSearchAtom); let metaUrl = blockData?.meta?.url || defaultUrl; metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch); const metaUrlRef = React.useRef(metaUrl); diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index bf392ce30..343e9d602 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -1,16 +1,15 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { ErrorBoundary } from "@/app/element/errorboundary"; +import { CenteredDiv } from "@/app/element/quickelems"; import { ModalsRenderer } from "@/app/modals/modalsrenderer"; import { TabBar } from "@/app/tab/tabbar"; import { TabContent } from "@/app/tab/tabcontent"; import { atoms, createBlock } from "@/store/global"; -import * as util from "@/util/util"; -import * as jotai from "jotai"; -import * as React from "react"; -import { CenteredDiv } from "../element/quickelems"; - -import { ErrorBoundary } from "@/app/element/errorboundary"; +import { isBlank, makeIconClass } from "@/util/util"; +import { useAtomValue } from "jotai"; +import { memo } from "react"; import "./workspace.less"; const iconRegex = /^[a-z0-9-]+$/; @@ -33,9 +32,8 @@ function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetCo return wlist; } -const Widgets = React.memo(() => { - const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); - const newWidgetModalVisible = React.useState(false); +const Widgets = memo(() => { + const fullConfig = useAtomValue(atoms.fullConfigAtom); const helpWidget: WidgetConfigType = { icon: "circle-question", label: "help", @@ -80,7 +78,7 @@ async function handleWidgetSelect(blockDef: BlockDef) { createBlock(blockDef); } -const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => { +const Widget = memo(({ widget }: { widget: WidgetConfigType }) => { return (
{ title={widget.description || widget.label} >
- +
- {!util.isBlank(widget.label) ?
{widget.label}
: null} + {!isBlank(widget.label) ?
{widget.label}
: null}
); }); -const WorkspaceElem = React.memo(() => { - const windowData = jotai.useAtomValue(atoms.waveWindow); +const WorkspaceElem = memo(() => { + const windowData = useAtomValue(atoms.waveWindow); const activeTabId = windowData?.activetabid; - const ws = jotai.useAtomValue(atoms.workspace); + const ws = useAtomValue(atoms.workspace); return (
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 04126bae7..98fb35a28 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -266,6 +266,7 @@ declare global { controller?: string; file?: string; url?: string; + pinnedurl?: string; connection?: string; edit?: boolean; history?: string[]; diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index df21805e5..02a738d7f 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -14,6 +14,8 @@ const ( MetaKey_Url = "url" + MetaKey_PinnedUrl = "pinnedurl" + MetaKey_Connection = "connection" MetaKey_Edit = "edit" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index de6a444ec..0fce994cd 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -16,6 +16,7 @@ type MetaTSType struct { Controller string `json:"controller,omitempty"` File string `json:"file,omitempty"` Url string `json:"url,omitempty"` + PinnedUrl string `json:"pinnedurl,omitempty"` Connection string `json:"connection,omitempty"` Edit bool `json:"edit,omitempty"` History []string `json:"history,omitempty"`