mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
vdom 4 (#1110)
This commit is contained in:
parent
8248637e00
commit
701d93884d
47
cmd/wsh/cmd/wshcmd-debug.go
Normal file
47
cmd/wsh/cmd/wshcmd-debug.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var debugCmd = &cobra.Command{
|
||||
Use: "debug",
|
||||
Short: "debug commands",
|
||||
PersistentPreRunE: preRunSetupRpcClient,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var debugBlockIdsCmd = &cobra.Command{
|
||||
Use: "block",
|
||||
Short: "list sub-blockids for block",
|
||||
RunE: debugBlockIdsRun,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugCmd.AddCommand(debugBlockIdsCmd)
|
||||
rootCmd.AddCommand(debugCmd)
|
||||
}
|
||||
|
||||
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
|
||||
oref, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockInfo, err := wshclient.BlockInfoCommand(RpcClient, oref.OID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
barr, err := json.MarshalIndent(blockInfo, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WriteStdout("%s\n", string(barr))
|
||||
return nil
|
||||
}
|
@ -65,11 +65,11 @@ func editorRun(cmd *cobra.Command, args []string) {
|
||||
return
|
||||
}
|
||||
doneCh := make(chan bool)
|
||||
RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
|
||||
RpcClient.EventListener.On(wps.Event_BlockClose, func(event *wps.WaveEvent) {
|
||||
if event.HasScope(blockRef.String()) {
|
||||
close(doneCh)
|
||||
}
|
||||
})
|
||||
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{blockRef.String()}}, nil)
|
||||
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{blockRef.String()}}, nil)
|
||||
<-doneCh
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ import (
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
var htmlCmdNewBlock bool
|
||||
|
||||
func init() {
|
||||
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
|
||||
rootCmd.AddCommand(htmlCmd)
|
||||
}
|
||||
|
||||
@ -30,7 +33,10 @@ func MakeVDom() *vdom.VDomElem {
|
||||
<h1 style="color:red; background-color: #bind:$.bgcolor; border-radius: 4px; padding: 5px;">hello vdom world</h1>
|
||||
<div><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
|
||||
<div>
|
||||
<button onClick="#globalevent:clickinc">increment</button>
|
||||
<button data-text="hello" onClick='#globalevent:clickinc'>increment</button>
|
||||
</div>
|
||||
<div>
|
||||
<wave:markdown text="*hello from markdown*"/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@ -39,7 +45,7 @@ func MakeVDom() *vdom.VDomElem {
|
||||
}
|
||||
|
||||
func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
|
||||
if event.PropName == "clickinc" {
|
||||
if event.EventType == "clickinc" {
|
||||
client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
|
||||
return
|
||||
}
|
||||
@ -58,7 +64,7 @@ func htmlRun(cmd *cobra.Command, args []string) error {
|
||||
client.SetAtomVal("text", "initial text")
|
||||
client.SetAtomVal("num", 0)
|
||||
client.SetRootElem(MakeVDom())
|
||||
err = client.CreateVDomContext()
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -70,8 +76,12 @@ func htmlRun(cmd *cobra.Command, args []string) error {
|
||||
log.Printf("created vdom context\n")
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
log.Printf("updating text\n")
|
||||
client.SetAtomVal("text", "updated text")
|
||||
client.SendAsyncInitiation()
|
||||
err := client.SendAsyncInitiation()
|
||||
if err != nil {
|
||||
log.Printf("error sending async initiation: %v\n", err)
|
||||
}
|
||||
}()
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
|
@ -71,6 +71,22 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveBlockArg() (*waveobj.ORef, error) {
|
||||
oref := blockArg
|
||||
if oref == "" {
|
||||
return nil, fmt.Errorf("blockid is required")
|
||||
}
|
||||
err := validateEasyORef(oref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving blockid: %w", err)
|
||||
}
|
||||
return fullORef, nil
|
||||
}
|
||||
|
||||
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
|
||||
func setupRpcClient(serverImpl wshutil.ServerImpl) error {
|
||||
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
|
||||
@ -101,7 +117,7 @@ func setupRpcClient(serverImpl wshutil.ServerImpl) error {
|
||||
func setTermHtmlMode() {
|
||||
wshutil.SetExtraShutdownFunc(extraShutdownFn)
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
Meta: map[string]any{"term:mode": "html"},
|
||||
Meta: map[string]any{"term:mode": "vdom"},
|
||||
}
|
||||
err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd, nil)
|
||||
if err != nil {
|
||||
|
@ -28,7 +28,7 @@ var webOpenCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var webGetCmd = &cobra.Command{
|
||||
Use: "get [--inner] [--all] [--json] blockid css-selector",
|
||||
Use: "get [--inner] [--all] [--json] css-selector",
|
||||
Short: "get the html for a css selector",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Hidden: true,
|
||||
@ -67,7 +67,7 @@ func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting block info: %w", err)
|
||||
}
|
||||
if blockInfo.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
|
||||
if blockInfo.Block.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
|
||||
return fmt.Errorf("block %s is not a web block", fullORef.OID)
|
||||
}
|
||||
data := wshrpc.CommandWebSelectorData{
|
||||
|
1
db/migrations-wstore/000005_blockparent.down.sql
Normal file
1
db/migrations-wstore/000005_blockparent.down.sql
Normal file
@ -0,0 +1 @@
|
||||
-- we don't need to remove parentoref
|
4
db/migrations-wstore/000005_blockparent.up.sql
Normal file
4
db/migrations-wstore/000005_blockparent.up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
UPDATE db_block
|
||||
SET data = json_set(db_block.data, '$.parentoref', 'tab:' || db_tab.oid)
|
||||
FROM db_tab
|
||||
WHERE db_block.oid IN (SELECT value FROM json_each(db_tab.data, '$.blockids'));
|
@ -5,6 +5,8 @@ import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
|
||||
import { PlotView } from "@/app/view/plotview/plotview";
|
||||
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
||||
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom-view";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
||||
@ -29,6 +31,7 @@ import { BlockFrame } from "./blockframe";
|
||||
import { blockViewToIcon, blockViewToName } from "./blockutil";
|
||||
|
||||
type FullBlockProps = {
|
||||
isSubBlock?: boolean;
|
||||
preview: boolean;
|
||||
nodeModel: NodeModel;
|
||||
viewModel: ViewModel;
|
||||
@ -51,6 +54,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
|
||||
// "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);
|
||||
}
|
||||
@ -100,6 +106,9 @@ function getViewElem(
|
||||
if (blockView == "tips") {
|
||||
return <QuickTipsView key={blockId} model={viewModel as QuickTipsViewModel} />;
|
||||
}
|
||||
if (blockView == "vdom") {
|
||||
return <VDomView key={blockId} blockId={blockId} model={viewModel as VDomModel} />;
|
||||
}
|
||||
return <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
|
||||
}
|
||||
|
||||
@ -137,6 +146,26 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
const BlockSubBlock = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const viewElem = useMemo(
|
||||
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
|
||||
[nodeModel.blockId, blockData?.meta?.view, viewModel]
|
||||
);
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key="content" className="block-content" ref={contentRef}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||
counterInc("render-BlockFull");
|
||||
const focusElemRef = useRef<HTMLInputElement>(null);
|
||||
@ -275,6 +304,9 @@ const Block = memo((props: BlockProps) => {
|
||||
if (props.preview) {
|
||||
return <BlockPreview {...props} viewModel={viewModel} />;
|
||||
}
|
||||
if (props.isSubBlock) {
|
||||
return <BlockSubBlock {...props} viewModel={viewModel} />;
|
||||
}
|
||||
return <BlockFull {...props} viewModel={viewModel} />;
|
||||
});
|
||||
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
useBlockAtom,
|
||||
WOS,
|
||||
} from "@/app/store/global";
|
||||
import * as services from "@/app/store/services";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
@ -60,17 +59,17 @@ function handleHeaderContextMenu(
|
||||
onMagnifyToggle();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Move to New Window",
|
||||
click: () => {
|
||||
const currentTabId = globalStore.get(atoms.staticTabId);
|
||||
try {
|
||||
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
} catch (e) {
|
||||
console.error("error moving block to new window", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: "Move to New Window",
|
||||
// click: () => {
|
||||
// const currentTabId = globalStore.get(atoms.staticTabId);
|
||||
// try {
|
||||
// services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
// } catch (e) {
|
||||
// console.error("error moving block to new window", e);
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Copy BlockId",
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import { NodeModel } from "@/layout/index";
|
||||
export interface BlockProps {
|
||||
isSubBlock?: boolean;
|
||||
preview: boolean;
|
||||
nodeModel: NodeModel;
|
||||
}
|
||||
|
@ -316,6 +316,7 @@ export {
|
||||
makeORef,
|
||||
reloadWaveObject,
|
||||
setObjectValue,
|
||||
splitORef,
|
||||
updateWaveObject,
|
||||
updateWaveObjects,
|
||||
useWaveObjectValue,
|
||||
|
@ -67,11 +67,21 @@ class RpcApiType {
|
||||
return client.wshRpcCall("createblock", data, opts);
|
||||
}
|
||||
|
||||
// command "createsubblock" [call]
|
||||
CreateSubBlockCommand(client: WshClient, data: CommandCreateSubBlockData, opts?: RpcOpts): Promise<ORef> {
|
||||
return client.wshRpcCall("createsubblock", data, opts);
|
||||
}
|
||||
|
||||
// command "deleteblock" [call]
|
||||
DeleteBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("deleteblock", data, opts);
|
||||
}
|
||||
|
||||
// command "deletesubblock" [call]
|
||||
DeleteSubBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("deletesubblock", data, opts);
|
||||
}
|
||||
|
||||
// command "dispose" [call]
|
||||
DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("dispose", data, opts);
|
||||
@ -228,7 +238,7 @@ class RpcApiType {
|
||||
}
|
||||
|
||||
// command "vdomcreatecontext" [call]
|
||||
VDomCreateContextCommand(client: WshClient, data: VDomCreateContext, opts?: RpcOpts): Promise<void> {
|
||||
VDomCreateContextCommand(client: WshClient, data: VDomCreateContext, opts?: RpcOpts): Promise<ORef> {
|
||||
return client.wshRpcCall("vdomcreatecontext", data, opts);
|
||||
}
|
||||
|
||||
@ -237,6 +247,11 @@ class RpcApiType {
|
||||
return client.wshRpcCall("vdomrender", data, opts);
|
||||
}
|
||||
|
||||
// command "waitforroute" [call]
|
||||
WaitForRouteCommand(client: WshClient, data: CommandWaitForRouteData, opts?: RpcOpts): Promise<boolean> {
|
||||
return client.wshRpcCall("waitforroute", data, opts);
|
||||
}
|
||||
|
||||
// command "webselector" [call]
|
||||
WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> {
|
||||
return client.wshRpcCall("webselector", data, opts);
|
||||
|
@ -1,12 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WOS } from "@/app/store/global";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { atoms, globalStore } from "@/app/store/global";
|
||||
import { makeORef, splitORef } from "@/app/store/wos";
|
||||
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
import { TermViewModel } from "@/app/view/term/term";
|
||||
import { isBlank } from "@/util/util";
|
||||
import debug from "debug";
|
||||
|
||||
const dlog = debug("wave:vdom");
|
||||
@ -21,32 +22,55 @@ export class TermWshClient extends WshClient {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
|
||||
console.log("vdom-create", rh.getSource(), data);
|
||||
this.model.vdomModel.reset();
|
||||
this.model.vdomModel.backendRoute = rh.getSource();
|
||||
if (!data.persist) {
|
||||
const unsubFn = waveEventSubscribe({
|
||||
eventType: "route:gone",
|
||||
scope: rh.getSource(),
|
||||
handler: () => {
|
||||
RpcApi.SetMetaCommand(this, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "term:mode": null },
|
||||
async handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
|
||||
const source = rh.getSource();
|
||||
if (isBlank(source)) {
|
||||
throw new Error("source cannot be blank");
|
||||
}
|
||||
console.log("vdom-create", source, data);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
if (data.target?.newblock) {
|
||||
const oref = await RpcApi.CreateBlockCommand(this, {
|
||||
tabid: tabId,
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "vdom",
|
||||
"vdom:route": rh.getSource(),
|
||||
},
|
||||
},
|
||||
magnified: data.target?.magnified,
|
||||
});
|
||||
unsubFn();
|
||||
return oref;
|
||||
} else {
|
||||
// in the terminal
|
||||
// check if there is a current active vdom block
|
||||
const oldVDomBlockId = globalStore.get(this.model.vdomBlockId);
|
||||
const oref = await RpcApi.CreateSubBlockCommand(this, {
|
||||
parentblockid: this.blockId,
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "vdom",
|
||||
"vdom:route": rh.getSource(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const [_, newVDomBlockId] = splitORef(oref);
|
||||
if (!isBlank(oldVDomBlockId)) {
|
||||
// dispose of the old vdom block
|
||||
setTimeout(() => {
|
||||
RpcApi.DeleteSubBlockCommand(this, { blockid: oldVDomBlockId });
|
||||
}, 500);
|
||||
}
|
||||
setTimeout(() => {
|
||||
RpcApi.SetMetaCommand(this, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "term:mode": "html" },
|
||||
oref: makeORef("block", this.model.blockId),
|
||||
meta: {
|
||||
"term:mode": "vdom",
|
||||
"term:vdomblockid": newVDomBlockId,
|
||||
},
|
||||
});
|
||||
this.model.vdomModel.queueUpdate(true);
|
||||
}, 50);
|
||||
return oref;
|
||||
}
|
||||
|
||||
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
|
||||
console.log("async-initiation", rh.getSource(), data);
|
||||
this.model.vdomModel.queueUpdate(true);
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.term-mode-html {
|
||||
&.term-mode-vdom {
|
||||
.term-connectelem {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,19 +1,28 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Block } from "@/app/block/block";
|
||||
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TermWshClient } from "@/app/view/term/term-wsh";
|
||||
import { VDomView } from "@/app/view/term/vdom";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { WOS, atoms, getConnStatusAtom, getSettingsKeyAtom, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getBlockComponentModel,
|
||||
getConnStatusAtom,
|
||||
getSettingsKeyAtom,
|
||||
globalStore,
|
||||
useSettingsPrefixAtom,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import clsx from "clsx";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { TermStickers } from "./termsticker";
|
||||
@ -22,6 +31,8 @@ import { computeTheme } from "./termutil";
|
||||
import { TermWrap } from "./termwrap";
|
||||
import "./xterm.css";
|
||||
|
||||
const dlog = debug("wave:term");
|
||||
|
||||
type InitialLoadDataType = {
|
||||
loaded: boolean;
|
||||
heldData: Uint8Array[];
|
||||
@ -37,12 +48,13 @@ class TermViewModel {
|
||||
blockId: string;
|
||||
viewIcon: jotai.Atom<string>;
|
||||
viewName: jotai.Atom<string>;
|
||||
viewText: jotai.Atom<HeaderElem[]>;
|
||||
blockBg: jotai.Atom<MetaType>;
|
||||
manageConnection: jotai.Atom<boolean>;
|
||||
connStatus: jotai.Atom<ConnStatus>;
|
||||
termWshClient: TermWshClient;
|
||||
shellProcStatusRef: React.MutableRefObject<string>;
|
||||
vdomModel: VDomModel;
|
||||
vdomBlockId: jotai.Atom<string>;
|
||||
|
||||
constructor(blockId: string, nodeModel: NodeModel) {
|
||||
this.viewType = "term";
|
||||
@ -50,23 +62,70 @@ class TermViewModel {
|
||||
this.termWshClient = new TermWshClient(blockId, this);
|
||||
DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient);
|
||||
this.nodeModel = nodeModel;
|
||||
this.vdomModel = new VDomModel(blockId, nodeModel, null, this.termWshClient);
|
||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
this.vdomBlockId = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
return blockData?.meta?.["term:vdomblockid"];
|
||||
});
|
||||
this.termMode = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
return blockData?.meta?.["term:mode"] ?? "term";
|
||||
});
|
||||
this.viewIcon = jotai.atom((get) => {
|
||||
const termMode = get(this.termMode);
|
||||
if (termMode == "vdom") {
|
||||
return "bolt";
|
||||
}
|
||||
return "terminal";
|
||||
});
|
||||
this.viewName = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const termMode = get(this.termMode);
|
||||
if (termMode == "vdom") {
|
||||
return "Wave App";
|
||||
}
|
||||
if (blockData?.meta?.controller == "cmd") {
|
||||
return "Command";
|
||||
}
|
||||
return "Terminal";
|
||||
});
|
||||
this.manageConnection = jotai.atom(true);
|
||||
this.viewText = jotai.atom((get) => {
|
||||
const termMode = get(this.termMode);
|
||||
if (termMode == "vdom") {
|
||||
return [
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
icon: "square-terminal",
|
||||
title: "Switch back to Terminal",
|
||||
click: () => {
|
||||
this.setTermMode("term");
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const vdomBlockId = get(this.vdomBlockId);
|
||||
if (vdomBlockId) {
|
||||
return [
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
icon: "bolt",
|
||||
title: "Switch to Wave App",
|
||||
click: () => {
|
||||
this.setTermMode("vdom");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
this.manageConnection = jotai.atom((get) => {
|
||||
const termMode = get(this.termMode);
|
||||
if (termMode == "vdom") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.blockBg = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const fullConfig = get(atoms.fullConfigAtom);
|
||||
@ -88,6 +147,28 @@ class TermViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
setTermMode(mode: "term" | "vdom") {
|
||||
if (mode == "term") {
|
||||
mode = null;
|
||||
}
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "term:mode": mode },
|
||||
});
|
||||
}
|
||||
|
||||
getVDomModel(): VDomModel {
|
||||
const vdomBlockId = globalStore.get(this.vdomBlockId);
|
||||
if (!vdomBlockId) {
|
||||
return null;
|
||||
}
|
||||
const bcm = getBlockComponentModel(vdomBlockId);
|
||||
if (!bcm) {
|
||||
return null;
|
||||
}
|
||||
return bcm.viewModel as VDomModel;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
||||
}
|
||||
@ -107,16 +188,18 @@ class TermViewModel {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${this.blockId}`);
|
||||
const blockData = globalStore.get(blockAtom);
|
||||
const newTermMode = blockData?.meta?.["term:mode"] == "html" ? null : "html";
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "term:mode": newTermMode },
|
||||
});
|
||||
const newTermMode = blockData?.meta?.["term:mode"] == "vdom" ? null : "vdom";
|
||||
const vdomBlockId = globalStore.get(this.vdomBlockId);
|
||||
if (newTermMode == "vdom" && !vdomBlockId) {
|
||||
return;
|
||||
}
|
||||
this.setTermMode(newTermMode);
|
||||
return true;
|
||||
}
|
||||
const blockData = globalStore.get(this.blockAtom);
|
||||
if (blockData.meta?.["term:mode"] == "html") {
|
||||
return this.vdomModel?.globalKeydownHandler(waveEvent);
|
||||
if (blockData.meta?.["term:mode"] == "vdom") {
|
||||
const vdomModel = this.getVDomModel();
|
||||
return vdomModel?.keyDownHandler(waveEvent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -241,6 +324,52 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
|
||||
return null;
|
||||
});
|
||||
|
||||
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
|
||||
React.useEffect(() => {
|
||||
const unsub = waveEventSubscribe({
|
||||
eventType: "blockclose",
|
||||
scope: WOS.makeORef("block", vdomBlockId),
|
||||
handler: (event) => {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", blockId),
|
||||
meta: {
|
||||
"term:mode": null,
|
||||
"term:vdomblockid": null,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, []);
|
||||
const isFocusedAtom = jotai.atom((get) => {
|
||||
return get(model.nodeModel.isFocused) && get(model.termMode) == "vdom";
|
||||
});
|
||||
let vdomNodeModel = {
|
||||
blockId: vdomBlockId,
|
||||
isFocused: isFocusedAtom,
|
||||
onClose: () => {
|
||||
if (vdomBlockId != null) {
|
||||
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div key="htmlElem" className="term-htmlelem">
|
||||
<Block key="vdom" isSubBlock={true} preview={false} nodeModel={vdomNodeModel} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
|
||||
const vdomBlockId = jotai.useAtomValue(model.vdomBlockId);
|
||||
if (vdomBlockId == null) {
|
||||
return null;
|
||||
}
|
||||
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
|
||||
};
|
||||
|
||||
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
const viewRef = React.useRef<HTMLDivElement>(null);
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
@ -252,7 +381,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
const termSettingsAtom = useSettingsPrefixAtom("term");
|
||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||
let termMode = blockData?.meta?.["term:mode"] ?? "term";
|
||||
if (termMode != "term" && termMode != "html") {
|
||||
if (termMode != "term" && termMode != "vdom") {
|
||||
termMode = "term";
|
||||
}
|
||||
const termModeRef = React.useRef(termMode);
|
||||
@ -307,7 +436,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
}, [blockId, termSettings]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (termModeRef.current == "html" && termMode == "term") {
|
||||
if (termModeRef.current == "vdom" && termMode == "term") {
|
||||
// focus the terminal
|
||||
model.giveFocus();
|
||||
}
|
||||
@ -356,11 +485,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||
<TermStickers config={stickerConfig} />
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
<div key="htmlElem" className="term-htmlelem">
|
||||
<div key="htmlElemContent" className="term-htmlelem-content">
|
||||
<VDomView blockId={blockId} nodeModel={model.nodeModel} viewRef={viewRef} model={model.vdomModel} />
|
||||
</div>
|
||||
</div>
|
||||
<TermVDomNode key="vdom" blockId={blockId} model={model} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { globalStore, WOS } from "@/app/store/global";
|
||||
import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global";
|
||||
import { makeORef } from "@/app/store/wos";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TermWshClient } from "@/app/view/term/term-wsh";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import debug from "debug";
|
||||
@ -61,22 +63,37 @@ function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
|
||||
return { type: "unknown" };
|
||||
}
|
||||
|
||||
class VDomWshClient extends WshClient {
|
||||
model: VDomModel;
|
||||
|
||||
constructor(model: VDomModel) {
|
||||
super(makeFeBlockRouteId(model.blockId));
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
|
||||
console.log("async-initiation", rh.getSource(), data);
|
||||
this.model.queueUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class VDomModel {
|
||||
blockId: string;
|
||||
nodeModel: NodeModel;
|
||||
viewRef: React.RefObject<HTMLDivElement>;
|
||||
viewType: string;
|
||||
viewIcon: jotai.Atom<string>;
|
||||
viewName: jotai.Atom<string>;
|
||||
viewRef: React.RefObject<HTMLDivElement> = { current: null };
|
||||
vdomRoot: jotai.PrimitiveAtom<VDomElem> = jotai.atom();
|
||||
atoms: Map<string, AtomContainer> = new Map(); // key is atomname
|
||||
refs: Map<string, RefContainer> = new Map(); // key is refid
|
||||
batchedEvents: VDomEvent[] = [];
|
||||
messages: VDomMessage[] = [];
|
||||
needsInitialization: boolean = true;
|
||||
needsResync: boolean = true;
|
||||
vdomNodeVersion: WeakMap<VDomElem, jotai.PrimitiveAtom<number>> = new WeakMap();
|
||||
compoundAtoms: Map<string, jotai.PrimitiveAtom<{ [key: string]: any }>> = new Map();
|
||||
rootRefId: string = crypto.randomUUID();
|
||||
termWshClient: TermWshClient;
|
||||
backendRoute: string;
|
||||
backendRoute: jotai.Atom<string>;
|
||||
backendOpts: VDomBackendOpts;
|
||||
shouldDispose: boolean;
|
||||
disposed: boolean;
|
||||
@ -86,18 +103,61 @@ export class VDomModel {
|
||||
needsImmediateUpdate: boolean;
|
||||
lastUpdateTs: number = 0;
|
||||
queuedUpdate: { timeoutId: any; ts: number; quick: boolean };
|
||||
contextActive: jotai.PrimitiveAtom<boolean>;
|
||||
wshClient: VDomWshClient;
|
||||
persist: jotai.Atom<boolean>;
|
||||
routeGoneUnsub: () => void;
|
||||
routeConfirmed: boolean = false;
|
||||
|
||||
constructor(
|
||||
blockId: string,
|
||||
nodeModel: NodeModel,
|
||||
viewRef: React.RefObject<HTMLDivElement>,
|
||||
termWshClient: TermWshClient
|
||||
) {
|
||||
constructor(blockId: string, nodeModel: NodeModel) {
|
||||
this.viewType = "vdom";
|
||||
this.blockId = blockId;
|
||||
this.nodeModel = nodeModel;
|
||||
this.viewRef = viewRef;
|
||||
this.termWshClient = termWshClient;
|
||||
this.contextActive = jotai.atom(false);
|
||||
this.reset();
|
||||
this.viewIcon = jotai.atom("bolt");
|
||||
this.viewName = jotai.atom("Wave App");
|
||||
this.backendRoute = jotai.atom((get) => {
|
||||
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
|
||||
return blockData?.meta?.["vdom:route"];
|
||||
});
|
||||
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
|
||||
this.wshClient = new VDomWshClient(this);
|
||||
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);
|
||||
const curBackendRoute = globalStore.get(this.backendRoute);
|
||||
if (curBackendRoute) {
|
||||
this.queueUpdate(true);
|
||||
}
|
||||
this.routeGoneUnsub = waveEventSubscribe({
|
||||
eventType: "route:gone",
|
||||
scope: curBackendRoute,
|
||||
handler: (event: WaveEvent) => {
|
||||
this.disposed = true;
|
||||
const shouldPersist = globalStore.get(this.persist);
|
||||
if (!shouldPersist) {
|
||||
this.nodeModel?.onClose?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
RpcApi.WaitForRouteCommand(TabRpcClient, { routeid: curBackendRoute, waitms: 4000 }, { timeout: 5000 }).then(
|
||||
(routeOk: boolean) => {
|
||||
if (routeOk) {
|
||||
this.routeConfirmed = true;
|
||||
this.queueUpdate(true);
|
||||
} else {
|
||||
this.disposed = true;
|
||||
const shouldPersist = globalStore.get(this.persist);
|
||||
if (!shouldPersist) {
|
||||
this.nodeModel?.onClose?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
DefaultRouter.unregisterRoute(this.wshClient.routeId);
|
||||
this.routeGoneUnsub?.();
|
||||
}
|
||||
|
||||
reset() {
|
||||
@ -107,11 +167,9 @@ export class VDomModel {
|
||||
this.batchedEvents = [];
|
||||
this.messages = [];
|
||||
this.needsResync = true;
|
||||
this.needsInitialization = true;
|
||||
this.vdomNodeVersion = new WeakMap();
|
||||
this.compoundAtoms.clear();
|
||||
this.rootRefId = crypto.randomUUID();
|
||||
this.backendRoute = null;
|
||||
this.backendOpts = {};
|
||||
this.shouldDispose = false;
|
||||
this.disposed = false;
|
||||
@ -121,9 +179,15 @@ export class VDomModel {
|
||||
this.needsImmediateUpdate = false;
|
||||
this.lastUpdateTs = 0;
|
||||
this.queuedUpdate = null;
|
||||
globalStore.set(this.contextActive, false);
|
||||
}
|
||||
|
||||
globalKeydownHandler(e: WaveKeyboardEvent): boolean {
|
||||
getBackendRoute(): string {
|
||||
const blockData = globalStore.get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
|
||||
return blockData?.meta?.["vdom:route"];
|
||||
}
|
||||
|
||||
keyDownHandler(e: WaveKeyboardEvent): boolean {
|
||||
if (this.backendOpts?.closeonctrlc && checkKeyPressed(e, "Ctrl:c")) {
|
||||
this.shouldDispose = true;
|
||||
this.queueUpdate(true);
|
||||
@ -135,7 +199,7 @@ export class VDomModel {
|
||||
}
|
||||
this.batchedEvents.push({
|
||||
waveid: null,
|
||||
propname: "onKeyDown",
|
||||
eventtype: "onKeyDown",
|
||||
eventdata: e,
|
||||
});
|
||||
this.queueUpdate();
|
||||
@ -179,6 +243,9 @@ export class VDomModel {
|
||||
}
|
||||
|
||||
queueUpdate(quick: boolean = false, delay: number = 10) {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
this.needsUpdate = true;
|
||||
let nowTs = Date.now();
|
||||
if (delay > this.maxNormalUpdateIntervalMs) {
|
||||
@ -220,7 +287,7 @@ export class VDomModel {
|
||||
|
||||
async _sendRenderRequest(force: boolean) {
|
||||
this.queuedUpdate = null;
|
||||
if (this.disposed) {
|
||||
if (this.disposed || !this.routeConfirmed) {
|
||||
return;
|
||||
}
|
||||
if (this.hasPendingRequest) {
|
||||
@ -232,7 +299,8 @@ export class VDomModel {
|
||||
if (!force && !this.needsUpdate) {
|
||||
return;
|
||||
}
|
||||
if (this.backendRoute == null) {
|
||||
const backendRoute = globalStore.get(this.backendRoute);
|
||||
if (backendRoute == null) {
|
||||
console.log("vdom-model", "no backend route");
|
||||
return;
|
||||
}
|
||||
@ -241,7 +309,7 @@ export class VDomModel {
|
||||
try {
|
||||
const feUpdate = this.createFeUpdate();
|
||||
dlog("fe-update", feUpdate);
|
||||
const beUpdate = await RpcApi.VDomRenderCommand(TabRpcClient, feUpdate, { route: this.backendRoute });
|
||||
const beUpdate = await RpcApi.VDomRenderCommand(TabRpcClient, feUpdate, { route: backendRoute });
|
||||
this.handleBackendUpdate(beUpdate);
|
||||
} finally {
|
||||
this.lastUpdateTs = Date.now();
|
||||
@ -454,6 +522,7 @@ export class VDomModel {
|
||||
if (update == null) {
|
||||
return;
|
||||
}
|
||||
globalStore.set(this.contextActive, true);
|
||||
const idMap = new Map<string, VDomElem>();
|
||||
const vdomRoot = globalStore.get(this.vdomRoot);
|
||||
if (update.opts != null) {
|
||||
@ -478,14 +547,14 @@ export class VDomModel {
|
||||
if (fnDecl.globalevent) {
|
||||
const waveEvent: VDomEvent = {
|
||||
waveid: null,
|
||||
propname: fnDecl.globalevent,
|
||||
eventtype: fnDecl.globalevent,
|
||||
eventdata: eventData,
|
||||
};
|
||||
this.batchedEvents.push(waveEvent);
|
||||
} else {
|
||||
const vdomEvent: VDomEvent = {
|
||||
waveid: compId,
|
||||
propname: propName,
|
||||
eventtype: propName,
|
||||
eventdata: eventData,
|
||||
};
|
||||
this.batchedEvents.push(vdomEvent);
|
||||
@ -510,7 +579,6 @@ export class VDomModel {
|
||||
type: "frontendupdate",
|
||||
ts: Date.now(),
|
||||
blockid: this.blockId,
|
||||
initialize: this.needsInitialization,
|
||||
rendercontext: renderContext,
|
||||
dispose: this.shouldDispose,
|
||||
resync: this.needsResync,
|
||||
@ -518,7 +586,6 @@ export class VDomModel {
|
||||
refupdates: this.getRefUpdates(),
|
||||
};
|
||||
this.needsResync = false;
|
||||
this.needsInitialization = false;
|
||||
this.batchedEvents = [];
|
||||
if (this.shouldDispose) {
|
||||
this.disposed = true;
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import { useAtomValueSafe } from "@/util/util";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
@ -20,6 +19,12 @@ const VDomObjType_Func = "func";
|
||||
|
||||
const dlog = debug("wave:vdom");
|
||||
|
||||
type VDomReactTagType = (props: { elem: VDomElem; model: VDomModel }) => JSX.Element;
|
||||
|
||||
const WaveTagMap: Record<string, VDomReactTagType> = {
|
||||
"wave:markdown": WaveMarkdown,
|
||||
};
|
||||
|
||||
const AllowedTags: { [tagName: string]: boolean } = {
|
||||
div: true,
|
||||
b: true,
|
||||
@ -191,7 +196,7 @@ function stringSetsEqual(set1: Set<string>, set2: Set<string>): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
|
||||
const version = jotai.useAtomValue(model.getVDomNodeVersionAtom(elem));
|
||||
const [oldAtomKeys, setOldAtomKeys] = React.useState<Set<string>>(new Set());
|
||||
let [props, atomKeys] = convertProps(elem, model);
|
||||
@ -208,18 +213,32 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
model.tagUnuseAtoms(elem.waveid, oldAtomKeys);
|
||||
};
|
||||
}, []);
|
||||
return props;
|
||||
}
|
||||
|
||||
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
const props = useVDom(model, elem);
|
||||
return (
|
||||
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
|
||||
);
|
||||
}
|
||||
|
||||
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
const props = useVDom(model, elem);
|
||||
if (elem.tag == WaveNullTag) {
|
||||
return null;
|
||||
}
|
||||
if (elem.tag == WaveTextTag) {
|
||||
return props.text;
|
||||
}
|
||||
const waveTag = WaveTagMap[elem.tag];
|
||||
if (waveTag) {
|
||||
return waveTag({ elem, model });
|
||||
}
|
||||
if (!AllowedTags[elem.tag]) {
|
||||
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
||||
}
|
||||
let childrenComps = convertChildren(elem, model);
|
||||
dlog("children", childrenComps);
|
||||
if (elem.tag == FragmentTag) {
|
||||
return childrenComps;
|
||||
}
|
||||
@ -251,25 +270,14 @@ const testVDom: VDomElem = {
|
||||
],
|
||||
};
|
||||
|
||||
function VDomView({
|
||||
blockId,
|
||||
nodeModel,
|
||||
viewRef,
|
||||
model,
|
||||
}: {
|
||||
blockId: string;
|
||||
nodeModel: NodeModel;
|
||||
viewRef: React.RefObject<HTMLDivElement>;
|
||||
model: VDomModel;
|
||||
}) {
|
||||
let rootNode = useAtomValueSafe(model?.vdomRoot);
|
||||
if (!model || viewRef.current == null || rootNode == null) {
|
||||
function VDomRoot({ model }: { model: VDomModel }) {
|
||||
let rootNode = jotai.useAtomValue(model.vdomRoot);
|
||||
if (model.viewRef.current == null || rootNode == null) {
|
||||
return null;
|
||||
}
|
||||
dlog("render", rootNode);
|
||||
model.viewRef = viewRef;
|
||||
let rtn = convertElemToTag(rootNode, model);
|
||||
return <div className="vdom">{rtn}</div>;
|
||||
}
|
||||
|
||||
export { VDomView };
|
||||
export { VDomRoot };
|
||||
|
28
frontend/app/view/vdom/vdom-view.tsx
Normal file
28
frontend/app/view/vdom/vdom-view.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { VDomRoot } from "@/app/view/term/vdom";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { useRef } from "react";
|
||||
|
||||
function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
|
||||
return new VDomModel(blockId, nodeModel);
|
||||
}
|
||||
|
||||
type VDomViewProps = {
|
||||
model: VDomModel;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function VDomView({ blockId, model }: VDomViewProps) {
|
||||
let viewRef = useRef(null);
|
||||
model.viewRef = viewRef;
|
||||
return (
|
||||
<div className="vdom-view" ref={viewRef}>
|
||||
<VDomRoot model={model} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { makeVDomModel, VDomView };
|
30
frontend/types/gotypes.d.ts
vendored
30
frontend/types/gotypes.d.ts
vendored
@ -7,9 +7,11 @@ declare global {
|
||||
|
||||
// waveobj.Block
|
||||
type Block = WaveObj & {
|
||||
parentoref?: string;
|
||||
blockdef: BlockDef;
|
||||
runtimeopts?: RuntimeOpts;
|
||||
stickers?: StickerType[];
|
||||
subblockids?: string[];
|
||||
};
|
||||
|
||||
// blockcontroller.BlockControllerRuntimeStatus
|
||||
@ -30,7 +32,7 @@ declare global {
|
||||
blockid: string;
|
||||
tabid: string;
|
||||
windowid: string;
|
||||
meta: MetaType;
|
||||
block: Block;
|
||||
};
|
||||
|
||||
// webcmd.BlockInputWSCommand
|
||||
@ -96,6 +98,12 @@ declare global {
|
||||
magnified?: boolean;
|
||||
};
|
||||
|
||||
// wshrpc.CommandCreateSubBlockData
|
||||
type CommandCreateSubBlockData = {
|
||||
parentblockid: string;
|
||||
blockdef: BlockDef;
|
||||
};
|
||||
|
||||
// wshrpc.CommandDeleteBlockData
|
||||
type CommandDeleteBlockData = {
|
||||
blockid: string;
|
||||
@ -167,6 +175,12 @@ declare global {
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// wshrpc.CommandWaitForRouteData
|
||||
type CommandWaitForRouteData = {
|
||||
routeid: string;
|
||||
waitms: number;
|
||||
};
|
||||
|
||||
// wshrpc.CommandWebSelectorData
|
||||
type CommandWebSelectorData = {
|
||||
windowid: string;
|
||||
@ -341,9 +355,12 @@ declare global {
|
||||
"term:localshellpath"?: string;
|
||||
"term:localshellopts"?: string[];
|
||||
"term:scrollback"?: number;
|
||||
"term:vdomblockid"?: string;
|
||||
"vdom:*"?: boolean;
|
||||
"vdom:initialized"?: boolean;
|
||||
"vdom:correlationid"?: string;
|
||||
"vdom:route"?: string;
|
||||
"vdom:persist"?: boolean;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
@ -645,7 +662,7 @@ declare global {
|
||||
type: "createcontext";
|
||||
ts: number;
|
||||
meta?: MetaType;
|
||||
newblock?: boolean;
|
||||
target?: VDomTarget;
|
||||
persist?: boolean;
|
||||
};
|
||||
|
||||
@ -661,7 +678,7 @@ declare global {
|
||||
// vdom.VDomEvent
|
||||
type VDomEvent = {
|
||||
waveid: string;
|
||||
propname: string;
|
||||
eventtype: string;
|
||||
eventdata: any;
|
||||
};
|
||||
|
||||
@ -671,7 +688,6 @@ declare global {
|
||||
ts: number;
|
||||
blockid: string;
|
||||
correlationid?: string;
|
||||
initialize?: boolean;
|
||||
dispose?: boolean;
|
||||
resync?: boolean;
|
||||
rendercontext?: VDomRenderContext;
|
||||
@ -755,6 +771,12 @@ declare global {
|
||||
value: any;
|
||||
};
|
||||
|
||||
// vdom.VDomTarget
|
||||
type VDomTarget = {
|
||||
newblock?: boolean;
|
||||
magnified?: boolean;
|
||||
};
|
||||
|
||||
type WSCommandType = {
|
||||
wscommand: string;
|
||||
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
|
||||
|
@ -36,7 +36,7 @@ const (
|
||||
|
||||
const (
|
||||
BlockFile_Term = "term" // used for main pty output
|
||||
BlockFile_Html = "html" // used for alt html layout
|
||||
BlockFile_VDom = "vdom" // used for alt html layout
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -201,7 +201,7 @@ func (svc *ObjectService) DeleteBlock(uiContext waveobj.UIContext, blockId strin
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
ctx = waveobj.ContextWithUpdates(ctx)
|
||||
err := wcore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId)
|
||||
err := wcore.DeleteBlock(ctx, blockId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deleting block: %w", err)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package vdom
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -72,7 +73,20 @@ func finalizeStack(stack []*VDomElem) *VDomElem {
|
||||
return rtnElem
|
||||
}
|
||||
|
||||
func getAttr(token htmltoken.Token, key string) string {
|
||||
func attrVal(attr htmltoken.Attribute) (any, error) {
|
||||
// if !attr.IsJson {
|
||||
// return attr.Val, nil
|
||||
// }
|
||||
var val any
|
||||
err := json.Unmarshal([]byte(attr.Val), &val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing json attr %q: %v", attr.Key, err)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// returns value, isjson
|
||||
func getAttrString(token htmltoken.Token, key string) string {
|
||||
for _, attr := range token.Attr {
|
||||
if attr.Key == key {
|
||||
return attr.Val
|
||||
@ -81,7 +95,7 @@ func getAttr(token htmltoken.Token, key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func attrToProp(attrVal string, params map[string]any) any {
|
||||
func attrToProp(attrVal string, isJson bool, params map[string]any) any {
|
||||
if strings.HasPrefix(attrVal, Html_ParamPrefix) {
|
||||
bindKey := attrVal[len(Html_ParamPrefix):]
|
||||
bindVal, ok := params[bindKey]
|
||||
@ -120,7 +134,7 @@ func tokenToElem(token htmltoken.Token, params map[string]any) *VDomElem {
|
||||
if attr.Key == "" || attr.Val == "" {
|
||||
continue
|
||||
}
|
||||
propVal := attrToProp(attr.Val, params)
|
||||
propVal := attrToProp(attr.Val, false, params)
|
||||
elem.Props[attr.Key] = propVal
|
||||
}
|
||||
return elem
|
||||
@ -253,7 +267,7 @@ func convertStyleToReactStyles(styleMap map[string]string, params map[string]any
|
||||
}
|
||||
rtn := make(map[string]any)
|
||||
for key, val := range styleMap {
|
||||
rtn[toReactName(key)] = attrToProp(val, params)
|
||||
rtn[toReactName(key)] = attrToProp(val, false, params)
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
@ -330,7 +344,7 @@ outer:
|
||||
elemStack = popElemStack(elemStack)
|
||||
case htmltoken.SelfClosingTagToken:
|
||||
if token.Data == Html_BindParamTagName {
|
||||
keyAttr := getAttr(token, "key")
|
||||
keyAttr := getAttrString(token, "key")
|
||||
dataVal := params[keyAttr]
|
||||
elemList := partToElems(dataVal)
|
||||
for _, elem := range elemList {
|
||||
@ -339,7 +353,7 @@ outer:
|
||||
continue
|
||||
}
|
||||
if token.Data == Html_BindTagName {
|
||||
keyAttr := getAttr(token, "key")
|
||||
keyAttr := getAttrString(token, "key")
|
||||
binding := &VDomBinding{Type: ObjectType_Binding, Bind: keyAttr}
|
||||
appendChildToStack(elemStack, &VDomElem{Tag: WaveTextTag, Props: map[string]any{"text": binding}})
|
||||
continue
|
||||
|
@ -37,7 +37,7 @@ type VDomCreateContext struct {
|
||||
Type string `json:"type" tstype:"\"createcontext\""`
|
||||
Ts int64 `json:"ts"`
|
||||
Meta waveobj.MetaMapType `json:"meta,omitempty"`
|
||||
NewBlock bool `json:"newblock,omitempty"`
|
||||
Target *VDomTarget `json:"target,omitempty"`
|
||||
Persist bool `json:"persist,omitempty"`
|
||||
}
|
||||
|
||||
@ -60,7 +60,6 @@ type VDomFrontendUpdate struct {
|
||||
Ts int64 `json:"ts"`
|
||||
BlockId string `json:"blockid"`
|
||||
CorrelationId string `json:"correlationid,omitempty"`
|
||||
Initialize bool `json:"initialize,omitempty"` // initialize the app
|
||||
Dispose bool `json:"dispose,omitempty"` // the vdom context was closed
|
||||
Resync bool `json:"resync,omitempty"` // resync (send all backend data). useful when the FE reloads
|
||||
RenderContext VDomRenderContext `json:"rendercontext,omitempty"`
|
||||
@ -129,8 +128,8 @@ type VDomRefPosition struct {
|
||||
///// subbordinate protocol types
|
||||
|
||||
type VDomEvent struct {
|
||||
WaveId string `json:"waveid"`
|
||||
PropName string `json:"propname"`
|
||||
WaveId string `json:"waveid"` // empty for global events
|
||||
EventType string `json:"eventtype"`
|
||||
EventData any `json:"eventdata"`
|
||||
}
|
||||
|
||||
@ -179,6 +178,13 @@ type VDomMessage struct {
|
||||
Params []any `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// target -- to support new targets in the future, like toolbars, partial blocks, splits, etc.
|
||||
// default is vdom context inside of a terminal block
|
||||
type VDomTarget struct {
|
||||
NewBlock bool `json:"newblock,omitempty"`
|
||||
Magnified bool `json:"magnified,omitempty"`
|
||||
}
|
||||
|
||||
// matches WaveKeyboardEvent
|
||||
type VDomKeyboardEvent struct {
|
||||
Type string `json:"type"`
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
@ -21,6 +20,7 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Lock *sync.Mutex
|
||||
Root *vdom.RootElem
|
||||
RootElem *vdom.VDomElem
|
||||
RpcClient *wshutil.WshRpc
|
||||
@ -28,8 +28,8 @@ type Client struct {
|
||||
ServerImpl *VDomServerImpl
|
||||
IsDone bool
|
||||
RouteId string
|
||||
VDomContextBlockId string
|
||||
DoneReason string
|
||||
DoneOnce *sync.Once
|
||||
DoneCh chan struct{}
|
||||
Opts vdom.VDomBackendOpts
|
||||
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
|
||||
@ -48,7 +48,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
impl.Client.doShutdown("got dispose from frontend")
|
||||
return nil, nil
|
||||
}
|
||||
if impl.Client.IsDone {
|
||||
if impl.Client.GetIsDone() {
|
||||
return nil, nil
|
||||
}
|
||||
// set atoms
|
||||
@ -62,21 +62,30 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
impl.Client.GlobalEventHandler(impl.Client, event)
|
||||
}
|
||||
} else {
|
||||
impl.Client.Root.Event(event.WaveId, event.PropName, event.EventData)
|
||||
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
|
||||
}
|
||||
}
|
||||
if feUpdate.Initialize || feUpdate.Resync {
|
||||
if feUpdate.Resync {
|
||||
return impl.Client.fullRender()
|
||||
}
|
||||
return impl.Client.incrementalRender()
|
||||
}
|
||||
|
||||
func (c *Client) GetIsDone() bool {
|
||||
c.Lock.Lock()
|
||||
defer c.Lock.Unlock()
|
||||
return c.IsDone
|
||||
}
|
||||
|
||||
func (c *Client) doShutdown(reason string) {
|
||||
c.DoneOnce.Do(func() {
|
||||
c.Lock.Lock()
|
||||
defer c.Lock.Unlock()
|
||||
if c.IsDone {
|
||||
return
|
||||
}
|
||||
c.DoneReason = reason
|
||||
c.IsDone = true
|
||||
close(c.DoneCh)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.VDomEvent)) {
|
||||
@ -85,9 +94,9 @@ func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.V
|
||||
|
||||
func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
|
||||
client := &Client{
|
||||
Lock: &sync.Mutex{},
|
||||
Root: vdom.MakeRoot(),
|
||||
DoneCh: make(chan struct{}),
|
||||
DoneOnce: &sync.Once{},
|
||||
}
|
||||
if opts != nil {
|
||||
client.Opts = *opts
|
||||
@ -126,13 +135,29 @@ func (c *Client) SetRootElem(elem *vdom.VDomElem) {
|
||||
c.RootElem = elem
|
||||
}
|
||||
|
||||
func (c *Client) CreateVDomContext() error {
|
||||
err := wshclient.VDomCreateContextCommand(c.RpcClient, vdom.VDomCreateContext{}, &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
|
||||
func (c *Client) CreateVDomContext(target *vdom.VDomTarget) error {
|
||||
blockORef, err := wshclient.VDomCreateContextCommand(
|
||||
c.RpcClient,
|
||||
vdom.VDomCreateContext{Target: target},
|
||||
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{
|
||||
waveobj.MakeORef("block", c.RpcContext.BlockId).String(),
|
||||
c.VDomContextBlockId = blockORef.OID
|
||||
log.Printf("created vdom context: %v\n", blockORef)
|
||||
gotRoute, err := wshclient.WaitForRouteCommand(c.RpcClient, wshrpc.CommandWaitForRouteData{
|
||||
RouteId: wshutil.MakeFeBlockRouteId(blockORef.OID),
|
||||
WaitMs: 4000,
|
||||
}, &wshrpc.RpcOpts{Timeout: 5000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error waiting for vdom context route: %v", err)
|
||||
}
|
||||
if !gotRoute {
|
||||
return fmt.Errorf("vdom context route could not be established")
|
||||
}
|
||||
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{
|
||||
blockORef.String(),
|
||||
}}, nil)
|
||||
c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
|
||||
c.doShutdown("got blockclose event")
|
||||
@ -140,8 +165,18 @@ func (c *Client) CreateVDomContext() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SendAsyncInitiation() {
|
||||
wshclient.VDomAsyncInitiationCommand(c.RpcClient, vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId), &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
|
||||
func (c *Client) SendAsyncInitiation() error {
|
||||
if c.VDomContextBlockId == "" {
|
||||
return fmt.Errorf("no vdom context block id")
|
||||
}
|
||||
if c.GetIsDone() {
|
||||
return fmt.Errorf("client is done")
|
||||
}
|
||||
return wshclient.VDomAsyncInitiationCommand(
|
||||
c.RpcClient,
|
||||
vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId),
|
||||
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.VDomContextBlockId)},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) SetAtomVals(m map[string]any) {
|
||||
|
@ -79,10 +79,13 @@ const (
|
||||
MetaKey_TermLocalShellPath = "term:localshellpath"
|
||||
MetaKey_TermLocalShellOpts = "term:localshellopts"
|
||||
MetaKey_TermScrollback = "term:scrollback"
|
||||
MetaKey_TermVDomSubBlockId = "term:vdomblockid"
|
||||
|
||||
MetaKey_VDomClear = "vdom:*"
|
||||
MetaKey_VDomInitialized = "vdom:initialized"
|
||||
MetaKey_VDomCorrelationId = "vdom:correlationid"
|
||||
MetaKey_VDomRoute = "vdom:route"
|
||||
MetaKey_VDomPersist = "vdom:persist"
|
||||
|
||||
MetaKey_Count = "count"
|
||||
)
|
||||
|
@ -94,6 +94,14 @@ func ParseORef(orefStr string) (ORef, error) {
|
||||
return ORef{OType: otype, OID: oid}, nil
|
||||
}
|
||||
|
||||
func ParseORefNoErr(orefStr string) *ORef {
|
||||
oref, err := ParseORef(orefStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &oref
|
||||
}
|
||||
|
||||
type WaveObj interface {
|
||||
GetOType() string // should not depend on object state (should work with nil value)
|
||||
}
|
||||
|
@ -252,11 +252,13 @@ type WinSize struct {
|
||||
|
||||
type Block struct {
|
||||
OID string `json:"oid"`
|
||||
ParentORef string `json:"parentoref,omitempty"`
|
||||
Version int `json:"version"`
|
||||
BlockDef *BlockDef `json:"blockdef"`
|
||||
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
||||
Stickers []*StickerType `json:"stickers,omitempty"`
|
||||
Meta MetaMapType `json:"meta"`
|
||||
SubBlockIds []string `json:"subblockids,omitempty"`
|
||||
}
|
||||
|
||||
func (*Block) GetOType() string {
|
||||
|
@ -80,10 +80,13 @@ type MetaTSType struct {
|
||||
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
|
||||
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
|
||||
TermScrollback *int `json:"term:scrollback,omitempty"`
|
||||
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
|
||||
|
||||
VDomClear bool `json:"vdom:*,omitempty"`
|
||||
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
||||
VDomCorrelationId string `json:"vdom:correlationid,omitempty"`
|
||||
VDomRoute string `json:"vdom:route,omitempty"`
|
||||
VDomPersist bool `json:"vdom:persist,omitempty"`
|
||||
|
||||
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
|
||||
}
|
||||
|
@ -26,21 +26,35 @@ import (
|
||||
const DefaultTimeout = 2 * time.Second
|
||||
const DefaultActivateBlockTimeout = 60 * time.Second
|
||||
|
||||
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
||||
err := wstore.DeleteBlock(ctx, tabId, blockId)
|
||||
func DeleteBlock(ctx context.Context, blockId string) error {
|
||||
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting block: %w", err)
|
||||
}
|
||||
if block == nil {
|
||||
return nil
|
||||
}
|
||||
if len(block.SubBlockIds) > 0 {
|
||||
for _, subBlockId := range block.SubBlockIds {
|
||||
err := DeleteBlock(ctx, subBlockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = wstore.DeleteBlock(ctx, blockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting block: %w", err)
|
||||
}
|
||||
go blockcontroller.StopBlockController(blockId)
|
||||
sendBlockCloseEvent(tabId, blockId)
|
||||
sendBlockCloseEvent(blockId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendBlockCloseEvent(tabId string, blockId string) {
|
||||
func sendBlockCloseEvent(blockId string) {
|
||||
waveEvent := wps.WaveEvent{
|
||||
Event: wps.Event_BlockClose,
|
||||
Scopes: []string{
|
||||
waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
||||
waveobj.MakeORef(waveobj.OType_Block, blockId).String(),
|
||||
},
|
||||
Data: blockId,
|
||||
@ -58,7 +72,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||
}
|
||||
// close blocks (sends events + stops block controllers)
|
||||
for _, blockId := range tabData.BlockIds {
|
||||
err := DeleteBlock(ctx, tabId, blockId)
|
||||
err := DeleteBlock(ctx, blockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting block %s: %w", blockId, err)
|
||||
}
|
||||
@ -205,6 +219,20 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
||||
if blockDef == nil {
|
||||
return nil, fmt.Errorf("blockDef is nil")
|
||||
}
|
||||
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
||||
return nil, fmt.Errorf("no view provided for new block")
|
||||
}
|
||||
blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating sub block: %w", err)
|
||||
}
|
||||
return blockData, nil
|
||||
}
|
||||
|
||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
||||
if blockDef == nil {
|
||||
return nil, fmt.Errorf("blockDef is nil")
|
||||
|
@ -86,12 +86,24 @@ func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, o
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "createsubblock", wshserver.CreateSubBlockCommand
|
||||
func CreateSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateSubBlockData, opts *wshrpc.RpcOpts) (waveobj.ORef, error) {
|
||||
resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createsubblock", data, opts)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "deleteblock", wshserver.DeleteBlockCommand
|
||||
func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "deleteblock", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "deletesubblock", wshserver.DeleteSubBlockCommand
|
||||
func DeleteSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "deletesubblock", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "dispose", wshserver.DisposeCommand
|
||||
func DisposeCommand(w *wshutil.WshRpc, data wshrpc.CommandDisposeData, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "dispose", data, opts)
|
||||
@ -274,9 +286,9 @@ func VDomAsyncInitiationCommand(w *wshutil.WshRpc, data vdom.VDomAsyncInitiation
|
||||
}
|
||||
|
||||
// command "vdomcreatecontext", wshserver.VDomCreateContextCommand
|
||||
func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "vdomcreatecontext", data, opts)
|
||||
return err
|
||||
func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, opts *wshrpc.RpcOpts) (*waveobj.ORef, error) {
|
||||
resp, err := sendRpcRequestCallHelper[*waveobj.ORef](w, "vdomcreatecontext", data, opts)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "vdomrender", wshserver.VDomRenderCommand
|
||||
@ -285,6 +297,12 @@ func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *ws
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "waitforroute", wshserver.WaitForRouteCommand
|
||||
func WaitForRouteCommand(w *wshutil.WshRpc, data wshrpc.CommandWaitForRouteData, opts *wshrpc.RpcOpts) (bool, error) {
|
||||
resp, err := sendRpcRequestCallHelper[bool](w, "waitforroute", data, opts)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "webselector", wshserver.WebSelectorCommand
|
||||
func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, opts *wshrpc.RpcOpts) ([]string, error) {
|
||||
resp, err := sendRpcRequestCallHelper[[]string](w, "webselector", data, opts)
|
||||
|
@ -103,7 +103,10 @@ type WshRpcInterface interface {
|
||||
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
|
||||
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
|
||||
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error)
|
||||
CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error)
|
||||
DeleteBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
|
||||
DeleteSubBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
|
||||
WaitForRouteCommand(ctx context.Context, data CommandWaitForRouteData) (bool, error)
|
||||
FileWriteCommand(ctx context.Context, data CommandFileData) error
|
||||
FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
|
||||
EventPublishCommand(ctx context.Context, data wps.WaveEvent) error
|
||||
@ -145,7 +148,7 @@ type WshRpcInterface interface {
|
||||
NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error
|
||||
|
||||
// terminal
|
||||
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) error
|
||||
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) (*waveobj.ORef, error)
|
||||
VDomAsyncInitiationCommand(ctx context.Context, data vdom.VDomAsyncInitiationRequest) error
|
||||
|
||||
// proc
|
||||
@ -248,6 +251,11 @@ type CommandCreateBlockData struct {
|
||||
Magnified bool `json:"magnified,omitempty"`
|
||||
}
|
||||
|
||||
type CommandCreateSubBlockData struct {
|
||||
ParentBlockId string `json:"parentblockid"`
|
||||
BlockDef *waveobj.BlockDef `json:"blockdef"`
|
||||
}
|
||||
|
||||
type CommandBlockSetViewData struct {
|
||||
BlockId string `json:"blockid" wshcontext:"BlockId"`
|
||||
View string `json:"view"`
|
||||
@ -279,6 +287,11 @@ type CommandAppendIJsonData struct {
|
||||
Data ijson.Command `json:"data"`
|
||||
}
|
||||
|
||||
type CommandWaitForRouteData struct {
|
||||
RouteId string `json:"routeid"`
|
||||
WaitMs int `json:"waitms"`
|
||||
}
|
||||
|
||||
type CommandDeleteBlockData struct {
|
||||
BlockId string `json:"blockid" wshcontext:"BlockId"`
|
||||
}
|
||||
@ -405,7 +418,7 @@ type BlockInfoData struct {
|
||||
BlockId string `json:"blockid"`
|
||||
TabId string `json:"tabid"`
|
||||
WindowId string `json:"windowid"`
|
||||
Meta waveobj.MetaMapType `json:"meta"`
|
||||
Block *waveobj.Block `json:"block"`
|
||||
}
|
||||
|
||||
type WaveNotificationOptions struct {
|
||||
|
@ -250,6 +250,16 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
|
||||
return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockRef.OID}, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) CreateSubBlockCommand(ctx context.Context, data wshrpc.CommandCreateSubBlockData) (*waveobj.ORef, error) {
|
||||
parentBlockId := data.ParentBlockId
|
||||
blockData, err := wcore.CreateSubBlock(ctx, parentBlockId, data.BlockDef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating block: %w", err)
|
||||
}
|
||||
blockRef := &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}
|
||||
return blockRef, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error {
|
||||
log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View)
|
||||
ctx = waveobj.ContextWithUpdates(ctx)
|
||||
@ -356,10 +366,10 @@ func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandF
|
||||
|
||||
func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error {
|
||||
tryCreate := true
|
||||
if data.FileName == blockcontroller.BlockFile_Html && tryCreate {
|
||||
if data.FileName == blockcontroller.BlockFile_VDom && tryCreate {
|
||||
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
|
||||
if err != nil && err != fs.ErrExist {
|
||||
return fmt.Errorf("error creating blockfile[html]: %w", err)
|
||||
return fmt.Errorf("error creating blockfile[vdom]: %w", err)
|
||||
}
|
||||
}
|
||||
err := filestore.WFS.AppendIJson(ctx, data.ZoneId, data.FileName, data.Data)
|
||||
@ -379,6 +389,14 @@ func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.Com
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
|
||||
err := wcore.DeleteBlock(ctx, data.BlockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting block: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
|
||||
ctx = waveobj.ContextWithUpdates(ctx)
|
||||
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
|
||||
@ -395,7 +413,7 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
|
||||
if windowId == "" {
|
||||
return fmt.Errorf("no window found for tab")
|
||||
}
|
||||
err = wcore.DeleteBlock(ctx, tabId, data.BlockId)
|
||||
err = wcore.DeleteBlock(ctx, data.BlockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting block: %w", err)
|
||||
}
|
||||
@ -408,6 +426,13 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) WaitForRouteCommand(ctx context.Context, data wshrpc.CommandWaitForRouteData) (bool, error) {
|
||||
waitCtx, cancelFn := context.WithTimeout(ctx, time.Duration(data.WaitMs)*time.Millisecond)
|
||||
defer cancelFn()
|
||||
err := wshutil.DefaultRouter.WaitForRegister(waitCtx, data.RouteId)
|
||||
return err == nil, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) EventRecvCommand(ctx context.Context, data wps.WaveEvent) error {
|
||||
return nil
|
||||
}
|
||||
@ -587,6 +612,6 @@ func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wsh
|
||||
BlockId: blockId,
|
||||
TabId: tabId,
|
||||
WindowId: windowId,
|
||||
Meta: blockData.Meta,
|
||||
Block: blockData,
|
||||
}, nil
|
||||
}
|
||||
|
@ -268,6 +268,9 @@ func (router *WshRouter) WaitForRegister(ctx context.Context, routeId string) er
|
||||
if router.GetRpc(routeId) != nil {
|
||||
return nil
|
||||
}
|
||||
if router.getAnnouncedRoute(routeId) != "" {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
@ -95,6 +95,27 @@ func UpdateTabName(ctx context.Context, tabId, name string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func CreateSubBlock(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
|
||||
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentBlockId)
|
||||
if parentBlock == nil {
|
||||
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
|
||||
}
|
||||
blockId := uuid.NewString()
|
||||
blockData := &waveobj.Block{
|
||||
OID: blockId,
|
||||
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
||||
BlockDef: blockDef,
|
||||
RuntimeOpts: nil,
|
||||
Meta: blockDef.Meta,
|
||||
}
|
||||
DBInsert(tx.Context(), blockData)
|
||||
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
|
||||
DBUpdate(tx.Context(), parentBlock)
|
||||
return blockData, nil
|
||||
})
|
||||
}
|
||||
|
||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
|
||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
|
||||
@ -104,6 +125,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
|
||||
blockId := uuid.NewString()
|
||||
blockData := &waveobj.Block{
|
||||
OID: blockId,
|
||||
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
||||
BlockDef: blockDef,
|
||||
RuntimeOpts: rtOpts,
|
||||
Meta: blockDef.Meta,
|
||||
@ -124,18 +146,34 @@ func findStringInSlice(slice []string, val string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
||||
func DeleteBlock(ctx context.Context, blockId string) error {
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
|
||||
if tab == nil {
|
||||
return fmt.Errorf("tab not found: %q", tabId)
|
||||
block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting block: %w", err)
|
||||
}
|
||||
blockIdx := findStringInSlice(tab.BlockIds, blockId)
|
||||
if blockIdx == -1 {
|
||||
if block == nil {
|
||||
return nil
|
||||
}
|
||||
tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...)
|
||||
if len(block.SubBlockIds) > 0 {
|
||||
return fmt.Errorf("block has subblocks, must delete subblocks first")
|
||||
}
|
||||
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
||||
if parentORef != nil {
|
||||
if parentORef.OType == waveobj.OType_Tab {
|
||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
|
||||
if tab != nil {
|
||||
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
|
||||
DBUpdate(tx.Context(), tab)
|
||||
}
|
||||
} else if parentORef.OType == waveobj.OType_Block {
|
||||
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
|
||||
if parentBlock != nil {
|
||||
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
|
||||
DBUpdate(tx.Context(), parentBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
DBDelete(tx.Context(), waveobj.OType_Block, blockId)
|
||||
return nil
|
||||
})
|
||||
@ -145,23 +183,18 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
||||
// also deletes LayoutState
|
||||
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId)
|
||||
if ws == nil {
|
||||
return fmt.Errorf("workspace not found: %q", workspaceId)
|
||||
}
|
||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
|
||||
if tab == nil {
|
||||
return fmt.Errorf("tab not found: %q", tabId)
|
||||
return nil
|
||||
}
|
||||
if len(tab.BlockIds) != 0 {
|
||||
return fmt.Errorf("tab has blocks, must delete blocks first")
|
||||
}
|
||||
tabIdx := findStringInSlice(ws.TabIds, tabId)
|
||||
if tabIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
|
||||
ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId)
|
||||
if ws != nil {
|
||||
ws.TabIds = utilfn.RemoveElemFromSlice(ws.TabIds, tabId)
|
||||
DBUpdate(tx.Context(), ws)
|
||||
}
|
||||
DBDelete(tx.Context(), waveobj.OType_Tab, tabId)
|
||||
DBDelete(tx.Context(), waveobj.OType_LayoutState, tab.LayoutState)
|
||||
return nil
|
||||
@ -190,6 +223,10 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta waveobj.MetaM
|
||||
|
||||
func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, blockId string) error {
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
block, _ := DBGet[*waveobj.Block](tx.Context(), blockId)
|
||||
if block == nil {
|
||||
return fmt.Errorf("block not found: %q", blockId)
|
||||
}
|
||||
currentTab, _ := DBGet[*waveobj.Tab](tx.Context(), currentTabId)
|
||||
if currentTab == nil {
|
||||
return fmt.Errorf("current tab not found: %q", currentTabId)
|
||||
@ -204,6 +241,8 @@ func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, b
|
||||
}
|
||||
currentTab.BlockIds = utilfn.RemoveElemFromSlice(currentTab.BlockIds, blockId)
|
||||
newTab.BlockIds = append(newTab.BlockIds, blockId)
|
||||
block.ParentORef = waveobj.MakeORef(waveobj.OType_Tab, newTabId).String()
|
||||
DBUpdate(tx.Context(), block)
|
||||
DBUpdate(tx.Context(), currentTab)
|
||||
DBUpdate(tx.Context(), newTab)
|
||||
return nil
|
||||
|
Loading…
Reference in New Issue
Block a user