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  }