go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/static/_nextjs/src/components/dialogAddNode.tsx (about) 1 /** 2 * Copyright (c) 2024 - Present. Will Charczuk. All rights reserved. 3 * Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 4 */ 5 import { 6 Button, 7 Dialog, 8 DialogBody, 9 DialogFooter, 10 FormGroup, 11 Intent, 12 Tooltip, 13 getKeyCombo, 14 } from "@blueprintjs/core"; 15 import { useEffect, useState } from "react"; 16 import * as api from "../api/nodes"; 17 import { nodeTypes, nodeTypesWithName } from "../refdata/nodeTypes"; 18 import { EditNode } from "./editNode"; 19 import { SelectNodeType } from "./selectNodeType"; 20 import { MODIFIER_BIT_MASKS } from "@blueprintjs/core/lib/esm/components/hotkeys/hotkeyParser"; 21 import { XYPosition } from "reactflow"; 22 23 export interface DialogAddNodeProps { 24 nodeType: string | null; 25 nodePosition?: XYPosition, 26 isOpen: boolean; 27 onClose: () => void; 28 doAddNode: (nodeType: string, node: api.Node, value: any) => void; 29 } 30 31 const defaultNodeType = 'var'; 32 33 export function DialogAddNode(props: DialogAddNodeProps) { 34 const [nodeType, setNodeType] = useState(props.nodeType !== undefined ? props.nodeType : defaultNodeType); 35 36 const emptyNode = { 37 id: '', 38 label: '', 39 metadata: { 40 node_type: defaultNodeType, 41 position_x: props.nodePosition?.x !== undefined ? props.nodePosition?.x : 0, 42 position_y: props.nodePosition?.y !== undefined ? props.nodePosition?.y : 0, 43 } 44 } 45 const [node, setNode] = useState<api.Node>(emptyNode); 46 const [value, setValue] = useState<api.NativeValueType>(''); 47 48 useEffect(() => { 49 if (props.nodeType) { 50 setNodeType(props.nodeType); 51 onNodeTypeChange(props.nodeType); 52 } 53 if (props.nodePosition) { 54 setNode({ 55 ...node, 56 metadata: { 57 ...node.metadata, 58 position_x: props.nodePosition?.x !== undefined ? props.nodePosition?.x : 0, 59 position_y: props.nodePosition?.y !== undefined ? props.nodePosition?.y : 0, 60 } 61 }) 62 } 63 }, [props.nodeType, props.nodePosition]) 64 65 const onNodeTypeChange = (nodeType: string) => { 66 const nodeTypeInfo = nodeTypes[nodeType]; 67 const exclusiveInputType = nodeTypeInfo.inputTypes?.length === 1 ? nodeTypeInfo.inputTypes[0] : undefined; 68 const exclusiveOutputType = nodeTypeInfo.outputTypes?.length === 1 ? nodeTypeInfo.outputTypes[0] : undefined; 69 setNodeType(nodeType); 70 setNode({ 71 ...node, 72 metadata: { 73 ...node.metadata, 74 node_type: nodeType, 75 input_type: exclusiveInputType !== undefined ? exclusiveInputType : node.metadata.input_type, 76 output_type: exclusiveOutputType !== undefined ? exclusiveOutputType : node.metadata.output_type, 77 }, 78 }) 79 } 80 const onNodeChanged = (n: api.Node) => { 81 setNode(n); 82 } 83 const onValueChanged = (v: api.NativeValueType) => { 84 setValue(v); 85 } 86 87 const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { 88 const combo = getKeyCombo(e.nativeEvent); 89 if (combo.key === "enter" && combo.modifiers === MODIFIER_BIT_MASKS['shift']) { 90 e.preventDefault(); 91 e.stopPropagation(); 92 if (!nodeType) { 93 throw new Error("node type is required to submit") 94 } 95 props.doAddNode(nodeType, node, value); 96 } 97 } 98 99 const onSaveClick = () => { 100 if (!nodeType) { 101 // do validation here later. 102 throw new Error("node type is required") 103 } 104 props.doAddNode(nodeType, node, value) 105 } 106 107 return ( 108 <div onKeyDown={handleKeyDown}> 109 <Dialog 110 title="Add Node" 111 icon="add" 112 autoFocus={false} 113 enforceFocus={true} 114 canOutsideClickClose={true} 115 canEscapeKeyClose={false} 116 isOpen={props.isOpen} 117 onClose={props.onClose} 118 isCloseButtonShown={true} 119 transitionDuration={100} 120 style={{ width: 'max-content' }} 121 > 122 <DialogBody> 123 <FormGroup label="Node Type" labelFor="node-type" inline={true}> 124 <SelectNodeType autoFocus={true} tabIndex={0} id="node-type" items={nodeTypesWithName} value={nodeType} onItemSelect={(nt) => { onNodeTypeChange(nt.name) }} fill={true} /> 125 </FormGroup> 126 <EditNode node={node} onNodeChanged={onNodeChanged} onValueChanged={onValueChanged} isCreate={true} /> 127 </DialogBody> 128 <DialogFooter actions={<> 129 <Tooltip content="Close the dialog and cancel creating a node"> 130 <Button tabIndex={0} onClick={props.onClose} icon="cross">Cancel</Button> 131 </Tooltip> 132 <Tooltip intent={Intent.PRIMARY} content={<div><p>Create a new node</p><p>You can also use <code>shift+enter</code> to submit the form.</p></div>}> 133 <Button tabIndex={0} intent={Intent.PRIMARY} onClick={onSaveClick} icon="floppy-disk">Save</Button> 134 </Tooltip> 135 </> 136 }> 137 </DialogFooter> 138 </Dialog> 139 </div> 140 ) 141 }