From af6134f3a933c2b336921dad5e810a914d30d10b Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Wed, 20 Sep 2023 15:54:13 +0000
Subject: [PATCH] feat(qb): auto focus logic input fields

---
 libs/shared/lib/graph-layout/types.ts         |  35 +++++
 .../querysidepanel/querySettingsDialog.tsx    | 144 ++++++++++++++++++
 .../customFlowPills/logicpill/logicInput.tsx  |  34 +++++
 .../customFlowPills/logicpill/logicpill.tsx   |  48 +++---
 4 files changed, 236 insertions(+), 25 deletions(-)
 create mode 100644 libs/shared/lib/graph-layout/types.ts
 create mode 100644 libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx
 create mode 100644 libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx

diff --git a/libs/shared/lib/graph-layout/types.ts b/libs/shared/lib/graph-layout/types.ts
new file mode 100644
index 000000000..eab11f8ed
--- /dev/null
+++ b/libs/shared/lib/graph-layout/types.ts
@@ -0,0 +1,35 @@
+import { Cytoscape, CytoscapeProvider } from './cytoscape-layouts';
+import { Graphology, GraphologyProvider } from './graphology-layouts';
+
+export type GraphologyLayoutAlgorithms = `Graphology_circular` | `Graphology_random` | `Graphology_noverlap` | `Graphology_forceAtlas2`;
+export type CytoscapeLayoutAlgorithms =
+  | 'Cytoscape_klay'
+  | 'Cytoscape_dagre'
+  | 'Cytoscape_elk'
+  | 'Cytoscape_fcose'
+  | 'Cytoscape_cose-bilkent'
+  | 'Cytoscape_cise';
+
+export type AllLayoutAlgorithms = GraphologyLayoutAlgorithms | CytoscapeLayoutAlgorithms;
+
+export type Providers = GraphologyProvider | CytoscapeProvider;
+export type LayoutAlgorithm<Provider extends Providers> = `${Provider}_${string}`;
+
+export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> = Algorithm extends GraphologyLayoutAlgorithms
+  ? Graphology
+  : Algorithm extends CytoscapeLayoutAlgorithms
+  ? Cytoscape
+  : Cytoscape | Graphology;
+
+export enum Layouts {
+  KLAY = 'Cytoscape_klay',
+  DAGRE = 'Cytoscape_dagre',
+  ELK = 'Cytoscape_elk',
+  FCOSE = 'Cytoscape_fcose',
+  COSE_BILKENT = 'Cytoscape_cose-bilkent',
+  CISE = 'Cytoscape_cise',
+  RANDOM = 'Graphology_random',
+  CIRCULAR = 'Graphology_circular',
+  NOVERLAP = 'Graphology_noverlap',
+  FORCEATLAS2 = 'Graphology_forceAtlas2',
+}
diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx
new file mode 100644
index 000000000..940dbd55c
--- /dev/null
+++ b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx
@@ -0,0 +1,144 @@
+import { PropsWithChildren, useEffect, useRef } from 'react';
+import { Dialog, DialogProps } from '../../../components/Dialog';
+import React from 'react';
+import CloseIcon from '@mui/icons-material/Close';
+import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access';
+import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice';
+import { addWarning } from '../../../data-access/store/configSlice';
+import { FormBody, FormCard, FormDiv, FormHBar, FormTitle } from '../../../components/forms';
+import { NodeAttribute, QueryGraphNodes, toHandleData } from '../../model';
+import { OnConnectStartParams, XYPosition } from 'reactflow';
+import { Layouts } from '@graphpolaris/shared/lib/graph-layout';
+
+type QuerySettingsDialogProps = DialogProps;
+
+export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySettingsDialogProps>((props, ref) => {
+  const qb = useQuerybuilderSettings();
+  const dispatch = useAppDispatch();
+  const [state, setState] = React.useState<QueryBuilderSettings>(qb);
+
+  useEffect(() => {
+    setState(qb);
+  }, [qb, props.open]);
+
+  function submit() {
+    if (state.depth.min < 0) {
+      dispatch(addWarning('The minimum depth cannot be smaller than 0'));
+    } else if (state.depth.max > 99) {
+      dispatch(addWarning('The maximum depth cannot be larger than 99'));
+    } else if (state.depth.min >= state.depth.max) {
+      dispatch(addWarning('The minimum depth cannot be larger than the maximum depth'));
+    } else {
+      dispatch(setQuerybuilderSettings(state));
+      props.onClose();
+    }
+  }
+
+  return (
+    <>
+      {props.open && (
+        <FormDiv ref={ref} className="" hAnchor="right">
+          <FormCard>
+            <FormBody
+              onSubmit={(e) => {
+                e.preventDefault();
+                submit();
+              }}
+            >
+              <FormTitle title="Query Settings" onClose={props.onClose} />
+              <FormHBar />
+              <div className="form-control px-5">
+                <label className="label">
+                  <span className="label-text">Limit - Max number of results</span>
+                </label>
+                <input
+                  type="number"
+                  className="input input-sm input-bordered"
+                  placeholder="500"
+                  value={state.limit}
+                  onChange={(e) => setState({ ...state, limit: parseInt(e.target.value) })}
+                />
+              </div>
+              <FormHBar />
+              <div className="form-control px-5 flex flex-row gap-3">
+                <div className="">
+                  <label className="label">
+                    <span className="label-text">Min Depth Default</span>
+                  </label>
+                  <input
+                    type="number"
+                    className="input input-sm input-bordered w-full"
+                    placeholder="0"
+                    min={0}
+                    max={state.depth.max - 1}
+                    value={state.depth.min}
+                    onChange={(e) => setState({ ...state, depth: { min: parseInt(e.target.value), max: state.depth.max } })}
+                    onKeyDown={(e) => {
+                      if (e.key === 'Enter') {
+                        submit();
+                      }
+                    }}
+                  />
+                </div>
+                <div className="">
+                  <label className="label">
+                    <span className="label-text">Max Depth Default</span>
+                  </label>
+                  <input
+                    type="number"
+                    className="input input-sm input-bordered w-full"
+                    placeholder="0"
+                    min={state.depth.min + 1}
+                    max={99}
+                    value={state.depth.max}
+                    onChange={(e) => setState({ ...state, depth: { max: parseInt(e.target.value), min: state.depth.min } })}
+                    onKeyDown={(e) => {
+                      if (e.key === 'Enter') {
+                        submit();
+                      }
+                    }}
+                  />
+                </div>
+              </div>
+              <FormHBar />
+              <div className="form-control px-5 ">
+                <label className="label">
+                  <span className="label-text">Layout Type</span>
+                </label>
+                <select
+                  className="select select-primary select-sm "
+                  value={state.layout}
+                  onChange={(e) => {
+                    setState({ ...state, layout: e.target.value as any });
+                  }}
+                >
+                  <option className="option" value={'manual'}>
+                    Manual
+                  </option>
+                  {Object.entries(Layouts).map(([k, v]) => (
+                    <option className="option" value={v} key={v}>
+                      {k}
+                    </option>
+                  ))}
+                </select>
+              </div>
+              <FormHBar />
+              <div className="card-actions mt-1 w-full px-5 flex flex-row">
+                <button
+                  className="btn btn-secondary flex-grow"
+                  onClick={(e) => {
+                    e.preventDefault();
+                    props.onClose();
+                  }}
+                >
+                  Cancel
+                </button>
+                <button className="btn btn-primary flex-grow">Apply</button>
+              </div>
+            </FormBody>
+          </FormCard>
+        </FormDiv>
+      )}
+    </>
+  );
+});
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx
new file mode 100644
index 000000000..7a1ae28b6
--- /dev/null
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx
@@ -0,0 +1,34 @@
+import React, { useRef, useEffect } from 'react';
+
+export const LogicInput = (props: { value: string; type: string; onChange(value: string): void; onEnter(): void; onBlur(): void }) => {
+  const ref = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    setTimeout(() => {
+      // need a timeout to make sure the input is rendered and no other interruption happens before focusing
+      if (ref?.current) {
+        ref.current.focus();
+      }
+    }, 100);
+  }, []);
+
+  return (
+    <input
+      ref={ref}
+      className="px-0.5 m-2 mt-0 h-5 border-logic-600 rounded-sm border-[1px]"
+      style={{ width: props.type === 'string' ? '9rem' : '4rem' }}
+      placeholder="empty"
+      value={props.value}
+      onMouseDownCapture={(e) => {
+        e.stopPropagation();
+      }}
+      onChange={(e) => {
+        props.onChange(e.target.value);
+      }}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter') props.onEnter();
+      }}
+      onBlur={(e) => props.onBlur()}
+    />
+  );
+};
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx
index 8f77275cb..7a1be153a 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx
@@ -9,13 +9,14 @@
  * We do not test components/renderfunctions/styling files.
  * See testing plan for more details.*/
 import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderHash } from '@graphpolaris/shared/lib/data-access';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { Handle, HandleType, Position } from 'reactflow';
 import { LogicNodeAttributes, SchemaReactflowLogicNode, toHandleId } from '../../../model';
 import { InputNode, InputNodeTypeTypes } from '../../../model/logic/general';
 import { styleHandleMap } from '../../utils';
 import styles from './logicpill.module.scss';
 import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
+import { LogicInput } from './logicInput';
 
 /**
  * Component to render an entity flow element
@@ -26,6 +27,7 @@ export default function LogicPill(node: SchemaReactflowLogicNode) {
   const data = node.data;
   const logic = data.logic;
   const output = data.logic.output;
+  const inputReference = useRef<HTMLInputElement>(null);
 
   const graph = useQuerybuilderGraph();
   const graphologyHash = useQuerybuilderHash();
@@ -71,6 +73,10 @@ export default function LogicPill(node: SchemaReactflowLogicNode) {
   // );
   // const leftInputsNumber = createLeftHandles(node.data.logic.inputs);
 
+  useEffect(() => {
+    if (inputReference?.current) inputReference.current.focus();
+  }, [node.id]);
+
   return (
     <div className={styles.logic + ' w-fit h-min-[3rem]'}>
       <div className="h-fit">
@@ -80,32 +86,24 @@ export default function LogicPill(node: SchemaReactflowLogicNode) {
           </div>
         }
         {node.data.logic.inputs.map((input, i) => {
-          let inputTextBox = null;
-          if (
-            !connectionsToLeft.some(
-              (edge) =>
-                edge?.attributes?.targetHandleData.nodeId === data.id && edge?.attributes?.targetHandleData.attributeName === input.name
-            )
-          ) {
-            inputTextBox = (
-              <input
-                className="px-0.5 m-2 mt-0 h-5 border-logic-600 rounded-sm border-[1px]"
-                style={{ width: input.type === 'string' ? '9rem' : '4rem' }}
-                placeholder="empty"
-                value={localInputCache?.[input.name] as string}
-                onChange={(e) => {
-                  setLocalInputCache({ ...localInputCache, [input.name]: e.target.value });
-                }}
-                onKeyDown={(e) => {
-                  if (e.key === 'Enter') onInputUpdated((e.target as HTMLInputElement).value, input, i);
-                }}
-                onBlur={(e) => onInputUpdated(e.target.value, input, i)}
-              />
-            );
-          }
           return (
             <div key={i}>
-              <div className="w-full flex">{inputTextBox}</div>
+              <div className="w-full flex">
+                {!connectionsToLeft.some(
+                  (edge) =>
+                    edge?.attributes?.targetHandleData.nodeId === data.id && edge?.attributes?.targetHandleData.attributeName === input.name
+                ) && (
+                  <LogicInput
+                    value={localInputCache?.[input.name] as string}
+                    type={input.type}
+                    onChange={(value: string) => {
+                      setLocalInputCache({ ...localInputCache, [input.name]: value });
+                    }}
+                    onEnter={() => onInputUpdated(localInputCache?.[input.name] as string, input, i)}
+                    onBlur={() => onInputUpdated(localInputCache?.[input.name] as string, input, i)}
+                  />
+                )}
+              </div>
               <Handle
                 type={'target'}
                 position={Position.Left}
-- 
GitLab