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