From 6499263a0b2281fb076847e5adea9721718b8101 Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Tue, 26 Sep 2023 15:55:59 +0000
Subject: [PATCH] feat(qb): support ambivalent relationship direction

---
 .../querybuilder/model/BackendQueryFormat.tsx |   2 +-
 .../querybuilder/model/graphology/model.ts    |   1 +
 .../lib/querybuilder/panel/querybuilder.tsx   |  10 ++
 .../pills/customFlowLines/connection.tsx      |   5 +-
 .../relationpill/relation-handles.tsx         |  61 +++++++++
 .../relationpill/relationpill.module.scss     | 117 ------------------
 .../relationpill.module.scss.d.ts             |  34 -----
 .../relationpill/relationpill.stories.tsx     |   1 +
 .../relationpill/relationpill.tsx             |  58 ++++-----
 .../query-utils/query2backend.spec.ts         |   8 +-
 .../querybuilder/query-utils/query2backend.ts |   3 +-
 11 files changed, 108 insertions(+), 192 deletions(-)
 create mode 100644 libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx
 delete mode 100644 libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss
 delete mode 100644 libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss.d.ts

diff --git a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
index 4b0cbe9a3..de4c9ef31 100644
--- a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
+++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
@@ -77,7 +77,7 @@ export interface RelationStruct {
   ID?: string;
   label?: string;
   depth: QuerySearchDepthStruct;
-  direction: 'TO' | 'FROM';
+  direction: 'TO' | 'FROM' | 'BOTH';
   node?: NodeStruct;
 }
 
diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts
index 09832ac55..02b336deb 100644
--- a/libs/shared/lib/querybuilder/model/graphology/model.ts
+++ b/libs/shared/lib/querybuilder/model/graphology/model.ts
@@ -39,6 +39,7 @@ export interface RelationData {
   depth: { min: number; max: number };
   leftEntityHandleId?: QueryGraphEdgeHandle;
   rightEntityHandleId?: QueryGraphEdgeHandle;
+  direction?: 'left' | 'right' | 'both';
 }
 
 export interface LogicData {
diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
index 694540acb..1ee444dba 100644
--- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
@@ -423,6 +423,16 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
           connection={connectingNodeId?.current}
         />
       </Dialog>
+      <svg height={0}>
+        <defs>
+          <marker id="arrowIn" markerWidth="9" markerHeight="10" refX={0} refY="5" orient="auto">
+            <polygon points="0 0, 9 5, 0 10" />
+          </marker>
+          <marker id="arrowOut" markerWidth="10" markerHeight="10" refX={0} refY="5" orient="auto">
+            <polygon points="0 0, 10 5, 0 10" />
+          </marker>
+        </defs>
+      </svg>
       <ReactFlow
         edges={elements.edges}
         nodes={elements.nodes}
diff --git a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx
index 9ac746889..9bded7d29 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
-import { EdgeProps, getSmoothStepPath, Position } from 'reactflow';
+import { EdgeProps, getSmoothStepPath, Position, Background, MarkerType, BaseEdge } from 'reactflow';
+
 import './connection.scss';
 
 /**
@@ -58,7 +59,7 @@ export function ConnectionLine({ id, sourceX, sourceY, targetX, targetY, style,
 
   return (
     <g stroke="#2e2e2e">
-      <path id={id} fill="none" strokeWidth={1.5} style={style} d={path[0]} />
+      <path id={id} fill="none" strokeWidth={1} style={style} d={path[0]} />
     </g>
   );
 }
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx
new file mode 100644
index 000000000..ba88189a6
--- /dev/null
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx
@@ -0,0 +1,61 @@
+import { Handle, HandleType, Position } from 'reactflow';
+import { QueryGraphEdgeHandle, toHandleId } from '../../..';
+import { tailwindColors } from 'config';
+
+const rightArrow = '0,0 8,5 0,10';
+const leftArrow = '8,0 0,5 8,10';
+const square = '9,9 1,9 1,1 9,1';
+const getArrow = {
+  right: rightArrow,
+  left: leftArrow,
+  both: square,
+};
+export type RelationshipHandleArrowType = 'right' | 'left' | 'both';
+
+type Props = { id: QueryGraphEdgeHandle; type: HandleType; point: RelationshipHandleArrowType; onDoubleClick?: () => void };
+
+export const LeftHandle = (props: Props) => {
+  const offset = 15;
+
+  return (
+    <Handle
+      id={toHandleId(props.id)}
+      type={props.type}
+      position={Position.Left}
+      className=""
+      style={{ transform: `translate(${offset}px, -3px)` }}
+      onDoubleClickCapture={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (props.onDoubleClick) props.onDoubleClick();
+      }}
+    >
+      <svg className="pointer-events-none" height={10} style={{ transform: `translate(-2px, -3px)` }}>
+        <polygon points={getArrow[props.point]} fill={tailwindColors.relation[200]} />
+      </svg>
+    </Handle>
+  );
+};
+
+export const RightHandle = (props: Props) => {
+  const offset = -13;
+
+  return (
+    <Handle
+      id={toHandleId(props.id)}
+      type={props.type}
+      position={Position.Right}
+      className=""
+      style={{ transform: `translate(${offset}px, -3px)` }}
+      onDoubleClickCapture={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (props.onDoubleClick) props.onDoubleClick();
+      }}
+    >
+      <svg height={10} className="pointer-events-none" style={{ transform: `translate(-2px, -3px)` }}>
+        <polygon points={getArrow[props.point]} fill={tailwindColors.relation[200]} />
+      </svg>
+    </Handle>
+  );
+};
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss
deleted file mode 100644
index bd38e97d0..000000000
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss
+++ /dev/null
@@ -1,117 +0,0 @@
-@import '../../querypills.module.scss';
-
-$height: 10px;
-$width: 325;
-// Relation element
-
-.relation {
-  min-width: $width + px;
-  text-align: center;
-  font-weight: bold;
-  border-left: 3px solid;
-  @apply bg-relation-50;
-  @apply border-l-relation-600;
-  font-size: 13px;
-}
-
-.relationWrapper {
-  display: inherit;
-  width: inherit;
-  align-items: center;
-  justify-content: space-between;
-}
-
-.relationHandleTriangle {
-  border-radius: 0px !important;
-  background: transparent !important;
-  width: 0 !important;
-  height: 0 !important;
-  border-left: 5px solid transparent !important;
-  border-right: 5px solid transparent !important;
-  border-bottom: 8px solid !important;
-  @apply border-b-relation-200 #{!important};
-}
-
-.relationHandleLeft {
-  @extend .relationHandleTriangle;
-  transform: rotate(-90deg) translate(50%, 150%) !important;
-}
-
-.relationHandleRight {
-  @extend .relationHandleTriangle;
-  transform: rotate(90deg) translate(-50%, 150%) !important;
-}
-
-// .relationHandleAttribute {
-//   // SECOND ONE
-//   border-radius: 1px !important;
-//   left: 22.5px !important;
-//   background: rgba(255, 255, 255, 0.6) !important;
-//   transform: rotate(45deg) translate(-68%, 0) scale(0.9) !important;
-//   border-color: rgba(22, 110, 110, 1) !important;
-//   border-width: 1px !important;
-//   transform-origin: center, center;
-// }
-
-// .relationHandleFunction {
-//   // THIRD ONE
-//   left: 39px !important;
-//   background: rgba(255, 255, 255, 0.6) !important;
-//   border-color: rgba(22, 110, 110, 1) !important;
-//   border-width: 1px !important;
-//   transform-origin: center, center;
-// }
-
-.relationDataWrapper {
-  display: flex;
-  width: 100%;
-  justify-content: center;
-
-  .relationHandleFiller {
-    flex: 1 1 0;
-  }
-
-  .relationSpan {
-    float: left;
-    margin-left: 5;
-  }
-
-  .relationInputHolder {
-    display: flex;
-    float: right;
-    margin-right: 20px;
-    margin-top: 4px;
-    margin-left: 5px;
-    max-width: 80px;
-    border-radius: 2px;
-    align-items: center;
-    max-height: 12px;
-
-    .relationInput {
-      z-index: 1;
-      cursor: text;
-      min-width: 0px;
-      max-width: 1.5ch;
-      border: none;
-      background: transparent;
-      text-align: center;
-      font-size: 0.8rem;
-      user-select: none;
-      &:focus {
-        outline: none;
-        user-select: none;
-      }
-      &::placeholder {
-        outline: none;
-        user-select: none;
-        font-style: italic;
-      }
-    }
-
-    .relationReadonly {
-      cursor: grab !important;
-      user-select: none;
-      font-style: normal !important;
-    }
-  }
-}
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss.d.ts b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss.d.ts
deleted file mode 100644
index 432ea72c9..000000000
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss.d.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-declare const classNames: {
-  readonly handle: 'handle';
-  readonly handle_logic: 'handle_logic';
-  readonly handle_logic_duration: 'handle_logic_duration';
-  readonly handle_logic_datetime: 'handle_logic_datetime';
-  readonly handle_logic_time: 'handle_logic_time';
-  readonly handle_logic_date: 'handle_logic_date';
-  readonly handle_logic_bool: 'handle_logic_bool';
-  readonly handle_logic_float: 'handle_logic_float';
-  readonly handle_logic_int: 'handle_logic_int';
-  readonly handle_logic_string: 'handle_logic_string';
-  readonly 'react-flow__node': 'react-flow__node';
-  readonly selected: 'selected';
-  readonly entityWrapper: 'entityWrapper';
-  readonly hidden: 'hidden';
-  readonly 'react-flow__edges': 'react-flow__edges';
-  readonly 'react-flow__edge-default': 'react-flow__edge-default';
-  readonly handleConnectedFill: 'handleConnectedFill';
-  readonly handleConnectedBorderRight: 'handleConnectedBorderRight';
-  readonly handleConnectedBorderLeft: 'handleConnectedBorderLeft';
-  readonly handleFunction: 'handleFunction';
-  readonly relation: 'relation';
-  readonly relationWrapper: 'relationWrapper';
-  readonly relationHandleTriangle: 'relationHandleTriangle';
-  readonly relationHandleRight: 'relationHandleRight';
-  readonly relationHandleLeft: 'relationHandleLeft';
-  readonly relationDataWrapper: 'relationDataWrapper';
-  readonly relationHandleFiller: 'relationHandleFiller';
-  readonly relationSpan: 'relationSpan';
-  readonly relationInputHolder: 'relationInputHolder';
-  readonly relationInput: 'relationInput';
-  readonly relationReadonly: 'relationReadonly';
-};
-export = classNames;
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
index 2dfb0e537..6c67d6869 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
@@ -42,6 +42,7 @@ export const Default: StoryObj<{ data: RelationData }> = {
       name: 'TestEntity',
       collection: 'test',
       depth: { min: 0, max: 1 },
+      direction: 'right',
     },
   },
 };
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
index dbda2f433..d1f3975ff 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
@@ -1,7 +1,6 @@
 import { memo, useRef, useState, useMemo, useEffect } from 'react';
 import { Handle, Position } from 'reactflow';
 import { RelationNodeAttributes, SchemaReactflowRelationNode, toHandleId } from '../../../model';
-import styles from './relationpill.module.scss';
 import {
   setQuerybuilderNodes,
   useAppDispatch,
@@ -12,6 +11,7 @@ import {
 import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import graphology from 'graphology';
+import { LeftHandle, RelationshipHandleArrowType, RightHandle } from './relation-handles';
 
 /**
  * Component to render a relation flow element
@@ -32,6 +32,8 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => {
     max: data.depth.max || settings.depth.max,
   });
 
+  const [direction, setDirection] = useState<RelationshipHandleArrowType>('right');
+
   useEffect(() => {
     setDepth({ min: data.depth.min || settings.depth.min, max: data.depth.max || settings.depth.max });
   }, [data.depth]);
@@ -49,37 +51,32 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => {
     }
   };
 
+  const onChangeDirection = () => {
+    if (direction === 'right') {
+      setDirection('both');
+      graphologyGraph.setNodeAttribute<any>(node.id, 'direction', 'both');
+    } else {
+      setDirection('right');
+      graphologyGraph.setNodeAttribute<any>(node.id, 'direction', 'right');
+    }
+    dispatch(setQuerybuilderGraphology(graphologyGraph));
+  };
+
   const calcWidth = (data: number) => {
     return data.toString().length + 0.5 + 'ch';
   };
 
   return (
-    <div className={styles.relation}>
-      <div className={styles.relationWrapper}>
-        <span className={styles.relationHandleFiller}>
-          {data.leftEntityHandleId && (
-            <Handle id={toHandleId(data.leftEntityHandleId)} type="target" position={Position.Left} className={styles.relationHandleLeft} />
+    <div className="text-center font-bold bg-relation-50 border-l-relation-600 border-l-[3px] text-[13px] min-w-[325px]">
+      <div>
+        <span>
+          {data.rightEntityHandleId && (
+            <RightHandle id={data.rightEntityHandleId} type="source" point={direction} onDoubleClick={onChangeDirection} />
           )}
         </span>
-        {/* <span className={styles.relationHandleFiller}>
-          <Handle
-            id={getHandleId(data.name, data.type, Handles.ToAttribute, '')}
-            type="target"
-            position={Position.Left}
-            className={styles.relationHandleAttribute + ' ' + (false ? styles.handleConnectedFill : '')}
-          />
-        </span>
-        <span className={styles.relationHandleFiller}>
-          <Handle
-            id={getHandleId(data.name, data.type, Handles.ReceiveFunction, '')}
-            type="target"
-            position={Position.Left}
-            className={styles.relationHandleFunction + ' ' + (false ? styles.handleConnectedFill : '')}
-          />
-        </span> */}
-        <div className={styles.relationDataWrapper}>
-          <span className={styles.relationSpan}>{data?.name}</span>
-          <span className={styles.relationInputHolder}>
+        <div>
+          <span>{data?.name}</span>
+          <span>
             <span>[</span>
             <input
               className={
@@ -129,14 +126,9 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => {
             <span>]</span>
           </span>
         </div>
-        <span className={styles.relationHandleFiller}>
-          {data.rightEntityHandleId && (
-            <Handle
-              id={toHandleId(data.rightEntityHandleId)}
-              type="source"
-              position={Position.Right}
-              className={styles.relationHandleRight + ' ' + (false ? styles.handleConnectedBorderRight : '')}
-            />
+        <span>
+          {data.leftEntityHandleId && (
+            <LeftHandle id={data.leftEntityHandleId} type="target" point={direction} onDoubleClick={onChangeDirection} />
           )}
         </span>
       </div>
diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
index 04b325bf1..085dcba4e 100644
--- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts
@@ -595,7 +595,7 @@ describe('QueryUtils Entity and Relations', () => {
           node: {
             ID: 'e1',
             label: 'Airport 1',
-            relation: { direction: 'TO', node: { ID: 'e2', label: 'Airport 1' } },
+            relation: { direction: 'BOTH', node: { ID: 'e2', label: 'Airport 1' } },
           },
         },
       ],
@@ -668,7 +668,7 @@ describe('QueryUtils Entity and Relations', () => {
           node: {
             ID: 'e1',
             label: 'Airport 1',
-            relation: { direction: 'TO', node: { ID: 'e2', label: 'Airport 1' } },
+            relation: { direction: 'BOTH', node: { ID: 'e2', label: 'Airport 1' } },
           },
         },
         {
@@ -676,7 +676,7 @@ describe('QueryUtils Entity and Relations', () => {
           node: {
             ID: 'e2',
             label: 'Airport 1',
-            relation: { direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } },
+            relation: { direction: 'BOTH', node: { ID: 'e1', label: 'Airport 1' } },
           },
         },
       ],
@@ -698,7 +698,7 @@ describe('QueryUtils Entity and Relations', () => {
       query: [
         {
           ID: 'path_0',
-          node: { ID: 'e1', label: 'Airport 1', relation: { direction: 'TO', node: { ID: 'e1', label: 'Airport 1' } } },
+          node: { ID: 'e1', label: 'Airport 1', relation: { direction: 'BOTH', node: { ID: 'e1', label: 'Airport 1' } } },
         },
       ],
     };
diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
index e50c0d2d7..57b65c31d 100644
--- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
@@ -42,6 +42,7 @@ const traverseEntityRelationPaths = (
           x: node.attributes.x,
           y: node.attributes.x,
           depth: { min: settings.depth.min, max: settings.depth.max },
+          direction: 'both',
         });
       } else {
         paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x });
@@ -258,7 +259,7 @@ export function Query2BackendQuery(
         ID: _currNode.id,
         label: _currNode.name || undefined,
         depth: _currNode.depth,
-        direction: 'TO',
+        direction: !_currNode.direction || _currNode.direction === 'right' ? 'TO' : _currNode.direction === 'left' ? 'FROM' : 'BOTH',
         node: chunk.length === position + 1 ? undefined : (processConnection(chunk, position + 1) as NodeStruct),
       };
       return ret;
-- 
GitLab