Workspace app menu (#1423)

Adds a new app menu for creating a new workspace or switching to an
existing one. This required adding a new WPS event any time a workspace
gets updated, since the Electron app menus are static.

This also fixes a bug where closing a workspace could delete it if it
didn't have both a pinned and an unpinned tab.
This commit is contained in:
Evan Simkowitz 2024-12-06 15:33:00 -08:00 committed by GitHub
parent 66d1686e84
commit 72ea58267d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 301 additions and 154 deletions

View File

@ -1,14 +1,21 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ClientService, FileService, WindowService, WorkspaceService } from "@/app/store/services";
import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";
import { fireAndForget } from "@/util/util";
import { BaseWindow, BaseWindowConstructorOptions, dialog, ipcMain, screen } from "electron";
import path from "path";
import { debounce } from "throttle-debounce";
import { getGlobalIsQuitting, getGlobalIsRelaunching, setWasActive, setWasInFg } from "./emain-activity";
import {
getGlobalIsQuitting,
getGlobalIsRelaunching,
setGlobalIsRelaunching,
setWasActive,
setWasInFg,
} from "./emain-activity";
import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview";
import { delay, ensureBoundsAreVisible } from "./emain-util";
import { log } from "./log";
import { getElectronAppBasePath, unamePlatform } from "./platform";
import { updater } from "./updater";
export type WindowOpts = {
@ -272,6 +279,10 @@ export class WaveBrowserWindow extends BaseWindow {
async switchWorkspace(workspaceId: string) {
console.log("switchWorkspace", workspaceId, this.waveWindowId);
if (workspaceId == this.workspaceId) {
console.log("switchWorkspace already on this workspace", this.waveWindowId);
return;
}
const curWorkspace = await WorkspaceService.GetWorkspace(this.workspaceId);
if (curWorkspace.tabids.length > 1 && (!curWorkspace.name || !curWorkspace.icon)) {
const choice = dialog.showMessageBoxSync(this, {
@ -603,19 +614,100 @@ ipcMain.on("close-tab", async (event, workspaceId, tabId) => {
return null;
});
ipcMain.on("switch-workspace", async (event, workspaceId) => {
const ww = getWaveWindowByWebContentsId(event.sender.id);
console.log("switch-workspace", workspaceId, ww?.waveWindowId);
await ww?.switchWorkspace(workspaceId);
ipcMain.on("switch-workspace", (event, workspaceId) => {
fireAndForget(async () => {
const ww = getWaveWindowByWebContentsId(event.sender.id);
console.log("switch-workspace", workspaceId, ww?.waveWindowId);
await ww?.switchWorkspace(workspaceId);
});
});
ipcMain.on("delete-workspace", async (event, workspaceId) => {
const ww = getWaveWindowByWebContentsId(event.sender.id);
console.log("delete-workspace", workspaceId, ww?.waveWindowId);
await WorkspaceService.DeleteWorkspace(workspaceId);
console.log("delete-workspace done", workspaceId, ww?.waveWindowId);
if (ww?.workspaceId == workspaceId) {
console.log("delete-workspace closing window", workspaceId, ww?.waveWindowId);
ww.destroy();
export async function createWorkspace(window: WaveBrowserWindow) {
if (!window) {
return;
}
const newWsId = await WorkspaceService.CreateWorkspace();
if (newWsId) {
await window.switchWorkspace(newWsId);
}
}
ipcMain.on("create-workspace", (event) => {
fireAndForget(async () => {
const ww = getWaveWindowByWebContentsId(event.sender.id);
console.log("create-workspace", ww?.waveWindowId);
await createWorkspace(ww);
});
});
ipcMain.on("delete-workspace", (event, workspaceId) => {
fireAndForget(async () => {
const ww = getWaveWindowByWebContentsId(event.sender.id);
console.log("delete-workspace", workspaceId, ww?.waveWindowId);
await WorkspaceService.DeleteWorkspace(workspaceId);
console.log("delete-workspace done", workspaceId, ww?.waveWindowId);
if (ww?.workspaceId == workspaceId) {
console.log("delete-workspace closing window", workspaceId, ww?.waveWindowId);
ww.destroy();
}
});
});
export async function createNewWaveWindow() {
log("createNewWaveWindow");
const clientData = await ClientService.GetClientData();
const fullConfig = await FileService.GetFullConfig();
let recreatedWindow = false;
const allWindows = getAllWaveWindows();
if (allWindows.length === 0 && clientData?.windowids?.length >= 1) {
console.log("no windows, but clientData has windowids, recreating first window");
// reopen the first window
const existingWindowId = clientData.windowids[0];
const existingWindowData = (await ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
if (existingWindowData != null) {
const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform });
await win.waveReadyPromise;
win.show();
recreatedWindow = true;
}
}
if (recreatedWindow) {
console.log("recreated window, returning");
return;
}
console.log("creating new window");
const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform });
await newBrowserWindow.waveReadyPromise;
newBrowserWindow.show();
}
export async function relaunchBrowserWindows() {
console.log("relaunchBrowserWindows");
setGlobalIsRelaunching(true);
const windows = getAllWaveWindows();
for (const window of windows) {
console.log("relaunch -- closing window", window.waveWindowId);
window.close();
}
setGlobalIsRelaunching(false);
const clientData = await ClientService.GetClientData();
const fullConfig = await FileService.GetFullConfig();
const wins: WaveBrowserWindow[] = [];
for (const windowId of clientData.windowids.slice().reverse()) {
const windowData: WaveWindow = await WindowService.GetWindow(windowId);
if (windowData == null) {
console.log("relaunch -- window data not found, closing window", windowId);
await WindowService.CloseWindow(windowId, true);
continue;
}
console.log("relaunch -- creating window", windowId, windowData);
const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
wins.push(win);
}
for (const win of wins) {
await win.waveReadyPromise;
console.log("show window", win.waveWindowId);
win.show();
}
}

View File

@ -55,6 +55,16 @@ export class ElectronWshClientType extends WshClient {
}
ww.focus();
}
// async handle_workspaceupdate(rh: RpcResponseHelper) {
// console.log("workspaceupdate");
// fireAndForget(async () => {
// console.log("workspace menu clicked");
// const updatedWorkspaceMenu = await getWorkspaceMenu();
// const workspaceMenu = Menu.getApplicationMenu().getMenuItemById("workspace-menu");
// workspaceMenu.submenu = Menu.buildFromTemplate(updatedWorkspaceMenu);
// });
// }
}
export let ElectronWshClient: ElectronWshClientType;

View File

@ -10,8 +10,6 @@ import * as path from "path";
import { PNG } from "pngjs";
import { sprintf } from "sprintf-js";
import { Readable } from "stream";
import * as util from "util";
import winston from "winston";
import * as services from "../frontend/app/store/services";
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil";
import { getWebServerEndpoint } from "../frontend/util/endpoints";
@ -25,7 +23,6 @@ import {
getGlobalIsRelaunching,
setForceQuit,
setGlobalIsQuitting,
setGlobalIsRelaunching,
setGlobalIsStarting,
setWasActive,
setWasInFg,
@ -35,16 +32,19 @@ import { handleCtrlShiftState } from "./emain-util";
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv";
import {
createBrowserWindow,
createNewWaveWindow,
focusedWaveWindow,
getAllWaveWindows,
getWaveWindowById,
getWaveWindowByWebContentsId,
getWaveWindowByWorkspaceId,
relaunchBrowserWindows,
WaveBrowserWindow,
} from "./emain-window";
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
import { getLaunchSettings } from "./launchsettings";
import { getAppMenu } from "./menu";
import { log } from "./log";
import { instantiateAppMenu, makeAppMenu } from "./menu";
import {
getElectronAppBasePath,
getElectronAppUnpackedBasePath,
@ -65,30 +65,7 @@ electron.nativeTheme.themeSource = "dark";
let webviewFocusId: number = null; // set to the getWebContentsId of the webview that has focus (null if not focused)
let webviewKeys: string[] = []; // the keys to trap when webview has focus
const oldConsoleLog = console.log;
const loggerTransports: winston.transport[] = [
new winston.transports.File({ filename: path.join(waveDataDir, "waveapp.log"), level: "info" }),
];
if (isDev) {
loggerTransports.push(new winston.transports.Console());
}
const loggerConfig = {
level: "info",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
),
transports: loggerTransports,
};
const logger = winston.createLogger(loggerConfig);
function log(...msg: any[]) {
try {
logger.info(util.format(...msg));
} catch (e) {
oldConsoleLog(...msg);
}
}
console.log = log;
console.log(
sprintf(
@ -375,34 +352,6 @@ electron.ipcMain.on("open-native-path", (event, filePath: string) => {
);
});
async function createNewWaveWindow(): Promise<void> {
log("createNewWaveWindow");
const clientData = await services.ClientService.GetClientData();
const fullConfig = await services.FileService.GetFullConfig();
let recreatedWindow = false;
const allWindows = getAllWaveWindows();
if (allWindows.length === 0 && clientData?.windowids?.length >= 1) {
console.log("no windows, but clientData has windowids, recreating first window");
// reopen the first window
const existingWindowId = clientData.windowids[0];
const existingWindowData = (await services.ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
if (existingWindowData != null) {
const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform });
await win.waveReadyPromise;
win.show();
recreatedWindow = true;
}
}
if (recreatedWindow) {
console.log("recreated window, returning");
return;
}
console.log("creating new window");
const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform });
await newBrowserWindow.waveReadyPromise;
newBrowserWindow.show();
}
electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-ready") => {
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
if (tabView == null || tabView.initResolve == null) {
@ -481,10 +430,10 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenu
if (menuDefArr?.length === 0) {
return;
}
const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : instantiateAppMenu();
// const { x, y } = electron.screen.getCursorScreenPoint();
// const windowPos = window.getPosition();
menu.popup();
fireAndForget(async () => {
const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : await instantiateAppMenu();
menu.popup();
});
event.returnValue = true;
});
@ -561,18 +510,6 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
return electron.Menu.buildFromTemplate(menuItems);
}
function instantiateAppMenu(): electron.Menu {
return getAppMenu({
createNewWaveWindow,
relaunchBrowserWindows,
});
}
function makeAppMenu() {
const menu = instantiateAppMenu();
electron.Menu.setApplicationMenu(menu);
}
function hideWindowWithCatch(window: WaveBrowserWindow) {
if (window == null) {
return;
@ -649,37 +586,6 @@ process.on("uncaughtException", (error) => {
electronApp.quit();
});
async function relaunchBrowserWindows(): Promise<void> {
console.log("relaunchBrowserWindows");
setGlobalIsRelaunching(true);
const windows = getAllWaveWindows();
for (const window of windows) {
console.log("relaunch -- closing window", window.waveWindowId);
window.close();
}
setGlobalIsRelaunching(false);
const clientData = await services.ClientService.GetClientData();
const fullConfig = await services.FileService.GetFullConfig();
const wins: WaveBrowserWindow[] = [];
for (const windowId of clientData.windowids.slice().reverse()) {
const windowData: WaveWindow = await services.WindowService.GetWindow(windowId);
if (windowData == null) {
console.log("relaunch -- window data not found, closing window", windowId);
await services.WindowService.CloseWindow(windowId, true);
continue;
}
console.log("relaunch -- creating window", windowId, windowData);
const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
wins.push(win);
}
for (const win of wins) {
await win.waveReadyPromise;
console.log("show window", win.waveWindowId);
win.show();
}
}
async function appMain() {
// Set disableHardwareAcceleration as early as possible, if required.
const launchSettings = getLaunchSettings();
@ -694,7 +600,6 @@ async function appMain() {
electronApp.quit();
return;
}
makeAppMenu();
try {
await runWaveSrv(handleWSEvent);
} catch (e) {
@ -715,6 +620,7 @@ async function appMain() {
} catch (e) {
console.log("error initializing wshrpc", e);
}
makeAppMenu();
await configureAutoUpdater();
setGlobalIsStarting(false);
if (fullConfig?.settings?.["window:maxtabcachesize"] != null) {

31
emain/log.ts Normal file
View File

@ -0,0 +1,31 @@
import path from "path";
import { format } from "util";
import winston from "winston";
import { getWaveDataDir, isDev } from "./platform";
const oldConsoleLog = console.log;
const loggerTransports: winston.transport[] = [
new winston.transports.File({ filename: path.join(getWaveDataDir(), "waveapp.log"), level: "info" }),
];
if (isDev) {
loggerTransports.push(new winston.transports.Console());
}
const loggerConfig = {
level: "info",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
),
transports: loggerTransports,
};
const logger = winston.createLogger(loggerConfig);
function log(...msg: any[]) {
try {
logger.info(format(...msg));
} catch (e) {
oldConsoleLog(...msg);
}
}
export { log };

View File

@ -1,10 +1,19 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import * as electron from "electron";
import { fireAndForget } from "../frontend/util/util";
import { clearTabCache } from "./emain-tabview";
import { focusedWaveWindow, WaveBrowserWindow } from "./emain-window";
import {
createNewWaveWindow,
createWorkspace,
focusedWaveWindow,
relaunchBrowserWindows,
WaveBrowserWindow,
} from "./emain-window";
import { ElectronWshClient } from "./emain-wsh";
import { unamePlatform } from "./platform";
import { updater } from "./updater";
@ -27,7 +36,35 @@ function getWindowWebContents(window: electron.BaseWindow): electron.WebContents
return null;
}
function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
async function getWorkspaceMenu(): Promise<Electron.MenuItemConstructorOptions[]> {
const workspaceList = await RpcApi.WorkspaceListCommand(ElectronWshClient);
console.log("workspaceList:", workspaceList);
const workspaceMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "Create New Workspace",
click: (_, window) => {
const ww = window as WaveBrowserWindow;
fireAndForget(() => createWorkspace(ww));
},
},
];
workspaceList?.length &&
workspaceMenu.push(
{ type: "separator" },
...workspaceList.map<Electron.MenuItemConstructorOptions>((workspace) => {
return {
label: `Switch to ${workspace.workspacedata.name} (${workspace.workspacedata.oid.slice(0, 5)})`,
click: (_, window) => {
const ww = window as WaveBrowserWindow;
ww.switchWorkspace(workspace.workspacedata.oid);
},
};
})
);
return workspaceMenu;
}
async function getAppMenu(callbacks: AppMenuCallbacks): Promise<Electron.Menu> {
const fileMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "New Window",
@ -224,6 +261,9 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
role: "togglefullscreen",
},
];
const workspaceMenu = await getWorkspaceMenu();
const windowMenu: Electron.MenuItemConstructorOptions[] = [
{ role: "minimize", accelerator: "" },
{ role: "zoom" },
@ -249,6 +289,11 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
role: "viewMenu",
submenu: viewMenu,
},
{
label: "Workspace",
id: "workspace-menu",
submenu: workspaceMenu,
},
{
role: "windowMenu",
submenu: windowMenu,
@ -257,4 +302,23 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
return electron.Menu.buildFromTemplate(menuTemplate);
}
export function instantiateAppMenu(): Promise<electron.Menu> {
return getAppMenu({
createNewWaveWindow,
relaunchBrowserWindows,
});
}
export function makeAppMenu() {
fireAndForget(async () => {
const menu = await instantiateAppMenu();
electron.Menu.setApplicationMenu(menu);
});
}
waveEventSubscribe({
eventType: "workspace:update",
handler: makeAppMenu,
});
export { getAppMenu };

View File

@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld("api", {
registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys),
onControlShiftStateUpdate: (callback) =>
ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)),
createWorkspace: () => ipcRenderer.send("create-workspace"),
switchWorkspace: (workspaceId) => ipcRenderer.send("switch-workspace", workspaceId),
deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId),
setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId),

View File

@ -183,6 +183,11 @@ class WorkspaceServiceType {
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
}
// @returns workspaceId
CreateWorkspace(): Promise<string> {
return WOS.callBackendService("workspace", "CreateWorkspace", Array.from(arguments))
}
// @returns object updates
DeleteWorkspace(workspaceId: string): Promise<void> {
return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments))

View File

@ -235,16 +235,23 @@ const WorkspaceSwitcher = () => {
</ExpandableMenu>
</OverlayScrollbarsComponent>
{!isActiveWorkspaceSaved && (
<div className="actions">
<div className="actions">
{isActiveWorkspaceSaved ? (
<ExpandableMenuItem onClick={() => getApi().createWorkspace()}>
<ExpandableMenuItemLeftElement>
<i className="fa-sharp fa-solid fa-plus"></i>
</ExpandableMenuItemLeftElement>
<div className="content">Create new workspace</div>
</ExpandableMenuItem>
) : (
<ExpandableMenuItem onClick={() => saveWorkspace()}>
<ExpandableMenuItemLeftElement>
<i className="fa-sharp fa-solid fa-floppy-disk"></i>
</ExpandableMenuItemLeftElement>
<div className="content">Save workspace</div>
</ExpandableMenuItem>
</div>
)}
)}
</div>
</PopoverContent>
</Popover>
);

View File

@ -89,6 +89,7 @@ declare global {
setWebviewFocus: (focusedId: number) => void; // focusedId si the getWebContentsId of the webview
registerGlobalWebviewKeys: (keys: string[]) => void;
onControlShiftStateUpdate: (callback: (state: boolean) => void) => void;
createWorkspace: () => void;
switchWorkspace: (workspaceId: string) => void;
deleteWorkspace: (workspaceId: string) => void;
setActiveTab: (tabId: string) => void;

View File

@ -12,6 +12,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wcore"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
@ -174,6 +175,10 @@ func (svc *ObjectService) UpdateObject(uiContext waveobj.UIContext, waveObj wave
if err != nil {
return nil, fmt.Errorf("error updating object: %w", err)
}
if (waveObj.GetOType() == waveobj.OType_Workspace) && (waveObj.(*waveobj.Workspace).Name != "") {
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate})
}
if returnUpdates {
return waveobj.ContextGetUpdatesRtn(ctx), nil
}

View File

@ -50,23 +50,14 @@ func (svc *WindowService) CreateWindow(ctx context.Context, winSize *waveobj.Win
if err != nil {
return nil, fmt.Errorf("error creating window: %w", err)
}
ws, err := wcore.GetWorkspace(ctx, window.WorkspaceId)
if err != nil {
return nil, fmt.Errorf("error getting workspace: %w", err)
return window, fmt.Errorf("error getting workspace: %w", err)
}
if len(ws.TabIds) == 0 {
_, err = wcore.CreateTab(ctx, ws.OID, "", true, false)
if err != nil {
return window, fmt.Errorf("error creating tab: %w", err)
}
ws, err = wcore.GetWorkspace(ctx, window.WorkspaceId)
if err != nil {
return nil, fmt.Errorf("error getting updated workspace: %w", err)
}
err = wlayout.BootstrapNewWorkspaceLayout(ctx, ws)
if err != nil {
return window, fmt.Errorf("error bootstrapping new workspace layout: %w", err)
}
err = wlayout.BootstrapNewWorkspaceLayout(ctx, ws)
if err != nil {
return window, fmt.Errorf("error bootstrapping new workspace layout: %w", err)
}
return window, nil
}

View File

@ -20,6 +20,25 @@ const DefaultTimeout = 2 * time.Second
type WorkspaceService struct{}
func (svc *WorkspaceService) CreateWorkspace_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{
ReturnDesc: "workspaceId",
}
}
func (svc *WorkspaceService) CreateWorkspace(ctx context.Context) (string, error) {
newWS, err := wcore.CreateWorkspace(ctx, "", "", "")
if err != nil {
return "", fmt.Errorf("error creating workspace: %w", err)
}
err = wlayout.BootstrapNewWorkspaceLayout(ctx, newWS)
if err != nil {
return newWS.OID, fmt.Errorf("error bootstrapping new workspace layout: %w", err)
}
return newWS.OID, nil
}
func (svc *WorkspaceService) GetWorkspace_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{
ArgNames: []string{"workspaceId"},

View File

@ -152,21 +152,18 @@ func DeleteBlock(ctx context.Context, blockId string, recursive bool) error {
log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
if parentORef.OType == waveobj.OType_Tab {
if parentBlockCount == 0 && recursive {
// 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, true)
if err != nil {
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
}
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
if recursive && parentORef.OType == waveobj.OType_Tab && 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, true)
if err != nil {
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
}
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
}
go blockcontroller.StopBlockController(blockId)
sendBlockCloseEvent(blockId)

View File

@ -11,6 +11,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
@ -25,7 +26,21 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string
Icon: icon,
Color: color,
}
wstore.DBInsert(ctx, ws)
err := wstore.DBInsert(ctx, ws)
if err != nil {
return nil, fmt.Errorf("error inserting workspace: %w", err)
}
_, err = CreateTab(ctx, ws.OID, "", true, false)
if err != nil {
return nil, fmt.Errorf("error creating tab: %w", err)
}
ws, err = GetWorkspace(ctx, ws.OID)
if err != nil {
return nil, fmt.Errorf("error getting updated workspace: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate})
return ws, nil
}
@ -38,7 +53,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
if err != nil {
return false, fmt.Errorf("error getting workspace: %w", err)
}
if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 && len(workspace.PinnedTabIds) > 0 {
if workspace.Name != "" && workspace.Icon != "" && !force && (len(workspace.TabIds) > 0 || len(workspace.PinnedTabIds) > 0) {
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
return false, nil
}
@ -56,6 +71,8 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
return false, fmt.Errorf("error deleting workspace: %w", err)
}
log.Printf("deleted workspace %s\n", workspaceId)
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate})
return true, nil
}
@ -163,7 +180,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
// if no tabs remaining, close window
if newActiveTabId == "" && recursive {
if recursive && newActiveTabId == "" {
log.Printf("no tabs remaining in workspace %s, closing window\n", workspaceId)
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
if err != nil {

View File

@ -12,6 +12,7 @@ const (
Event_Config = "config"
Event_UserInput = "userinput"
Event_RouteGone = "route:gone"
Event_WorkspaceUpdate = "workspace:update"
)
type WaveEvent struct {