Rght sidebar (#461)

* init

* model

* pass model

* increase snap threshold

* move saving of state to model. backend implementation

* fix wrong clientdata prop

* right header space for right sidebar toggles

* button component refactor

* set default classname to primary

* remove debugging code

* hide trigger for now

* disable right sidebar in production (only show on dev for now)
This commit is contained in:
Red J Adaya 2024-03-15 13:59:55 +08:00 committed by GitHub
parent 61c9d21014
commit 07e0b53b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 414 additions and 142 deletions

View File

@ -44,6 +44,10 @@
--floating-logo-width: 40px;
--floating-logo-height: var(--screentabs-height);
/* right sidebar triggers */
--floating-right-sidebar-triggers-width-darwin: 50px;
--floating-right-sidebar-triggers-width: 40px;
/* global colors */
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);

View File

@ -302,6 +302,25 @@ a.a-block {
}
}
.right-sidebar-triggers {
position: absolute;
z-index: 25;
right: 0px;
display: flex;
flex-direction: row;
align-items: center;
pointer-events: none;
height: 38px;
padding: 0 5px;
.right-sidebar-trigger {
cursor: pointer;
user-select: none;
-webkit-app-region: no-drag;
pointer-events: all;
}
}
.copied-indicator {
position: absolute;
top: 0;

View File

@ -16,10 +16,12 @@ import { BookmarksView } from "./bookmarks/bookmarks";
import { HistoryView } from "./history/history";
import { ConnectionsView } from "./connections/connections";
import { ClientSettingsView } from "./clientsettings/clientsettings";
import { MainSideBar } from "./sidebar/sidebar";
import { DisconnectedModal, ClientStopModal } from "./common/modals";
import { ModalsProvider } from "./common/modals/provider";
import { ErrorBoundary } from "./common/error/errorboundary";
import { MainSideBar } from "./sidebar/main";
import { RightSideBar } from "./sidebar/right";
import { DisconnectedModal, ClientStopModal } from "@/modals";
import { ModalsProvider } from "@/modals/provider";
import { Button } from "@/elements";
import { ErrorBoundary } from "@/common/error/errorboundary";
import cn from "classnames";
import "./app.less";
@ -65,9 +67,17 @@ class App extends React.Component<{}, {}> {
}
@boundMethod
openSidebar() {
const width = GlobalModel.mainSidebarModel.getWidth(true);
GlobalCommandRunner.clientSetSidebar(width, false);
openMainSidebar() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.saveState(width, false);
}
@boundMethod
openRightSidebar() {
const rightSidebarModel = GlobalModel.rightSidebarModel;
const width = rightSidebarModel.getWidth(true);
rightSidebarModel.saveState(width, false);
}
render() {
@ -110,23 +120,36 @@ class App extends React.Component<{}, {}> {
}
// used to force a full reload of the application
const renderVersion = GlobalModel.renderVersion.get();
const sidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isThemeDark() ? "is-dark" : "is-light";
return (
<div
key={"version-" + renderVersion}
id="main"
className={cn("platform-" + platform, { "sidebar-collapsed": sidebarCollapsed }, lightDarkClass)}
className={cn(
"platform-" + platform,
{ "mainsidebar-collapsed": mainSidebarCollapsed, "rightsidebar-collapsed": rightSidebarCollapsed },
lightDarkClass
)}
onContextMenu={this.handleContextMenu}
>
<If condition={sidebarCollapsed}>
<If condition={mainSidebarCollapsed}>
<div key="logo-button" className="logo-button-container">
<div className="logo-button-spacer" />
<div className="logo-button" onClick={this.openSidebar}>
<div className="logo-button" onClick={this.openMainSidebar}>
<img src="public/logos/wave-logo.png" alt="logo" />
</div>
</div>
</If>
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
<div className="right-sidebar-triggers">
<Button className="secondary ghost right-sidebar-trigger" onClick={this.openRightSidebar}>
<i className="fa-sharp fa-regular fa-lightbulb"></i>
</Button>
</div>
</If>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary>
@ -137,6 +160,7 @@ class App extends React.Component<{}, {}> {
<ConnectionsView model={remotesModel} />
<ClientSettingsView model={remotesModel} />
</ErrorBoundary>
<RightSideBar parentRef={this.mainContentRef} clientData={clientData} />
</div>
<ModalsProvider />
</div>

View File

@ -5,7 +5,6 @@
font: inherit;
cursor: pointer;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
@ -15,7 +14,8 @@
line-height: 1.5;
&.primary {
background: none;
color: var(--form-element-text-color);
background: var(--form-element-primary-color);
i {
fill: var(--form-element-primary-color);
@ -31,11 +31,12 @@
}
&.outlined {
background: none;
border: 1px solid var(--form-element-primary-color);
}
&.ghost {
// Styles for .ghost are already defined above
background: none;
}
&:hover {
@ -46,6 +47,9 @@
&.secondary {
color: var(--form-element-text-color);
background: none;
color: var(--form-element-text-color);
background: var(--form-element-secondary-color);
box-shadow: none;
&.solid {
color: var(--form-element-text-color);
@ -54,10 +58,12 @@
}
&.outlined {
background: none;
border: 1px solid var(--form-element-secondary-color);
}
&.ghost {
background: none;
padding: 6px 10px;
i {

View File

@ -1,24 +1,15 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import "./button.less";
type ButtonVariantType = "outlined" | "solid" | "ghost";
type ButtonThemeType = "primary" | "secondary";
interface ButtonProps {
theme?: ButtonThemeType;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: ButtonVariantType;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
color?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
className?: string;
@ -27,10 +18,8 @@ interface ButtonProps {
class Button extends React.Component<ButtonProps> {
static defaultProps = {
theme: "primary",
variant: "solid",
color: "",
style: {},
className: "primary",
};
@boundMethod
@ -41,31 +30,11 @@ class Button extends React.Component<ButtonProps> {
}
render() {
const {
leftIcon,
rightIcon,
theme,
children,
disabled,
variant,
color,
style,
autoFocus,
termInline,
className,
} = this.props;
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className } = this.props;
return (
<button
className={cn(
"wave-button",
theme,
variant,
color,
{ disabled: disabled },
{ "term-inline": termInline },
className
)}
className={cn("wave-button", { disabled }, { "term-inline": termInline }, className)}
onClick={this.handleClick}
disabled={disabled}
style={style}

View File

@ -1,21 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { Button } from "./button";
class IconButton extends Button {
render() {
const { children, theme, variant = "solid", ...rest } = this.props;
const className = `wave-button icon-button ${theme} ${variant}`;
return (
<button {...rest} className={className}>
{children}
</button>
);
}
}
export default IconButton;
export { IconButton };

View File

@ -3,7 +3,6 @@ export { Checkbox } from "./checkbox";
export { CmdStrCode } from "./cmdstrcode";
export { renderCmdText } from "./cmdtext";
export { Dropdown } from "./dropdown";
export { IconButton } from "./iconbutton";
export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
export { InputDecoration } from "./inputdecoration";
export { LinkButton } from "./linkbutton";

View File

@ -6,7 +6,6 @@ import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { Button } from "./button";
import { IconButton } from "./iconbutton";
import "./modal.less";
@ -19,9 +18,9 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
<div className="wave-modal-header">
{<div className="wave-modal-title">{title}</div>}
<If condition={onClose}>
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
<Button className="secondary ghost" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</IconButton>
</Button>
</If>
</div>
);
@ -36,11 +35,15 @@ interface ModalFooterProps {
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
<div className="wave-modal-footer">
{onCancel && (
<Button theme="secondary" onClick={onCancel}>
<Button className="secondary" onClick={onCancel}>
{cancelLabel}
</Button>
)}
{onOk && <Button onClick={onOk}>{okLabel}</Button>}
{onOk && (
<Button className="primary" onClick={onOk}>
{okLabel}
</Button>
)}
</div>
);

View File

@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import { GlobalCommandRunner, SidebarModel } from "@/models";
import { MagicLayout } from "@/app/magiclayout";
import "./resizablesidebar.less";
@ -14,6 +14,7 @@ import "./resizablesidebar.less";
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
model: SidebarModel;
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
@ -32,7 +33,7 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
const { parentRef, position } = this.props;
const { parentRef, position, model } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
@ -43,17 +44,16 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
const collapsed = model.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
this.resizeStartWidth = model.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
model.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
model.isDragging.set(true);
})();
}
@ -61,11 +61,10 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const { parentRef, enableSnap, position, model } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
if (!model.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
@ -100,34 +99,27 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
model.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
model.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
const { model } = this.props;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
model.saveState(model.tempWidth.get(), model.tempCollapsed.get());
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
@ -136,19 +128,18 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
@boundMethod
toggleCollapsed() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const { model } = this.props;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
const tempCollapsed = model.getCollapsed();
const width = model.getWidth(true);
model.setTempWidthAndTempCollapsed(width, !tempCollapsed);
model.saveState(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const isCollapsed = mainSidebarModel.getCollapsed();
const { className, children, model } = this.props;
const width = model.getWidth();
const isCollapsed = model.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width, minWidth: width }}>

View File

@ -54,7 +54,7 @@ class AlertModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<If condition={isConfirm}>
<Button theme="secondary" onClick={this.closeModal}>
<Button className="secondary" onClick={this.closeModal}>
Cancel
</Button>
<Button autoFocus={true} onClick={this.handleOK}>

View File

@ -31,7 +31,7 @@ class ClientStopModal extends React.Component<{}, {}> {
</If>
<div>
<Button
theme="secondary"
className="secondary"
onClick={this.refreshClient}
leftIcon={<i className="fa-sharp fa-solid fa-rotate"></i>}
>

View File

@ -75,7 +75,7 @@ class DisconnectedModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<Button
theme="secondary"
className="secondary"
onClick={this.tryReconnect}
leftIcon={
<span className="icon">
@ -86,7 +86,7 @@ class DisconnectedModal extends React.Component<{}, {}> {
Try Reconnect
</Button>
<Button
theme="secondary"
className="secondary"
onClick={this.restartServer}
leftIcon={<i className="fa-sharp fa-solid fa-triangle-exclamation"></i>}
>

View File

@ -162,42 +162,42 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
renderHeaderBtns(remote: RemoteType): React.ReactNode {
let buttons: React.ReactNode[] = [];
const disconnectButton = (
<Button theme="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
Disconnect Now
</Button>
);
const connectButton = (
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
Connect Now
</Button>
);
const tryReconnectButton = (
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
Try Reconnect
</Button>
);
let updateAuthButton = (
<Button theme="secondary" onClick={() => this.openEditModal()}>
<Button className="secondary" onClick={() => this.openEditModal()}>
Edit
</Button>
);
let cancelInstallButton = (
<Button theme="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
<Button className="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
Cancel Install
</Button>
);
let installNowButton = (
<Button theme="secondary" onClick={() => this.installRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.installRemote(remote.remoteid)}>
Install Now
</Button>
);
let archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
<Button className="secondary" onClick={() => this.clickArchive()}>
Delete
</Button>
);
const reinstallButton = (
<Button theme="secondary" onClick={this.clickReinstall}>
<Button className="secondary" onClick={this.clickReinstall}>
Reinstall
</Button>
);
@ -207,7 +207,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
}
if (remote.sshconfigsrc == "sshconfig-import") {
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
<Button className="secondary" onClick={() => this.clickArchive()}>
Delete
<Tooltip
message={
@ -383,7 +383,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<Button
theme="secondary"
className="secondary"
disabled={selectedRemoteStatus == "connecting"}
onClick={this.handleClose}
>

View File

@ -180,14 +180,14 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
</table>
<footer>
<Button
theme="secondary"
className="secondary"
leftIcon={<i className="fa-sharp fa-solid fa-plus"></i>}
onClick={this.handleAddConnection}
>
New Connection
</Button>
<Button
theme="secondary"
className="secondary"
leftIcon={<i className="fa-sharp fa-solid fa-fw fa-file-import"></i>}
onClick={this.handleImportSshConfig}
>

View File

@ -16,11 +16,17 @@ let MagicLayout = {
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 26,
MainSidebarMinWidth: 75,
MainSidebarMinWidth: 0,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarSnapThreshold: 165,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
RightSidebarMinWidth: 0,
RightSidebarMaxWidth: 300,
RightSidebarSnapThreshold: 90,
RightSidebarDragResistance: 50,
RightSidebarDefaultWidth: 240,
};
let m = MagicLayout;

View File

@ -21,7 +21,7 @@ import { isBlank, openLink } from "@/util/util";
import { ResizableSidebar } from "@/common/elements";
import * as appconst from "@/app/appconst";
import "./sidebar.less";
import "./main.less";
import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "@/common/icons/icons";
dayjs.extend(localizedFormat);
@ -251,6 +251,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
render() {
return (
<ResizableSidebar
model={GlobalModel.mainSidebarModel}
className="main-sidebar"
position="left"
enableSnap={true}

View File

@ -0,0 +1,28 @@
@import "@/common/icons/icons.less";
.right-sidebar {
padding: 0;
display: flex;
flex-direction: column;
position: relative;
line-height: 20px;
backdrop-filter: blur(4px);
z-index: 20;
font-size: var(--sidebar-font-size);
font-family: var(--base-font-family);
font-weight: var(--sidebar-font-weight);
border-left: 1px solid var(--app-border-color);
.header {
height: 39px;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 5px;
border-bottom: 1px solid var(--app-border-color);
}
&.collapsed {
display: none;
}
}

46
src/app/sidebar/right.tsx Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import { ResizableSidebar, Button } from "@/elements";
import "./right.less";
dayjs.extend(localizedFormat);
interface RightSideBarProps {
parentRef: React.RefObject<HTMLElement>;
clientData: ClientDataType;
}
@mobxReact.observer
class RightSideBar extends React.Component<RightSideBarProps, {}> {
render() {
return (
<ResizableSidebar
model={GlobalModel.rightSidebarModel}
className="right-sidebar"
position="right"
enableSnap={true}
parentRef={this.props.parentRef}
>
{(toggleCollapse) => (
<React.Fragment>
<div className="header">
<Button className="secondary ghost" onClick={toggleCollapse}>
<i className="fa-sharp fa-regular fa-xmark"></i>
</Button>
</div>
</React.Fragment>
)}
</ResizableSidebar>
);
}
}
export { RightSideBar };

View File

@ -325,7 +325,7 @@ class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {
<br />
</div>
<div onClick={this.sidebarClose} className="close-button-container">
<Button theme="secondary" onClick={this.sidebarClose}>
<Button className="secondary" onClick={this.sidebarClose}>
Close Sidebar
</Button>
</div>

View File

@ -189,10 +189,19 @@
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
// More margin is given on macOS to account for the traffic light buttons
#main.platform-darwin.sidebar-collapsed .screen-tabs-container {
#main.platform-darwin.mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width-darwin);
}
#main:not(.platform-darwin).sidebar-collapsed .screen-tabs-container {
#main:not(.platform-darwin).mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width);
}
// This ensures the tab bar does not collide with the right sidebar triggers.
#main.platform-darwin.rightsidebar-collapsed .screen-tabs-container {
margin-right: var(--floating-right-sidebar-triggers-width-darwin);
}
#main:not(.platform-darwin).rightsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-right-sidebar-triggers-width);
}

View File

@ -39,16 +39,12 @@ class WorkspaceView extends React.Component<{}, {}> {
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("mainview", "session-view", { "is-hidden": isHidden })}
data-sessionid={session.sessionId}
style={{
width: `${width}px`,
width: `${window.innerWidth - mainSidebarModel.getWidth()}px`,
}}
>
<ScreenTabs key={"tabs-" + session.sessionId} session={session} />

View File

@ -399,9 +399,14 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
}
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
clientSetMainSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
return GlobalModel.submitCommand("client", "setmainsidebar", null, kwargs, false);
}
clientSetRightSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setrightsidebar", null, kwargs, false);
}
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {

View File

@ -1,3 +1,4 @@
export type { SidebarModel } from "./sidebar";
export * from "./global";
export * from "./model";
export { BookmarksModel } from "./bookmarks";
@ -6,6 +7,7 @@ export { Cmd } from "./cmd";
export { ConnectionsViewModel } from "./connectionsview";
export { InputModel } from "./input";
export { MainSidebarModel } from "./mainsidebar";
export { RightSidebarModel } from "./rightsidebar";
export { ModalsModel } from "./modals";
export { PluginsModel } from "./plugins";
export { RemotesModel } from "./remotes";

View File

@ -4,6 +4,7 @@
import * as mobx from "mobx";
import { MagicLayout } from "@/app/magiclayout";
import { Model } from "./model";
import { GlobalCommandRunner } from "@/models";
class MainSidebarModel {
globalModel: Model = null;
@ -80,6 +81,14 @@ class MainSidebarModel {
}
return collapsed;
}
saveState(width: number, collapsed: boolean): void {
GlobalCommandRunner.clientSetMainSidebar(width, collapsed).finally(() => {
mobx.action(() => {
this.isDragging.set(false);
})();
});
}
}
export { MainSidebarModel };

View File

@ -13,7 +13,6 @@ import {
isModKeyPress,
isBlank,
} from "@/util/util";
import { loadFonts } from "@/util/fontutil";
import { loadTheme } from "@/util/themeutil";
import { WSControl } from "./ws";
import { cmdStatusIsRunning } from "@/app/line/lineutil";
@ -31,6 +30,7 @@ import { ClientSettingsViewModel } from "./clientsettingsview";
import { RemotesModel } from "./remotes";
import { ModalsModel } from "./modals";
import { MainSidebarModel } from "./mainsidebar";
import { RightSidebarModel } from "./rightsidebar";
import { Screen } from "./screen";
import { Cmd } from "./cmd";
import { GlobalCommandRunner } from "./global";
@ -117,6 +117,7 @@ class Model {
clientSettingsViewModel: ClientSettingsViewModel;
modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
rightSidebarModel: RightSidebarModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData",
});
@ -156,6 +157,7 @@ class Model {
this.remotesModel = new RemotesModel(this);
this.modalsModel = new ModalsModel();
this.mainSidebarModel = new MainSidebarModel(this);
this.rightSidebarModel = new RightSidebarModel(this);
const isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running",

100
src/models/rightsidebar.ts Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx";
import { MagicLayout } from "@/app/magiclayout";
import { Model } from "./model";
import { GlobalCommandRunner } from "@/models";
interface SidebarModel {}
class RightSidebarModel implements SidebarModel {
globalModel: Model = null;
tempWidth: OV<number> = mobx.observable.box(null, {
name: "RightSidebarModel-tempWidth",
});
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
name: "RightSidebarModel-tempCollapsed",
});
isDragging: OV<boolean> = mobx.observable.box(false, {
name: "RightSidebarModel-isDragging",
});
constructor(globalModel: Model) {
this.globalModel = globalModel;
}
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
const width = Math.max(MagicLayout.RightSidebarMinWidth, Math.min(newWidth, MagicLayout.RightSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
/**
* Gets the intended width for the sidebar. If the sidebar is being dragged, returns the tempWidth. If the sidebar is collapsed, returns the default width.
* @param ignoreCollapse If true, returns the persisted width even if the sidebar is collapsed.
* @returns The intended width for the sidebar or the default width if the sidebar is collapsed. Can be overridden using ignoreCollapse.
*/
getWidth(ignoreCollapse: boolean = false): number {
const clientData = this.globalModel.clientData.get();
let width = clientData?.clientopts?.rightsidebar?.width ?? MagicLayout.RightSidebarDefaultWidth;
if (this.isDragging.get()) {
if (this.tempWidth.get() == null && width == null) {
return MagicLayout.RightSidebarDefaultWidth;
}
if (this.tempWidth.get() == null) {
return width;
}
return this.tempWidth.get();
}
// Set by CLI and collapsed
if (this.getCollapsed()) {
if (ignoreCollapse) {
return width;
} else {
return MagicLayout.RightSidebarMinWidth;
}
} else {
if (width <= MagicLayout.RightSidebarMinWidth) {
width = MagicLayout.RightSidebarDefaultWidth;
}
const snapPoint = MagicLayout.RightSidebarMinWidth + MagicLayout.RightSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.RightSidebarMaxWidth) {
width = MagicLayout.RightSidebarDefaultWidth;
}
}
return width;
}
getCollapsed(): boolean {
// disable right sidebar in production
if (!this.globalModel.isDev) {
return true;
}
const clientData = this.globalModel.clientData.get();
const collapsed = clientData?.clientopts?.rightsidebar?.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;
}
saveState(width: number, collapsed: boolean): void {
GlobalCommandRunner.clientSetRightSidebar(width, collapsed).finally(() => {
mobx.action(() => {
this.isDragging.set(false);
})();
});
}
}
export { RightSidebarModel };

13
src/models/sidebar.ts Normal file
View File

@ -0,0 +1,13 @@
import { Model } from "./model";
export interface SidebarModel {
readonly globalModel: Model;
readonly tempWidth: OV<number>;
readonly tempCollapsed: OV<boolean>;
readonly isDragging: OV<boolean>;
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void;
getWidth(ignoreCollapse?: boolean): number;
getCollapsed(): boolean;
saveState(width: number, collapsed: boolean): void;
}

View File

@ -433,7 +433,7 @@ class SourceCodeRenderer extends React.Component<
return (
<>
<If condition={isPreviewerAvailable}>
<Button theme="primary" termInline={true}>
<Button className="primary" termInline={true}>
<div onClick={this.togglePreview} className={`preview`}>
{`${showPreview ? "hide" : "show"} preview (`}
{renderCmdText("P")}
@ -449,7 +449,7 @@ class SourceCodeRenderer extends React.Component<
))}
</select>
<If condition={allowEditing}>
<Button theme="primary" termInline={true}>
<Button className="primary" termInline={true}>
<div onClick={() => this.doSave()}>
{`save (`}
{renderCmdText("S")}

View File

@ -579,6 +579,10 @@ declare global {
collapsed: boolean;
width: number;
};
rightsidebar: {
collapsed: boolean;
width: number;
};
globalshortcut: string;
globalshortcutenabled: boolean;
};

View File

@ -234,7 +234,8 @@ func init() {
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
registerCmdFn("client:setmainsidebar", ClientSetMainSidebarCommand)
registerCmdFn("client:setrightsidebar", ClientSetRightSidebarCommand)
registerCmdFn("client:setglobalshortcut", ClientSetGlobalShortcut)
registerCmdFn("sidebar:open", SidebarOpenCommand)
@ -5271,7 +5272,7 @@ func ClientSetGlobalShortcut(ctx context.Context, pk *scpacket.FeCommandPacketTy
return update, nil
}
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
func ClientSetMainSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
@ -5326,6 +5327,61 @@ func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketTy
return update, nil
}
func ClientSetRightSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.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.RightSidebar != nil {
width = clientData.ClientOpts.RightSidebar.Width
}
// Initialize SidebarCollapsed if it's nil
if clientData.ClientOpts.RightSidebar == nil {
clientData.ClientOpts.RightSidebar = new(sstore.SidebarValueType)
}
// Set the sidebar values
var sv sstore.SidebarValueType
sv.Collapsed = collapsedValue
if width != 0 {
sv.Width = width
}
clientData.ClientOpts.RightSidebar = &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 := scbus.MakeUpdatePacket()
update.AddUpdate(*clientData)
return update, nil
}
func validateOpenAIAPIToken(key string) error {
if len(key) > MaxOpenAIAPITokenLen {
return fmt.Errorf("invalid openai token, too long")

View File

@ -294,6 +294,7 @@ type ClientOptsType struct {
AcceptedTos int64 `json:"acceptedtos,omitempty"`
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
RightSidebar *SidebarValueType `json:"rightsidebar,omitempty"`
GlobalShortcut string `json:"globalshortcut,omitempty"`
GlobalShortcutEnabled bool `json:"globalshortcutenabled,omitempty"`
}