From 908a4eca6b5b045b2ca4c0023f886aa7463f13be Mon Sep 17 00:00:00 2001
From: Sivan Duijn <sivanduijn@gmail.com>
Date: Mon, 14 Mar 2022 10:37:05 +0100
Subject: [PATCH] feat(querybuilder): attributes move with entity/relation
 pills

---
 .../attributepill/attributepill.tsx           | 128 ++++++++++--------
 .../attributepill/operatorselect.module.scss  |   2 +-
 .../entitypill/entitypill.module.scss         |   1 +
 .../querybuilder/querybuilder.stories.tsx     |  74 ++++++----
 .../components/querybuilder/querybuilder.tsx  |   2 +-
 libs/querybuilder/usecases/src/index.ts       |   1 +
 libs/querybuilder/usecases/src/lib/addPill.ts |  94 +++++++++++++
 .../src/lib/createReactFlowElements.ts        |   1 +
 libs/shared/data-access/store/src/index.ts    |   2 +
 .../store/src/lib/querybuilderSlice.ts        |  34 ++++-
 .../data-access/store/src/lib/schemaSlice.ts  |   6 +-
 11 files changed, 255 insertions(+), 90 deletions(-)
 create mode 100644 libs/querybuilder/usecases/src/lib/addPill.ts

diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx
index a2a70d95b..41b5803cc 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx
+++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx
@@ -2,6 +2,11 @@ import {
   CheckDatatypeConstraint,
   GetAttributeBoolOperators,
 } from '@graphpolaris/querybuilder/usecases';
+import {
+  updateQBAttributeOperator,
+  updateQBAttributeValue,
+  useAppDispatch,
+} from '@graphpolaris/shared/data-access/store';
 import { useTheme } from '@mui/material';
 import React, { useMemo, useState } from 'react';
 import styles from './attributepill.module.scss';
@@ -11,74 +16,81 @@ import AttributeOperatorSelect from './operatorselect';
  * Component to render an attribute flow element
  * @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
  */
-export const AttributeRFPill = React.memo(({ data }: { data: any }) => {
-  const theme = useTheme();
-  const [value, setValue] = useState(data?.value || '');
+export const AttributeRFPill = React.memo(
+  ({ id, data }: { id: string; data: any }) => {
+    const theme = useTheme();
+    const dispatch = useAppDispatch();
+    const [value, setValue] = useState(data?.value || '');
 
-  const onChange = (e: any) => {
-    setValue(e.target.value);
-  };
-  const validateInput = () => {
-    setValue(CheckDatatypeConstraint(data.datatype, value));
-  };
+    const onChange = (e: any) => {
+      setValue(e.target.value);
+    };
+    const validateInput = () => {
+      const newValue = CheckDatatypeConstraint(data.datatype, value);
+      setValue(newValue);
+      dispatch(updateQBAttributeValue({ id, value: newValue }));
+    };
 
-  // Calculates the size of the input
-  const getInputWidth = () => {
-    if (value == '') return 1;
-    else if (value.length > 10) return 10;
-    return value.length;
-  };
+    // Calculates the size of the input
+    const getInputWidth = () => {
+      if (value == '') return 1;
+      else if (value.length > 10) return 10;
+      return value.length;
+    };
 
-  const boolOperators = useMemo(
-    () => GetAttributeBoolOperators(data?.datatype),
-    [data?.datatype]
-  );
+    const boolOperators = useMemo(
+      () => GetAttributeBoolOperators(data?.datatype),
+      [data?.datatype]
+    );
 
-  // Determine the backgroundcolor based on if the attribute is connected to a entity or relation
-  let bgcolor;
-  if (data?.attributeOfA == 'entity')
-    bgcolor = theme.palette.queryBuilder.entity.lighterbg;
-  else if (data?.attributeOfA == 'relation')
-    bgcolor = theme.palette.queryBuilder.relation.lighterbg;
-  else bgcolor = theme.palette.queryBuilder.attribute.background;
+    // Determine the backgroundcolor based on if the attribute is connected to a entity or relation
+    let bgcolor;
+    if (data?.attributeOfA == 'entity')
+      bgcolor = theme.palette.queryBuilder.entity.lighterbg;
+    else if (data?.attributeOfA == 'relation')
+      bgcolor = theme.palette.queryBuilder.relation.lighterbg;
+    else bgcolor = theme.palette.queryBuilder.attribute.background;
 
-  return (
-    <div
-      className={styles.attribute}
-      style={{
-        background: bgcolor,
-        color: theme.palette.queryBuilder.text,
-      }}
-    >
-      {/* <Handle
+    return (
+      <div
+        className={styles.attribute}
+        style={{
+          background: bgcolor,
+          color: theme.palette.queryBuilder.text,
+        }}
+      >
+        {/* <Handle
         id={Handles.Attribute}
         type="source"
         position={Position.Bottom}
         className={styles.handle}
       /> */}
-      <div className={styles.contentWrapper}>
-        <span className={styles.content} title={data.name}>
-          {data.name}
-        </span>
-        <AttributeOperatorSelect
-          selected={data?.operator}
-          options={boolOperators}
-          changed={(o) => console.log(o)}
-        />
-        <span className={styles.attributeInput}>
-          <input
-            style={{ maxWidth: `${getInputWidth()}ch` }}
-            type="string"
-            placeholder={'?'}
-            value={value}
-            onChange={onChange}
-            onBlur={validateInput}
-            onKeyDown={(e) => e.key == 'Enter' && validateInput()}
-          ></input>
-        </span>
+        <div className={styles.contentWrapper}>
+          <span className={styles.content} title={data.name}>
+            {data.name}
+          </span>
+          <AttributeOperatorSelect
+            selected={data?.operator}
+            options={boolOperators}
+            changed={(o) =>
+              dispatch(updateQBAttributeOperator({ id, operator: o.value }))
+            }
+          />
+          <span className={styles.attributeInput}>
+            <input
+              style={{ maxWidth: `${getInputWidth()}ch` }}
+              type="string"
+              placeholder={'?'}
+              value={value}
+              onChange={onChange}
+              onBlur={validateInput}
+              onKeyDown={(e) => e.key == 'Enter' && validateInput()}
+            ></input>
+          </span>
+        </div>
       </div>
-    </div>
-  );
-});
+    );
+  }
+);
 
 export default AttributeRFPill;
diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss
index 29c33051c..1c31d2744 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss
+++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss
@@ -5,7 +5,7 @@
   vertical-align: baseline;
   margin: 0 1ch;
   font-weight: normal;
-  font-size: 1.2em;
+  font-size: 7px;
 }
 
 .valueContainer {
diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss
index 0deacb109..e774208e2 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss
+++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss
@@ -40,5 +40,6 @@
     text-overflow: ellipsis;
     overflow: hidden;
     white-space: nowrap;
+    display: block;
   }
 }
diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx
index 9727183be..b406d60b5 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx
+++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx
@@ -10,7 +10,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { Provider } from 'react-redux';
 import QueryBuilder from './querybuilder';
 import { MultiGraph } from 'graphology';
-import { handles } from '@graphpolaris/querybuilder/usecases';
+import { addPill, handles } from '@graphpolaris/querybuilder/usecases';
 
 export default {
   component: QueryBuilder,
@@ -28,32 +28,52 @@ const mockStore = configureStore({
   },
 });
 const graph = new MultiGraph();
-graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' });
-graph.addNode('1', { type: 'relation', x: 140, y: 140, name: 'Relation Pill' });
-graph.addNode('2', {
-  type: 'attribute',
-  x: 170,
-  y: 160,
-  name: 'Attr string',
-  datatype: 'string',
-  operator: 'EQ',
-});
-graph.addNode('3', {
-  type: 'attribute',
-  x: 170,
-  y: 170,
-  name: 'Attr number',
-  datatype: 'float',
-  operator: 'EQ',
-});
-graph.addNode('4', {
-  type: 'attribute',
-  x: 130,
-  y: 120,
-  name: 'Attr bool',
-  datatype: 'bool',
-  operator: 'EQ',
-});
+addPill('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }, graph);
+// graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' });
+addPill(
+  '1',
+  { type: 'relation', x: 140, y: 140, name: 'Relation Pill' },
+  graph
+);
+addPill(
+  '2',
+  {
+    type: 'attribute',
+    x: 170,
+    y: 160,
+    name: 'Attr string',
+    datatype: 'string',
+    operator: 'EQ',
+    value: 'mark',
+  },
+  graph
+);
+addPill(
+  '3',
+  {
+    type: 'attribute',
+    x: 170,
+    y: 170,
+    name: 'Attr number',
+    datatype: 'float',
+    operator: 'EQ',
+  },
+  graph
+);
+addPill(
+  '4',
+  {
+    type: 'attribute',
+    x: 130,
+    y: 120,
+    name: 'Attr bool',
+    datatype: 'bool',
+    operator: 'EQ',
+    value: 'true',
+  },
+  graph
+);
+console.log(graph.getNodeAttributes('2'));
 graph.addEdge('2', '1', { type: 'attribute_connection' });
 graph.addEdge('3', '1', { type: 'attribute_connection' });
 graph.addEdge('4', '0', { type: 'attribute_connection' });
diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
index 031156107..65aa3450e 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
+++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
@@ -85,7 +85,7 @@ const QueryBuilder = (props: {}) => {
           nodeTypes={nodeTypes}
           edgeTypes={edgeTypes}
           connectionLineComponent={ConnectionDragLine}
-          onLoad={onLoad}
+          // onLoad={onLoad}
           onNodeDrag={onNodeDrag}
           className={styles.reactflow}
         >
diff --git a/libs/querybuilder/usecases/src/index.ts b/libs/querybuilder/usecases/src/index.ts
index b84282029..fa59d4c1f 100644
--- a/libs/querybuilder/usecases/src/index.ts
+++ b/libs/querybuilder/usecases/src/index.ts
@@ -4,3 +4,4 @@ export * from './lib/createReactFlowElements';
 export * from './lib/pillHandles';
 export * from './lib/dragging/dragAttribute';
 export * from './lib/dragging/dragAttributesAlong';
+export * from './lib/addPill';
diff --git a/libs/querybuilder/usecases/src/lib/addPill.ts b/libs/querybuilder/usecases/src/lib/addPill.ts
new file mode 100644
index 000000000..008127598
--- /dev/null
+++ b/libs/querybuilder/usecases/src/lib/addPill.ts
@@ -0,0 +1,94 @@
+import {
+  setQuerybuilderNodes,
+  store,
+} from '@graphpolaris/shared/data-access/store';
+import Graph from 'graphology';
+import { Attributes } from 'graphology-types';
+
+/** monospace fontsize table */
+const widthPerFontsize = {
+  6: 3.6167,
+  7: 4.2167,
+  10: 6.0167,
+};
+
+/** Adds a query builder pill to the graphology nodes object. */
+export function addPill(
+  id: string,
+  attributes: Attributes,
+  nodes: Graph
+): boolean {
+  const { type, name } = attributes;
+  if (!type || !name) return false;
+  let { x, y } = attributes;
+
+  // Check if x and y are present, otherwise set them to 0
+  if (!x) x = 0;
+  if (!y) y = 0;
+
+  // Get the width and height of a node
+  const { w, h } = calcWidthHeightOfPill(attributes);
+
+  // Add a node to the graphology object
+  nodes.addNode(id, { ...attributes, x, y, w, h });
+
+  // Set the new nodes in the query builder slice
+  store.dispatch(setQuerybuilderNodes(nodes.export()));
+
+  return true;
+}
+
+/** Calculates the width and height of a query builder pill.
+ * DEPENDS ON STYLING, if styling changed, change this.
+ */
+function calcWidthHeightOfPill(attributes: Attributes): {
+  w: number;
+  h: number;
+} {
+  const { type, name } = attributes;
+
+  let w = 0;
+  let h = 0;
+  switch (type) {
+    case 'entity': {
+      // calculate width and height of entity pill
+      w = Math.min(name.length, 20) * widthPerFontsize[10]; // for fontsize 10px
+
+      const widthOfPillWithoutText = 42.1164; // WARNING: depends on styling
+      w += widthOfPillWithoutText;
+      h = 21;
+      break;
+    }
+    case 'relation': {
+      // calculate width and height of relation pill
+      w = Math.min(name.length, 20) * widthPerFontsize[10]; // for fontsize 10px
+
+      const widthOfPillWithoutText = 56.0666; // WARNING: depends on styling
+      w += widthOfPillWithoutText;
+      h = 20;
+      break;
+    }
+    case 'attribute': {
+      // calculate width and height of relation pill
+      const pixelsPerChar = widthPerFontsize[6]; // for fontsize 10px
+      w = name.length * pixelsPerChar;
+
+      const { datatype, operator } = attributes;
+      let value = attributes['value'];
+      if (!datatype || !operator) return { w: 0, h: 0 };
+      if (!value) value = '?';
+
+      // Add width of operator
+      w += operator.length * widthPerFontsize[7];
+      // use a max of 10, because max-width is set to 10ch;
+      w += Math.min(value.length, 10) * widthPerFontsize[6];
+
+      const widthOfPillWithoutText = 25.6666; // WARNING: depends on styling
+      w += widthOfPillWithoutText;
+      h = 12;
+      break;
+    }
+  }
+
+  return { w, h };
+}
diff --git a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts
index 1757b3291..eea786d7f 100644
--- a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts
+++ b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts
@@ -38,6 +38,7 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> {
         data = {
           datatype: attributes.datatype,
           operator: attributes.operator,
+          value: attributes.value,
           attributeOfA: attributeOfA,
         };
         break;
diff --git a/libs/shared/data-access/store/src/index.ts b/libs/shared/data-access/store/src/index.ts
index 1f385bec2..925033dab 100644
--- a/libs/shared/data-access/store/src/index.ts
+++ b/libs/shared/data-access/store/src/index.ts
@@ -9,6 +9,8 @@ export {
 export {
   querybuilderSlice,
   setQuerybuilderNodes,
+  updateQBAttributeOperator,
+  updateQBAttributeValue,
 } from './lib/querybuilderSlice';
 export {
   selectGraphQueryResult,
diff --git a/libs/shared/data-access/store/src/lib/querybuilderSlice.ts b/libs/shared/data-access/store/src/lib/querybuilderSlice.ts
index 716995a7b..877444a06 100644
--- a/libs/shared/data-access/store/src/lib/querybuilderSlice.ts
+++ b/libs/shared/data-access/store/src/lib/querybuilderSlice.ts
@@ -20,13 +20,45 @@ export const querybuilderSlice = createSlice({
     ) => {
       state.graphologySerialized = action.payload;
     },
+    updateQBAttributeOperator: (
+      state,
+      action: PayloadAction<{ id: string; operator: string }>
+    ) => {
+      const graph = MultiGraph.from(state.graphologySerialized);
+      graph.setNodeAttribute(
+        action.payload.id,
+        'operator',
+        action.payload.operator
+      );
+      state.graphologySerialized = graph.export();
+    },
+    updateQBAttributeValue: (
+      state,
+      action: PayloadAction<{ id: string; value: string }>
+    ) => {
+      const graph = MultiGraph.from(state.graphologySerialized);
+      graph.setNodeAttribute(action.payload.id, 'value', action.payload.value);
+      state.graphologySerialized = graph.export();
+    },
+    // addQuerybuilderNode: (
+    //   state,
+    //   action: PayloadAction<{ id: string; attributes: Attributes }>
+    // ) => {
+    //   const graph = MultiGraph.from(state.graphologySerialized);
+    //   graph.addNode(action.payload.id, action.payload.attributes);
+    //   state.graphologySerialized = graph.export();
+    // },
     // setGraphLayout: (state, action: PayloadAction<AllLayoutAlgorithms>) => {
     //   state.schemaLayout = action.payload;
     // },
   },
 });
 
-export const { setQuerybuilderNodes } = querybuilderSlice.actions;
+export const {
+  setQuerybuilderNodes,
+  updateQBAttributeOperator,
+  updateQBAttributeValue,
+} = querybuilderSlice.actions;
 
 /** Select the querybuilder nodes and convert it to a graphology object */
 export const selectQuerybuilderNodes = (state: RootState): MultiGraph => {
diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.ts b/libs/shared/data-access/store/src/lib/schemaSlice.ts
index 697631d86..47ca37d1b 100644
--- a/libs/shared/data-access/store/src/lib/schemaSlice.ts
+++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts
@@ -102,8 +102,10 @@ export const { readInSchemaFromBackend, setSchema } = schemaSlice.actions;
  * Select the schema and convert it to a graphology object
  * */
 export const selectSchema = (state: RootState) => {
-  console.log(state);
-  return MultiGraph.from(state.schema.graphologySerialized);
+  // This is really weird but for some reason all the attributes appeared as read-only otherwise
+  return MultiGraph.from(
+    MultiGraph.from(state.schema.graphologySerialized).export()
+  );
 };
 
 /**
-- 
GitLab