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