From 78f3cd04722eff61986b1c4011c9da06f87acc48 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 16 Dec 2024 21:54:13 -0800 Subject: [PATCH] Synchronize workspace edits across windows, close window when workspace is deleted (#1540) --- frontend/app/tab/workspaceeditor.scss | 70 ++++++++ frontend/app/tab/workspaceeditor.tsx | 125 +++++++++++++++ frontend/app/tab/workspaceswitcher.scss | 115 +++----------- frontend/app/tab/workspaceswitcher.tsx | 149 +++--------------- .../workspaceservice/workspaceservice.go | 3 + pkg/wcore/window.go | 19 +-- pkg/wcore/workspace.go | 7 + 7 files changed, 256 insertions(+), 232 deletions(-) create mode 100644 frontend/app/tab/workspaceeditor.scss create mode 100644 frontend/app/tab/workspaceeditor.tsx diff --git a/frontend/app/tab/workspaceeditor.scss b/frontend/app/tab/workspaceeditor.scss new file mode 100644 index 000000000..d850d0a94 --- /dev/null +++ b/frontend/app/tab/workspaceeditor.scss @@ -0,0 +1,70 @@ +.workspace-editor { + width: 100%; + .input { + margin: 5px 0 10px; + } + + .color-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(15px, 15px)); // Ensures each color circle has a fixed 14px size + grid-gap: 18.5px; // Space between items + justify-content: center; + align-items: center; + margin-top: 5px; + padding-bottom: 15px; + border-bottom: 1px solid var(--modal-border-color); + + .color-circle { + width: 15px; + height: 15px; + border-radius: 50%; + cursor: pointer; + position: relative; + + // Border offset outward + &:before { + content: ""; + position: absolute; + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + border-radius: 50%; + border: 1px solid transparent; + } + + &.selected:before { + border-color: var(--main-text-color); // Highlight for the selected circle + } + } + } + + .icon-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16px, 16px)); // Ensures each color circle has a fixed 14px size + grid-column-gap: 17.5px; // Space between items + grid-row-gap: 13px; // Space between items + justify-content: center; + align-items: center; + margin-top: 15px; + + .icon-item { + font-size: 15px; + color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h); + cursor: pointer; + transition: color 0.3s ease; + + &.selected, + &:hover { + color: var(--main-text-color); + } + } + } + + .delete-ws-btn-wrapper { + display: flex; + align-items: center; + justify-content: center; + margin-top: 10px; + } +} diff --git a/frontend/app/tab/workspaceeditor.tsx b/frontend/app/tab/workspaceeditor.tsx new file mode 100644 index 000000000..e2fde28e9 --- /dev/null +++ b/frontend/app/tab/workspaceeditor.tsx @@ -0,0 +1,125 @@ +import { fireAndForget, makeIconClass } from "@/util/util"; +import clsx from "clsx"; +import { memo, useEffect, useRef, useState } from "react"; +import { Button } from "../element/button"; +import { Input } from "../element/input"; +import { WorkspaceService } from "../store/services"; +import "./workspaceeditor.scss"; + +interface ColorSelectorProps { + colors: string[]; + selectedColor?: string; + onSelect: (color: string) => void; + className?: string; +} + +const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => { + const handleColorClick = (color: string) => { + onSelect(color); + }; + + return ( +
+ {colors.map((color) => ( +
handleColorClick(color)} + /> + ))} +
+ ); +}); + +interface IconSelectorProps { + icons: string[]; + selectedIcon?: string; + onSelect: (icon: string) => void; + className?: string; +} + +const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => { + const handleIconClick = (icon: string) => { + onSelect(icon); + }; + + return ( +
+ {icons.map((icon) => { + const iconClass = makeIconClass(icon, true); + return ( + handleIconClick(icon)} + /> + ); + })} +
+ ); +}); + +interface WorkspaceEditorProps { + title: string; + icon: string; + color: string; + focusInput: boolean; + onTitleChange: (newTitle: string) => void; + onColorChange: (newColor: string) => void; + onIconChange: (newIcon: string) => void; + onDeleteWorkspace: () => void; +} +const WorkspaceEditorComponent = ({ + title, + icon, + color, + focusInput, + onTitleChange, + onColorChange, + onIconChange, + onDeleteWorkspace, +}: 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(); + inputRef.current.select(); + } + }, [focusInput]); + + return ( +
+ + + +
+ +
+
+ ); +}; + +export const WorkspaceEditor = memo(WorkspaceEditorComponent) as typeof WorkspaceEditorComponent; diff --git a/frontend/app/tab/workspaceswitcher.scss b/frontend/app/tab/workspaceswitcher.scss index 596622501..c8b3e1fa9 100644 --- a/frontend/app/tab/workspaceswitcher.scss +++ b/frontend/app/tab/workspaceswitcher.scss @@ -26,25 +26,6 @@ } } -.icon-left, -.icon-right { - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; -} - -.divider { - width: 1px; - height: 20px; - background: rgba(255, 255, 255, 0.08); -} - -.scrollable { - max-height: 400px; - width: 100%; -} - .workspace-switcher-content { min-height: auto; display: flex; @@ -55,6 +36,25 @@ border-radius: 8px; box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); + .icon-left, + .icon-right { + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + } + + .divider { + width: 1px; + height: 20px; + background: rgba(255, 255, 255, 0.08); + } + + .scrollable { + max-height: 400px; + width: 100%; + } + .title { font-size: 12px; line-height: 19px; @@ -144,83 +144,6 @@ padding: 0; } - .workspace-editor { - width: 100%; - .input { - margin: 5px 0 10px; - } - - .color-selector { - display: grid; - grid-template-columns: repeat( - auto-fit, - minmax(15px, 15px) - ); // Ensures each color circle has a fixed 14px size - grid-gap: 18.5px; // Space between items - justify-content: center; - align-items: center; - margin-top: 5px; - padding-bottom: 15px; - border-bottom: 1px solid var(--modal-border-color); - - .color-circle { - width: 15px; - height: 15px; - border-radius: 50%; - cursor: pointer; - position: relative; - - // Border offset outward - &:before { - content: ""; - position: absolute; - top: -3px; - left: -3px; - right: -3px; - bottom: -3px; - border-radius: 50%; - border: 1px solid transparent; - } - - &.selected:before { - border-color: var(--main-text-color); // Highlight for the selected circle - } - } - } - - .icon-selector { - display: grid; - grid-template-columns: repeat( - auto-fit, - minmax(16px, 16px) - ); // Ensures each color circle has a fixed 14px size - grid-column-gap: 17.5px; // Space between items - grid-row-gap: 13px; // Space between items - justify-content: center; - align-items: center; - margin-top: 15px; - - .icon-item { - font-size: 15px; - color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h); - cursor: pointer; - transition: color 0.3s ease; - - &.selected, - &:hover { - color: var(--main-text-color); - } - } - } - - .delete-ws-btn-wrapper { - display: flex; - align-items: center; - justify-content: center; - margin-top: 10px; - } - } - .actions { width: 100%; padding: 3px 0; diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 9f204b2ae..bd995a611 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -1,7 +1,6 @@ // Copyright 2024, Command Line // SPDX-License-Identifier: Apache-2.0 -import { Button } from "@/element/button"; import { ExpandableMenu, ExpandableMenuItem, @@ -10,139 +9,22 @@ import { ExpandableMenuItemLeftElement, ExpandableMenuItemRightElement, } from "@/element/expandablemenu"; -import { Input } from "@/element/input"; import { Popover, PopoverButton, PopoverContent } from "@/element/popover"; import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util"; 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, useState } from "react"; +import { CSSProperties, forwardRef, useCallback, useEffect } from "react"; import WorkspaceSVG from "../asset/workspace.svg"; import { IconButton } from "../element/iconbutton"; import { atoms, getApi } from "../store/global"; import { WorkspaceService } from "../store/services"; -import { getObjectValue, makeORef, setObjectValue } from "../store/wos"; +import { getObjectValue, makeORef } from "../store/wos"; +import { waveEventSubscribe } from "../store/wps"; +import { WorkspaceEditor } from "./workspaceeditor"; import "./workspaceswitcher.scss"; -interface ColorSelectorProps { - colors: string[]; - selectedColor?: string; - onSelect: (color: string) => void; - className?: string; -} - -const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => { - const handleColorClick = (color: string) => { - onSelect(color); - }; - - return ( -
- {colors.map((color) => ( -
handleColorClick(color)} - /> - ))} -
- ); -}); - -interface IconSelectorProps { - icons: string[]; - selectedIcon?: string; - onSelect: (icon: string) => void; - className?: string; -} - -const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => { - const handleIconClick = (icon: string) => { - onSelect(icon); - }; - - return ( -
- {icons.map((icon) => { - const iconClass = makeIconClass(icon, true); - return ( - handleIconClick(icon)} - /> - ); - })} -
- ); -}); - -interface WorkspaceEditorProps { - title: string; - icon: string; - color: string; - focusInput: boolean; - onTitleChange: (newTitle: string) => void; - onColorChange: (newColor: string) => void; - onIconChange: (newIcon: string) => void; - onDeleteWorkspace: () => void; -} -const WorkspaceEditor = memo( - ({ - title, - icon, - color, - focusInput, - onTitleChange, - onColorChange, - onIconChange, - onDeleteWorkspace, - }: 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(); - inputRef.current.select(); - } - }, [focusInput]); - - return ( -
- - - -
- -
-
- ); - } -); - type WorkspaceListEntry = { windowId: string; workspace: Workspace; @@ -175,15 +57,21 @@ const WorkspaceSwitcher = forwardRef((_, ref) => { setWorkspaceList(newList); }, []); + useEffect( + () => + waveEventSubscribe({ + eventType: "workspace:update", + handler: () => fireAndForget(updateWorkspaceList), + }), + [] + ); + useEffect(() => { fireAndForget(updateWorkspaceList); }, []); const onDeleteWorkspace = useCallback((workspaceId: string) => { getApi().deleteWorkspace(workspaceId); - setTimeout(() => { - fireAndForget(updateWorkspaceList); - }, 10); }, []); const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon); @@ -266,9 +154,16 @@ const WorkspaceSwitcherItem = ({ const setWorkspace = useCallback((newWorkspace: Workspace) => { if (newWorkspace.name != "") { - setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true); + fireAndForget(() => + WorkspaceService.UpdateWorkspace( + workspace.oid, + newWorkspace.name, + newWorkspace.icon, + newWorkspace.color, + false + ) + ); } - setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace }); }, []); const isActive = !!workspaceEntry.windowId; diff --git a/pkg/service/workspaceservice/workspaceservice.go b/pkg/service/workspaceservice/workspaceservice.go index b48bafcc8..1c86ff77b 100644 --- a/pkg/service/workspaceservice/workspaceservice.go +++ b/pkg/service/workspaceservice/workspaceservice.go @@ -50,6 +50,9 @@ func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId st return nil, fmt.Errorf("error updating workspace: %w", err) } + wps.Broker.Publish(wps.WaveEvent{ + Event: wps.Event_WorkspaceUpdate}) + updates := waveobj.ContextGetUpdatesRtn(ctx) go func() { defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents") diff --git a/pkg/wcore/window.go b/pkg/wcore/window.go index 53fd823c1..19dea5392 100644 --- a/pkg/wcore/window.go +++ b/pkg/wcore/window.go @@ -28,7 +28,8 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) ( if err != nil { return nil, fmt.Errorf("error getting window: %w", err) } - if window.WorkspaceId == workspaceId { + curWsId := window.WorkspaceId + if curWsId == workspaceId { return nil, nil } @@ -45,24 +46,24 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) ( return nil, err } } - - curWs, err := GetWorkspace(ctx, window.WorkspaceId) + window.WorkspaceId = workspaceId + err = wstore.DBUpdate(ctx, window) if err != nil { - return nil, fmt.Errorf("error getting current workspace: %w", err) + return nil, fmt.Errorf("error updating window: %w", err) } - deleted, err := DeleteWorkspace(ctx, curWs.OID, false) + + deleted, err := DeleteWorkspace(ctx, curWsId, false) if err != nil { return nil, fmt.Errorf("error deleting current workspace: %w", err) } if !deleted { - log.Printf("current workspace %s was not deleted\n", curWs.OID) + log.Printf("current workspace %s was not deleted\n", curWsId) } else { - log.Printf("deleted current workspace %s\n", curWs.OID) + log.Printf("deleted current workspace %s\n", curWsId) } - window.WorkspaceId = workspaceId log.Printf("switching window %s to workspace %s\n", windowId, workspaceId) - return ws, wstore.DBUpdate(ctx, window) + return ws, nil } func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) { diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index c0b2da324..b3143b2ab 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -122,6 +122,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, return false, fmt.Errorf("error closing tab: %w", err) } } + windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId) err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId) if err != nil { return false, fmt.Errorf("error deleting workspace: %w", err) @@ -129,6 +130,12 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, log.Printf("deleted workspace %s\n", workspaceId) wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_WorkspaceUpdate}) + if windowId != "" { + err = CloseWindow(ctx, windowId, false) + if err != nil { + return false, fmt.Errorf("error closing window: %w", err) + } + } return true, nil }