Create workspace will always create a saved workspace (#1509)

Also moves icon and color definitions to the backend and makes it so the
new workspaces get created with a different icon color for each index.

If an ephemeral window has more than one tab, always create a new window
when switching workspaces

Also fixes workspace accelerators on macOS
This commit is contained in:
Evan Simkowitz 2024-12-12 17:21:09 -08:00 committed by GitHub
parent c34f2da0c0
commit ec4f6c01de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 175 additions and 95 deletions

View File

@ -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

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -42,15 +42,13 @@ async function getWorkspaceMenu(ww?: WaveBrowserWindow): Promise<Electron.MenuIt
console.log("workspaceList:", workspaceList);
const workspaceMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "Create New Workspace",
click: (_, window) => {
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<Electron.MenuIt
{ type: "separator" },
...workspaceList.map<Electron.MenuItemConstructorOptions>((workspace, i) => {
return {
label: `Switch to ${workspace.workspacedata.name}`,
label: `${workspace.workspacedata.name}`,
click: (_, window) => {
((window as WaveBrowserWindow) ?? ww)?.switchWorkspace(workspace.workspacedata.oid);
},

View File

@ -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<CloseTabRtnType> {
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
}
@ -184,7 +184,7 @@ class WorkspaceServiceType {
}
// @returns workspaceId
CreateWorkspace(): Promise<string> {
CreateWorkspace(name: string, icon: string, color: string, applyDefaults: boolean): Promise<string> {
return WOS.callBackendService("workspace", "CreateWorkspace", Array.from(arguments))
}
@ -192,6 +192,18 @@ class WorkspaceServiceType {
DeleteWorkspace(workspaceId: string): Promise<void> {
return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments))
}
// @returns colors
GetColors(): Promise<string[]> {
return WOS.callBackendService("workspace", "GetColors", Array.from(arguments))
}
// @returns icons
GetIcons(): Promise<string[]> {
return WOS.callBackendService("workspace", "GetIcons", Array.from(arguments))
}
// @returns workspace
GetWorkspace(workspaceId: string): Promise<Workspace> {
return WOS.callBackendService("workspace", "GetWorkspace", Array.from(arguments))
}
@ -208,6 +220,11 @@ class WorkspaceServiceType {
UpdateTabIds(workspaceId: string, tabIds: string[], pinnedTabIds: string[]): Promise<void> {
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
}
// @returns object updates
UpdateWorkspace(workspaceId: string, name: string, icon: string, color: string, applyDefaults: boolean): Promise<void> {
return WOS.callBackendService("workspace", "UpdateWorkspace", Array.from(arguments))
}
}
export const WorkspaceService = new WorkspaceServiceType();

View File

@ -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<HTMLInputElement>(null);
const [colors, setColors] = useState<string[]>([]);
const [icons, setIcons] = useState<string[]>([]);
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<HTMLDivElement>((_, 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 (

View File

@ -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",
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}