diff --git a/.github/workflows/build-helper.yml b/.github/workflows/build-helper.yml index ec92aaae64..ada14a17cc 100644 --- a/.github/workflows/build-helper.yml +++ b/.github/workflows/build-helper.yml @@ -69,7 +69,6 @@ jobs: corepack enable yarn install timeout_minutes: 5 - retry_on: error max_attempts: 3 - name: Install Task uses: arduino/setup-task@v2 diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 13cb19d833..eb54fbd052 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -53,7 +53,6 @@ jobs: corepack enable yarn install timeout_minutes: 5 - retry_on: error max_attempts: 3 - name: Install Task uses: arduino/setup-task@v2 diff --git a/emain/emain-window.ts b/emain/emain-window.ts index dd6aab7405..9f3112616b 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -58,7 +58,7 @@ type WindowActionQueueEntry = workspaceId: string; }; -function showCloseConfirmDialog(workspace: Workspace): boolean { +function isNonEmptyUnsavedWorkspace(workspace: Workspace): boolean { return !workspace.name && !workspace.icon && (workspace.tabids?.length > 1 || workspace.pinnedtabids?.length > 1); } @@ -233,7 +233,7 @@ export class WaveBrowserWindow extends BaseWindow { console.log("numWindows > 1", numWindows); const workspace = await WorkspaceService.GetWorkspace(this.workspaceId); console.log("workspace", workspace); - if (showCloseConfirmDialog(workspace)) { + if (isNonEmptyUnsavedWorkspace(workspace)) { console.log("workspace has no name, icon, and multiple tabs", workspace); const choice = dialog.showMessageBoxSync(this, { type: "question", @@ -303,29 +303,12 @@ export class WaveBrowserWindow extends BaseWindow { const workspaceList = await WorkspaceService.ListWorkspaces(); if (!workspaceList?.find((wse) => wse.workspaceid === workspaceId)?.windowid) { const curWorkspace = await WorkspaceService.GetWorkspace(this.workspaceId); - if (showCloseConfirmDialog(curWorkspace)) { - const choice = dialog.showMessageBoxSync(this, { - type: "question", - buttons: ["Cancel", "Open in New Window", "Switch Workspace"], - title: "Confirm", - message: "Window has unsaved tabs, switching workspaces will delete existing tabs.\n\nContinue?", - }); - if (choice === 0) { - console.log("user cancelled switch workspace", this.waveWindowId); - await WorkspaceService.DeleteWorkspace(workspaceId); - return; - } else if (choice === 1) { - console.log("user chose open in new window", this.waveWindowId); - const newWin = await WindowService.CreateWindow(null, workspaceId); - if (!newWin) { - console.log("error creating new window", this.waveWindowId); - } - const newBwin = await createBrowserWindow(newWin, await FileService.GetFullConfig(), { - unamePlatform, - }); - newBwin.show(); - return; - } + if (isNonEmptyUnsavedWorkspace(curWorkspace)) { + console.log( + `existing unsaved workspace ${this.workspaceId} has content, opening workspace ${workspaceId} in new window` + ); + await createWindowForWorkspace(workspaceId); + return; } } await this._queueActionInternal({ op: "switchworkspace", workspaceId }); @@ -606,6 +589,17 @@ export function getAllWaveWindows(): WaveBrowserWindow[] { return Array.from(waveWindowMap.values()); } +export async function createWindowForWorkspace(workspaceId: string) { + const newWin = await WindowService.CreateWindow(null, workspaceId); + if (!newWin) { + console.log("error creating new window", this.waveWindowId); + } + const newBwin = await createBrowserWindow(newWin, await FileService.GetFullConfig(), { + unamePlatform, + }); + newBwin.show(); +} + // note, this does not *show* the window. // to show, await win.readyPromise and then win.show() export async function createBrowserWindow( @@ -668,12 +662,13 @@ ipcMain.on("switch-workspace", (event, workspaceId) => { }); export async function createWorkspace(window: WaveBrowserWindow) { - if (!window) { - return; - } - const newWsId = await WorkspaceService.CreateWorkspace(); + const newWsId = await WorkspaceService.CreateWorkspace("", "", "", true); if (newWsId) { - await window.switchWorkspace(newWsId); + if (window) { + await window.switchWorkspace(newWsId); + } else { + await createWindowForWorkspace(newWsId); + } } } diff --git a/emain/menu.ts b/emain/menu.ts index 99f1757782..0aea5f1258 100644 --- a/emain/menu.ts +++ b/emain/menu.ts @@ -42,15 +42,13 @@ async function getWorkspaceMenu(ww?: WaveBrowserWindow): Promise { - fireAndForget(() => createWorkspace((window as WaveBrowserWindow) ?? ww)); - }, + label: "Create Workspace", + click: (_, window) => fireAndForget(() => createWorkspace((window as WaveBrowserWindow) ?? ww)), }, ]; function getWorkspaceSwitchAccelerator(i: number): string { if (i < 9) { - return unamePlatform == "darwin" ? `Command+Control+${i}` : `Alt+Control+${i + 1}`; + return unamePlatform == "darwin" ? `Command+Control+${i + 1}` : `Alt+Control+${i + 1}`; } } workspaceList?.length && @@ -58,7 +56,7 @@ async function getWorkspaceMenu(ww?: WaveBrowserWindow): Promise((workspace, i) => { return { - label: `Switch to ${workspace.workspacedata.name}`, + label: `${workspace.workspacedata.name}`, click: (_, window) => { ((window as WaveBrowserWindow) ?? ww)?.switchWorkspace(workspace.workspacedata.oid); }, diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 8884917a46..9a9e5f4537 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -173,7 +173,7 @@ class WorkspaceServiceType { return WOS.callBackendService("workspace", "ChangeTabPinning", Array.from(arguments)) } - // @returns object updates + // @returns CloseTabRtn (and object updates) CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise { return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments)) } @@ -184,7 +184,7 @@ class WorkspaceServiceType { } // @returns workspaceId - CreateWorkspace(): Promise { + CreateWorkspace(name: string, icon: string, color: string, applyDefaults: boolean): Promise { return WOS.callBackendService("workspace", "CreateWorkspace", Array.from(arguments)) } @@ -192,6 +192,18 @@ class WorkspaceServiceType { DeleteWorkspace(workspaceId: string): Promise { return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments)) } + + // @returns colors + GetColors(): Promise { + return WOS.callBackendService("workspace", "GetColors", Array.from(arguments)) + } + + // @returns icons + GetIcons(): Promise { + return WOS.callBackendService("workspace", "GetIcons", Array.from(arguments)) + } + + // @returns workspace GetWorkspace(workspaceId: string): Promise { return WOS.callBackendService("workspace", "GetWorkspace", Array.from(arguments)) } @@ -208,6 +220,11 @@ class WorkspaceServiceType { UpdateTabIds(workspaceId: string, tabIds: string[], pinnedTabIds: string[]): Promise { return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments)) } + + // @returns object updates + UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, applyDefaults: boolean): Promise { + return WOS.callBackendService("workspace", "UpdateWorkspace", Array.from(arguments)) + } } export const WorkspaceService = new WorkspaceServiceType(); diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 50080b64b4..9f204b2ae8 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -17,7 +17,7 @@ import clsx from "clsx"; import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { splitAtom } from "jotai/utils"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import { CSSProperties, forwardRef, memo, useCallback, useEffect, useRef } from "react"; +import { CSSProperties, forwardRef, memo, useCallback, useEffect, useRef, useState } from "react"; import WorkspaceSVG from "../asset/workspace.svg"; import { IconButton } from "../element/iconbutton"; import { atoms, getApi } from "../store/global"; @@ -32,33 +32,6 @@ interface ColorSelectorProps { className?: string; } -const colors = [ - "#58C142", // Green (accent) - "#00FFDB", // Teal - "#429DFF", // Blue - "#BF55EC", // Purple - "#FF453A", // Red - "#FF9500", // Orange - "#FFE900", // Yellow -]; - -const icons = [ - "custom@wave-logo-solid", - "triangle", - "star", - "heart", - "bolt", - "solid@cloud", - "moon", - "layer-group", - "rocket", - "flask", - "paperclip", - "chart-line", - "graduation-cap", - "mug-hot", -]; - const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => { const handleColorClick = (color: string) => { onSelect(color); @@ -129,6 +102,18 @@ const WorkspaceEditor = memo( }: WorkspaceEditorProps) => { const inputRef = useRef(null); + const [colors, setColors] = useState([]); + const [icons, setIcons] = useState([]); + + useEffect(() => { + fireAndForget(async () => { + const colors = await WorkspaceService.GetColors(); + const icons = await WorkspaceService.GetIcons(); + setColors(colors); + setIcons(icons); + }); + }, []); + useEffect(() => { if (focusInput && inputRef.current) { inputRef.current.focus(); @@ -210,20 +195,11 @@ const WorkspaceSwitcher = forwardRef((_, ref) => { ); const saveWorkspace = () => { - setObjectValue( - { - ...activeWorkspace, - name: `New Workspace (${activeWorkspace.oid.slice(0, 5)})`, - icon: icons[0], - color: colors[0], - }, - undefined, - true - ); - setTimeout(() => { - fireAndForget(updateWorkspaceList); - }, 10); - setEditingWorkspace(activeWorkspace.oid); + fireAndForget(async () => { + await WorkspaceService.UpdateWorkspace(activeWorkspace.oid, "", "", "", true); + await updateWorkspaceList(); + setEditingWorkspace(activeWorkspace.oid); + }); }; return ( diff --git a/pkg/service/workspaceservice/workspaceservice.go b/pkg/service/workspaceservice/workspaceservice.go index ef5081f2fd..b48bafcc8c 100644 --- a/pkg/service/workspaceservice/workspaceservice.go +++ b/pkg/service/workspaceservice/workspaceservice.go @@ -24,21 +24,44 @@ type WorkspaceService struct{} func (svc *WorkspaceService) CreateWorkspace_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "name", "icon", "color", "applyDefaults"}, ReturnDesc: "workspaceId", } } -func (svc *WorkspaceService) CreateWorkspace(ctx context.Context) (string, error) { - newWS, err := wcore.CreateWorkspace(ctx, "", "", "", false) +func (svc *WorkspaceService) CreateWorkspace(ctx context.Context, name string, icon string, color string, applyDefaults bool) (string, error) { + newWS, err := wcore.CreateWorkspace(ctx, name, icon, color, applyDefaults, false) if err != nil { return "", fmt.Errorf("error creating workspace: %w", err) } return newWS.OID, nil } +func (svc *WorkspaceService) UpdateWorkspace_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "workspaceId", "name", "icon", "color", "applyDefaults"}, + } +} + +func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (waveobj.UpdatesRtnType, error) { + ctx = waveobj.ContextWithUpdates(ctx) + _, err := wcore.UpdateWorkspace(ctx, workspaceId, name, icon, color, applyDefaults) + if err != nil { + return nil, fmt.Errorf("error updating workspace: %w", err) + } + + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + return updates, nil +} + func (svc *WorkspaceService) GetWorkspace_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ - ArgNames: []string{"workspaceId"}, + ArgNames: []string{"workspaceId"}, + ReturnDesc: "workspace", } } @@ -77,7 +100,7 @@ func (svc *WorkspaceService) DeleteWorkspace(workspaceId string) (waveobj.Update return updates, nil } -func (svg *WorkspaceService) ListWorkspaces() (waveobj.WorkspaceList, error) { +func (svc *WorkspaceService) ListWorkspaces() (waveobj.WorkspaceList, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() return wcore.ListWorkspaces(ctx) @@ -90,6 +113,26 @@ func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta { } } +func (svc *WorkspaceService) GetColors_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ReturnDesc: "colors", + } +} + +func (svc *WorkspaceService) GetColors() []string { + return wcore.WorkspaceColors[:] +} + +func (svc *WorkspaceService) GetIcons_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ReturnDesc: "icons", + } +} + +func (svc *WorkspaceService) GetIcons() []string { + return wcore.WorkspaceIcons[:] +} + func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool, pinned bool) (string, waveobj.UpdatesRtnType, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() @@ -188,7 +231,8 @@ type CloseTabRtnType struct { func (svc *WorkspaceService) CloseTab_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ - ArgNames: []string{"ctx", "workspaceId", "tabId", "fromElectron"}, + ArgNames: []string{"ctx", "workspaceId", "tabId", "fromElectron"}, + ReturnDesc: "CloseTabRtn", } } diff --git a/pkg/wcore/wcore.go b/pkg/wcore/wcore.go index f765901590..780344135b 100644 --- a/pkg/wcore/wcore.go +++ b/pkg/wcore/wcore.go @@ -54,7 +54,7 @@ func EnsureInitialData() error { return nil } log.Println("client has no windows, creating starter workspace") - starterWs, err := CreateWorkspace(ctx, "Starter workspace", "custom@wave-logo-solid", "#58C142", true) + starterWs, err := CreateWorkspace(ctx, "Starter workspace", "custom@wave-logo-solid", "#58C142", false, true) if err != nil { return fmt.Errorf("error creating starter workspace: %w", err) } diff --git a/pkg/wcore/window.go b/pkg/wcore/window.go index 1eda94d866..53fd823c11 100644 --- a/pkg/wcore/window.go +++ b/pkg/wcore/window.go @@ -78,7 +78,7 @@ func CreateWindow(ctx context.Context, winSize *waveobj.WinSize, workspaceId str log.Printf("CreateWindow %v %v\n", winSize, workspaceId) var ws *waveobj.Workspace if workspaceId == "" { - ws1, err := CreateWorkspace(ctx, "", "", "", false) + ws1, err := CreateWorkspace(ctx, "", "", "", false, false) if err != nil { return nil, fmt.Errorf("error creating workspace: %w", err) } diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index ec7d19c4c2..c0b2da3249 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -20,14 +20,41 @@ import ( "github.com/wavetermdev/waveterm/pkg/wstore" ) -func CreateWorkspace(ctx context.Context, name string, icon string, color string, isInitialLaunch bool) (*waveobj.Workspace, error) { +var WorkspaceColors = [...]string{ + "#58C142", // Green (accent) + "#00FFDB", // Teal + "#429DFF", // Blue + "#BF55EC", // Purple + "#FF453A", // Red + "#FF9500", // Orange + "#FFE900", // Yellow +} + +var WorkspaceIcons = [...]string{ + "custom@wave-logo-solid", + "triangle", + "star", + "heart", + "bolt", + "solid@cloud", + "moon", + "layer-group", + "rocket", + "flask", + "paperclip", + "chart-line", + "graduation-cap", + "mug-hot", +} + +func CreateWorkspace(ctx context.Context, name string, icon string, color string, applyDefaults bool, isInitialLaunch bool) (*waveobj.Workspace, error) { ws := &waveobj.Workspace{ OID: uuid.NewString(), TabIds: []string{}, PinnedTabIds: []string{}, - Name: name, - Icon: icon, - Color: color, + Name: "", + Icon: "", + Color: "", } err := wstore.DBInsert(ctx, ws) if err != nil { @@ -41,10 +68,35 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_WorkspaceUpdate}) - ws, err = GetWorkspace(ctx, ws.OID) + return UpdateWorkspace(ctx, ws.OID, name, icon, color, applyDefaults) +} + +func UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (*waveobj.Workspace, error) { + ws, err := GetWorkspace(ctx, workspaceId) if err != nil { - return nil, fmt.Errorf("error getting updated workspace: %w", err) + return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + if name != "" { + ws.Name = name + } else if applyDefaults && ws.Name == "" { + ws.Name = fmt.Sprintf("New Workspace (%s)", ws.OID[0:5]) } + if icon != "" { + ws.Icon = icon + } else if applyDefaults && ws.Icon == "" { + ws.Icon = WorkspaceIcons[0] + } + if color != "" { + ws.Color = color + } else if applyDefaults && ws.Color == "" { + wsList, err := ListWorkspaces(ctx) + if err != nil { + log.Printf("error listing workspaces: %v", err) + wsList = waveobj.WorkspaceList{} + } + ws.Color = WorkspaceColors[len(wsList)%len(WorkspaceColors)] + } + wstore.DBUpdate(ctx, ws) return ws, nil }