From 555ab078618444195af6b6bc2c94bcee0b785bc8 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Fri, 13 Sep 2024 03:36:15 -0700 Subject: [PATCH] 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. --- frontend/app/element/iconbutton.tsx | 1 + frontend/app/modals/modalregistry.tsx | 2 + frontend/app/modals/tipsmodal.less | 47 +++++++++++++++ frontend/app/modals/tipsmodal.tsx | 71 +++++++++++++++++++++++ frontend/app/view/preview/preview.tsx | 77 ++++++++++++++++++++++++- frontend/types/custom.d.ts | 1 + frontend/types/gotypes.d.ts | 2 + pkg/wconfig/defaultconfig/settings.json | 3 +- pkg/wconfig/metaconsts.go | 3 + pkg/wconfig/settingsconfig.go | 3 + 10 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 frontend/app/modals/tipsmodal.less create mode 100644 frontend/app/modals/tipsmodal.tsx diff --git a/frontend/app/element/iconbutton.tsx b/frontend/app/element/iconbutton.tsx index 88a130138..772eb55ef 100644 --- a/frontend/app/element/iconbutton.tsx +++ b/frontend/app/element/iconbutton.tsx @@ -12,6 +12,7 @@ export const IconButton = memo(({ decl, className }: { decl: IconButtonDecl; cla ref={buttonRef} className={clsx("iconbutton", className, decl.className, { disabled: decl.disabled })} title={decl.title} + style={{ color: decl.iconColor ?? "inherit" }} > {typeof decl.icon === "string" ? : decl.icon} diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx index f77d790f2..cde96919f 100644 --- a/frontend/app/modals/modalregistry.tsx +++ b/frontend/app/modals/modalregistry.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AboutModal } from "./about"; +import { TipsModal } from "./tipsmodal"; import { TosModal } from "./tos"; import { UserInputModal } from "./userinputmodal"; @@ -9,6 +10,7 @@ const modalRegistry: { [key: string]: React.ComponentType } = { [TosModal.displayName || "TosModal"]: TosModal, [UserInputModal.displayName || "UserInputModal"]: UserInputModal, [AboutModal.displayName || "AboutModal"]: AboutModal, + [TipsModal.displayName || "TipsModal"]: TipsModal, }; export const getModalComponent = (key: string): React.ComponentType | undefined => { diff --git a/frontend/app/modals/tipsmodal.less b/frontend/app/modals/tipsmodal.less new file mode 100644 index 000000000..631812a89 --- /dev/null +++ b/frontend/app/modals/tipsmodal.less @@ -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); + } + } +} diff --git a/frontend/app/modals/tipsmodal.tsx b/frontend/app/modals/tipsmodal.tsx new file mode 100644 index 000000000..9f9d0098c --- /dev/null +++ b/frontend/app/modals/tipsmodal.tsx @@ -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 ; + } + return {tipsContent.querytext}; + }, [tipsContent.markdown, tipsContent.querytext]); + + const inputBox = useMemo(() => { + if (tipsContent.responsetype === "confirm") { + return <>; + } + return ( + 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 ( + handleClose()} onCancel={() => handleClose()} onClose={() => handleClose()}> +
{tipsContent.title}
+
+ {queryText} + {inputBox} +
+
+ ); +}; + +TipsModal.displayName = "TipsModal"; + +export { TipsModal }; diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index ea671403d..dfda2d061 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -7,7 +7,8 @@ import { tryReinjectKey } from "@/app/store/keymodel"; import { WshServer } from "@/app/store/wshserver"; import { Markdown } from "@/element/markdown"; 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 WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; @@ -96,6 +97,53 @@ function isStreamingType(mimeType: string): boolean { 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 { viewType: string; @@ -303,7 +351,34 @@ export class PreviewModel implements ViewModel { const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit"; if (mimeType == "directory") { 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 [ + ...tipIcon, { elemtype: "iconbutton", icon: showHiddenFiles ? "eye" : "eye-slash", diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 9118844ad..cd79683c4 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -154,6 +154,7 @@ declare global { type IconButtonDecl = { elemtype: "iconbutton"; icon: string | React.ReactNode; + iconColor?: string; className?: string; title?: string; click?: (e: React.MouseEvent) => void; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c1fb6a82c..a6b96e646 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -421,6 +421,8 @@ declare global { "window:tilegapsize"?: number; "telemetry:*"?: boolean; "telemetry:enabled"?: boolean; + "tips:*"?: boolean; + "tips:show"?: boolean; }; // waveobj.StickerClickOptsType diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index fcb805cb1..c65a140eb 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -7,5 +7,6 @@ "autoupdate:intervalms": 3600000, "editor:minimapenabled": true, "window:tilegapsize": 3, - "telemetry:enabled": true + "telemetry:enabled": true, + "tips:show": true } diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index d5cb9830e..92c31ae34 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -46,5 +46,8 @@ const ( ConfigKey_TelemetryClear = "telemetry:*" ConfigKey_TelemetryEnabled = "telemetry:enabled" + + ConfigKey_TipsClear = "tips:*" + ConfigKey_TipsShow = "tips:show" ) diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 691e15983..7f1da030b 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -78,6 +78,9 @@ type SettingsType struct { TelemetryClear bool `json:"telemetry:*,omitempty"` TelemetryEnabled bool `json:"telemetry:enabled,omitempty"` + + TipsClear bool `json:"tips:*,omitempty"` + TipsShow bool `json:"tips:show,omitempty"` } type ConfigError struct {