From 847de1fc60d57a3291d84fac2c677f8678dc0c01 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 7 Nov 2023 16:04:25 +0800 Subject: [PATCH] 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 --- package.json | 2 + src/app/common/common.less | 77 ++++++++++- src/app/common/common.tsx | 149 ++++++++++++++++++++ src/app/common/themes/themes.less | 16 +-- src/app/workspace/screen/screenview.less | 61 ++++++++- src/app/workspace/screen/screenview.tsx | 166 ++++++++++++++++------- src/app/workspace/screen/tabs.less | 119 ++++++++++++++-- src/app/workspace/screen/tabs.tsx | 18 ++- src/model/model.ts | 32 ++++- src/types/types.ts | 3 +- src/util/util.ts | 2 +- tsconfig.json | 3 +- wavesrv/pkg/cmdrunner/cmdrunner.go | 25 +++- wavesrv/pkg/sstore/dbops.go | 5 + wavesrv/pkg/sstore/sstore.go | 1 + yarn.lock | 10 ++ 16 files changed, 600 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index db3e5da8..76545dac 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/common/common.less b/src/app/common/common.less index fdd74c29..75e84c07 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -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; } } -} - +} \ No newline at end of file diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index b7ef7b27..aacc1c4a 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -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 { + render() { + const { children, onClick } = this.props; + + return
{children}
; + } +} + +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 { + inputRef: React.RefObject; + 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) { + 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 ( +
+ {decoration?.startDecoration && <>{decoration.startDecoration}} +
+ + +
+ {decoration?.endDecoration &&
{decoration.endDecoration}
} +
+ ); + } +} + @mobxReact.observer class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> { render() { @@ -362,4 +509,6 @@ export { InfoMessage, Markdown, SettingsError, + TextField, + InputDecoration, }; diff --git a/src/app/common/themes/themes.less b/src/app/common/themes/themes.less index a5bab673..5a993b89 100644 --- a/src/app/common/themes/themes.less +++ b/src/app/common/themes/themes.less @@ -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; diff --git a/src/app/workspace/screen/screenview.less b/src/app/workspace/screen/screenview.less index 2f24eaf4..aa92926c 100644 --- a/src/app/workspace/screen/screenview.less +++ b/src/app/workspace/screen/screenview.less @@ -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; diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index 8a7a25c1..346f3a84 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -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 = mobx.IObservableValue; @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 = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" }); - + connDropdownActive: OV = mobx.observable.box(false, { name: "NewTabSettings-connDropdownActive" }); + errorMessage: OV = 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 ( + <> +
Select the icon
+
+ +
this.selectTabIcon(icon || "")} + > + +
+
+
+ + ); + } + + 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 ( + <> +
Select the color
+
+ +
this.selectTabColor(color || "")} + > + + + + +
+
+
+ + ); } 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 (
+
+
Name
+ + + + ), + }} + /> +
+
- You're connected to [{getRemoteStr(rptr)}]. Do you want to change it? + You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
- +
To change connection from the command line use `cr [alias|user@host]`
-
-
-
- Name -
-
- -
-
-
+
-
- Select the color -
-
- -
this.selectTabColor(color)}> - - - - -
-
-
+
{this.renderTabIconSelector()}
+
+
+
+
{this.renderTabColorSelector()}
); @@ -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; @@ -309,13 +375,17 @@ class ScreenWindowView extends React.Component<{ session: Session, screen: Scree
- +
-
[workspace="{session.name.get()}" screen="{screen.name.get()}"]
+
+ + [workspace="{session.name.get()}" screen="{screen.name.get()}"] + +
diff --git a/src/app/workspace/screen/tabs.less b/src/app/workspace/screen/tabs.less index 6aae3459..2a33c970 100644 --- a/src/app/workspace/screen/tabs.less +++ b/src/app/workspace/screen/tabs.less @@ -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% + ); } } diff --git a/src/app/workspace/screen/tabs.tsx b/src/app/workspace/screen/tabs.tsx index 564b8c45..f51c24ea 100644 --- a/src/app/workspace/screen/tabs.tsx +++ b/src/app/workspace/screen/tabs.tsx @@ -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 ; + } + return ( +
+ +
+ ); + }; + + renderTab(screen: Screen, activeScreenId: string, index: number): JSX.Element { let tabIndex = null; if (index + 1 <= 9) { tabIndex =
{renderCmdText(String(index + 1))}
; @@ -132,7 +144,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> { onClick={() => this.handleSwitchScreen(screen.screenId)} onContextMenu={(event) => this.openScreenSettings(event, screen)} > - + {this.renderTabIcon(screen)}
{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(); diff --git a/src/model/model.ts b/src/model/model.ts index 92d9e319..7c423036 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -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, interactive: boolean ): Promise { @@ -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 { - 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, diff --git a/src/types/types.ts b/src/types/types.ts index 9462cce0..26f1ec75 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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; rawstr?: string; uicontext: UIContextType; diff --git a/src/util/util.ts b/src/util/util.ts index ef4104ec..0407f9b2 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -390,7 +390,7 @@ function getColorRGB(colorInput) { return computedColorStyle; } -function commandRtnHandler(prtn: Promise, errorMessage: OV) { +function commandRtnHandler(prtn: Promise, errorMessage: OV) { prtn.then((crtn) => { if (crtn.success) { return; diff --git a/tsconfig.json b/tsconfig.json index 8d912820..f49c13b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "moduleResolution": "node", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "isolatedModules": true + "isolatedModules": true, + "experimentalDecorators": true } } diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index c8e6013b..f6133795 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -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 { diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index ca446a1c..a4967cf0 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -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) diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index 34a2ff04..bff9537f 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -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"` } diff --git a/yarn.lock b/yarn.lock index b946b058..c49de9ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"