mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
allow resizing of left sidebar (#244)
* wip * integrate original sidebar content * ResizableSidebar component * trigger toggleCollapse * remove debugging code * minor refactor. disable text select on mousemove * replace icons with fontawesome icons. fix alignment issues * fix session view width when tabs overflow * prevent index and icon from shifting when resizing * snap effect * minor refactor * apply collapsed mode to sidebar contents * change default width to 240px * backend implementation * fix wrong subcmd * save collapsed state * retore sidebar state on reload/launch * use collapse data form db on first load. use previously saved width on expand. * persist width as well collapse state * various fixes and improvements * bind methods * refactor * more refactor * fix minor bug * fix merge issues * various fixes * refactor * fixes * fix issues * fix all issues * resolve undefind tempWidth * fix toggleCollapsed * use Promise in stopResizing method * use tempCollapsed to for real time toggling between logos * minor method name change * refactor * remove debugging code * fix conflict * fix setting collapsed state via CLI * minor refactor * remove debugging code * create setTempWidthAndTempCollapsed method * handle invalid width set via cli * refactor: setbycli not actually needed * remove unused code
This commit is contained in:
parent
40757fa7f4
commit
37ab1bca90
@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
|
||||
@mobxReact.observer
|
||||
class App extends React.Component<{}, {}> {
|
||||
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
|
||||
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
|
||||
let hasClientStop = GlobalModel.getHasClientStop();
|
||||
let dcWait = this.dcWait.get();
|
||||
let platform = GlobalModel.getPlatform();
|
||||
let clientData = GlobalModel.clientData.get();
|
||||
|
||||
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
|
||||
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
|
||||
if (clientData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (disconnected || hasClientStop) {
|
||||
if (!dcWait) {
|
||||
@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
return (
|
||||
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
|
||||
<div className="main-content">
|
||||
<MainSideBar />
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
<div className="session-view" />
|
||||
</div>
|
||||
<If condition={dcWait}>
|
||||
@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
return (
|
||||
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
|
||||
<div className="main-content">
|
||||
<MainSideBar />
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
<ErrorBoundary>
|
||||
<PluginsView />
|
||||
<WorkspaceView />
|
||||
|
@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { RemoteType, StatusIndicatorLevel } from "../../types/types";
|
||||
import { RemoteType } from "../../types/types";
|
||||
import ReactDOM from "react-dom";
|
||||
import { GlobalModel } from "../../model/model";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||
import * as appconst from "../appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
import { MagicLayout } from "../magiclayout";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||
@ -1265,6 +1266,169 @@ In order to use Wave's advanced features like unified history and persistent ses
|
||||
});
|
||||
}
|
||||
|
||||
interface ResizableSidebarProps {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
position: "left" | "right";
|
||||
enableSnap?: boolean;
|
||||
className?: string;
|
||||
children?: (toggleCollapsed: () => void) => React.ReactNode;
|
||||
toggleCollapse?: () => void;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
|
||||
resizeStartWidth: number = 0;
|
||||
startX: number = 0;
|
||||
prevDelta: number = 0;
|
||||
prevDragDirection: string = null;
|
||||
disposeReaction: any;
|
||||
|
||||
@boundMethod
|
||||
startResizing(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
let { parentRef, position } = this.props;
|
||||
let parentRect = parentRef.current?.getBoundingClientRect();
|
||||
|
||||
if (!parentRect) return;
|
||||
|
||||
if (position === "right") {
|
||||
this.startX = parentRect.right - event.clientX;
|
||||
} else {
|
||||
this.startX = event.clientX - parentRect.left;
|
||||
}
|
||||
|
||||
this.resizeStartWidth = GlobalModel.mainSidebarModel.getWidth();
|
||||
document.addEventListener("mousemove", this.onMouseMove);
|
||||
document.addEventListener("mouseup", this.stopResizing);
|
||||
|
||||
document.body.style.cursor = "col-resize";
|
||||
mobx.action(() => {
|
||||
GlobalModel.mainSidebarModel.isDragging.set(true);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onMouseMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
let { parentRef, enableSnap, position } = this.props;
|
||||
let parentRect = parentRef.current?.getBoundingClientRect();
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
|
||||
|
||||
let delta, newWidth;
|
||||
|
||||
if (position === "right") {
|
||||
delta = parentRect.right - event.clientX - this.startX;
|
||||
} else {
|
||||
delta = event.clientX - parentRect.left - this.startX;
|
||||
}
|
||||
|
||||
newWidth = this.resizeStartWidth + delta;
|
||||
|
||||
if (enableSnap) {
|
||||
let minWidth = MagicLayout.MainSidebarMinWidth;
|
||||
let snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
|
||||
let dragResistance = MagicLayout.MainSidebarDragResistance;
|
||||
let dragDirection;
|
||||
|
||||
if (delta - this.prevDelta > 0) {
|
||||
dragDirection = "+";
|
||||
} else if (delta - this.prevDelta == 0) {
|
||||
if (this.prevDragDirection == "+") {
|
||||
dragDirection = "+";
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
|
||||
this.prevDelta = delta;
|
||||
this.prevDragDirection = dragDirection;
|
||||
|
||||
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
|
||||
newWidth = snapPoint;
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
|
||||
newWidth = minWidth;
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
|
||||
} else if (newWidth > snapPoint) {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
}
|
||||
} else {
|
||||
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
|
||||
} else {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
stopResizing() {
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
GlobalCommandRunner.clientSetSidebar(
|
||||
mainSidebarModel.tempWidth.get(),
|
||||
mainSidebarModel.tempCollapsed.get()
|
||||
).finally(() => {
|
||||
mobx.action(() => {
|
||||
mainSidebarModel.isDragging.set(false);
|
||||
})();
|
||||
});
|
||||
|
||||
document.removeEventListener("mousemove", this.onMouseMove);
|
||||
document.removeEventListener("mouseup", this.stopResizing);
|
||||
document.body.style.cursor = "";
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleCollapsed() {
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
let tempCollapsed = mainSidebarModel.getCollapsed();
|
||||
let width = MagicLayout.MainSidebarDefaultWidth;
|
||||
let newWidth;
|
||||
if (tempCollapsed) {
|
||||
newWidth = width;
|
||||
} else {
|
||||
newWidth = MagicLayout.MainSidebarMinWidth;
|
||||
}
|
||||
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, !tempCollapsed);
|
||||
GlobalCommandRunner.clientSetSidebar(newWidth, !tempCollapsed);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { className, children } = this.props;
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
let width = mainSidebarModel.getWidth();
|
||||
let isCollapsed = mainSidebarModel.getCollapsed();
|
||||
|
||||
return (
|
||||
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
|
||||
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
|
||||
<div
|
||||
className="sidebar-handle"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
[this.props.position === "left" ? "right" : "left"]: 0,
|
||||
bottom: 0,
|
||||
width: "5px",
|
||||
cursor: "col-resize",
|
||||
}}
|
||||
onMouseDown={this.startResizing}
|
||||
onDoubleClick={this.toggleCollapsed}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CmdStrCode,
|
||||
Toggle,
|
||||
@ -1286,5 +1450,6 @@ export {
|
||||
LinkButton,
|
||||
Status,
|
||||
Modal,
|
||||
ResizableSidebar,
|
||||
ShowWaveShellInstallPrompt,
|
||||
};
|
||||
|
@ -27,6 +27,12 @@ let MagicLayout = {
|
||||
ScreenSidebarWidthPadding: 5,
|
||||
ScreenSidebarMinWidth: 200,
|
||||
ScreenSidebarHeaderHeight: 28,
|
||||
|
||||
MainSidebarMinWidth: 75,
|
||||
MainSidebarMaxWidth: 300,
|
||||
MainSidebarSnapThreshold: 90,
|
||||
MainSidebarDragResistance: 50,
|
||||
MainSidebarDefaultWidth: 240,
|
||||
};
|
||||
|
||||
let m = MagicLayout;
|
||||
|
@ -3,14 +3,13 @@
|
||||
|
||||
.main-sidebar {
|
||||
padding: 0;
|
||||
min-width: 20rem;
|
||||
max-width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
font-size: 12.5px;
|
||||
line-height: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 20;
|
||||
|
||||
.title-bar-drag {
|
||||
-webkit-app-region: drag;
|
||||
@ -24,7 +23,6 @@
|
||||
&.collapsed {
|
||||
width: 6em;
|
||||
min-width: 6em;
|
||||
|
||||
.arrow-container,
|
||||
.collapse-button {
|
||||
transform: rotate(180deg);
|
||||
@ -34,7 +32,7 @@
|
||||
margin-top: 26px;
|
||||
|
||||
.top,
|
||||
.workspaces-item,
|
||||
.workspaces,
|
||||
.middle,
|
||||
.bottom,
|
||||
.separator {
|
||||
@ -50,7 +48,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.logo-container img {
|
||||
.logo-container {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
@ -86,10 +84,14 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.logo-container {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
@ -150,7 +152,6 @@
|
||||
margin-left: 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
|
||||
width: inherit;
|
||||
max-width: inherit;
|
||||
min-width: inherit;
|
||||
@ -177,6 +178,7 @@
|
||||
float: right;
|
||||
margin-right: 6px;
|
||||
letter-spacing: 6px;
|
||||
margin-left: auto;
|
||||
}
|
||||
&:hover {
|
||||
:not(.disabled) .hotkey {
|
||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
import type { RemoteType } from "../../types/types";
|
||||
import type { ClientDataType, RemoteType } from "../../types/types";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { compareLoose } from "semver";
|
||||
|
||||
@ -19,6 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
||||
import { isBlank, openLink } from "../../util/util";
|
||||
import { ResizableSidebar } from "../common/common";
|
||||
import * as constants from "../appconst";
|
||||
|
||||
import "./sidebar.less";
|
||||
@ -26,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
class SideBarItem extends React.Component<{
|
||||
frontIcon: React.ReactNode;
|
||||
contents: React.ReactNode | string;
|
||||
@ -59,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class MainSideBar extends React.Component<{}, {}> {
|
||||
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
||||
interface MainSideBarProps {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
clientData: ClientDataType;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleCollapsed() {
|
||||
mobx.action(() => {
|
||||
this.collapsed.set(!this.collapsed.get());
|
||||
})();
|
||||
}
|
||||
@mobxReact.observer
|
||||
class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
||||
sidebarRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
handleSessionClick(sessionId: string) {
|
||||
GlobalCommandRunner.switchSession(sessionId);
|
||||
@ -208,106 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isCollapsed = this.collapsed.get();
|
||||
const clientData = GlobalModel.clientData.get();
|
||||
let clientData = this.props.clientData;
|
||||
let needsUpdate = false;
|
||||
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
|
||||
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
|
||||
}
|
||||
let mainSidebar = GlobalModel.mainSidebarModel;
|
||||
let isCollapsed = mainSidebar.getCollapsed();
|
||||
return (
|
||||
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
|
||||
<div className="title-bar-drag" />
|
||||
<div className="contents">
|
||||
<div className="logo">
|
||||
<If condition={isCollapsed}>
|
||||
<div className="logo-container" onClick={this.toggleCollapsed}>
|
||||
<img src="public/logos/wave-logo.png" />
|
||||
<ResizableSidebar
|
||||
className="main-sidebar"
|
||||
position="left"
|
||||
enableSnap={true}
|
||||
parentRef={this.props.parentRef}
|
||||
>
|
||||
{(toggleCollapse) => (
|
||||
<React.Fragment>
|
||||
<div className="title-bar-drag" />
|
||||
<div className="contents">
|
||||
<div className="logo">
|
||||
<If condition={isCollapsed}>
|
||||
<div className="logo-container" onClick={toggleCollapse}>
|
||||
<img src="public/logos/wave-logo.png" />
|
||||
</div>
|
||||
</If>
|
||||
<If condition={!isCollapsed}>
|
||||
<div className="logo-container">
|
||||
<img src="public/logos/wave-dark.png" />
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
<div className="collapse-button" onClick={toggleCollapse}>
|
||||
<LeftChevronIcon className="icon" />
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={!isCollapsed}>
|
||||
<div className="logo-container">
|
||||
<img src="public/logos/wave-dark.png" />
|
||||
<div className="separator" />
|
||||
<div className="top">
|
||||
<SideBarItem
|
||||
key="history"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
|
||||
contents="History"
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
|
||||
onClick={this.handleHistoryClick}
|
||||
/>
|
||||
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">⌘B</span>} onClick={this.handleBookmarksClick}/> */}
|
||||
<SideBarItem
|
||||
key="connections"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
|
||||
contents="Connections"
|
||||
onClick={this.handleConnectionsClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
<div className="collapse-button" onClick={this.toggleCollapsed}>
|
||||
<LeftChevronIcon className="icon" />
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
<div className="separator" />
|
||||
<div className="top">
|
||||
<SideBarItem
|
||||
key="history"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
|
||||
contents="History"
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
|
||||
onClick={this.handleHistoryClick}
|
||||
/>
|
||||
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">⌘B</span>} onClick={this.handleBookmarksClick}/> */}
|
||||
<SideBarItem
|
||||
key="connections"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
|
||||
contents="Connections"
|
||||
onClick={this.handleConnectionsClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="separator" />
|
||||
<SideBarItem
|
||||
key="workspaces"
|
||||
className="workspaces"
|
||||
frontIcon={<WorkspacesIcon className="icon" />}
|
||||
contents="Workspaces"
|
||||
endIcons={[
|
||||
<CenteredIcon
|
||||
key="add-workspace"
|
||||
className="add-workspace hoverEffect"
|
||||
onClick={this.handleNewSession}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-plus"></i>
|
||||
</CenteredIcon>,
|
||||
]}
|
||||
/>
|
||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||
<div className="bottom">
|
||||
<If condition={needsUpdate}>
|
||||
<div className="separator" />
|
||||
<SideBarItem
|
||||
key="update-available"
|
||||
className="updateBanner"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
|
||||
contents="Update Available"
|
||||
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
||||
key="workspaces"
|
||||
className="workspaces"
|
||||
frontIcon={<WorkspacesIcon className="icon" />}
|
||||
contents="Workspaces"
|
||||
endIcons={[
|
||||
<CenteredIcon
|
||||
key="add-workspace"
|
||||
className="add-workspace hoverEffect"
|
||||
onClick={this.handleNewSession}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-plus"></i>
|
||||
</CenteredIcon>,
|
||||
]}
|
||||
/>
|
||||
</If>
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<SideBarItem
|
||||
key="apps"
|
||||
frontIcon={<AppsIcon className="icon" />}
|
||||
contents="Apps"
|
||||
onClick={this.handlePluginsClick}
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||
/>
|
||||
</If>
|
||||
<SideBarItem
|
||||
key="settings"
|
||||
frontIcon={<SettingsIcon className="icon" />}
|
||||
contents="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
<SideBarItem
|
||||
key="documentation"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
|
||||
contents="Documentation"
|
||||
onClick={() => openLink("https://docs.waveterm.dev")}
|
||||
/>
|
||||
<SideBarItem
|
||||
key="discord"
|
||||
frontIcon={<i className="fa-brands fa-discord icon" />}
|
||||
contents="Discord"
|
||||
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||
<div className="bottom">
|
||||
<If condition={needsUpdate}>
|
||||
<SideBarItem
|
||||
key="update-available"
|
||||
className="updateBanner"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
|
||||
contents="Update Available"
|
||||
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
||||
/>
|
||||
</If>
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<SideBarItem
|
||||
key="apps"
|
||||
frontIcon={<AppsIcon className="icon" />}
|
||||
contents="Apps"
|
||||
onClick={this.handlePluginsClick}
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||
/>
|
||||
</If>
|
||||
<SideBarItem
|
||||
key="settings"
|
||||
frontIcon={<SettingsIcon className="icon" />}
|
||||
contents="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
<SideBarItem
|
||||
key="documentation"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
|
||||
contents="Documentation"
|
||||
onClick={() => openLink("https://docs.waveterm.dev")}
|
||||
/>
|
||||
<SideBarItem
|
||||
key="discord"
|
||||
frontIcon={<i className="fa-brands fa-discord icon" />}
|
||||
contents="Discord"
|
||||
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ResizableSidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,12 @@
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
max-width: calc(100% - 20.5em);
|
||||
background: @background-session;
|
||||
border: 1px solid @base-border;
|
||||
border-radius: 8px;
|
||||
transition: width 0.2s ease;
|
||||
// transition: width 0.2s ease;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
||||
.center-message {
|
||||
display: flex;
|
||||
@ -24,6 +24,3 @@
|
||||
color: @text-secondary;
|
||||
}
|
||||
}
|
||||
.collapsed + .session-view {
|
||||
max-width: calc(100% - 6.7em);
|
||||
}
|
||||
|
@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
|
||||
if (session == null) {
|
||||
return (
|
||||
<div className="session-view">
|
||||
<div className="center-message"><div>(no active workspace)</div></div>
|
||||
<div className="center-message">
|
||||
<div>(no active workspace)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let activeScreen = session.getActiveScreen();
|
||||
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
|
||||
if (cmdInputHeight == 0) {
|
||||
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
|
||||
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
|
||||
}
|
||||
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
|
||||
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
|
||||
let isHidden = GlobalModel.activeMainView.get() != "session";
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
// Has to calc manually because when tabs overflow, the width of the session view is increased for some reason causing inconsistent width.
|
||||
// 6px is the right margin of session view.
|
||||
let width = window.innerWidth - 6 - mainSidebarModel.getWidth();
|
||||
|
||||
return (
|
||||
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}>
|
||||
<div
|
||||
className={cn("session-view", { "is-hidden": isHidden })}
|
||||
data-sessionid={session.sessionId}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
}}
|
||||
>
|
||||
<ScreenTabs key={"tabs-" + session.sessionId} session={session} />
|
||||
<ErrorBoundary>
|
||||
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
|
||||
|
@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import {
|
||||
handleJsonFetchResponse,
|
||||
base64ToString,
|
||||
@ -2617,6 +2618,74 @@ class ClientSettingsViewModel {
|
||||
})();
|
||||
}
|
||||
}
|
||||
class MainSidebarModel {
|
||||
tempWidth: OV<number> = mobx.observable.box(null, {
|
||||
name: "MainSidebarModel-tempWidth",
|
||||
});
|
||||
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
|
||||
name: "MainSidebarModel-tempCollapsed",
|
||||
});
|
||||
isDragging: OV<boolean> = mobx.observable.box(false, {
|
||||
name: "MainSidebarModel-isDragging",
|
||||
});
|
||||
|
||||
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
|
||||
let width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
|
||||
|
||||
mobx.action(() => {
|
||||
this.tempWidth.set(width);
|
||||
this.tempCollapsed.set(newCollapsed);
|
||||
})();
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
let clientData = GlobalModel.clientData.get();
|
||||
let width = clientData.clientopts.mainsidebar.width;
|
||||
if (this.isDragging.get()) {
|
||||
if (this.tempWidth.get() == null && width == null) {
|
||||
return MagicLayout.MainSidebarDefaultWidth;
|
||||
}
|
||||
if (this.tempWidth.get() == null) {
|
||||
return width;
|
||||
}
|
||||
return this.tempWidth.get();
|
||||
}
|
||||
// Set by CLI and collapsed
|
||||
if (this.getCollapsed() && width != MagicLayout.MainSidebarMinWidth) {
|
||||
this.setTempWidthAndTempCollapsed(MagicLayout.MainSidebarMinWidth, true);
|
||||
return MagicLayout.MainSidebarMinWidth;
|
||||
}
|
||||
// Set by CLI and not collapsed
|
||||
if (!this.getCollapsed()) {
|
||||
if (width <= MagicLayout.MainSidebarMinWidth) {
|
||||
width = MagicLayout.MainSidebarDefaultWidth;
|
||||
}
|
||||
let snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
|
||||
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
|
||||
width = MagicLayout.MainSidebarDefaultWidth;
|
||||
}
|
||||
this.setTempWidthAndTempCollapsed(width, false);
|
||||
return width;
|
||||
}
|
||||
this.setTempWidthAndTempCollapsed(width, this.getCollapsed());
|
||||
return width;
|
||||
}
|
||||
|
||||
getCollapsed(): boolean {
|
||||
let clientData = GlobalModel.clientData.get();
|
||||
let collapsed = clientData.clientopts.mainsidebar.collapsed;
|
||||
if (this.isDragging.get()) {
|
||||
if (this.tempCollapsed.get() == null && collapsed == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.tempCollapsed.get() == null) {
|
||||
return collapsed;
|
||||
}
|
||||
return this.tempCollapsed.get();
|
||||
}
|
||||
return collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarksModel {
|
||||
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
|
||||
@ -3387,6 +3456,7 @@ class Model {
|
||||
connectionViewModel: ConnectionsViewModel;
|
||||
clientSettingsViewModel: ClientSettingsViewModel;
|
||||
modalsModel: ModalsModel;
|
||||
mainSidebarModel: MainSidebarModel;
|
||||
clientData: OV<ClientDataType> = mobx.observable.box(null, {
|
||||
name: "clientData",
|
||||
});
|
||||
@ -3413,6 +3483,7 @@ class Model {
|
||||
this.remotesModalModel = new RemotesModalModel();
|
||||
this.remotesModel = new RemotesModel();
|
||||
this.modalsModel = new ModalsModel();
|
||||
this.mainSidebarModel = new MainSidebarModel();
|
||||
let isWaveSrvRunning = getApi().getWaveSrvStatus();
|
||||
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
|
||||
name: "model-wavesrv-running",
|
||||
@ -4980,6 +5051,11 @@ class CommandRunner {
|
||||
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
|
||||
}
|
||||
|
||||
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
|
||||
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
|
||||
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
|
||||
}
|
||||
|
||||
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
|
@ -524,6 +524,10 @@ type ClientOptsType = {
|
||||
noreleasecheck: boolean;
|
||||
acceptedtos: number;
|
||||
confirmflags: ConfirmFlagsType;
|
||||
mainsidebar: {
|
||||
collapsed: boolean;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ReleaseInfoType = {
|
||||
|
@ -92,6 +92,7 @@ var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass"
|
||||
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
|
||||
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
||||
var ConfirmFlags = []string{"hideshellprompt"}
|
||||
var SidebarNames = []string{"main"}
|
||||
|
||||
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
||||
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
||||
@ -218,6 +219,7 @@ func init() {
|
||||
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
||||
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
||||
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
|
||||
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
|
||||
|
||||
registerCmdFn("sidebar:open", SidebarOpenCommand)
|
||||
registerCmdFn("sidebar:close", SidebarCloseCommand)
|
||||
@ -4463,6 +4465,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||
clientData, err := sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
||||
}
|
||||
|
||||
// Handle collapsed
|
||||
collapsed, ok := pk.Kwargs["collapsed"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("collapsed key not provided")
|
||||
}
|
||||
collapsedValue := resolveBool(collapsed, false)
|
||||
|
||||
// Handle width
|
||||
var width int
|
||||
if w, exists := pk.Kwargs["width"]; exists {
|
||||
width, err = resolveNonNegInt(w, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error resolving width: %v", err)
|
||||
}
|
||||
} else if clientData.ClientOpts.MainSidebar != nil {
|
||||
width = clientData.ClientOpts.MainSidebar.Width
|
||||
}
|
||||
|
||||
// Initialize SidebarCollapsed if it's nil
|
||||
if clientData.ClientOpts.MainSidebar == nil {
|
||||
clientData.ClientOpts.MainSidebar = new(sstore.SidebarValueType)
|
||||
}
|
||||
|
||||
// Set the sidebar values
|
||||
var sv sstore.SidebarValueType
|
||||
sv.Collapsed = collapsedValue
|
||||
if width != 0 {
|
||||
sv.Width = width
|
||||
}
|
||||
clientData.ClientOpts.MainSidebar = &sv
|
||||
|
||||
// Update client data
|
||||
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating client data: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve updated client data
|
||||
clientData, err = sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
||||
}
|
||||
|
||||
update := &sstore.ModelUpdate{
|
||||
ClientData: clientData,
|
||||
}
|
||||
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func validateOpenAIAPIToken(key string) error {
|
||||
if len(key) > MaxOpenAIAPITokenLen {
|
||||
return fmt.Errorf("invalid openai token, too long")
|
||||
|
@ -275,11 +275,17 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
|
||||
return quickScanJson(tdata, val)
|
||||
}
|
||||
|
||||
type SidebarValueType struct {
|
||||
Collapsed bool `json:"collapsed"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
||||
type ClientOptsType struct {
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||
MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
|
||||
}
|
||||
|
||||
type FeOptsType struct {
|
||||
|
Loading…
Reference in New Issue
Block a user