Delete a tab when it's out of blocks, cascade to window (#1374)

Updates `DeleteBlock` to close its parent tab if the tab has no more
blocks. This will also cascade to close the workspace if it no longer
has any tabs, same for window.

I had to move some block-related functionality around on the backend.
This commit is contained in:
Evan Simkowitz 2024-12-03 12:38:46 -05:00 committed by GitHub
parent 04c4f0a203
commit 90e31dfa48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 90 deletions

View File

@ -39,6 +39,7 @@ import {
getAllWaveWindows, getAllWaveWindows,
getWaveWindowById, getWaveWindowById,
getWaveWindowByWebContentsId, getWaveWindowByWebContentsId,
getWaveWindowByWorkspaceId,
WaveBrowserWindow, WaveBrowserWindow,
} from "./emain-window"; } from "./emain-window";
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh"; import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
@ -125,6 +126,14 @@ function handleWSEvent(evtMsg: WSEventType) {
if (ww != null) { if (ww != null) {
ww.destroy(); // bypass the "are you sure?" dialog ww.destroy(); // bypass the "are you sure?" dialog
} }
} else if (evtMsg.eventtype == "electron:updateactivetab") {
const activeTabUpdate: { workspaceid: string; newactivetabid: string } = evtMsg.data;
console.log("electron:updateactivetab", activeTabUpdate);
const ww = getWaveWindowByWorkspaceId(activeTabUpdate.workspaceid);
if (ww == null) {
return;
}
await ww.setActiveTab(activeTabUpdate.newactivetabid, false);
} else { } else {
console.log("unhandled electron ws eventtype", evtMsg.eventtype); console.log("unhandled electron ws eventtype", evtMsg.eventtype);
} }

View File

@ -13,9 +13,10 @@ import (
) )
const ( const (
WSEvent_ElectronNewWindow = "electron:newwindow" WSEvent_ElectronNewWindow = "electron:newwindow"
WSEvent_ElectronCloseWindow = "electron:closewindow" WSEvent_ElectronCloseWindow = "electron:closewindow"
WSEvent_Rpc = "rpc" WSEvent_ElectronUpdateActiveTab = "electron:updateactivetab"
WSEvent_Rpc = "rpc"
) )
type WSEventType struct { type WSEventType struct {

View File

@ -175,15 +175,7 @@ func (svc *WorkspaceService) CloseTab(ctx context.Context, workspaceId string, t
} }
rtn := &CloseTabRtnType{} rtn := &CloseTabRtnType{}
if newActiveTabId == "" { if newActiveTabId == "" {
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
if err != nil {
return rtn, nil, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
}
rtn.CloseWindow = true rtn.CloseWindow = true
err = wcore.CloseWindow(ctx, windowId, fromElectron)
if err != nil {
return rtn, nil, err
}
} else { } else {
rtn.NewActiveTabId = newActiveTabId rtn.NewActiveTabId = newActiveTabId
} }

View File

@ -159,6 +159,11 @@ type WorkspaceListEntry struct {
type WorkspaceList []*WorkspaceListEntry type WorkspaceList []*WorkspaceListEntry
type ActiveTabUpdate struct {
WorkspaceId string `json:"workspaceid"`
NewActiveTabId string `json:"newactivetabid"`
}
type Workspace struct { type Workspace struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`

View File

@ -3,11 +3,14 @@ package wcore
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"time" "time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "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"
@ -21,13 +24,34 @@ func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.Block
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
return nil, fmt.Errorf("no view provided for new block") return nil, fmt.Errorf("no view provided for new block")
} }
blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef) blockData, err := createSubBlockObj(ctx, blockId, blockDef)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating sub block: %w", err) return nil, fmt.Errorf("error creating sub block: %w", err)
} }
return blockData, nil return blockData, nil
} }
func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
parentBlock, _ := wstore.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,
}
wstore.DBInsert(tx.Context(), blockData)
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
wstore.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) {
if blockDef == nil { if blockDef == nil {
return nil, fmt.Errorf("blockDef is nil") return nil, fmt.Errorf("blockDef is nil")
@ -35,7 +59,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
return nil, fmt.Errorf("no view provided for new block") return nil, fmt.Errorf("no view provided for new block")
} }
blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts) blockData, err := createBlockObj(ctx, tabId, blockDef, rtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return nil, fmt.Errorf("error creating block: %w", err)
} }
@ -54,6 +78,27 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
return blockData, nil return blockData, nil
} }
func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return nil, fmt.Errorf("tab not found: %q", tabId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef,
RuntimeOpts: rtOpts,
Meta: blockDef.Meta,
}
wstore.DBInsert(tx.Context(), blockData)
tab.BlockIds = append(tab.BlockIds, blockId)
wstore.DBUpdate(tx.Context(), tab)
return blockData, nil
})
}
func DeleteBlock(ctx context.Context, blockId string) error { func DeleteBlock(ctx context.Context, blockId string) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil { if err != nil {
@ -70,15 +115,71 @@ func DeleteBlock(ctx context.Context, blockId string) error {
} }
} }
} }
err = wstore.DeleteBlock(ctx, blockId) parentBlockCount, err := deleteBlockObj(ctx, blockId)
if err != nil { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
if parentORef.OType == waveobj.OType_Tab {
if parentBlockCount == 0 {
// if parent tab has no blocks, delete the tab
log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID)
parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID)
if err != nil {
return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err)
}
newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID)
if err != nil {
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
}
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
}
}
go blockcontroller.StopBlockController(blockId) go blockcontroller.StopBlockController(blockId)
sendBlockCloseEvent(blockId) sendBlockCloseEvent(blockId)
return nil return nil
} }
// returns the updated block count for the parent object
func deleteBlockObj(ctx context.Context, blockId string) (int, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {
block, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return -1, fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return -1, fmt.Errorf("block not found: %q", blockId)
}
if len(block.SubBlockIds) > 0 {
return -1, fmt.Errorf("block has subblocks, must delete subblocks first")
}
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
parentBlockCount := -1
if parentORef != nil {
if parentORef.OType == waveobj.OType_Tab {
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
if tab != nil {
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
wstore.DBUpdate(tx.Context(), tab)
parentBlockCount = len(tab.BlockIds)
}
} else if parentORef.OType == waveobj.OType_Block {
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
if parentBlock != nil {
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
wstore.DBUpdate(tx.Context(), parentBlock)
parentBlockCount = len(parentBlock.SubBlockIds)
}
}
}
wstore.DBDelete(tx.Context(), waveobj.OType_Block, blockId)
return parentBlockCount, nil
})
}
func sendBlockCloseEvent(blockId string) { func sendBlockCloseEvent(blockId string) {
waveEvent := wps.WaveEvent{ waveEvent := wps.WaveEvent{
Event: wps.Event_BlockClose, Event: wps.Event_BlockClose,

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/eventbus"
"github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
@ -139,6 +140,17 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, e
wstore.DBUpdate(ctx, ws) wstore.DBUpdate(ctx, ws)
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId) wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState) wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
if newActiveTabId == "" {
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
if err != nil {
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
}
err = CloseWindow(ctx, windowId, false)
if err != nil {
return newActiveTabId, err
}
}
return newActiveTabId, nil return newActiveTabId, nil
} }
@ -158,6 +170,13 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
return nil return nil
} }
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
eventbus.SendEventToElectron(eventbus.WSEventType{
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
Data: &waveobj.ActiveTabUpdate{WorkspaceId: workspaceId, NewActiveTabId: newActiveTabId},
})
}
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error { func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil { if ws == nil {

View File

@ -7,7 +7,6 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
) )
@ -32,81 +31,6 @@ func UpdateTabName(ctx context.Context, tabId, name string) error {
}) })
} }
func CreateSubBlock(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentBlockId)
if parentBlock == nil {
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
BlockDef: blockDef,
RuntimeOpts: nil,
Meta: blockDef.Meta,
}
DBInsert(tx.Context(), blockData)
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
DBUpdate(tx.Context(), parentBlock)
return blockData, nil
})
}
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return nil, fmt.Errorf("tab not found: %q", tabId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef,
RuntimeOpts: rtOpts,
Meta: blockDef.Meta,
}
DBInsert(tx.Context(), blockData)
tab.BlockIds = append(tab.BlockIds, blockId)
DBUpdate(tx.Context(), tab)
return blockData, nil
})
}
func DeleteBlock(ctx context.Context, blockId string) error {
return WithTx(ctx, func(tx *TxWrap) error {
block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return nil
}
if len(block.SubBlockIds) > 0 {
return fmt.Errorf("block has subblocks, must delete subblocks first")
}
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
if parentORef != nil {
if parentORef.OType == waveobj.OType_Tab {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
if tab != nil {
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
DBUpdate(tx.Context(), tab)
}
} else if parentORef.OType == waveobj.OType_Block {
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
if parentBlock != nil {
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
DBUpdate(tx.Context(), parentBlock)
}
}
}
DBDelete(tx.Context(), waveobj.OType_Block, blockId)
return nil
})
}
// must delete all blocks individually first // must delete all blocks individually first
// 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 {