mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-04 18:59:08 +01:00
Add Tips Modal for Directory (#374)
This is an experimental modal to show tips. If it helps improve discoverability, it will be improved in the future.
This commit is contained in:
parent
2715c2ce30
commit
555ab07861
@ -12,6 +12,7 @@ export const IconButton = memo(({ decl, className }: { decl: IconButtonDecl; cla
|
|||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className={clsx("iconbutton", className, decl.className, { disabled: decl.disabled })}
|
className={clsx("iconbutton", className, decl.className, { disabled: decl.disabled })}
|
||||||
title={decl.title}
|
title={decl.title}
|
||||||
|
style={{ color: decl.iconColor ?? "inherit" }}
|
||||||
>
|
>
|
||||||
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true)} /> : decl.icon}
|
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true)} /> : decl.icon}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { AboutModal } from "./about";
|
import { AboutModal } from "./about";
|
||||||
|
import { TipsModal } from "./tipsmodal";
|
||||||
import { TosModal } from "./tos";
|
import { TosModal } from "./tos";
|
||||||
import { UserInputModal } from "./userinputmodal";
|
import { UserInputModal } from "./userinputmodal";
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
|
|||||||
[TosModal.displayName || "TosModal"]: TosModal,
|
[TosModal.displayName || "TosModal"]: TosModal,
|
||||||
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
|
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
|
||||||
[AboutModal.displayName || "AboutModal"]: AboutModal,
|
[AboutModal.displayName || "AboutModal"]: AboutModal,
|
||||||
|
[TipsModal.displayName || "TipsModal"]: TipsModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {
|
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {
|
||||||
|
47
frontend/app/modals/tipsmodal.less
Normal file
47
frontend/app/modals/tipsmodal.less
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
.userinput-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinput-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 0 1rem 1rem 1rem;
|
||||||
|
|
||||||
|
font: var(--fixed-font);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
|
||||||
|
.userinput-markdown {
|
||||||
|
color: inherit;
|
||||||
|
height: 300px;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinput-text {
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinput-inputbox {
|
||||||
|
resize: none;
|
||||||
|
background-color: var(--panel-bg-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0;
|
||||||
|
border: var(--border-color);
|
||||||
|
padding: 5px 0 5px 16px;
|
||||||
|
min-height: 30px;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
frontend/app/modals/tipsmodal.tsx
Normal file
71
frontend/app/modals/tipsmodal.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { Modal } from "@/app/modals/modal";
|
||||||
|
import { Markdown } from "@/element/markdown";
|
||||||
|
import { modalsModel } from "@/store/modalmodel";
|
||||||
|
import * as keyutil from "@/util/keyutil";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import "./tipsmodal.less";
|
||||||
|
|
||||||
|
const TipsModal = (tipsContent: UserInputRequest) => {
|
||||||
|
const [responseText, setResponseText] = useState("");
|
||||||
|
const checkboxStatus = useRef(false);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
modalsModel.popModal();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(waveEvent: WaveKeyboardEvent): boolean => {
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||||
|
handleClose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryText = useMemo(() => {
|
||||||
|
if (tipsContent.markdown) {
|
||||||
|
return <Markdown text={tipsContent.querytext} className="userinput-markdown" />;
|
||||||
|
}
|
||||||
|
return <span className="userinput-text">{tipsContent.querytext}</span>;
|
||||||
|
}, [tipsContent.markdown, tipsContent.querytext]);
|
||||||
|
|
||||||
|
const inputBox = useMemo(() => {
|
||||||
|
if (tipsContent.responsetype === "confirm") {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={tipsContent.publictext ? "text" : "password"}
|
||||||
|
onChange={(e) => setResponseText(e.target.value)}
|
||||||
|
value={responseText}
|
||||||
|
maxLength={400}
|
||||||
|
className="userinput-inputbox"
|
||||||
|
autoFocus={true}
|
||||||
|
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [tipsContent.responsetype, tipsContent.publictext, responseText, handleKeyDown, setResponseText]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onOk={() => handleClose()} onCancel={() => handleClose()} onClose={() => handleClose()}>
|
||||||
|
<div className="userinput-header">{tipsContent.title}</div>
|
||||||
|
<div className="userinput-body">
|
||||||
|
{queryText}
|
||||||
|
{inputBox}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TipsModal.displayName = "TipsModal";
|
||||||
|
|
||||||
|
export { TipsModal };
|
@ -7,7 +7,8 @@ import { tryReinjectKey } from "@/app/store/keymodel";
|
|||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { Markdown } from "@/element/markdown";
|
import { Markdown } from "@/element/markdown";
|
||||||
import { NodeModel } from "@/layout/index";
|
import { NodeModel } from "@/layout/index";
|
||||||
import { createBlock, getConnStatusAtom, globalStore, refocusNode } from "@/store/global";
|
import { atoms, createBlock, getConnStatusAtom, globalStore, refocusNode } from "@/store/global";
|
||||||
|
import { modalsModel } from "@/store/modalmodel";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
@ -96,6 +97,53 @@ function isStreamingType(mimeType: string): boolean {
|
|||||||
mimeType.startsWith("image/")
|
mimeType.startsWith("image/")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const previewTipText = `
|
||||||
|
### Preview
|
||||||
|
Preview is the generic type of block used for viewing files. This can take many different forms based on the type of file being viewed.
|
||||||
|
You can use \`wsh view [path]\` from any Wave terminal window to open a preview block with the contents of the specified path (e.g. \`wsh view .\` or \`wsh view ~/myimage.jpg\`).
|
||||||
|
|
||||||
|
#### Directory
|
||||||
|
When looking at a directory, preview will show a file viewer much like MacOS' *Finder* application or Windows' *File Explorer* application. This variant is slightly more geared toward software development with the focus on seeing what is shown by the \`ls -alh\` command.
|
||||||
|
|
||||||
|
##### View a New File
|
||||||
|
The simplest way to view a new file is to double click its row in the file viewer. Alternatively, while the block is focused, you can use the ↑ and ↓ arrow keys to select a row and press enter to preview the associated file.
|
||||||
|
|
||||||
|
##### View the Parent Directory
|
||||||
|
In the directory view, this is as simple as opening the \`..\` file as if it were a regular file. This can be done with the method above. You can also use the keyboard shortcut \`Cmd + ArrowUp\`.
|
||||||
|
|
||||||
|
##### Navigate Back and Forward
|
||||||
|
When looking at a file, you can navigate back by clicking the back button in the block header or the keyboard shortcut \`Cmd + ArrowLeft\`. You can always navigate back and forward using \`Cmd + ArrowLeft\` and \`Cmd + ArrowRight\`.
|
||||||
|
|
||||||
|
##### Filter the List of Files
|
||||||
|
While the block is focused, you can filter by filename by typing a substring of the filename you're working for. To clear the filter, you can click the ✕ on the filter dropdown or press esc.
|
||||||
|
|
||||||
|
##### Sort by a File Column
|
||||||
|
To sort a file by a specific column, click on the header for that column. If you click the header again, it will reverse the sort order.
|
||||||
|
|
||||||
|
##### Hide and Show Hidden Files
|
||||||
|
At the right of the block header, there is an 👁️ button. Clicking this button hides and shows hidden files.
|
||||||
|
|
||||||
|
##### Refresh the Directory
|
||||||
|
At the right of the block header, there is a refresh button. Clicking this button refreshes the directory contents.
|
||||||
|
|
||||||
|
##### Navigate to Common Directories
|
||||||
|
At the left of the block header, there is a file icon. Clicking and holding on this icon opens a menu where you can select a common folder to navigate to. The available options are *Home*, *Desktop*, *Downloads*, and *Root*.
|
||||||
|
|
||||||
|
##### Open a New Terminal in the Current Directory
|
||||||
|
If you right click the header of the block (alternatively, click the gear icon), one of the menu items listed is **Open Terminal in New Block**. This will create a new terminal block at your current directory.
|
||||||
|
|
||||||
|
##### Open a New Terminal in a Child Directory
|
||||||
|
If you want to open a terminal for a child directory instead, you can right click on that file's row to get the **Open Terminal in New Block** option. Clicking this will open a terminal at that directory. Note that this option is only available for children that are directories.
|
||||||
|
|
||||||
|
##### Open a New Preview for a Child
|
||||||
|
To open a new Preview Block for a Child, you can right click on that file's row and select the **Open Preview in New Block** option.
|
||||||
|
|
||||||
|
#### Markdown
|
||||||
|
Opening a markdown file will bring up a view of the rendered markdown. These files cannot be edited in the preview at this time.
|
||||||
|
|
||||||
|
#### Images/Media
|
||||||
|
Opening a picture will bring up the image of that picture. Opening a video will bring up a player that lets you watch the video.
|
||||||
|
`;
|
||||||
|
|
||||||
export class PreviewModel implements ViewModel {
|
export class PreviewModel implements ViewModel {
|
||||||
viewType: string;
|
viewType: string;
|
||||||
@ -303,7 +351,34 @@ export class PreviewModel implements ViewModel {
|
|||||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||||
if (mimeType == "directory") {
|
if (mimeType == "directory") {
|
||||||
const showHiddenFiles = get(this.showHiddenFiles);
|
const showHiddenFiles = get(this.showHiddenFiles);
|
||||||
|
const settings = get(atoms.settingsAtom);
|
||||||
|
let tipIcon: IconButtonDecl[];
|
||||||
|
if (settings["tips:show"]) {
|
||||||
|
tipIcon = [
|
||||||
|
{
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "lightbulb-on",
|
||||||
|
iconColor: "var(--warning-color)",
|
||||||
|
click: () => {
|
||||||
|
const tips: UserInputRequest = {
|
||||||
|
requestid: "",
|
||||||
|
querytext: previewTipText,
|
||||||
|
responsetype: "confirm",
|
||||||
|
title: "Preview Tips",
|
||||||
|
markdown: true,
|
||||||
|
timeoutms: 0,
|
||||||
|
checkboxmsg: "",
|
||||||
|
publictext: true,
|
||||||
|
};
|
||||||
|
modalsModel.pushModal("TipsModal", tips);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
tipIcon = [];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
|
...tipIcon,
|
||||||
{
|
{
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: showHiddenFiles ? "eye" : "eye-slash",
|
icon: showHiddenFiles ? "eye" : "eye-slash",
|
||||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -154,6 +154,7 @@ declare global {
|
|||||||
type IconButtonDecl = {
|
type IconButtonDecl = {
|
||||||
elemtype: "iconbutton";
|
elemtype: "iconbutton";
|
||||||
icon: string | React.ReactNode;
|
icon: string | React.ReactNode;
|
||||||
|
iconColor?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
click?: (e: React.MouseEvent<any>) => void;
|
click?: (e: React.MouseEvent<any>) => void;
|
||||||
|
2
frontend/types/gotypes.d.ts
vendored
2
frontend/types/gotypes.d.ts
vendored
@ -421,6 +421,8 @@ declare global {
|
|||||||
"window:tilegapsize"?: number;
|
"window:tilegapsize"?: number;
|
||||||
"telemetry:*"?: boolean;
|
"telemetry:*"?: boolean;
|
||||||
"telemetry:enabled"?: boolean;
|
"telemetry:enabled"?: boolean;
|
||||||
|
"tips:*"?: boolean;
|
||||||
|
"tips:show"?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// waveobj.StickerClickOptsType
|
// waveobj.StickerClickOptsType
|
||||||
|
@ -7,5 +7,6 @@
|
|||||||
"autoupdate:intervalms": 3600000,
|
"autoupdate:intervalms": 3600000,
|
||||||
"editor:minimapenabled": true,
|
"editor:minimapenabled": true,
|
||||||
"window:tilegapsize": 3,
|
"window:tilegapsize": 3,
|
||||||
"telemetry:enabled": true
|
"telemetry:enabled": true,
|
||||||
|
"tips:show": true
|
||||||
}
|
}
|
||||||
|
@ -46,5 +46,8 @@ const (
|
|||||||
|
|
||||||
ConfigKey_TelemetryClear = "telemetry:*"
|
ConfigKey_TelemetryClear = "telemetry:*"
|
||||||
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
||||||
|
|
||||||
|
ConfigKey_TipsClear = "tips:*"
|
||||||
|
ConfigKey_TipsShow = "tips:show"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,6 +78,9 @@ type SettingsType struct {
|
|||||||
|
|
||||||
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
||||||
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
||||||
|
|
||||||
|
TipsClear bool `json:"tips:*,omitempty"`
|
||||||
|
TipsShow bool `json:"tips:show,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigError struct {
|
type ConfigError struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user