Merging dev into main #49
75
src/components/MultilineTextField.tsx
Normal file
75
src/components/MultilineTextField.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import styles from "./TextField.module.css";
|
||||||
|
|
||||||
|
export function MultilineTextField({
|
||||||
|
value = "",
|
||||||
|
setValue,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
ariaLabel,
|
||||||
|
invalid = false,
|
||||||
|
minRows = 3,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
invalid?: boolean;
|
||||||
|
minRows?: number;
|
||||||
|
}) {
|
||||||
|
const [readOnly, setReadOnly] = useState(true);
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Auto-grow logic
|
||||||
|
useEffect(() => {
|
||||||
|
const el = textareaRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
el.style.height = "auto";
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
const onCommit = () => {
|
||||||
|
setReadOnly(true);
|
||||||
|
setValue(inputValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
(e.target as HTMLTextAreaElement).blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
rows={minRows}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onFocus={() => setReadOnly(false)}
|
||||||
|
onBlur={onCommit}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
readOnly={readOnly}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={`
|
||||||
|
${readOnly ? "drag" : "nodrag"}
|
||||||
|
flex-1
|
||||||
|
${styles.textField}
|
||||||
|
${styles.multiline}
|
||||||
|
${invalid ? styles.invalid : ""}
|
||||||
|
${className ?? ""}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,3 +27,13 @@
|
|||||||
.text-field:read-only:hover:not(.invalid) {
|
.text-field:read-only:hover:not(.invalid) {
|
||||||
border-color: color-mix(in srgb, canvas, #777 10%);
|
border-color: color-mix(in srgb, canvas, #777 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiline {
|
||||||
|
resize: none; /* no manual resizing */
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: hidden; /* needed for auto-grow */
|
||||||
|
max-width: 100%;
|
||||||
|
width: 95%;
|
||||||
|
min-width: 95%;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import type { GoalNodeData } from "./GoalNode";
|
|||||||
*/
|
*/
|
||||||
export const GoalNodeDefaults: GoalNodeData = {
|
export const GoalNodeDefaults: GoalNodeData = {
|
||||||
label: "Goal Node",
|
label: "Goal Node",
|
||||||
|
name: "",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
description: "",
|
description: "",
|
||||||
achieved: false,
|
achieved: false,
|
||||||
|
|||||||
@@ -10,18 +10,20 @@ import { TextField } from '../../../../components/TextField';
|
|||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { DoesPlanIterate, PlanReduce, type Plan } from '../components/Plan';
|
import { DoesPlanIterate, PlanReduce, type Plan } from '../components/Plan';
|
||||||
import PlanEditorDialog from '../components/PlanEditor';
|
import PlanEditorDialog from '../components/PlanEditor';
|
||||||
|
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data dot a phase node
|
* The default data dot a phase node
|
||||||
* @param label: the label of this phase
|
* @param label: the label of this phase
|
||||||
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
||||||
* @param desciption: description of the goal
|
* @param desciption: description of the goal - this will be checked for completion
|
||||||
* @param hasReduce: whether this node has reducing functionality (true by default)
|
* @param hasReduce: whether this node has reducing functionality (true by default)
|
||||||
* @param can_fail: whether this plan should be checked- this plan could possible fail
|
* @param can_fail: whether this plan should be checked- this plan could possible fail
|
||||||
* @param plan: The (possible) attached plan to this goal
|
* @param plan: The (possible) attached plan to this goal
|
||||||
*/
|
*/
|
||||||
export type GoalNodeData = {
|
export type GoalNodeData = {
|
||||||
label: string;
|
label: string;
|
||||||
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
achieved: boolean;
|
achieved: boolean;
|
||||||
@@ -49,6 +51,10 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
updateNodeData(id, {...data, description: value});
|
updateNodeData(id, {...data, description: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setName= (value: string) => {
|
||||||
|
updateNodeData(id, {...data, name: value})
|
||||||
|
}
|
||||||
|
|
||||||
const setFailable = (value: boolean) => {
|
const setFailable = (value: boolean) => {
|
||||||
updateNodeData(id, {...data, can_fail: value});
|
updateNodeData(id, {...data, can_fail: value});
|
||||||
}
|
}
|
||||||
@@ -60,18 +66,29 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
<label htmlFor={text_input_id}>Goal:</label>
|
<label htmlFor={text_input_id}>Goal:</label>
|
||||||
<TextField
|
<TextField
|
||||||
id={text_input_id}
|
id={text_input_id}
|
||||||
value={data.description}
|
value={data.name}
|
||||||
setValue={(val) => setDescription(val)}
|
setValue={(val) => setName(val)}
|
||||||
placeholder={"To ..."}
|
placeholder={"To ..."}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{data.can_fail && (<div>
|
||||||
|
<label htmlFor={text_input_id}>Description/ Condition of goal:</label>
|
||||||
|
<div className={"flex-wrap"}>
|
||||||
|
<MultilineTextField
|
||||||
|
id={text_input_id}
|
||||||
|
value={data.description}
|
||||||
|
setValue={setDescription}
|
||||||
|
placeholder={"Describe the condition of this goal..."}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>)}
|
||||||
<div>
|
<div>
|
||||||
<label> {!data.plan ? "No plan set to execute while goal is not reached. 🔴" : "Will follow plan '" + data.plan.name + "' until all steps complete. 🟢"} </label>
|
<label> {!data.plan ? "No plan set to execute while goal is not reached. 🔴" : "Will follow plan '" + data.plan.name + "' until all steps complete. 🟢"} </label>
|
||||||
</div>
|
</div>
|
||||||
{data.plan && (<div className={"flex-row gap-md align-center " + (planIterate ? "" : styles.planNoIterate)}>
|
{data.plan && (<div className={"flex-row gap-md align-center " + (planIterate ? "" : styles.planNoIterate)}>
|
||||||
{planIterate ? "" : <s></s>}
|
{planIterate ? "" : <s></s>}
|
||||||
<label htmlFor={checkbox_id}>{!planIterate ? "This plan always succeeds!" : ("We " + (data.can_fail ? "will " : "wont ") + "check if this plan fails")}:</label>
|
<label htmlFor={checkbox_id}>{!planIterate ? "This plan always succeeds!" : "Check if this plan fails"}:</label>
|
||||||
<input
|
<input
|
||||||
id={checkbox_id}
|
id={checkbox_id}
|
||||||
type={"checkbox"}
|
type={"checkbox"}
|
||||||
@@ -91,7 +108,7 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
plan,
|
plan,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
description={data.description}
|
description={data.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" position={Position.Right} id="GoalSource"/>
|
<Handle type="source" position={Position.Right} id="GoalSource"/>
|
||||||
@@ -109,7 +126,8 @@ export function GoalReduce(node: Node, _nodes: Node[]) {
|
|||||||
const data = node.data as GoalNodeData;
|
const data = node.data as GoalNodeData;
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: data.description,
|
name: data.label,
|
||||||
|
description: data.description,
|
||||||
can_fail: data.can_fail,
|
can_fail: data.can_fail,
|
||||||
plan: data.plan ? PlanReduce(data.plan) : "",
|
plan: data.plan ? PlanReduce(data.plan) : "",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user