mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Synchronize workspace edits across windows, close window when workspace is deleted (#1540)
This commit is contained in:
parent
1ba370e4dd
commit
78f3cd0472
70
frontend/app/tab/workspaceeditor.scss
Normal file
70
frontend/app/tab/workspaceeditor.scss
Normal 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;
|
||||
}
|
||||
}
|
125
frontend/app/tab/workspaceeditor.tsx
Normal file
125
frontend/app/tab/workspaceeditor.tsx
Normal 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;
|
@ -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;
|
||||
|
@ -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 (
|
||||
<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 = {
|
||||
windowId: string;
|
||||
workspace: Workspace;
|
||||
@ -175,15 +57,21 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, 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;
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user