Synchronize workspace edits across windows, close window when workspace is deleted (#1540)

This commit is contained in:
Evan Simkowitz 2024-12-16 21:54:13 -08:00 committed by GitHub
parent 1ba370e4dd
commit 78f3cd0472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 256 additions and 232 deletions

View File

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

View File

@ -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 (
<div className={clsx("color-selector", className)}>
{colors.map((color) => (
<div
key={color}
className={clsx("color-circle", { selected: selectedColor === color })}
style={{ backgroundColor: color }}
onClick={() => handleColorClick(color)}
/>
))}
</div>
);
});
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 (
<div className={clsx("icon-selector", className)}>
{icons.map((icon) => {
const iconClass = makeIconClass(icon, true);
return (
<i
key={icon}
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
onClick={() => handleIconClick(icon)}
/>
);
})}
</div>
);
});
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<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();
inputRef.current.select();
}
}, [focusInput]);
return (
<div className="workspace-editor">
<Input
ref={inputRef}
className={clsx("vertical-padding-3", { error: title === "" })}
onChange={onTitleChange}
value={title}
autoFocus
autoSelect
/>
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
<div className="delete-ws-btn-wrapper">
<Button className="ghost red font-size-12 bold" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
</div>
);
};
export const WorkspaceEditor = memo(WorkspaceEditorComponent) as typeof WorkspaceEditorComponent;

View File

@ -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 { .workspace-switcher-content {
min-height: auto; min-height: auto;
display: flex; display: flex;
@ -55,6 +36,25 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); 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 { .title {
font-size: 12px; font-size: 12px;
line-height: 19px; line-height: 19px;
@ -144,83 +144,6 @@
padding: 0; 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 { .actions {
width: 100%; width: 100%;
padding: 3px 0; padding: 3px 0;

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line // Copyright 2024, Command Line
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Button } from "@/element/button";
import { import {
ExpandableMenu, ExpandableMenu,
ExpandableMenuItem, ExpandableMenuItem,
@ -10,139 +9,22 @@ import {
ExpandableMenuItemLeftElement, ExpandableMenuItemLeftElement,
ExpandableMenuItemRightElement, ExpandableMenuItemRightElement,
} from "@/element/expandablemenu"; } from "@/element/expandablemenu";
import { Input } from "@/element/input";
import { Popover, PopoverButton, PopoverContent } from "@/element/popover"; import { Popover, PopoverButton, PopoverContent } from "@/element/popover";
import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util"; import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { splitAtom } from "jotai/utils"; import { splitAtom } from "jotai/utils";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; 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 WorkspaceSVG from "../asset/workspace.svg";
import { IconButton } from "../element/iconbutton"; import { IconButton } from "../element/iconbutton";
import { atoms, getApi } from "../store/global"; import { atoms, getApi } from "../store/global";
import { WorkspaceService } from "../store/services"; 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"; 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 (
<div className={clsx("color-selector", className)}>
{colors.map((color) => (
<div
key={color}
className={clsx("color-circle", { selected: selectedColor === color })}
style={{ backgroundColor: color }}
onClick={() => handleColorClick(color)}
/>
))}
</div>
);
});
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 (
<div className={clsx("icon-selector", className)}>
{icons.map((icon) => {
const iconClass = makeIconClass(icon, true);
return (
<i
key={icon}
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
onClick={() => handleIconClick(icon)}
/>
);
})}
</div>
);
});
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<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();
inputRef.current.select();
}
}, [focusInput]);
return (
<div className="workspace-editor">
<Input
ref={inputRef}
className={clsx("vertical-padding-3", { error: title === "" })}
onChange={onTitleChange}
value={title}
autoFocus
autoSelect
/>
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
<div className="delete-ws-btn-wrapper">
<Button className="ghost red font-size-12 bold" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
</div>
);
}
);
type WorkspaceListEntry = { type WorkspaceListEntry = {
windowId: string; windowId: string;
workspace: Workspace; workspace: Workspace;
@ -175,15 +57,21 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, ref) => {
setWorkspaceList(newList); setWorkspaceList(newList);
}, []); }, []);
useEffect(
() =>
waveEventSubscribe({
eventType: "workspace:update",
handler: () => fireAndForget(updateWorkspaceList),
}),
[]
);
useEffect(() => { useEffect(() => {
fireAndForget(updateWorkspaceList); fireAndForget(updateWorkspaceList);
}, []); }, []);
const onDeleteWorkspace = useCallback((workspaceId: string) => { const onDeleteWorkspace = useCallback((workspaceId: string) => {
getApi().deleteWorkspace(workspaceId); getApi().deleteWorkspace(workspaceId);
setTimeout(() => {
fireAndForget(updateWorkspaceList);
}, 10);
}, []); }, []);
const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon); const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon);
@ -266,9 +154,16 @@ const WorkspaceSwitcherItem = ({
const setWorkspace = useCallback((newWorkspace: Workspace) => { const setWorkspace = useCallback((newWorkspace: Workspace) => {
if (newWorkspace.name != "") { 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; const isActive = !!workspaceEntry.windowId;

View File

@ -50,6 +50,9 @@ func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId st
return nil, fmt.Errorf("error updating workspace: %w", err) return nil, fmt.Errorf("error updating workspace: %w", err)
} }
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate})
updates := waveobj.ContextGetUpdatesRtn(ctx) updates := waveobj.ContextGetUpdatesRtn(ctx)
go func() { go func() {
defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents") defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents")

View File

@ -28,7 +28,8 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting window: %w", err) return nil, fmt.Errorf("error getting window: %w", err)
} }
if window.WorkspaceId == workspaceId { curWsId := window.WorkspaceId
if curWsId == workspaceId {
return nil, nil return nil, nil
} }
@ -45,24 +46,24 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
return nil, err return nil, err
} }
} }
window.WorkspaceId = workspaceId
curWs, err := GetWorkspace(ctx, window.WorkspaceId) err = wstore.DBUpdate(ctx, window)
if err != nil { 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 { if err != nil {
return nil, fmt.Errorf("error deleting current workspace: %w", err) return nil, fmt.Errorf("error deleting current workspace: %w", err)
} }
if !deleted { if !deleted {
log.Printf("current workspace %s was not deleted\n", curWs.OID) log.Printf("current workspace %s was not deleted\n", curWsId)
} else { } 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) 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) { func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) {

View File

@ -122,6 +122,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
return false, fmt.Errorf("error closing tab: %w", err) return false, fmt.Errorf("error closing tab: %w", err)
} }
} }
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId) err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId)
if err != nil { if err != nil {
return false, fmt.Errorf("error deleting workspace: %w", err) 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) log.Printf("deleted workspace %s\n", workspaceId)
wps.Broker.Publish(wps.WaveEvent{ wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate}) 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 return true, nil
} }