mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-08 19:38:51 +01:00
04c4f0a203
New context menu options are available in the directory preview to create and rename files and directories It's missing three pieces of functionality, none of which are a regression: - Editing or creating an entry does not update the focused index. Focus index right now is pretty dumb, it doesn't factor in the column sorting so if you change that, the selected item will change to whatever is now at that index. We should update this so we use the actual file name to determine which element to focus and let the table determine which index to then highlight given the current sorting algo - Open in native preview should not be an option on remote connections with the exception of WSL, where it should resolve the file in the Windows filesystem, rather than the WSL one - We should catch CRUD errors in the dir preview and display a popup
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
// Copyright 2024, Command Line
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { Button } from "@/element/button";
|
|
import {
|
|
ExpandableMenu,
|
|
ExpandableMenuItem,
|
|
ExpandableMenuItemGroup,
|
|
ExpandableMenuItemGroupTitle,
|
|
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, memo, useCallback, useEffect, useRef } 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 "./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, false);
|
|
return (
|
|
<i
|
|
key={icon}
|
|
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
|
|
onClick={() => handleIconClick(icon)}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
interface ColorAndIconSelectorProps {
|
|
title: string;
|
|
icon: string;
|
|
color: string;
|
|
focusInput: boolean;
|
|
onTitleChange: (newTitle: string) => void;
|
|
onColorChange: (newColor: string) => void;
|
|
onIconChange: (newIcon: string) => void;
|
|
onDeleteWorkspace: () => void;
|
|
}
|
|
const ColorAndIconSelector = memo(
|
|
({
|
|
title,
|
|
icon,
|
|
color,
|
|
focusInput,
|
|
onTitleChange,
|
|
onColorChange,
|
|
onIconChange,
|
|
onDeleteWorkspace,
|
|
}: ColorAndIconSelectorProps) => {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (focusInput && inputRef.current) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [focusInput]);
|
|
|
|
return (
|
|
<div className="color-icon-selector">
|
|
<Input ref={inputRef} className="vertical-padding-3" onChange={onTitleChange} value={title} autoFocus />
|
|
<ColorSelector
|
|
selectedColor={color}
|
|
colors={["#e91e63", "#8bc34a", "#ff9800", "#ffc107", "#03a9f4", "#3f51b5", "#f44336"]}
|
|
onSelect={onColorChange}
|
|
/>
|
|
<IconSelector
|
|
selectedIcon={icon}
|
|
icons={[
|
|
"triangle",
|
|
"star",
|
|
"cube",
|
|
"gem",
|
|
"chess-knight",
|
|
"heart",
|
|
"plane",
|
|
"rocket",
|
|
"shield-cat",
|
|
"paw-simple",
|
|
"umbrella",
|
|
"graduation-cap",
|
|
"mug-hot",
|
|
"circle",
|
|
]}
|
|
onSelect={onIconChange}
|
|
/>
|
|
<div className="delete-ws-btn-wrapper">
|
|
<Button className="ghost red font-size-12" onClick={onDeleteWorkspace}>
|
|
Delete workspace
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
type WorkspaceListEntry = {
|
|
windowId: string;
|
|
workspace: Workspace;
|
|
};
|
|
|
|
type WorkspaceList = WorkspaceListEntry[];
|
|
const workspaceMapAtom = atom<WorkspaceList>([]);
|
|
const workspaceSplitAtom = splitAtom(workspaceMapAtom);
|
|
const editingWorkspaceAtom = atom<string>();
|
|
const WorkspaceSwitcher = () => {
|
|
const setWorkspaceList = useSetAtom(workspaceMapAtom);
|
|
const activeWorkspace = useAtomValueSafe(atoms.workspace);
|
|
const workspaceList = useAtomValue(workspaceSplitAtom);
|
|
const setEditingWorkspace = useSetAtom(editingWorkspaceAtom);
|
|
|
|
const updateWorkspaceList = useCallback(async () => {
|
|
const workspaceList = await WorkspaceService.ListWorkspaces();
|
|
if (!workspaceList) {
|
|
return;
|
|
}
|
|
const newList: WorkspaceList = [];
|
|
for (const entry of workspaceList) {
|
|
// This just ensures that the atom exists for easier setting of the object
|
|
getObjectValue(makeORef("workspace", entry.workspaceid));
|
|
newList.push({
|
|
windowId: entry.windowid,
|
|
workspace: await WorkspaceService.GetWorkspace(entry.workspaceid),
|
|
});
|
|
}
|
|
setWorkspaceList(newList);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fireAndForget(updateWorkspaceList);
|
|
}, []);
|
|
|
|
const onDeleteWorkspace = useCallback((workspaceId: string) => {
|
|
fireAndForget(async () => {
|
|
getApi().deleteWorkspace(workspaceId);
|
|
setTimeout(() => {
|
|
fireAndForget(updateWorkspaceList);
|
|
}, 10);
|
|
});
|
|
}, []);
|
|
|
|
const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon);
|
|
|
|
const workspaceIcon = isActiveWorkspaceSaved ? (
|
|
<i className={makeIconClass(activeWorkspace.icon, false)} style={{ color: activeWorkspace.color }}></i>
|
|
) : (
|
|
<WorkspaceSVG />
|
|
);
|
|
|
|
const saveWorkspace = () => {
|
|
setObjectValue({ ...activeWorkspace, name: "New Workspace", icon: "circle", color: "green" }, undefined, true);
|
|
setTimeout(() => {
|
|
fireAndForget(updateWorkspaceList);
|
|
}, 10);
|
|
};
|
|
|
|
return (
|
|
<Popover
|
|
className="workspace-switcher-popover"
|
|
placement="bottom-start"
|
|
onDismiss={() => setEditingWorkspace(null)}
|
|
>
|
|
<PopoverButton
|
|
className="workspace-switcher-button grey"
|
|
as="div"
|
|
onClick={() => {
|
|
fireAndForget(updateWorkspaceList);
|
|
}}
|
|
>
|
|
<span className="workspace-icon">{workspaceIcon}</span>
|
|
</PopoverButton>
|
|
<PopoverContent className="workspace-switcher-content">
|
|
<div className="title">{isActiveWorkspaceSaved ? "Switch workspace" : "Open workspace"}</div>
|
|
<OverlayScrollbarsComponent className={"scrollable"} options={{ scrollbars: { autoHide: "leave" } }}>
|
|
<ExpandableMenu noIndent singleOpen>
|
|
{workspaceList.map((entry, i) => (
|
|
<WorkspaceSwitcherItem key={i} entryAtom={entry} onDeleteWorkspace={onDeleteWorkspace} />
|
|
))}
|
|
</ExpandableMenu>
|
|
</OverlayScrollbarsComponent>
|
|
|
|
{!isActiveWorkspaceSaved && (
|
|
<div className="actions">
|
|
<ExpandableMenuItem onClick={() => saveWorkspace()}>
|
|
<ExpandableMenuItemLeftElement>
|
|
<i className="fa-sharp fa-solid fa-floppy-disk"></i>
|
|
</ExpandableMenuItemLeftElement>
|
|
<div className="content">Save workspace</div>
|
|
</ExpandableMenuItem>
|
|
</div>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
const WorkspaceSwitcherItem = ({
|
|
entryAtom,
|
|
onDeleteWorkspace,
|
|
}: {
|
|
entryAtom: PrimitiveAtom<WorkspaceListEntry>;
|
|
onDeleteWorkspace: (workspaceId: string) => void;
|
|
}) => {
|
|
const activeWorkspace = useAtomValueSafe(atoms.workspace);
|
|
const [workspaceEntry, setWorkspaceEntry] = useAtom(entryAtom);
|
|
const [editingWorkspace, setEditingWorkspace] = useAtom(editingWorkspaceAtom);
|
|
|
|
const workspace = workspaceEntry.workspace;
|
|
const isCurrentWorkspace = activeWorkspace.oid === workspace.oid;
|
|
|
|
const setWorkspace = useCallback((newWorkspace: Workspace) => {
|
|
fireAndForget(async () => {
|
|
setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true);
|
|
setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace });
|
|
});
|
|
}, []);
|
|
|
|
const isActive = !!workspaceEntry.windowId;
|
|
const editIconDecl: IconButtonDecl = {
|
|
elemtype: "iconbutton",
|
|
className: "edit",
|
|
icon: "pencil",
|
|
title: "Edit workspace",
|
|
click: (e) => {
|
|
e.stopPropagation();
|
|
if (editingWorkspace === workspace.oid) {
|
|
setEditingWorkspace(null);
|
|
} else {
|
|
setEditingWorkspace(workspace.oid);
|
|
}
|
|
},
|
|
};
|
|
const windowIconDecl: IconButtonDecl = {
|
|
elemtype: "iconbutton",
|
|
className: "window",
|
|
disabled: true,
|
|
icon: isCurrentWorkspace ? "check" : "window",
|
|
title: isCurrentWorkspace ? "This is your current workspace" : "This workspace is open",
|
|
};
|
|
|
|
const isEditing = editingWorkspace === workspace.oid;
|
|
|
|
return (
|
|
<ExpandableMenuItemGroup
|
|
key={workspace.oid}
|
|
isOpen={isEditing}
|
|
className={clsx({ "is-current": isCurrentWorkspace })}
|
|
>
|
|
<ExpandableMenuItemGroupTitle
|
|
onClick={() => {
|
|
getApi().switchWorkspace(workspace.oid);
|
|
// Create a fake escape key event to close the popover
|
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
}}
|
|
>
|
|
<div
|
|
className="menu-group-title-wrapper"
|
|
style={
|
|
{
|
|
"--workspace-color": workspace.color,
|
|
} as CSSProperties
|
|
}
|
|
>
|
|
<ExpandableMenuItemLeftElement>
|
|
<i
|
|
className={clsx("left-icon", makeIconClass(workspace.icon, false))}
|
|
style={{ color: workspace.color }}
|
|
/>
|
|
</ExpandableMenuItemLeftElement>
|
|
<div className="label">{workspace.name}</div>
|
|
<ExpandableMenuItemRightElement>
|
|
<div className="icons">
|
|
<IconButton decl={editIconDecl} />
|
|
{isActive && <IconButton decl={windowIconDecl} />}
|
|
</div>
|
|
</ExpandableMenuItemRightElement>
|
|
</div>
|
|
</ExpandableMenuItemGroupTitle>
|
|
<ExpandableMenuItem>
|
|
<ColorAndIconSelector
|
|
title={workspace.name}
|
|
icon={workspace.icon}
|
|
color={workspace.color}
|
|
focusInput={isEditing}
|
|
onTitleChange={(title) => setWorkspace({ ...workspace, name: title })}
|
|
onColorChange={(color) => setWorkspace({ ...workspace, color })}
|
|
onIconChange={(icon) => setWorkspace({ ...workspace, icon })}
|
|
onDeleteWorkspace={() => onDeleteWorkspace(workspace.oid)}
|
|
/>
|
|
</ExpandableMenuItem>
|
|
</ExpandableMenuItemGroup>
|
|
);
|
|
};
|
|
|
|
export { WorkspaceSwitcher };
|