multi-window support (#62)

This commit is contained in:
Mike Sawka 2024-06-19 19:10:53 -07:00 committed by GitHub
parent 5c6cfbc112
commit fb668fd4e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 423 additions and 126 deletions

View File

@ -21,12 +21,14 @@ const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8190";
const ProdServerEndpoint = "http://127.0.0.1:1719";
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string };
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
let waveSrvReadyResolve = (value: boolean) => {};
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
waveSrvReadyResolve = resolve;
});
let globalIsQuitting = false;
let globalIsStarting = true;
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
@ -74,6 +76,11 @@ function getWaveSrvCwd(): string {
return getWaveHomeDir();
}
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
const windowId = event.sender.id;
return electron.BrowserWindow.fromId(windowId);
}
function runWaveSrv(): Promise<boolean> {
let pResolve: (value: boolean) => void;
let pReject: (reason?: any) => void;
@ -99,6 +106,9 @@ function runWaveSrv(): Promise<boolean> {
env: envCopy,
});
proc.on("exit", (e) => {
if (globalIsQuitting) {
return;
}
console.log("wavesrv exited, shutting down");
electronApp.quit();
});
@ -189,31 +199,21 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
console.log("frame navigation canceled");
}
function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
const primaryDisplay = electron.screen.getPrimaryDisplay();
let winHeight = waveWindow.winsize.height;
let winWidth = waveWindow.winsize.width;
if (winHeight > primaryDisplay.workAreaSize.height) {
winHeight = primaryDisplay.workAreaSize.height;
}
if (winWidth > primaryDisplay.workAreaSize.width) {
winWidth = primaryDisplay.workAreaSize.width;
}
let winX = waveWindow.pos.x;
let winY = waveWindow.pos.y;
if (winX + winWidth > primaryDisplay.workAreaSize.width) {
winX = Math.floor((primaryDisplay.workAreaSize.width - winWidth) / 2);
}
if (winY + winHeight > primaryDisplay.workAreaSize.height) {
winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2);
}
function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
let winBounds = {
x: waveWindow.pos.x,
y: waveWindow.pos.y,
width: waveWindow.winsize.width,
height: waveWindow.winsize.height,
};
winBounds = ensureBoundsAreVisible(winBounds);
const bwin = new electron.BrowserWindow({
x: winX,
y: winY,
titleBarStyle: "hiddenInset",
width: winWidth,
height: winHeight,
minWidth: 500,
x: winBounds.x,
y: winBounds.y,
width: winBounds.width,
height: winBounds.height,
minWidth: 400,
minHeight: 300,
icon:
unamePlatform == "linux"
@ -227,10 +227,11 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
backgroundColor: "#000000",
});
(bwin as any).waveWindowId = waveWindow.oid;
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
win.once("ready-to-show", () => {
win.show();
let readyResolve: (value: void) => void;
(bwin as any).readyPromise = new Promise((resolve, _) => {
readyResolve = resolve;
});
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
// const indexHtml = isDev ? "index-dev.html" : "index.html";
let usp = new URLSearchParams();
usp.set("clientid", client.oid);
@ -243,7 +244,9 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
console.log("running as file");
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
}
win.once("ready-to-show", () => {
readyResolve();
});
win.webContents.on("will-navigate", shNavHandler);
win.webContents.on("will-frame-navigate", shFrameNavHandler);
win.on(
@ -254,6 +257,33 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
"move",
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
);
win.on("focus", () => {
if (globalIsStarting) {
return;
}
console.log("focus", waveWindow.oid);
services.ClientService.FocusWindow(waveWindow.oid);
});
win.on("close", (e) => {
if (globalIsQuitting) {
return;
}
const choice = electron.dialog.showMessageBoxSync(win, {
type: "question",
buttons: ["Cancel", "Yes"],
title: "Confirm",
message: "Are you sure you want to close this window (all tabs and blocks will be deleted)?",
});
if (choice === 0) {
e.preventDefault();
}
});
win.on("closed", () => {
if (globalIsQuitting) {
return;
}
services.WindowService.CloseWindow(waveWindow.oid);
});
win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed");
});
@ -268,6 +298,86 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
return win;
}
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
const displays = electron.screen.getAllDisplays();
// Helper function to check if a point is inside any display
function isPointInDisplay(x, y) {
for (let display of displays) {
const { x: dx, y: dy, width, height } = display.bounds;
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
return true;
}
}
return false;
}
// Check all corners of the window
const topLeft = isPointInDisplay(bounds.x, bounds.y);
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
return topLeft && topRight && bottomLeft && bottomRight;
}
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
const displays = electron.screen.getAllDisplays();
let maxArea = 0;
let bestDisplay = null;
for (let display of displays) {
const { x, y, width, height } = display.bounds;
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
const overlapArea = overlapX * overlapY;
if (overlapArea > maxArea) {
maxArea = overlapArea;
bestDisplay = display;
}
}
return bestDisplay;
}
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
let { x, y, width, height } = bounds;
// Adjust width and height to fit within the display's work area
width = Math.min(width, dWidth);
height = Math.min(height, dHeight);
// Adjust x to ensure the window fits within the display
if (x < dx) {
x = dx;
} else if (x + width > dx + dWidth) {
x = dx + dWidth - width;
}
// Adjust y to ensure the window fits within the display
if (y < dy) {
y = dy;
} else if (y + height > dy + dHeight) {
y = dy + dHeight - height;
}
return { x, y, width, height };
}
function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
if (!isWindowFullyVisible(bounds)) {
let targetDisplay = findDisplayWithMostArea(bounds);
if (!targetDisplay) {
targetDisplay = electron.screen.getPrimaryDisplay();
}
return adjustBoundsToFitDisplay(bounds, targetDisplay);
}
return bounds;
}
electron.ipcMain.on("isDev", (event) => {
event.returnValue = isDev;
});
@ -287,7 +397,13 @@ electron.ipcMain.on("getCursorPoint", (event) => {
event.returnValue = retVal;
});
electron.ipcMain.on("openNewWindow", (event) => {});
async function createNewWaveWindow() {
let clientData = await services.ClientService.GetClientData();
const newWindow = await services.ClientService.MakeWindow();
createBrowserWindow(clientData, newWindow);
}
electron.ipcMain.on("openNewWindow", createNewWaveWindow);
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
if (opts == null) {
@ -337,7 +453,45 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
return electron.Menu.buildFromTemplate(menuItems);
}
(async () => {
function makeAppMenu() {
let fileMenu: Electron.MenuItemConstructorOptions[] = [];
fileMenu.push({
label: "New Window",
click: createNewWaveWindow,
});
fileMenu.push({
label: "Close Window",
click: () => {
electron.BrowserWindow.getFocusedWindow()?.close();
},
});
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
{
role: "appMenu",
},
{
role: "fileMenu",
submenu: fileMenu,
},
{
role: "editMenu",
},
{
role: "viewMenu",
},
{
role: "windowMenu",
},
];
const menu = electron.Menu.buildFromTemplate(menuTemplate);
electron.Menu.setApplicationMenu(menu);
}
electron.app.on("before-quit", () => {
globalIsQuitting = true;
});
async function appMain() {
const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock();
if (!instanceLock) {
@ -349,6 +503,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
if (!fs.existsSync(waveHomeDir)) {
fs.mkdirSync(waveHomeDir);
}
makeAppMenu();
try {
await runWaveSrv();
} catch (e) {
@ -358,17 +513,30 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
console.log("get client data");
let clientData = (await services.ClientService.GetClientData().catch((e) => console.log(e))) as Client;
let clientData = await services.ClientService.GetClientData();
console.log("client data ready");
let windowData: WaveWindow = (await services.ObjectService.GetObject(
"window:" + clientData.mainwindowid
)) as WaveWindow;
await electronApp.whenReady();
createWindow(clientData, windowData);
let wins: WaveBrowserWindow[] = [];
for (let windowId of clientData.windowids.slice().reverse()) {
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
const win = createBrowserWindow(clientData, windowData);
wins.push(win);
}
for (let win of wins) {
await win.readyPromise;
console.log("show", win.waveWindowId);
win.show();
}
globalIsStarting = false;
electronApp.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
createWindow(clientData, windowData);
createNewWaveWindow();
}
});
})();
}
appMain().catch((e) => {
console.log("appMain error", e);
electronApp.quit();
});

View File

@ -60,6 +60,10 @@
&.block-preview {
background-color: var(--main-bg-color);
.block-frame-tech-close {
display: none;
}
}
&.block-focused {
@ -68,10 +72,6 @@
.block-frame-tech-header {
color: var(--main-text-color);
}
.block-frame-tech-close {
display: none;
}
}
.block-frame-tech-header {

View File

@ -21,18 +21,24 @@ export const BlockService = new BlockServiceType()
// clientservice.ClientService (client)
class ClientServiceType {
FocusWindow(arg2: string): Promise<void> {
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
}
GetClientData(): Promise<Client> {
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
}
GetTab(arg1: string): Promise<Tab> {
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
}
GetWindow(arg1: string): Promise<Window> {
GetWindow(arg1: string): Promise<WaveWindow> {
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
}
GetWorkspace(arg1: string): Promise<Workspace> {
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
}
MakeWindow(): Promise<WaveWindow> {
return WOS.callBackendService("client", "MakeWindow", Array.from(arguments))
}
}
export const ClientService = new ClientServiceType()
@ -59,11 +65,6 @@ class ObjectServiceType {
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
}
// @returns object updates
CloseTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
}
// @returns blockId (and object updates)
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
@ -109,6 +110,14 @@ export const ObjectService = new ObjectServiceType()
// windowservice.WindowService (window)
class WindowServiceType {
// @returns object updates
CloseTab(arg3: string): Promise<void> {
return WOS.callBackendService("window", "CloseTab", Array.from(arguments))
}
CloseWindow(arg2: string): Promise<void> {
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
}
// @returns object updates
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))

View File

@ -313,7 +313,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
};
const handleCloseTab = (tabId: string) => {
services.ObjectService.CloseTab(tabId);
services.WindowService.CloseTab(tabId);
deleteLayoutStateAtomForTab(tabId);
};

View File

@ -31,7 +31,7 @@ declare global {
type BlockCommand = {
command: string;
} & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
} & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand );
// wstore.BlockDef
type BlockDef = {
@ -84,6 +84,7 @@ declare global {
// wstore.Client
type Client = WaveObj & {
mainwindowid: string;
windowids: string[];
meta: MetaType;
};
@ -253,6 +254,18 @@ declare global {
obj?: WaveObj;
};
// wstore.Window
type WaveWindow = WaveObj & {
workspaceid: string;
activetabid: string;
activeblockid?: string;
activeblockmap: {[key: string]: string};
pos: Point;
winsize: WinSize;
lastfocusts: number;
meta: MetaType;
};
// service.WebCallType
type WebCallType = {
service: string;
@ -275,18 +288,6 @@ declare global {
height: number;
};
// wstore.Window
type WaveWindow = WaveObj & {
workspaceid: string;
activetabid: string;
activeblockid?: string;
activeblockmap: {[key: string]: string};
pos: Point;
winsize: WinSize;
lastfocusts: number;
meta: MetaType;
};
// wstore.Workspace
type Workspace = WaveObj & {
name: string;

View File

@ -27,6 +27,8 @@ function matchViewportSize() {
document.body.style.height = window.visualViewport.height + "px";
}
document.title = `The Next Wave (${windowId.substring(0, 8)})`;
matchViewportSize();
document.addEventListener("DOMContentLoaded", async () => {

View File

@ -8,6 +8,7 @@ import (
"fmt"
"time"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
@ -54,3 +55,21 @@ func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) {
}
return window, nil
}
func (cs *ClientService) MakeWindow(ctx context.Context) (*wstore.Window, error) {
return wstore.CreateWindow(ctx)
}
// moves the window to the front of the windowId stack
func (cs *ClientService) FocusWindow(ctx context.Context, windowId string) error {
client, err := cs.GetClientData()
if err != nil {
return err
}
winIdx := utilfn.SliceIdx(client.WindowIds, windowId)
if winIdx == -1 {
return nil
}
client.WindowIds = utilfn.MoveSliceIdxToFront(client.WindowIds, winIdx)
return wstore.DBUpdate(ctx, client)
}

View File

@ -205,41 +205,6 @@ func (svc *ObjectService) CloseTab_Meta() tsgenmeta.MethodMeta {
}
}
func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx)
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
if err != nil {
return nil, fmt.Errorf("error getting window: %w", err)
}
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
if err != nil {
return nil, fmt.Errorf("error getting tab: %w", err)
}
for _, blockId := range tab.BlockIds {
blockcontroller.StopBlockController(blockId)
}
err = wstore.CloseTab(ctx, window.WorkspaceId, tabId)
if err != nil {
return nil, fmt.Errorf("error closing tab: %w", err)
}
if window.ActiveTabId == tabId {
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
if err != nil {
return nil, fmt.Errorf("error getting workspace: %w", err)
}
var newActiveTabId string
if len(ws.TabIds) > 0 {
newActiveTabId = ws.TabIds[0]
} else {
newActiveTabId = ""
}
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
}
return wstore.ContextGetUpdatesRtn(ctx), nil
}
func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{
ArgNames: []string{"uiContext", "oref", "meta"},

View File

@ -5,10 +5,15 @@ package windowservice
import (
"context"
"fmt"
"time"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
const DefaultTimeout = 2 * time.Second
type WindowService struct{}
func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) {
@ -32,3 +37,64 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin
}
return wstore.ContextGetUpdatesRtn(ctx), nil
}
func (svc *WindowService) CloseTab(ctx context.Context, uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
ctx = wstore.ContextWithUpdates(ctx)
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
if err != nil {
return nil, fmt.Errorf("error getting window: %w", err)
}
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
if err != nil {
return nil, fmt.Errorf("error getting tab: %w", err)
}
for _, blockId := range tab.BlockIds {
blockcontroller.StopBlockController(blockId)
}
err = wstore.DeleteTab(ctx, window.WorkspaceId, tabId)
if err != nil {
return nil, fmt.Errorf("error closing tab: %w", err)
}
if window.ActiveTabId == tabId {
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
if err != nil {
return nil, fmt.Errorf("error getting workspace: %w", err)
}
var newActiveTabId string
if len(ws.TabIds) > 0 {
newActiveTabId = ws.TabIds[0]
} else {
newActiveTabId = ""
}
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
}
return wstore.ContextGetUpdatesRtn(ctx), nil
}
func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) error {
ctx = wstore.ContextWithUpdates(ctx)
window, err := wstore.DBMustGet[*wstore.Window](ctx, windowId)
if err != nil {
return fmt.Errorf("error getting window: %w", err)
}
workspace, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
if err != nil {
return fmt.Errorf("error getting workspace: %w", err)
}
for _, tabId := range workspace.TabIds {
uiContext := wstore.UIContext{WindowId: windowId}
_, err := svc.CloseTab(ctx, uiContext, tabId)
if err != nil {
return fmt.Errorf("error closing tab: %w", err)
}
}
err = wstore.DBDelete(ctx, wstore.OType_Workspace, window.WorkspaceId)
if err != nil {
return fmt.Errorf("error deleting workspace: %w", err)
}
err = wstore.DBDelete(ctx, wstore.OType_Window, windowId)
if err != nil {
return fmt.Errorf("error deleting window: %w", err)
}
return nil
}

View File

@ -101,7 +101,11 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [
}
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
case reflect.Struct:
return t.Name(), []reflect.Type{t}
name := t.Name()
if tsRename := tsRenameMap[name]; tsRename != "" {
name = tsRename
}
return name, []reflect.Type{t}
case reflect.Ptr:
return TypeToTSType(t.Elem(), tsTypesMap)
case reflect.Interface:

View File

@ -740,3 +740,25 @@ func DoMapStucture(out any, input any) error {
}
return decoder.Decode(input)
}
func SliceIdx[T comparable](arr []T, elem T) int {
for idx, e := range arr {
if e == elem {
return idx
}
}
return -1
}
func MoveSliceIdxToFront[T any](arr []T, idx int) []T {
// create and return a new slice with idx moved to the front
if idx == 0 || idx >= len(arr) {
// make a copy still
return append([]T(nil), arr...)
}
rtn := make([]T, 0, len(arr))
rtn = append(rtn, arr[idx])
rtn = append(rtn, arr[0:idx]...)
rtn = append(rtn, arr[idx+1:]...)
return rtn
}

View File

@ -259,7 +259,7 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
})
}
func CloseTab(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 {
ws, _ := DBGet[*Workspace](tx.Context(), workspaceId)
if ws == nil {
@ -319,29 +319,11 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]an
})
}
func EnsureInitialData() error {
// does not need to run in a transaction since it is called on startup
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
clientCount, err := DBGetCount[*Client](ctx)
if err != nil {
return fmt.Errorf("error getting client count: %w", err)
}
if clientCount > 0 {
return nil
}
func CreateWindow(ctx context.Context) (*Window, error) {
windowId := uuid.NewString()
workspaceId := uuid.NewString()
tabId := uuid.NewString()
layoutNodeId := uuid.NewString()
client := &Client{
OID: uuid.NewString(),
MainWindowId: windowId,
}
err = DBInsert(ctx, client)
if err != nil {
return fmt.Errorf("error inserting client: %w", err)
}
window := &Window{
OID: windowId,
WorkspaceId: workspaceId,
@ -356,28 +338,28 @@ func EnsureInitialData() error {
Height: 600,
},
}
err = DBInsert(ctx, window)
err := DBInsert(ctx, window)
if err != nil {
return fmt.Errorf("error inserting window: %w", err)
return nil, fmt.Errorf("error inserting window: %w", err)
}
ws := &Workspace{
OID: workspaceId,
Name: "default",
Name: "w" + workspaceId[0:8],
TabIds: []string{tabId},
}
err = DBInsert(ctx, ws)
if err != nil {
return fmt.Errorf("error inserting workspace: %w", err)
return nil, fmt.Errorf("error inserting workspace: %w", err)
}
tab := &Tab{
OID: tabId,
Name: "Tab-1",
Name: "T1",
BlockIds: []string{},
LayoutNode: layoutNodeId,
}
err = DBInsert(ctx, tab)
if err != nil {
return fmt.Errorf("error inserting tab: %w", err)
return nil, fmt.Errorf("error inserting tab: %w", err)
}
layoutNode := &LayoutNode{
@ -385,7 +367,62 @@ func EnsureInitialData() error {
}
err = DBInsert(ctx, layoutNode)
if err != nil {
return fmt.Errorf("error inserting layout node: %w", err)
return nil, fmt.Errorf("error inserting layout node: %w", err)
}
client, err := DBGetSingleton[*Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client: %w", err)
}
client.WindowIds = append(client.WindowIds, windowId)
err = DBUpdate(ctx, client)
if err != nil {
return nil, fmt.Errorf("error updating client: %w", err)
}
return DBMustGet[*Window](ctx, windowId)
}
func CreateClient(ctx context.Context) (*Client, error) {
client := &Client{
OID: uuid.NewString(),
WindowIds: []string{},
}
err := DBInsert(ctx, client)
if err != nil {
return nil, fmt.Errorf("error inserting client: %w", err)
}
return client, nil
}
func EnsureInitialData() error {
// does not need to run in a transaction since it is called on startup
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
client, err := DBGetSingleton[*Client](ctx)
if err == ErrNotFound {
client, err = CreateClient(ctx)
if err != nil {
return fmt.Errorf("error creating client: %w", err)
}
}
if client.MainWindowId != "" {
// convert to windowIds
client.WindowIds = []string{client.MainWindowId}
client.MainWindowId = ""
err = DBUpdate(ctx, client)
if err != nil {
return fmt.Errorf("error updating client: %w", err)
}
client, err = DBGetSingleton[*Client](ctx)
if err != nil {
return fmt.Errorf("error getting client (after main window update): %w", err)
}
}
if len(client.WindowIds) > 0 {
return nil
}
_, err = CreateWindow(ctx)
if err != nil {
return fmt.Errorf("error creating window: %w", err)
}
return nil
}

View File

@ -67,7 +67,10 @@ func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, e
table := tableNameFromOType(otype)
query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table)
var row idDataType
tx.Get(&row, query)
found := tx.Get(&row, query)
if !found {
return nil, ErrNotFound
}
rtn, err := waveobj.FromJson(row.Data)
if err != nil {
return rtn, err

View File

@ -115,7 +115,8 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error {
type Client struct {
OID string `json:"oid"`
Version int `json:"version"`
MainWindowId string `json:"mainwindowid"`
MainWindowId string `json:"mainwindowid"` // deprecated
WindowIds []string `json:"windowids"`
Meta map[string]any `json:"meta"`
}