import { FilterAlt as FilterAltIcon, Functions as FunctionsIcon, GridOn as GridOnIcon, Numbers as NumbersIcon, Abc as AbcIcon, } from '@mui/icons-material'; import { useState } from 'react'; import { AllLogicDescriptions, AllLogicMap, QueryElementTypes, SchemaReactflowLogicNode, toHandleData } from '../../model'; import { ConnectingNodeDataI } from '../utils/connectorDrop'; import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import { toQuerybuilderGraphology, setQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { useDispatch } from 'react-redux'; import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../..'; export const QueryBuilderLogicPillsPanel = (props: { reactFlowWrapper: HTMLDivElement | null; className?: string; title?: string; onClick: (item: AllLogicDescriptions) => void; onDrag?: (item: AllLogicDescriptions) => void; connection?: ConnectingNodeDataI | null; editNode?: SchemaReactflowLogicNode; }) => { const graph = useQuerybuilderGraph(); const dispatch = useDispatch(); const graphologyGraph = toQuerybuilderGraphology(graph); const [selectedOp, setSelectedOp] = useState(-1); const [selectedType, setSelectedType] = useState(-1); let filterType = props.editNode ? props.editNode.data.logic.input.type : ((props.connection?.params?.handleId ? toHandleData(props.connection.params.handleId).attributeType : null) as string); if (!filterType) return <></>; else if (filterType === 'string') filterType = 'string'; else if (filterType === 'int' || filterType === 'float') filterType = 'number'; const dataOps = [ { title: 'aggregation', description: 'Aggregation Functions', icon: <GridOnIcon fontSize="small" />, }, { title: 'function', description: 'Math Functions', icon: <FunctionsIcon fontSize="small" />, }, { title: 'filter', description: 'Filter Functions', icon: <FilterAltIcon fontSize="small" />, }, ]; const dataTypes = [ { title: 'number', description: 'Number', icon: <NumbersIcon fontSize="small" />, }, { title: 'string', description: 'Text', icon: <AbcIcon fontSize="small" />, }, ].filter((item) => !filterType || item.title === filterType); const onDragStart = (event: React.DragEvent, value: AllLogicDescriptions) => { console.log('drag start'); event.dataTransfer.setData('application/reactflow', JSON.stringify({ value })); event.dataTransfer.effectAllowed = 'move'; if (props.onDrag) props.onDrag(value); }; const onNewNodeFromPopup = (value: AllLogicDescriptions) => { const logic = AllLogicMap[value.key]; if (props.connection === null || props.connection?.params?.handleId == null) { const bounds = props.reactFlowWrapper?.getBoundingClientRect() || { x: 0, y: 0, width: 0, height: 0 }; const logicNode = graphologyGraph.addLogicPill2Graphology({ name: value.name, type: QueryElementTypes.Logic, x: bounds.width / 2, y: bounds.height / 2, logic: logic, }); } else { const params = props.connection.params; const position = props.connection.position; const logicNode = graphologyGraph.addLogicPill2Graphology({ name: value.name, type: QueryElementTypes.Logic, x: position.x, y: position.y, logic: logic, }); if (!logicNode?.id) throw new Error('Logic node has no id'); if (!logicNode?.name) throw new Error('Logic node has no name'); if (!params.handleId) throw new Error('Connection has no source or target'); const sourceHandleData = toHandleData(params.handleId); graphologyGraph.addEdge2Graphology( graphologyGraph.getNodeAttributes(params.nodeId), graphologyGraph.getNodeAttributes(logicNode.id), { type: 'connection' }, { sourceHandleName: sourceHandleData.attributeName, targetHandleName: logic.input.name }, ); } dispatch(setQuerybuilderGraphology(graphologyGraph)); props.onClick(value); }; const onEditNodeFromPopup = (value: AllLogicDescriptions) => { if (!props.editNode?.id) return; const logic = AllLogicMap[value.key]; const edges = graphologyGraph .edges(props.editNode.id) .map((edge) => ({ id: edge, attributes: graphologyGraph.getEdgeAttributes(edge) })); console.log('edit node', props.editNode.data, edges); // graphologyGraph graphologyGraph.addLogicPill2Graphology({ ...props.editNode.data, name: value.name, type: QueryElementTypes.Logic, logic: logic, }); edges.forEach((edge, i) => { graphologyGraph.dropEdge(edge.id); if (i >= logic.numExtraInputs + 1) return; // ignore edges not present in new logic pill graphologyGraph.addEdge2Graphology( graphologyGraph.getNodeAttributes(edge.attributes.sourceHandleData.nodeId), graphologyGraph.getNodeAttributes(edge.attributes.targetHandleData.nodeId), { type: 'connection' }, { sourceHandleName: edge.attributes.sourceHandleData.attributeName, targetHandleName: edge.attributes.targetHandleData.attributeName, }, ); }); // graphologyGraph.addEdge2Graphology( // graphologyGraph.getNodeAttributes(params.nodeId), // graphologyGraph.getNodeAttributes(logicNode.id), // { type: 'connection' }, // { sourceHandleName: sourceHandleData.attributeName, targetHandleName: logic.input.name }, // ); dispatch(setQuerybuilderGraphology(graphologyGraph)); props.onClick(value); }; return ( <div className={props.className + ' card'}> {props.title && <h1 className="card-title mb-7">{props.title}</h1>} <div className="gap-1 flex"> <TooltipProvider delayDuration={50}> {dataOps.map((item, index) => ( <Tooltip key={item.title}> <TooltipContent>{item.description}</TooltipContent> <TooltipTrigger> <Button iconComponent={item.icon} size="sm" variant={selectedOp === index ? 'solid' : 'outline'} onClick={(e) => { e.preventDefault(); index === selectedOp ? setSelectedOp(-1) : setSelectedOp(index); }} ></Button> </TooltipTrigger> </Tooltip> ))} </TooltipProvider> <div className="w-2" /> {dataTypes.map((item, index) => ( <div key={item.title} data-tip={item.description} className="tooltip tooltip-top m-0 p-0"> <Button iconComponent={item.icon} size="sm" variant={selectedType === index ? 'solid' : 'outline'} onClick={(e) => { e.preventDefault(); index === selectedType ? setSelectedType(-1) : setSelectedType(index); }} ></Button> </div> ))} </div> <div className="overflow-x-hidden h-[75rem] w-full mt-1"> <ul className="menu p-0 [&_li>*]:rounded-none w-full pb-10 h-full gap-1"> {Object.values(AllLogicMap) .filter((item) => !filterType || item.key.toLowerCase().includes(filterType)) .filter((item) => selectedOp === -1 || item.key.toLowerCase().includes(dataOps?.[selectedOp].title)) .filter((item) => selectedType === -1 || item.key.toLowerCase().includes(dataTypes?.[selectedType].title)) .map((item, index) => ( <li key={item.key + item.description} className="h-fit bg-white border-[1px] border-secondary-500 rounded-sm"> <span data-tip={item.description} className="flex before:w-[10rem] before:text-center tooltip tooltip-bottom text-start " onDragStart={(e) => onDragStart(e, item)} draggable={true} onClick={() => { if (props.editNode) onEditNodeFromPopup(item); else onNewNodeFromPopup(item); }} > {item.icon && ( <div className="w-[1rem] rounded-sm justify-center flex"> <span>{item.icon}</span> </div> )} <span className="w-full">{item.name}</span> <span className="flex scale-75"> {item.key.toLowerCase().includes('filter') && <FilterAltIcon fontSize="small" />} {item.key.toLowerCase().includes('function') && <FunctionsIcon fontSize="small" />} {item.key.toLowerCase().includes('aggregation') && <GridOnIcon fontSize="small" />} {item.key.toLowerCase().includes('number') && <NumbersIcon fontSize="small" />} {item.key.toLowerCase().includes('string') && <AbcIcon fontSize="small" />} </span> </span> </li> ))} </ul> </div> </div> ); };