fix(GenericNode): import missing InputComponent and Textarea components

feat(GenericNode): add support for editing node name and description
fix(InputComponent): add autoFocus and onBlur props
fix(tabsContext): update sourceHandle object in edge data
fix(PageComponent): generate random name for new group node
fix(reactflowUtils): update handleKeyDown function to accept HTMLTextAreaElement
This commit is contained in:
anovazzi1 2023-09-14 19:01:09 -03:00
commit f84df04f8a
6 changed files with 93 additions and 22 deletions

View file

@ -10,10 +10,12 @@ import { typesContext } from "../../contexts/typesContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components"; import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { cleanEdges, getGroupStatus, scapedJSONStringfy } from "../../utils/reactflowUtils"; import { cleanEdges, getGroupStatus, handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils"; import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils"; import { classNames, toTitleCase } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
export default function GenericNode({ export default function GenericNode({
data: olddata, data: olddata,
@ -31,6 +33,12 @@ export default function GenericNode({
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance } = useContext(typesContext); const { types, deleteNode, reactFlowInstance } = useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(true);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
// State for outline color // State for outline color
@ -58,7 +66,7 @@ export default function GenericNode({
// New useEffect to watch for changes in sseData and update validation status // New useEffect to watch for changes in sseData and update validation status
useEffect(() => { useEffect(() => {
const relevantData = data.node?.flow?getGroupStatus(data.node.flow,sseData):sseData[data.id]; const relevantData = data.node?.flow ? getGroupStatus(data.node.flow, sseData) : sseData[data.id];
if (relevantData) { if (relevantData) {
// Extract validation information from relevantData and update the validationStatus state // Extract validation information from relevantData and update the validationStatus state
setValidationStatus(relevantData); setValidationStatus(relevantData);
@ -97,11 +105,31 @@ export default function GenericNode({
iconColor={`${nodeColors[types[data.type]]}`} iconColor={`${nodeColors[types[data.type]]}`}
/> />
<div className="generic-node-tooltip-div"> <div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}> {data.node?.flow && inputName ?
<div className="generic-node-tooltip-div text-primary"> <div >
{data.node?.display_name} <InputComponent
</div> autoFocus
</ShadTooltip> onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div> : <ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary" onDoubleClick={() => setInputName(true)}>
{nodeName}
</div>
</ShadTooltip>
}
</div> </div>
</div> </div>
<div className="round-button-div"> <div className="round-button-div">
@ -123,10 +151,10 @@ export default function GenericNode({
<div className="max-h-96 overflow-auto"> <div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string" {typeof validationStatus.params === "string"
? validationStatus.params ? validationStatus.params
.split("\n") .split("\n")
.map((line: string, index: number) => ( .map((line: string, index: number) => (
<div key={index}>{line}</div> <div key={index}>{line}</div>
)) ))
: ""} : ""}
</div> </div>
) )
@ -164,7 +192,35 @@ export default function GenericNode({
</div> </div>
<div className="generic-node-desc"> <div className="generic-node-desc">
<div className="generic-node-desc-text">{data.node?.description}</div> {data.node?.flow && inputDescription ?
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}}
value={nodeDescription}
onChange={(e)=>setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if(e.key === "Enter" && e.shiftKey === false && e.ctrlKey === false && e.altKey === false){
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}
}}
/> :
<div className="generic-node-desc-text" onDoubleClick={() => setInputDescription(true)}>{nodeDescription}</div>
}
<> <>
{Object.keys(data.node!.template) {Object.keys(data.node!.template)
@ -172,7 +228,7 @@ export default function GenericNode({
.map((templateField: string, idx) => ( .map((templateField: string, idx) => (
<div key={idx}> <div key={idx}>
{data.node!.template[templateField].show && {data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? ( !data.node!.template[templateField].advanced ? (
<ParameterComponent <ParameterComponent
key={scapedJSONStringfy({ key={scapedJSONStringfy({
inputTypes: inputTypes:
@ -186,7 +242,7 @@ export default function GenericNode({
setData={setData} setData={setData}
color={ color={
nodeColors[ nodeColors[
types[data.node?.template[templateField].type!] types[data.node?.template[templateField].type!]
] ?? ] ??
nodeColors[data.node?.template[templateField].type!] ?? nodeColors[data.node?.template[templateField].type!] ??
nodeColors.unknown nodeColors.unknown
@ -195,8 +251,8 @@ export default function GenericNode({
data.node?.template[templateField].display_name data.node?.template[templateField].display_name
? data.node.template[templateField].display_name ? data.node.template[templateField].display_name
: data.node?.template[templateField].name : data.node?.template[templateField].name
? toTitleCase(data.node.template[templateField].name) ? toTitleCase(data.node.template[templateField].name)
: toTitleCase(templateField) : toTitleCase(templateField)
} }
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils"; import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
export default function InputComponent({ export default function InputComponent({
autoFocus =false,
onBlur,
value, value,
onChange, onChange,
disabled, disabled,
@ -15,9 +17,10 @@ export default function InputComponent({
editNode = false, editNode = false,
placeholder = "Type something...", placeholder = "Type something...",
className, className,
blurOnEnter=false
}: InputComponentType): JSX.Element { }: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -30,6 +33,9 @@ export default function InputComponent({
{isForm ? ( {isForm ? (
<Form.Control asChild> <Form.Control asChild>
<Input <Input
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"} type={password && !pwdVisible ? "password" : "text"}
value={value} value={value}
disabled={disabled} disabled={disabled}
@ -49,13 +55,17 @@ export default function InputComponent({
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if(blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
</Form.Control> </Form.Control>
) : ( ) : (
<Input <Input
ref={refInput}
type="text" type="text"
onBlur={onBlur}
value={value} value={value}
autoFocus={autoFocus}
disabled={disabled} disabled={disabled}
required={required} required={required}
className={classNames( className={classNames(
@ -73,6 +83,7 @@ export default function InputComponent({
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if(blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
)} )}

View file

@ -230,6 +230,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
edge.sourceHandle! edge.sourceHandle!
); );
sourceHandleObject.baseClasses = template["base_classes"]; sourceHandleObject.baseClasses = template["base_classes"];
edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject); edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
} }
}); });

View file

@ -42,7 +42,7 @@ import {
scapeJSONParse, scapeJSONParse,
validateSelection, validateSelection,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { isWrappedWithClass } from "../../../../utils/utils"; import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent"; import ConnectionLineComponent from "../ConnectionLineComponent";
import ExtraSidebar from "../extraSidebarComponent"; import ExtraSidebar from "../extraSidebarComponent";
import SelectionMenu from "../SelectionMenuComponent"; import SelectionMenu from "../SelectionMenuComponent";
@ -447,7 +447,7 @@ export default function Page({
<SelectionMenu isVisible={selectionMenuVisible} nodes={lastSelection?.nodes} <SelectionMenu isVisible={selectionMenuVisible} nodes={lastSelection?.nodes}
onClick={()=>{ onClick={()=>{
if(validateSelection(lastSelection!).length===0){ if(validateSelection(lastSelection!).length===0){
const {newFlow} = generateFlow(lastSelection!,reactFlowInstance!,"new Component"); const {newFlow} = generateFlow(lastSelection!,reactFlowInstance!,getRandomName());
const newGroupNode = generateNodeFromFlow(newFlow) const newGroupNode = generateNodeFromFlow(newFlow)
setNodes((oldNodes)=>[...oldNodes.filter((oldNodes)=>!lastSelection?.nodes.some((selectionNode)=>selectionNode.id===oldNodes.id)),newGroupNode]) setNodes((oldNodes)=>[...oldNodes.filter((oldNodes)=>!lastSelection?.nodes.some((selectionNode)=>selectionNode.id===oldNodes.id)),newGroupNode])
setEdges((oldEdges) => setEdges((oldEdges) =>

View file

@ -6,6 +6,8 @@ import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
import { typesContextType } from "../typesContext"; import { typesContextType } from "../typesContext";
import { sourceHandleType, targetHandleType } from "./../flow/index"; import { sourceHandleType, targetHandleType } from "./../flow/index";
export type InputComponentType = { export type InputComponentType = {
autoFocus?: boolean;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
value: string; value: string;
disabled?: boolean; disabled?: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
@ -17,6 +19,7 @@ export type InputComponentType = {
showPass?: boolean; showPass?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
blurOnEnter?: boolean;
}; };
export type ToggleComponentType = { export type ToggleComponentType = {
enabled: boolean; enabled: boolean;

View file

@ -315,7 +315,7 @@ export function updateEdgesHandleIds({
} }
export function handleKeyDown( export function handleKeyDown(
e: React.KeyboardEvent<HTMLInputElement>, e: React.KeyboardEvent<HTMLInputElement |HTMLTextAreaElement>,
inputValue: string | string[] | null, inputValue: string | string[] | null,
block: string block: string
) { ) {
@ -648,7 +648,7 @@ export function generateNodeFromFlow(flow: FlowType): NodeType {
display_name: "group Node", display_name: "group Node",
documentation: "", documentation: "",
base_classes: outputNode!.data.node!.base_classes, base_classes: outputNode!.data.node!.base_classes,
description: "group Node", description: "double click to edit description",
template: generateNodeTemplate(data), template: generateNodeTemplate(data),
flow: data, flow: data,
}, },