diff --git a/libs/shared/lib/graph-layout/types.ts b/libs/shared/lib/graph-layout/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..eab11f8ed89e0692d9ecbb84f2e309a4dbb618da --- /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 0000000000000000000000000000000000000000..940dbd55cd00ae15e4b0f006aba670fdf735db7e --- /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 0000000000000000000000000000000000000000..7a1ae28b68a94e3e499ca479cc7b4e00e72122fc --- /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 8f77275cb18c3abde492b5ddbe68305c45d79910..7a1be153aebf2c47385a399c2deee56381807858 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}