User Input (#119)

Port the User Input feature from the previous version of the app. This
is currently being used to verify a few different prompts for ssh.
This commit is contained in:
Sylvie Crowe
2024-07-18 15:21:33 -07:00
committed by GitHub
parent 01d61dabec
commit f3743f90ec
16 changed files with 438 additions and 126 deletions
+2 -3
View File
@@ -6,7 +6,7 @@
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: 100%;
z-index: 100;
background-color: rgba(21, 23, 21, 0.7);
@@ -15,8 +15,7 @@
flex-direction: column;
border-radius: 10px;
padding: 0;
width: 80vw;
height: 80vh;
width: 80%;
margin-top: 25vh;
margin-left: auto;
margin-right: auto;
+1 -1
View File
@@ -70,7 +70,7 @@ interface WaveModalProps {
function WaveModal({ title, description, onSubmit, onCancel, buttonLabel = "Ok", children }: WaveModalProps) {
return (
<Modal id="text-box-modal" onClickOut={onCancel}>
<Modal onClickOut={onCancel}>
<ModalHeader title={title} description={description} />
<ModalContent>{children}</ModalContent>
<ModalFooter>
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.userinput-body {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
font: var(--fixed-font);
color: var(--main-text-color);
.userinput-markdown {
color: var(--main-text-color);
}
.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;
&:hover {
cursor: text;
}
&:focus {
outline-color: var(--accent-color);
}
}
}
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/element/markdown";
import { WaveModal } from "@/element/modal";
import { atoms } from "@/store/global";
import * as keyutil from "@/util/keyutil";
import * as jotai from "jotai";
import * as React from "react";
import { UserInputService } from "../store/services";
import "./userinputmodal.less";
export const UserInputModal = (userInputRequest: UserInputRequest) => {
const setModals = jotai.useSetAtom(atoms.userInput);
const [responseText, setResponseText] = React.useState("");
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
const checkboxStatus = React.useRef(false);
const handleSendCancel = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
errormsg: "Canceled by the user",
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [responseText, userInputRequest]);
const handleSendText = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
checkboxstat: checkboxStatus.current,
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [responseText, userInputRequest]);
const handleSendConfirm = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: true,
checkboxstat: checkboxStatus.current,
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [userInputRequest]);
const handleSubmit = React.useCallback(() => {
switch (userInputRequest.responsetype) {
case "text":
handleSendText();
break;
case "confirm":
handleSendConfirm();
break;
}
}, [handleSendConfirm, handleSendText, userInputRequest.responsetype]);
const handleKeyDown = React.useCallback(
(waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
handleSendCancel();
return;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
handleSubmit();
return true;
}
},
[handleSendCancel, handleSubmit]
);
const queryText = React.useMemo(() => {
if (userInputRequest.markdown) {
return <Markdown text={userInputRequest.querytext} className="userinput-markdown" />;
}
return <span className="userinput-text">{userInputRequest.querytext}</span>;
}, [userInputRequest.markdown, userInputRequest.querytext]);
const inputBox = React.useMemo(() => {
if (userInputRequest.responsetype === "confirm") {
return <></>;
}
return (
<input
type={userInputRequest.publictext ? "text" : "password"}
onChange={(e) => setResponseText(e.target.value)}
value={responseText}
maxLength={400}
className="userinput-inputbox"
autoFocus={true}
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
/>
);
}, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]);
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown == 0) {
timeout = setTimeout(() => {
handleSendCancel();
}, 300);
} else {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return (
<WaveModal
title={userInputRequest.title + ` (${countdown}s)`}
onSubmit={() => handleSubmit()}
onCancel={() => handleSendCancel()}
>
<div className="userinput-body">
{queryText}
{inputBox}
</div>
</WaveModal>
);
};
+9
View File
@@ -81,6 +81,7 @@ const activeTabIdAtom: jotai.Atom<string> = jotai.atom((get) => {
}
return windowData.activetabid;
});
const userInputAtom = jotai.atom([]) as jotai.PrimitiveAtom<Array<UserInputRequest>>;
const atoms = {
// initialized in wave.ts (will not be null inside of application)
@@ -93,6 +94,7 @@ const atoms = {
settingsConfigAtom: settingsConfigAtom,
tabAtom: tabAtom,
activeTabId: activeTabIdAtom,
userInput: userInputAtom,
};
// key is "eventType" or "eventType|oref"
@@ -208,6 +210,13 @@ function handleWSEventMessage(msg: WSEventType) {
console.log("config", data);
return;
}
if (msg.eventtype == "userinput") {
// handle user input
const data: UserInputRequest = msg.data;
console.log(data);
globalStore.set(userInputAtom, (prev) => [...prev, data]);
return;
}
if (msg.eventtype == "blockfile") {
const fileData: WSFileEventData = msg.data;
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
+9
View File
@@ -126,6 +126,15 @@ class ObjectServiceType {
export const ObjectService = new ObjectServiceType();
// userinputservice.UserInputService (userinput)
class UserInputServiceType {
SendUserInputResponse(arg1: UserInputResponse): Promise<void> {
return WOS.callBackendService("userinput", "SendUserInputResponse", Array.from(arguments))
}
}
export const UserInputService = new UserInputServiceType();
// windowservice.WindowService (window)
class WindowServiceType {
// @returns object updates
+3
View File
@@ -3,6 +3,7 @@
import { TabBar } from "@/app/tab/tabbar";
import { TabContent } from "@/app/tab/tabcontent";
import { UserInputModal } from "@/element/userinputmodal";
import { atoms, createBlock } from "@/store/global";
import * as services from "@/store/services";
import * as util from "@/util/util";
@@ -106,9 +107,11 @@ const Widgets = React.memo(() => {
const WorkspaceElem = React.memo(() => {
const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData?.activetabid;
const modals = jotai.useAtomValue(atoms.userInput);
const ws = jotai.useAtomValue(atoms.workspace);
return (
<div className="workspace">
{modals.length > 0 && <UserInputModal {...modals[modals.length - 1]} />}
<TabBar key={ws.oid} workspace={ws} />
<div className="workspace-tabcontent">
{activeTabId == "" ? (
+22
View File
@@ -262,6 +262,28 @@ declare global {
activetabid: string;
};
// userinput.UserInputRequest
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
title: string;
markdown: boolean;
timeoutms: number;
checkboxmsg: string;
publictext: boolean;
};
// userinput.UserInputResponse
type UserInputResponse = {
type: string;
requestid: string;
text?: string;
confirm?: boolean;
errormsg?: string;
checkboxstat?: boolean;
};
type WSCommandType = {
wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );