fix: fixed scrolling behavior inside editor when plan editor window is opened
ref: N25B-412
This commit is contained in:
@@ -48,7 +48,8 @@ const selector = (state: FlowState) => ({
|
|||||||
undo: state.undo,
|
undo: state.undo,
|
||||||
redo: state.redo,
|
redo: state.redo,
|
||||||
beginBatchAction: state.beginBatchAction,
|
beginBatchAction: state.beginBatchAction,
|
||||||
endBatchAction: state.endBatchAction
|
endBatchAction: state.endBatchAction,
|
||||||
|
scrollable: state.scrollable
|
||||||
});
|
});
|
||||||
|
|
||||||
// --| define ReactFlow editor |--
|
// --| define ReactFlow editor |--
|
||||||
@@ -72,7 +73,8 @@ const VisProgUI = () => {
|
|||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
beginBatchAction,
|
beginBatchAction,
|
||||||
endBatchAction
|
endBatchAction,
|
||||||
|
scrollable
|
||||||
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
||||||
|
|
||||||
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
||||||
@@ -101,6 +103,7 @@ const VisProgUI = () => {
|
|||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDragStart={beginBatchAction}
|
onNodeDragStart={beginBatchAction}
|
||||||
onNodeDragStop={endBatchAction}
|
onNodeDragStop={endBatchAction}
|
||||||
|
preventScrolling={scrollable}
|
||||||
snapToGrid
|
snapToGrid
|
||||||
fitView
|
fitView
|
||||||
proOptions={{hideAttribution: true}}
|
proOptions={{hideAttribution: true}}
|
||||||
|
|||||||
@@ -72,6 +72,15 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
nodes: initialNodes,
|
nodes: initialNodes,
|
||||||
edges: initialEdges,
|
edges: initialEdges,
|
||||||
edgeReconnectSuccessful: true,
|
edgeReconnectSuccessful: true,
|
||||||
|
scrollable: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles changing the scrollable state of the editor,
|
||||||
|
* this is used to control if scrolling is captured by the editor
|
||||||
|
* or if it's available to other components within the reactFlowProvider
|
||||||
|
* @param {boolean} val - the desired state
|
||||||
|
*/
|
||||||
|
setScrollable: (val) => set({scrollable: val}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles changes to nodes triggered by ReactFlow.
|
* Handles changes to nodes triggered by ReactFlow.
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export type FlowState = {
|
|||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
edgeReconnectSuccessful: boolean;
|
edgeReconnectSuccessful: boolean;
|
||||||
|
scrollable: boolean;
|
||||||
|
|
||||||
|
/** Handler for managing scrollable state */
|
||||||
|
setScrollable: (value: boolean) => void;
|
||||||
|
|
||||||
/** Handler for changes to nodes triggered by ReactFlow */
|
/** Handler for changes to nodes triggered by ReactFlow */
|
||||||
onNodesChange: OnNodesChange;
|
onNodesChange: OnNodesChange;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import {useRef, useState} from "react";
|
||||||
|
import useFlowStore from "../VisProgStores.tsx";
|
||||||
import styles from './PlanEditor.module.css';
|
import styles from './PlanEditor.module.css';
|
||||||
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
|
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
|
||||||
import { defaultPlan } from "../components/Plan.default";
|
import { defaultPlan } from "../components/Plan.default";
|
||||||
@@ -16,220 +17,225 @@ export default function PlanEditorDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
description,
|
description,
|
||||||
}: PlanEditorDialogProps) {
|
}: PlanEditorDialogProps) {
|
||||||
// UseStates and references
|
// UseStates and references
|
||||||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
||||||
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
||||||
const [newActionValue, setNewActionValue] = useState("");
|
const [newActionValue, setNewActionValue] = useState("");
|
||||||
|
const { setScrollable } = useFlowStore();
|
||||||
|
|
||||||
//Button Actions
|
//Button Actions
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
setScrollable(false);
|
||||||
dialogRef.current?.showModal();
|
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
||||||
};
|
dialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateWithDescription = () => {
|
const openCreateWithDescription = () => {
|
||||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
|
setScrollable(false);
|
||||||
setNewActionType("llm")
|
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
|
||||||
setNewActionValue(description!)
|
setNewActionType("llm")
|
||||||
dialogRef.current?.showModal();
|
setNewActionValue(description!)
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = () => {
|
||||||
|
setScrollable(false);
|
||||||
|
if (!plan) return;
|
||||||
|
setDraftPlan(structuredClone(plan));
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setScrollable(true);
|
||||||
|
dialogRef.current?.close();
|
||||||
|
setDraftPlan(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildAction = (): Action => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
switch (newActionType) {
|
||||||
|
case "speech":
|
||||||
|
return { id, text: newActionValue, type: "speech" };
|
||||||
|
case "gesture":
|
||||||
|
return { id, gesture: newActionValue, type: "gesture" };
|
||||||
|
case "llm":
|
||||||
|
return { id, goal: newActionValue, type: "llm" };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openEdit = () => {
|
return (<>
|
||||||
if (!plan) return;
|
{/* Create and edit buttons */}
|
||||||
setDraftPlan(structuredClone(plan));
|
{!plan && (
|
||||||
dialogRef.current?.showModal();
|
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
|
||||||
};
|
Create Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{plan && (
|
||||||
|
<button className={styles.nodeButton} onClick={openEdit}>
|
||||||
|
Edit Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
const close = () => {
|
{/* Start of dialog (plan editor) */}
|
||||||
dialogRef.current?.close();
|
<dialog
|
||||||
setDraftPlan(null);
|
ref={dialogRef}
|
||||||
};
|
className={`${styles.planDialog}`}
|
||||||
|
//onWheel={(e) => e.stopPropagation()}
|
||||||
|
data-testid={"PlanEditorDialogTestID"}
|
||||||
|
>
|
||||||
|
<form method="dialog" className="flex-col gap-md">
|
||||||
|
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
|
||||||
|
{/* Plan name text field */}
|
||||||
|
{draftPlan && (
|
||||||
|
<TextField
|
||||||
|
value={draftPlan.name}
|
||||||
|
setValue={(name) =>
|
||||||
|
setDraftPlan({ ...draftPlan, name })}
|
||||||
|
placeholder="Plan name"
|
||||||
|
data-testid="name_text_field"/>
|
||||||
|
)}
|
||||||
|
|
||||||
const buildAction = (): Action => {
|
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
|
||||||
const id = crypto.randomUUID();
|
{draftPlan && (<div className={styles.planEditor}>
|
||||||
switch (newActionType) {
|
<div className={styles.planEditorLeft}>
|
||||||
case "speech":
|
{/* Left Side (Action Adder) */}
|
||||||
return { id, text: newActionValue, type: "speech" };
|
<h4>Add Action</h4>
|
||||||
case "gesture":
|
{(!plan && description && draftPlan.steps.length === 0) && (<div className={styles.stepSuggestion}>
|
||||||
return { id, gesture: newActionValue, type: "gesture" };
|
<label> Filled in as a suggestion! </label>
|
||||||
case "llm":
|
<label> Feel free to change! </label>
|
||||||
return { id, goal: newActionValue, type: "llm" };
|
</div>)}
|
||||||
}
|
<label>
|
||||||
};
|
Action Type <wbr />
|
||||||
|
{/* Type selection */}
|
||||||
|
<select
|
||||||
|
value={newActionType}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewActionType(e.target.value as ActionTypes);
|
||||||
|
// Reset value when action type changes
|
||||||
|
setNewActionValue("");
|
||||||
|
}}>
|
||||||
|
<option value="speech">Speech Action</option>
|
||||||
|
<option value="gesture">Gesture Action</option>
|
||||||
|
<option value="llm">LLM Action</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
return (<>
|
{/* Action value editor*/}
|
||||||
{/* Create and edit buttons */}
|
{newActionType === "gesture" ? (
|
||||||
{!plan && (
|
// Gesture get their own editor component
|
||||||
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
|
<GestureValueEditor
|
||||||
Create Plan
|
value={newActionValue}
|
||||||
</button>
|
setValue={setNewActionValue}
|
||||||
)}
|
placeholder="Gesture name"
|
||||||
{plan && (
|
/>
|
||||||
<button className={styles.nodeButton} onClick={openEdit}>
|
) : (
|
||||||
Edit Plan
|
<TextField
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Start of dialog (plan editor) */}
|
|
||||||
<dialog
|
|
||||||
ref={dialogRef}
|
|
||||||
className={`${styles.planDialog}`}
|
|
||||||
onWheel={(e) => e.stopPropagation()}
|
|
||||||
data-testid={"PlanEditorDialogTestID"}
|
|
||||||
>
|
|
||||||
<form method="dialog" className="flex-col gap-md">
|
|
||||||
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
|
|
||||||
{/* Plan name text field */}
|
|
||||||
{draftPlan && (
|
|
||||||
<TextField
|
|
||||||
value={draftPlan.name}
|
|
||||||
setValue={(name) =>
|
|
||||||
setDraftPlan({ ...draftPlan, name })}
|
|
||||||
placeholder="Plan name"
|
|
||||||
data-testid="name_text_field"/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
|
|
||||||
{draftPlan && (<div className={styles.planEditor}>
|
|
||||||
<div className={styles.planEditorLeft}>
|
|
||||||
{/* Left Side (Action Adder) */}
|
|
||||||
<h4>Add Action</h4>
|
|
||||||
{(!plan && description && draftPlan.steps.length === 0) && (<div className={styles.stepSuggestion}>
|
|
||||||
<label> Filled in as a suggestion! </label>
|
|
||||||
<label> Feel free to change! </label>
|
|
||||||
</div>)}
|
|
||||||
<label>
|
|
||||||
Action Type <wbr />
|
|
||||||
{/* Type selection */}
|
|
||||||
<select
|
|
||||||
value={newActionType}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNewActionType(e.target.value as ActionTypes);
|
|
||||||
// Reset value when action type changes
|
|
||||||
setNewActionValue("");
|
|
||||||
}}>
|
|
||||||
<option value="speech">Speech Action</option>
|
|
||||||
<option value="gesture">Gesture Action</option>
|
|
||||||
<option value="llm">LLM Action</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Action value editor*/}
|
|
||||||
{newActionType === "gesture" ? (
|
|
||||||
// Gesture get their own editor component
|
|
||||||
<GestureValueEditor
|
|
||||||
value={newActionValue}
|
value={newActionValue}
|
||||||
setValue={setNewActionValue}
|
setValue={setNewActionValue}
|
||||||
placeholder="Gesture name"
|
placeholder={
|
||||||
/>
|
newActionType === "speech" ? "Speech text"
|
||||||
) : (
|
: "LLM goal"
|
||||||
<TextField
|
}
|
||||||
value={newActionValue}
|
/>
|
||||||
setValue={setNewActionValue}
|
)}
|
||||||
placeholder={
|
|
||||||
newActionType === "speech" ? "Speech text"
|
|
||||||
: "LLM goal"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Adding steps */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!newActionValue}
|
|
||||||
onClick={() => {
|
|
||||||
if (!draftPlan) return;
|
|
||||||
// Add action to steps
|
|
||||||
const action = buildAction();
|
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: [...draftPlan.steps, action],});
|
|
||||||
|
|
||||||
// Reset current action building
|
|
||||||
setNewActionValue("");
|
|
||||||
setNewActionType("speech");
|
|
||||||
}}>
|
|
||||||
Add Step
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Side (Steps shown) */}
|
|
||||||
<div className={styles.planEditorRight}>
|
|
||||||
<h4>Steps</h4>
|
|
||||||
|
|
||||||
{/* Show if there are no steps yet */}
|
{/* Adding steps */}
|
||||||
{draftPlan.steps.length === 0 && (
|
<button
|
||||||
<div className={styles.emptySteps}>
|
type="button"
|
||||||
No steps yet
|
disabled={!newActionValue}
|
||||||
</div>
|
onClick={() => {
|
||||||
)}
|
if (!draftPlan) return;
|
||||||
|
// Add action to steps
|
||||||
|
const action = buildAction();
|
||||||
{/* Map over all steps */}
|
setDraftPlan({
|
||||||
{draftPlan.steps.map((step, index) => (
|
...draftPlan,
|
||||||
<div
|
steps: [...draftPlan.steps, action],});
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
// Reset current action building
|
||||||
key={step.id}
|
setNewActionValue("");
|
||||||
className={styles.planStep}
|
setNewActionType("speech");
|
||||||
// Extra logic for screen readers to access using keyboard
|
}}>
|
||||||
onKeyDown={(e) => {
|
Add Step
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
</button>
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
|
||||||
}}}
|
|
||||||
onClick={() => {
|
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<span className={styles.stepIndex}>{index + 1}.</span>
|
|
||||||
<span className={styles.stepType}>{step.type}:</span>
|
|
||||||
<span className={styles.stepName}>{
|
|
||||||
step.type == "goal" ? ""/* TODO: Add support for goals */
|
|
||||||
: GetActionValue(step)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Right Side (Steps shown) */}
|
||||||
<div className="flex-row gap-md">
|
<div className={styles.planEditorRight}>
|
||||||
{/* Close button */}
|
<h4>Steps</h4>
|
||||||
<button type="button" onClick={close}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Confirm/ Create button */}
|
{/* Show if there are no steps yet */}
|
||||||
<button
|
{draftPlan.steps.length === 0 && (
|
||||||
type="button"
|
<div className={styles.emptySteps}>
|
||||||
disabled={!draftPlan}
|
No steps yet
|
||||||
onClick={() => {
|
</div>
|
||||||
if (!draftPlan) return;
|
)}
|
||||||
onSave(draftPlan);
|
|
||||||
close();
|
|
||||||
}}>
|
|
||||||
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Reset button */}
|
|
||||||
<button
|
{/* Map over all steps */}
|
||||||
type="button"
|
{draftPlan.steps.map((step, index) => (
|
||||||
disabled={!draftPlan}
|
<div
|
||||||
onClick={() => {
|
role="button"
|
||||||
onSave(undefined);
|
tabIndex={0}
|
||||||
close();
|
key={step.id}
|
||||||
}}>
|
className={styles.planStep}
|
||||||
Reset
|
// Extra logic for screen readers to access using keyboard
|
||||||
</button>
|
onKeyDown={(e) => {
|
||||||
</div>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
</form>
|
setDraftPlan({
|
||||||
</dialog>
|
...draftPlan,
|
||||||
</>
|
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
||||||
);
|
}}}
|
||||||
|
onClick={() => {
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<span className={styles.stepIndex}>{index + 1}.</span>
|
||||||
|
<span className={styles.stepType}>{step.type}:</span>
|
||||||
|
<span className={styles.stepName}>{
|
||||||
|
step.type == "goal" ? ""/* TODO: Add support for goals */
|
||||||
|
: GetActionValue(step)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex-row gap-md">
|
||||||
|
{/* Close button */}
|
||||||
|
<button type="button" onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Confirm/ Create button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!draftPlan}
|
||||||
|
onClick={() => {
|
||||||
|
if (!draftPlan) return;
|
||||||
|
onSave(draftPlan);
|
||||||
|
close();
|
||||||
|
}}>
|
||||||
|
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Reset button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!draftPlan}
|
||||||
|
onClick={() => {
|
||||||
|
onSave(undefined);
|
||||||
|
close();
|
||||||
|
}}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user