From 81b5e5ca66599b02dbb04d0a05cbbf5942930d69 Mon Sep 17 00:00:00 2001 From: Milho001 <l.milhomemfrancochristino@uu.nl> Date: Mon, 1 Jul 2024 08:24:38 +0000 Subject: [PATCH] feat(qb): simple connection groupBy functionality --- .../model/graphology/metaAttributes.ts | 28 +++++++ .../querybuilder/model/graphology/model.ts | 16 ++-- .../querybuilder/model/graphology/utils.ts | 7 +- .../lib/querybuilder/model/reactflow/utils.ts | 8 +- .../relationpill/QueryRelationPill.tsx | 69 ++--------------- .../pills/pilldropdown/PillDropdown.tsx | 74 +++++-------------- .../pills/pilldropdown/PillDropdownItem.tsx | 65 ++++++++++++++++ .../querybuilder/query-utils/query2backend.ts | 3 + 8 files changed, 133 insertions(+), 137 deletions(-) create mode 100644 libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts create mode 100644 libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx diff --git a/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts b/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts new file mode 100644 index 000000000..4c34d0884 --- /dev/null +++ b/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts @@ -0,0 +1,28 @@ +import { SchemaAttribute } from '../../..'; +import { Handles, QueryElementTypes } from '../reactflow'; +import { QueryGraphEdgeAttribute, QueryGraphEdgeHandle, QueryGraphNodes } from './model'; + +const metaAttribute: Record<string, QueryGraphEdgeAttribute> = { + '(# Connection)': { + attributeName: '(# Connection)', + attributeType: 'float', + attributeDimension: 'numerical', + }, +}; + +export function checkForMetaAttributes(graphologyAttributes: QueryGraphNodes): QueryGraphEdgeHandle[] { + const ret: QueryGraphEdgeHandle[] = []; + const defaultHandleData = { + nodeId: graphologyAttributes.id, + nodeName: graphologyAttributes.name || '', + nodeType: graphologyAttributes.type, + handleType: graphologyAttributes.type === QueryElementTypes.Entity ? Handles.EntityAttribute : Handles.RelationAttribute, + }; + + // Only include if not already there + const metaAttributesToInclude = Object.keys(metaAttribute).filter((attributeName) => !(attributeName in graphologyAttributes.attributes)); + return metaAttributesToInclude.map((attributeName) => ({ + ...defaultHandleData, + ...metaAttribute[attributeName], + })) as QueryGraphEdgeHandle[]; +} diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts index dc4a2bc1a..4e4cc935d 100644 --- a/libs/shared/lib/querybuilder/model/graphology/model.ts +++ b/libs/shared/lib/querybuilder/model/graphology/model.ts @@ -23,7 +23,7 @@ export type NodeDefaults = { type: QueryElementTypes; width?: number; height?: number; - attributes?: NodeAttribute[]; + attributes: NodeAttribute[]; selected?: boolean; }; @@ -62,9 +62,9 @@ export type LogicNodeAttributes = XYPosition & LogicData & NodeDefaults; export type QueryGraphNodes = EntityNodeAttributes | RelationNodeAttributes | LogicNodeAttributes; export type QueryGraphEdgeAttribute = { - attributeName?: string; - attributeType?: InputNodeType; - attributeDimension?: InputNodeDimension; + attributeName: string; + attributeType: InputNodeType; + attributeDimension: InputNodeDimension; }; export type QueryGraphEdgeHandle = { @@ -72,7 +72,7 @@ export type QueryGraphEdgeHandle = { nodeName: string; nodeType: QueryElementTypes; handleType: Handles; -} & QueryGraphEdgeAttribute; +} & Partial<QueryGraphEdgeAttribute>; export type QueryGraphEdges = { type: string; @@ -80,10 +80,6 @@ export type QueryGraphEdges = { targetHandleData: QueryGraphEdgeHandle; }; -export type QueryGraphEdgesOpt = { - type?: string; - sourceHandleData?: QueryGraphEdgeHandle; - targetHandleData?: QueryGraphEdgeHandle; -}; +export type QueryGraphEdgesOpt = Partial<QueryGraphEdges>; // export class QueryGraph extends Graph<QueryGraphNodes, GAttributes, GAttributes>; // is in utils.ts diff --git a/libs/shared/lib/querybuilder/model/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts index 041fe0cde..c2d3ae399 100644 --- a/libs/shared/lib/querybuilder/model/graphology/utils.ts +++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts @@ -4,7 +4,6 @@ import { Attributes as GAttributes, Attributes, SerializedGraph } from 'grapholo import { EntityNodeAttributes, LogicNodeAttributes, - QueryGraphEdgeAttribute, QueryGraphEdgeHandle, QueryGraphEdges, QueryGraphEdgesOpt, @@ -15,6 +14,7 @@ import { XYPosition } from 'reactflow'; import { Handles, QueryElementTypes } from '../reactflow'; import { SchemaAttribute, SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema'; import { InputNodeType, InputNodeTypeTypes } from '../logic/general'; +import { checkForMetaAttributes } from './metaAttributes'; /** monospace fontsize table */ const widthPerFontsize = { @@ -54,6 +54,11 @@ export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString(); + attributes.attributes = attributes.attributes || []; + + // Add to the beginning the meta attributes, such as (# Connection) + attributes.attributes = [...checkForMetaAttributes(attributes).map((a) => ({ handleData: a })), ...attributes.attributes]; + return attributes; } diff --git a/libs/shared/lib/querybuilder/model/reactflow/utils.ts b/libs/shared/lib/querybuilder/model/reactflow/utils.ts index b8a0dd791..7ee9c55c0 100644 --- a/libs/shared/lib/querybuilder/model/reactflow/utils.ts +++ b/libs/shared/lib/querybuilder/model/reactflow/utils.ts @@ -4,7 +4,7 @@ import { toHandleId } from '..'; // Takes the querybuilder graph as an input and creates react flow elements for them. export function createReactFlowElements<T extends Graph>( - graph: T + graph: T, ): { nodes: Array<Node>; edges: Array<Edge>; @@ -13,8 +13,6 @@ export function createReactFlowElements<T extends Graph>( const edges: Array<Edge> = []; graph.forEachNode((node, attributes): void => { - // console.log('attributes', attributes); - let position = { x: attributes?.x || 0, y: attributes?.y || 0 }; const RFNode: Node<typeof attributes> = { id: node, @@ -28,8 +26,6 @@ export function createReactFlowElements<T extends Graph>( // Add the reactflow edges graph.forEachEdge((edge, attributes, source, target): void => { // connection from attributes don't have visible connection lines - // if (attributes.type == 'attribute_connection') return; - const RFEdge: Edge<typeof attributes> = { id: edge, source: source, @@ -43,7 +39,5 @@ export function createReactFlowElements<T extends Graph>( edges.push(RFEdge); }); - // console.log('nodes', nodes, 'edges', edges); - return { nodes, edges }; } diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx index 3bb9e75d3..1fd01d879 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx @@ -62,10 +62,6 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { // dispatch(setQuerybuilderGraphology(graphologyGraph)); // }; - const calcWidth = (data: number) => { - return data.toString().length + 0.5 + 'ch'; - }; - return ( <div className="w-fit h-fit p-3 bg-transparent nowheel"> <RelationPill @@ -84,55 +80,6 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { }} className={openDropdown ? 'border-secondary-200' : ''} /> - {/* <span className="pr-1"> - <span> [</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + - (depth.min < 0 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.min) }} - type="number" - min={0} - placeholder={'?'} - value={depth.min} - onChange={(e) => { - setDepth({ ...depth, min: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>..</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + - (depth.max > 99 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.max) }} - type="number" - min={1} - placeholder={'?'} - value={depth.max} - onChange={(e) => { - setDepth({ ...depth, max: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>]</span> - </span> */} </div> } withHandles="horizontal" @@ -155,15 +102,13 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { ></Handle> } > - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - open={openDropdown} - mr={-pillWidth * 0.05} - /> - )} + <PillDropdown + node={node} + attributes={data?.attributes || []} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + open={openDropdown} + mr={-pillWidth * 0.05} + /> </RelationPill> </div> ); diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index 07caaabbf..d26c952c0 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -1,14 +1,11 @@ import { useMemo, ReactElement, useState, useContext } from 'react'; -import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; -import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; +import { Handles, NodeAttribute, QueryElementTypes, QueryGraphEdges, SchemaReactflowEntityNode } from '../../model'; import { Abc, CalendarToday, Map, Numbers, Place, QuestionMarkOutlined } from '@mui/icons-material'; -import { Icon } from '@graphpolaris/shared/lib/components/icon'; -import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; -import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; import { Button, TextInput, useAppDispatch, useQuerybuilderAttributesShown } from '../../..'; import { attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { isEqual } from 'lodash-es'; import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher'; +import { PillDropdownItem } from './PillDropdownItem'; type PillDropdownProps = { node: SchemaReactflowEntityNode; @@ -36,7 +33,6 @@ export const PillDropdown = (props: PillDropdownProps) => { const [filter, setFilter] = useState<string>(''); const dispatch = useAppDispatch(); const attributesBeingShown = useQuerybuilderAttributesShown(); - const { openLogicPillCreate } = useContext(QueryBuilderDispatcherContext); const attributesOfInterest = useMemo(() => { return props.attributes.map((attribute) => @@ -48,58 +44,22 @@ export const PillDropdown = (props: PillDropdownProps) => { <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200 !z-50'}> {attributesOfInterest && attributesOfInterest.map((showing, i) => { - if (showing === false) return null; - - const attribute = props.attributes[i]; - if (attribute.handleData.attributeName === undefined) { - throw new Error('attribute.handleData.attributeName is undefined'); - } - - const handleId = toHandleId(handleDataFromReactflowToDataId(props.node, attribute)); - const handleType = 'source'; - + if (!showing) return null; return ( - <div - className="px-2 py-1 bg-secondary-100 flex justify-between items-center" - key={(attribute.handleData.attributeName || '') + i} - > - <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p> - <Button - variantType="secondary" - variant="ghost" - size="2xs" - iconComponent={ - attribute.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : <QuestionMarkOutlined /> - } - onClick={() => { - openLogicPillCreate( - { - nodeId: props.node.id, - handleId: handleId, - handleType: handleType, - }, - { - x: props.node.xPos + 200, - y: props.node.yPos + 50, - }, - ); - }} - /> - <PillHandle - mr={-pillDropdownPadding + (props.mr || 0)} - handleTop="auto" - position={Position.Right} - className={`stroke-white${props.className ? ` ${props.className}` : ''}`} - type="square" - > - <Handle - id={handleId} - type={handleType} - position={Position.Right} - className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'} - ></Handle> - </PillHandle> - </div> + <PillDropdownItem + key={props.attributes[i].handleData.attributeName || i} + node={props.node} + attribute={props.attributes[i]} + mr={props.mr} + className={props.className} + icon={ + props.attributes[i].handleData?.attributeDimension ? ( + IconMap[props.attributes[i].handleData.attributeDimension || 0] + ) : ( + <QuestionMarkOutlined /> + ) + } + /> ); })} {(props.open || forceOpen) && ( diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx new file mode 100644 index 000000000..161a42ab6 --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx @@ -0,0 +1,65 @@ +import { ReactElement, useContext } from 'react'; +import { NodeAttribute, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; +import { Handle, Position } from 'reactflow'; +import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; +import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; +import { Button } from '../../..'; +import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher'; + +type PillDropdownItemProps = { + attribute: NodeAttribute; + node: SchemaReactflowEntityNode; + className?: string; + mr?: number; + icon: ReactElement; +}; + +export const PillDropdownItem = (props: PillDropdownItemProps) => { + const { openLogicPillCreate } = useContext(QueryBuilderDispatcherContext); + + if (props.attribute.handleData.attributeName === undefined) { + throw new Error('attribute.handleData.attributeName is undefined'); + } + + const handleId = toHandleId(handleDataFromReactflowToDataId(props.node, props.attribute)); + const handleType = 'source'; + + return ( + <div className="px-2 py-1 bg-secondary-100 flex justify-between items-center"> + <p className="truncate text-[0.6rem]">{props.attribute.handleData.attributeName}</p> + <Button + variantType="secondary" + variant="ghost" + size="2xs" + iconComponent={props.icon} + onClick={() => { + openLogicPillCreate( + { + nodeId: props.node.id, + handleId: handleId, + handleType: handleType, + }, + { + x: props.node.xPos + 200, + y: props.node.yPos + 50, + }, + ); + }} + /> + <PillHandle + mr={-pillDropdownPadding + (props.mr || 0)} + handleTop="auto" + position={Position.Right} + className={`stroke-white${props.className ? ` ${props.className}` : ''}`} + type="square" + > + <Handle + id={handleId} + type={handleType} + position={Position.Right} + className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'} + ></Handle> + </PillHandle> + </div> + ); +}; diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts index 92fd9e6f2..fc1aa9942 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts @@ -139,6 +139,9 @@ export function calculateQueryLogic( if (!connectionToInputRef.attributes?.sourceHandleData) throw Error('Malformed Graph! Logic node is connected but has no sourceHandleData'); // Is connected to entity or relation node + if (connectionToInputRef.attributes.sourceHandleData.attributeName === '(# Connection)') { + return ['Count', `@${connectionToInputRef.attributes.sourceHandleData.nodeId}`]; + } return `@${connectionToInputRef.attributes.sourceHandleData.nodeId}.${connectionToInputRef.attributes.sourceHandleData.attributeName}`; } } else { -- GitLab