mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
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:
parent
66d1686e84
commit
72ea58267d
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
112
emain/emain.ts
112
emain/emain.ts
@ -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
31
emain/log.ts
Normal 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 };
|
@ -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 };
|
||||
|
@ -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),
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
);
|
||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"},
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -12,6 +12,7 @@ const (
|
||||
Event_Config = "config"
|
||||
Event_UserInput = "userinput"
|
||||
Event_RouteGone = "route:gone"
|
||||
Event_WorkspaceUpdate = "workspace:update"
|
||||
)
|
||||
|
||||
type WaveEvent struct {
|
||||
|
Loading…
Reference in New Issue
Block a user