new tab flow (#60)

* init

* init

* error handling

* use css

* minor improvements

* fix some issues and tabicon init

* show error indicator when empty and is required

* debounce input

* fix decorator linting issue

* icon system init

* fix bugs

* color custom icons and fix regression

* remove debugging code

* remove @tab-magenta. fix formatting.

* swap magenta for mint

* change tab color order
This commit is contained in:
Red J Adaya
2023-11-07 00:04:25 -08:00
committed by GitHub
parent f8d3c0c19c
commit 847de1fc60
16 changed files with 600 additions and 89 deletions
+2
View File
@@ -58,6 +58,8 @@
"@types/node": "^20.4.0",
"@types/papaparse": "^5.3.10",
"@types/react": "^18.0.12",
"@types/sprintf-js": "^1.1.3",
"@types/throttle-debounce": "^5.0.1",
"@types/uuid": "9.0.6",
"@types/webpack-env": "^1.18.3",
"babel-loader": "^9.1.3",
+75 -2
View File
@@ -625,7 +625,81 @@
}
}
.textfield {
display: flex;
align-items: center;
border: 1px solid @term-white;
border-radius: 6px;
position: relative;
margin-bottom: 1rem;
background-color: transparent;
height: 44px;
min-width: 412px;
gap: 6px;
&.focused {
border-color: @term-green;
}
&.error {
border-color: @term-red;
}
.textfield-inner {
display: flex;
align-items: flex-end;
height: 100%;
position: relative;
flex-grow: 1;
.textfield-label {
position: absolute;
left: 16px;
top: 16px;
font-size: 12.5px;
transition: all 0.3s;
color: @term-white;
line-height: 10px;
&.float {
font-size: 10px;
top: 5px;
}
&.start {
left: 0;
}
}
.textfield-input {
width: 100%;
height: 30px;
border: none;
padding: 5px 0 5px 16px;
font-size: 16px;
outline: none;
background-color: transparent;
color: @term-bright-white;
line-height: 20px;
&.start {
padding: 5px 16px 5px 0;
}
}
}
i {
font-size: 16px;
}
}
.input-decoration {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
margin: 8px;
}
.inline-edit {
.icon {
@@ -664,5 +738,4 @@
height: 20px;
}
}
}
}
+149
View File
@@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import type { RemoteType } from "../../types/types";
import { debounce } from "throttle-debounce";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@@ -123,6 +124,152 @@ class Checkbox extends React.Component<
}
}
interface InputDecorationProps {
children: React.ReactNode;
}
@mobxReact.observer
class InputDecoration extends React.Component<InputDecorationProps, {}> {
render() {
const { children, onClick } = this.props;
return <div className="input-decoration">{children}</div>;
}
}
interface TextFieldDecorationProps {
startDecoration?: React.ReactNode;
endDecoration?: React.ReactNode;
}
interface TextFieldProps {
label: string;
value?: string;
className?: string;
onChange?: (value: string) => void;
placeholder?: string;
defaultValue?: string;
decoration?: TextFieldDecorationProps;
required?: boolean;
}
interface TextFieldState {
focused: boolean;
internalValue: string;
error: boolean;
showHelpText: boolean;
hasContent: boolean;
}
@mobxReact.observer
class TextField extends React.Component<TextFieldProps, TextFieldState> {
inputRef: React.RefObject<HTMLInputElement>;
state: TextFieldState;
constructor(props: TextFieldProps) {
super(props);
const hasInitialContent = Boolean(props.value || props.defaultValue);
this.state = {
focused: false,
hasContent: hasInitialContent,
internalValue: props.defaultValue || "",
error: false,
showHelpText: false,
};
this.inputRef = React.createRef();
}
componentDidUpdate(prevProps: TextFieldProps) {
// Only update the focus state if using as controlled
if (this.props.value !== undefined && this.props.value !== prevProps.value) {
this.setState({ focused: Boolean(this.props.value) });
}
}
@boundMethod
handleFocus() {
this.setState({ focused: true });
}
@boundMethod
handleBlur() {
const { required } = this.props;
if (this.inputRef.current) {
const value = this.inputRef.current.value;
if (required && !value) {
this.setState({ error: true, focused: false });
} else {
this.setState({ error: false, focused: false });
}
}
}
@boundMethod
handleHelpTextClick() {
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
}
debouncedOnChange = debounce(300, (value) => {
const { onChange } = this.props;
onChange?.(value);
});
@boundMethod
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const { required } = this.props;
const inputValue = e.target.value;
// Check if value is empty and the field is required
if (required && !inputValue) {
this.setState({ error: true, hasContent: false });
} else {
this.setState({ error: false, hasContent: Boolean(inputValue) });
}
// Update the internal state for uncontrolled version
if (this.props.value === undefined) {
this.setState({ internalValue: inputValue });
}
this.debouncedOnChange(inputValue);
}
render() {
const { label, value, placeholder, decoration, className } = this.props;
const { focused, internalValue, error } = this.state;
// Decide if the input should behave as controlled or uncontrolled
const inputValue = value !== undefined ? value : internalValue;
return (
<div className={cn(`textfield ${className || ""}`, { focused: focused, error: error })}>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="textfield-inner">
<label
className={cn("textfield-label", {
float: this.state.hasContent || this.state.focused || placeholder,
start: decoration?.startDecoration,
})}
htmlFor={label}
>
{label}
</label>
<input
className={cn("textfield-input", { start: decoration?.startDecoration })}
ref={this.inputRef}
id={label}
value={inputValue}
onChange={this.handleInputChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
placeholder={placeholder}
/>
</div>
{decoration?.endDecoration && <div>{decoration.endDecoration}</div>}
</div>
);
}
}
@mobxReact.observer
class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> {
render() {
@@ -362,4 +509,6 @@ export {
InfoMessage,
Markdown,
SettingsError,
TextField,
InputDecoration,
};
+8 -8
View File
@@ -19,15 +19,15 @@
@textarea-background: #2a2a2a;
@text-primary: #fff;
@text-secondary: #C3C8C2;
@text-secondary: #c3c8c2;
@text-caption: #8b918a;
@accent-color: #3B3F3A;
@accent-color: #3b3f3a;
@status-outline: #151715;
@dropdown-menu: rgba(21, 23, 21, 1);
@status-connected: #46A758;
@status-connected: #46a758;
@status-connecting: #f5d90a;
@status-error: #e54d2e;
@status-disconnected: #c3c8c2;
@@ -53,11 +53,11 @@
@tab-orange: #ef713b;
@tab-yellow: #e0b956;
@tab-green: #58c142;
@tab-mint: #4BFFA9;
@tab-cyan: #4BDFFF;
@tab-blue: #3971FF;
@tab-violet: #BA76FF;
@tab-pink: #E05677;
@tab-mint: #4bffa9;
@tab-cyan: #4bdfff;
@tab-blue: #3971ff;
@tab-violet: #ba76ff;
@tab-pink: #e05677;
@tab-white: #ffffff;
@tab-black-text: #333;
+55 -6
View File
@@ -49,12 +49,12 @@
padding: 10px;
height: 100%;
color: #ccc;
code {
background-color: transparent;
color: #4e9a06;
}
&.should-fade {
opacity: 1;
animation: fade-in 2.5s;
@@ -119,11 +119,12 @@
margin: 16px;
.newtab-section {
padding: 16px;
display: flex;
padding: 16px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
gap: 8px;
align-self: stretch;
&.conn-section {
gap: 8px;
@@ -145,13 +146,14 @@
padding: 8px 0;
align-items: flex-start;
gap: 12px;
.icondiv {
width: 20px;
height: 20px;
cursor: pointer;
position: relative;
font-size: 14px;
.icon {
width: 20px;
height: 20px;
@@ -169,6 +171,53 @@
}
}
.status-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 3px;
svg.status-icon {
width: 10px;
height: 10px;
}
}
.add-div {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
svg.add-icon {
width: 16px;
height: 16px;
path {
fill: @text-primary;
}
}
}
.text-standard {
color: @text-secondary;
}
.text-caption {
color: @text-caption;
}
.ellipsis {
text-overflow: ellipsis;
}
&:hover {
background-color: rgba(241, 246, 243, 0.08);
}
.icon.color-white + .check-icon {
path {
fill: black;
+118 -48
View File
@@ -10,7 +10,7 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, TabColors } from "../../../model/model";
import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types";
import * as T from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
@@ -20,7 +20,8 @@ import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"
import { Line } from "../../line/linecomps";
import { LinesView } from "../../line/linesview";
import { ConnectionDropdown } from "../../connections/connections";
import * as util from "../../../util/util";
import * as util from "../../../util/util";
import { TextField, InputDecoration } from "../../common/common";
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
@@ -37,7 +38,7 @@ dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class ScreenView extends React.Component<{ session: Session, screen: Screen }, {}> {
class ScreenView extends React.Component<{ session: Session; screen: Screen }, {}> {
render() {
let { session, screen } = this.props;
if (screen == null) {
@@ -54,8 +55,9 @@ class ScreenView extends React.Component<{ session: Session, screen: Screen }, {
@mobxReact.observer
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "NewTabSettings-connDropdownActive" });
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
@boundMethod
selectTabColor(color: string): void {
let { screen } = this.props;
@@ -67,15 +69,29 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
}
@boundMethod
inlineUpdateName(val: string): void {
selectTabIcon(icon: string): void {
let { screen } = this.props;
if (util.isStrEq(val, screen.name.get())) {
if (screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
updateName(val: string): void {
let { screen } = this.props;
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { name: val }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleConnDropdown(): void {
mobx.action(() => {
this.connDropdownActive.set(!this.connDropdownActive.get());
})();
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
@@ -84,62 +100,112 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
@boundMethod
clickNewConnection(): void {
GlobalModel.remotesModalModel.openModalForEdit({remoteedit: true}, true);
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true }, true);
}
renderTabIconSelector(): React.ReactNode {
let { screen } = this.props;
let curIcon = screen.getTabIcon();
if (util.isBlank(curIcon) || curIcon == "default") {
curIcon = "square";
}
let icon: string | null = null;
return (
<>
<div className="text-s1 unselectable">Select the icon</div>
<div className="control-iconlist">
<For each="icon" of={TabIcons}>
<div
className="icondiv"
key={icon}
title={icon || ""}
onClick={() => this.selectTabIcon(icon || "")}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</>
);
}
renderTabColorSelector(): React.ReactNode {
let { screen } = this.props;
let curColor = screen.getTabColor();
if (util.isBlank(curColor) || curColor == "default") {
curColor = "green";
}
let color: string | null = null;
return (
<>
<div className="text-s1 unselectable">Select the color</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<div
className="icondiv"
key={color}
title={color || ""}
onClick={() => this.selectTabColor(color || "")}
>
<EllipseIcon className={cn("icon", "color-" + color)} />
<If condition={color == curColor}>
<Check12Icon className="check-icon" />
</If>
</div>
</For>
</div>
</>
);
}
render() {
let { screen } = this.props;
let rptr = screen.curRemote.get();
let curColor = screen.getTabColor();
if (util.isBlank(curColor) || curColor == "default") {
curColor = "green";
}
let color: string = null;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<div className="newtab-container">
<div className="newtab-section name-section">
<div className="text-standard">Name</div>
<TextField
label="Title"
required={true}
defaultValue={screen.name.get() ?? ""}
onChange={this.updateName}
decoration={{
endDecoration: (
<InputDecoration>
<i className="fa-sharp fa-regular fa-circle-question"></i>
</InputDecoration>
),
}}
/>
</div>
<div className="newtab-spacer" />
<div className="newtab-section conn-section">
<div className="text-s1 unselectable">
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
</div>
<div>
<ConnectionDropdown curRemote={curRemote} allowNewConn={true} onSelectRemote={this.selectRemote} onNewConn={this.clickNewConnection}/>
<ConnectionDropdown
curRemote={curRemote}
allowNewConn={true}
onSelectRemote={this.selectRemote}
onNewConn={this.clickNewConnection}
/>
</div>
<div className="text-caption cr-help-text">
To change connection from the command line use `cr [alias|user@host]`
</div>
</div>
<div className="newtab-spacer"/>
<div className="newtab-section settings-field">
<div className="text-s1 unselectable">
Name
</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={screen.name.get() ?? "(none)"}
value={screen.name.get() ?? ""}
onChange={this.inlineUpdateName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="newtab-spacer"/>
<div className="newtab-spacer" />
<div className="newtab-section">
<div className="text-s1 unselectable">
Select the color
</div>
<div className="control-iconlist">
<For each="color" of={TabColors}>
<div className="icondiv" key={color} title={color} onClick={() => this.selectTabColor(color)}>
<EllipseIcon className={cn("icon", "color-" + color)}/>
<If condition={color == curColor}>
<Check12Icon className="check-icon"/>
</If>
</div>
</For>
</div>
<div>{this.renderTabIconSelector()}</div>
</div>
<div className="newtab-spacer" />
<div className="newtab-section">
<div>{this.renderTabColorSelector()}</div>
</div>
</div>
);
@@ -148,7 +214,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
// screen is not null
@mobxReact.observer
class ScreenWindowView extends React.Component<{ session: Session, screen: Screen }, {}> {
class ScreenWindowView extends React.Component<{ session: Session; screen: Screen }, {}> {
rszObs: any;
windowViewRef: React.RefObject<any>;
@@ -309,13 +375,17 @@ class ScreenWindowView extends React.Component<{ session: Session, screen: Scree
</div>
<If condition={lines.length == 0}>
<If condition={screen.nextLineNum.get() == 1}>
<NewTabSettings screen={screen}/>
<NewTabSettings screen={screen} />
</If>
<If condition={screen.nextLineNum.get() != 1}>
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}>
<div><code className="text-standard">[workspace="{session.name.get()}" screen="{screen.name.get()}"]</code></div>
<div>
<code className="text-standard">
[workspace="{session.name.get()}" screen="{screen.name.get()}"]
</code>
</div>
</div>
</div>
</If>
+107 -12
View File
@@ -8,14 +8,28 @@
border-radius: 12px 0px 0px 0px;
}
&.color-green, &.color-default {
&.color-green,
&.color-default {
svg.left-icon path {
fill: @tab-green;
}
.icon i {
color: @tab-green;
}
&.is-active {
border-top: 1px solid @tab-green;
background: linear-gradient(180deg, rgba(88, 193, 66, 0.20) 9.34%, rgba(88, 193, 66, 0.03) 44.16%, rgba(88, 193, 66, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(88, 193, 66, 0.2) 9.34%,
rgba(88, 193, 66, 0.03) 44.16%,
rgba(88, 193, 66, 0) 86.79%
);
}
.icon i {
color: @tab-green;
}
}
@@ -24,9 +38,18 @@
fill: @tab-orange;
}
.icon i {
color: @tab-orange;
}
&.is-active {
border-top: 1px solid @tab-orange;
background: linear-gradient(180deg, rgba(239, 113, 59, 0.20) 9.34%, rgba(239, 113, 59, 0.03) 44.16%, rgba(239, 113, 59, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(239, 113, 59, 0.2) 9.34%,
rgba(239, 113, 59, 0.03) 44.16%,
rgba(239, 113, 59, 0) 86.79%
);
}
}
@@ -35,9 +58,18 @@
fill: @tab-red;
}
.icon i {
color: @tab-red;
}
&.is-active {
border-top: 1px solid @tab-red;
background: linear-gradient(180deg, rgba(229, 77, 46, 0.20) 9.34%, rgba(229, 77, 46, 0.03) 44.16%, rgba(229, 77, 46, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(229, 77, 46, 0.2) 9.34%,
rgba(229, 77, 46, 0.03) 44.16%,
rgba(229, 77, 46, 0) 86.79%
);
}
}
@@ -46,9 +78,18 @@
fill: @tab-yellow;
}
&.is-active {
.icon i {
color: @tab-yellow;
}
&.is-active {
border-top: 1px solid @tab-yellow;
background: linear-gradient(180deg, rgba(224, 185, 86, 0.20) 9.34%, rgba(224, 185, 86, 0.03) 44.16%, rgba(224, 185, 86, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(224, 185, 86, 0.2) 9.34%,
rgba(224, 185, 86, 0.03) 44.16%,
rgba(224, 185, 86, 0) 86.79%
);
}
}
@@ -57,9 +98,18 @@
fill: @tab-blue;
}
.icon i {
color: @tab-blue;
}
&.is-active {
border-top: 1px solid @tab-blue;
background: linear-gradient(180deg, rgba(57, 113, 255, 0.20) 9.34%, rgba(57, 113, 255, 0.03) 44.16%, rgba(57, 113, 255, 0.00) 77.18%);
background: linear-gradient(
180deg,
rgba(57, 113, 255, 0.2) 9.34%,
rgba(57, 113, 255, 0.03) 44.16%,
rgba(57, 113, 255, 0) 77.18%
);
}
}
@@ -68,9 +118,18 @@
fill: @tab-mint;
}
.icon i {
color: @tab-mint;
}
&.is-active {
border-top: 1px solid @tab-mint;
background: linear-gradient(180deg, rgba(75, 255, 169, 0.20) 9.34%, rgba(75, 255, 169, 0.03) 44.16%, rgba(75, 255, 169, 0.00) 77.18%);
background: linear-gradient(
180deg,
rgba(75, 255, 169, 0.2) 9.34%,
rgba(75, 255, 169, 0.03) 44.16%,
rgba(75, 255, 169, 0) 77.18%
);
}
}
@@ -79,9 +138,18 @@
fill: @tab-cyan;
}
.icon i {
color: @tab-cyan;
}
&.is-active {
border-top: 1px solid @tab-cyan;
background: linear-gradient(180deg, rgba(75, 223, 255, 0.20) 9.34%, rgba(75, 223, 255, 0.03) 44.16%, rgba(58, 186, 214, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(75, 223, 255, 0.2) 9.34%,
rgba(75, 223, 255, 0.03) 44.16%,
rgba(58, 186, 214, 0) 86.79%
);
}
}
@@ -90,9 +158,18 @@
fill: @tab-white;
}
.icon i {
color: @tab-white;
}
&.is-active {
border-top: 1px solid @tab-white;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.20) 9.34%, rgba(255, 255, 255, 0.03) 44.16%, rgba(255, 255, 255, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 9.34%,
rgba(255, 255, 255, 0.03) 44.16%,
rgba(255, 255, 255, 0) 86.79%
);
}
}
@@ -101,9 +178,18 @@
fill: @tab-violet;
}
.icon i {
color: @tab-violet;
}
&.is-active {
border-top: 1px solid @tab-violet;
background: linear-gradient(180deg, rgba(186, 118, 255, 0.20) 9.34%, rgba(186, 118, 255, 0.03) 44.16%, rgba(186, 118, 255, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(186, 118, 255, 0.2) 9.34%,
rgba(186, 118, 255, 0.03) 44.16%,
rgba(186, 118, 255, 0) 86.79%
);
}
}
@@ -112,9 +198,18 @@
fill: @tab-pink;
}
.icon i {
color: @tab-pink;
}
&.is-active {
border-top: 1px solid @tab-pink;
background: linear-gradient(180deg, rgba(255, 136, 165, 0.20) 9.34%, rgba(255, 136, 165, 0.03) 44.16%, rgba(255, 136, 165, 0.00) 86.79%);
background: linear-gradient(
180deg,
rgba(255, 136, 165, 0.2) 9.34%,
rgba(255, 136, 165, 0.03) 44.16%,
rgba(255, 136, 165, 0) 86.79%
);
}
}
+15 -3
View File
@@ -102,7 +102,19 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
})();
}
renderTab(screen: Screen, activeScreenId: string, index: number): any {
renderTabIcon = (screen: Screen): React.ReactNode => {
const tabIcon = screen.getTabIcon();
if (tabIcon === "default") {
return <SquareIcon className="icon left-icon" />;
}
return (
<div className="icon">
<i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>
</div>
);
};
renderTab(screen: Screen, activeScreenId: string, index: number): JSX.Element {
let tabIndex = null;
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
@@ -132,7 +144,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
onClick={() => this.handleSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
>
<SquareIcon className="icon left-icon" />
{this.renderTabIcon(screen)}
<div className="tab-name truncate">
{archived}
{webShared}
@@ -149,7 +161,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
if (session == null) {
return null;
}
let screen: Screen = null;
let screen: Screen | null = null;
let index = 0;
let showingScreens = [];
let activeScreenId = session.activeScreenId.get();
+27 -5
View File
@@ -95,6 +95,18 @@ const MaxFontSize = 15;
const InputChunkSize = 500;
const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
const TabIcons = [
"sparkle",
"fire",
"ghost",
"cloud",
"compass",
"crown",
"droplet",
"graduation-cap",
"heart",
"file",
];
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
@@ -469,6 +481,15 @@ class Screen {
return tabColor;
}
getTabIcon(): string {
let tabIcon = "default";
let screenOpts = this.opts.get();
if (screenOpts != null && !isBlank(screenOpts.tabicon)) {
tabIcon = screenOpts.tabicon;
}
return tabIcon;
}
getCurRemoteInstance(): RemoteInstanceType {
let session = GlobalModel.getSessionById(this.sessionId);
let rptr = this.curRemote.get();
@@ -3492,7 +3513,7 @@ class Model {
submitCommand(
metaCmd: string,
metaSubCmd: string,
args: string[],
args: string[] | null,
kwargs: Record<string, string>,
interactive: boolean
): Promise<CommandRtnType> {
@@ -3505,7 +3526,7 @@ class Model {
uicontext: this.getUIContext(),
interactive: interactive,
};
/**
/**
console.log(
"CMD",
pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
@@ -3513,7 +3534,7 @@ class Model {
pk.kwargs,
pk.interactive
);
*/
*/
return this.submitCommandPacket(pk, interactive);
}
@@ -3950,10 +3971,10 @@ class CommandRunner {
screenSetSettings(
screenId: string,
settings: { tabcolor?: string; name?: string; sharename?: string },
settings: { tabcolor?: string; tabicon?: string; name?: string; sharename?: string },
interactive: boolean
): Promise<CommandRtnType> {
let kwargs = Object.assign({}, settings);
let kwargs: { [key: string]: any } = Object.assign({}, settings);
kwargs["nohist"] = "1";
kwargs["screen"] = screenId;
return GlobalModel.submitCommand("screen", "set", null, kwargs, interactive);
@@ -4169,6 +4190,7 @@ export {
Screen,
riToRPtr,
TabColors,
TabIcons,
RemoteColors,
getTermPtyData,
RemotesModalModel,
+2 -1
View File
@@ -50,6 +50,7 @@ type LineType = {
type ScreenOptsType = {
tabcolor?: string;
tabicon?: string;
pterm?: string;
};
@@ -167,7 +168,7 @@ type FeCmdPacketType = {
type: string;
metacmd: string;
metasubcmd?: string;
args: string[];
args: string[] | null;
kwargs: Record<string, string>;
rawstr?: string;
uicontext: UIContextType;
+1 -1
View File
@@ -390,7 +390,7 @@ function getColorRGB(colorInput) {
return computedColorStyle;
}
function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<string>) {
function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<string>) {
prtn.then((crtn) => {
if (crtn.success) {
return;
+2 -1
View File
@@ -11,6 +11,7 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
"isolatedModules": true,
"experimentalDecorators": true
}
}
+23 -2
View File
@@ -72,6 +72,7 @@ const (
)
var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "orange", "green", "red", "white"}
var TabIcons = []string{"sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
@@ -81,6 +82,7 @@ var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telem
var SetVarNameMap map[string]string = map[string]string{
"tabcolor": "screen.tabcolor",
"tabicon": "screen.tabicon",
"pterm": "screen.pterm",
"anchor": "screen.anchor",
"focus": "screen.focus",
@@ -91,7 +93,7 @@ var SetVarScopes = []SetVarScope{
SetVarScope{ScopeName: "global", VarNames: []string{}},
SetVarScope{ScopeName: "client", VarNames: []string{"telemetry"}},
SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}},
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos", "pterm", "anchor", "focus", "line"}},
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line"}},
SetVarScope{ScopeName: "line", VarNames: []string{}},
// connection = remote, remote = remoteinstance
SetVarScope{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}},
@@ -757,6 +759,16 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
varsUpdated = append(varsUpdated, "tabcolor")
setNonAnchor = true
}
if pk.Kwargs["tabicon"] != "" {
icon := pk.Kwargs["tabicon"]
err = validateIcon(icon, "screen tabicon")
if err != nil {
return nil, err
}
updateMap[sstore.ScreenField_TabIcon] = icon
varsUpdated = append(varsUpdated, "tabicon")
setNonAnchor = true
}
if pk.Kwargs["pos"] != "" {
varsUpdated = append(varsUpdated, "pos")
setNonAnchor = true
@@ -806,7 +818,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
}
}
if len(varsUpdated) == 0 {
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line", "sharename"}, "or", false))
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "tabicon", "focus", "anchor", "line", "sharename"}, "or", false))
}
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
if err != nil {
@@ -1978,6 +1990,15 @@ func validateColor(color string, typeStr string) error {
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false))
}
func validateIcon(icon string, typeStr string) error {
for _, c := range TabIcons {
if icon == c {
return nil
}
}
return fmt.Errorf("invalid %s, valid icons are: %s", typeStr, formatStrs(TabIcons, "or", false))
}
func validateRemoteColor(color string, typeStr string) error {
for _, c := range RemoteColorNames {
if color == c {
+5
View File
@@ -1709,6 +1709,7 @@ const (
ScreenField_SelectedLine = "selectedline" // int
ScreenField_Focus = "focustype" // string
ScreenField_TabColor = "tabcolor" // string
ScreenField_TabIcon = "tabicon" // string
ScreenField_PTerm = "pterm" // string
ScreenField_Name = "name" // string
ScreenField_ShareName = "sharename" // string
@@ -1743,6 +1744,10 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabcolor', ?) WHERE screenid = ?`
tx.Exec(query, tabColor, screenId)
}
if tabIcon, found := editMap[ScreenField_TabIcon]; found {
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabicon', ?) WHERE screenid = ?`
tx.Exec(query, tabIcon, screenId)
}
if pterm, found := editMap[ScreenField_PTerm]; found {
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.pterm', ?) WHERE screenid = ?`
tx.Exec(query, pterm, screenId)
+1
View File
@@ -433,6 +433,7 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
type ScreenOptsType struct {
TabColor string `json:"tabcolor,omitempty"`
TabIcon string `json:"tabicon,omitempty"`
PTerm string `json:"pterm,omitempty"`
}
+10
View File
@@ -2083,6 +2083,16 @@
dependencies:
"@types/node" "*"
"@types/sprintf-js@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/sprintf-js/-/sprintf-js-1.1.3.tgz#cdad9076d288921cea5b336ab07b915ab27f7921"
integrity sha512-Z9z6EMpwsroPp4BivsuVi/LYWi7jnuMCz9gWD/tZYhlEOSV2MO3fawrwHUEl05mshHyzHejPwe6PM0ZBDQD9yw==
"@types/throttle-debounce@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-5.0.1.tgz#8ce917e41580b2cf16f8ee840e227947f4152b04"
integrity sha512-/fifasjlhpz/r4YsH0r0ZXJvivXFB3F6bmezMnqgsn/NK/fYJn7vN84k7eYn/oALu/aenXo+t8Pv+QlkS6iYBg==
"@types/triple-beam@^1.3.2":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.3.tgz#726ae98a5f6418c8f24f9b0f2a9f81a8664876ae"