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 e774208e2edc900f4b6a14a50e8a742cc85745e6..755d2b41d564abe3f9e4eb41f56865ef7d2432f4 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 @@ -7,6 +7,10 @@ border-radius: 3px; } +.highlighted { + box-shadow: black 0 0 2px; +} + .handleLeft { border: 0px; border-radius: 0px; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx index 2598bf4581edd6c9ca1225b2b8351155289350e0..1e5f6f7ba5d6a97bd5b47daea744a3a033cd09e7 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx @@ -3,6 +3,7 @@ import { useTheme } from '@mui/material'; import React, { useEffect } from 'react'; import { FlowElement, Handle, Position } from 'react-flow-renderer'; import styles from './entitypill.module.scss'; +import cn from 'classnames'; // export const Handless = { // entity: { @@ -31,7 +32,9 @@ export const EntityRFPill = React.memo(({ data }: { data: any }) => { return ( <div - className={styles.entity} + className={cn(styles.entity, { + [styles.highlighted]: data.suggestedForConnection, + })} style={{ background: theme.palette.queryBuilder.entity.background, color: theme.palette.queryBuilder.text, diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss index c0b913a4537276fa249d66b21fb846fc6fd18635..aff84b2e5d8b9caba6dd9fdd34a99b54614946d4 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss @@ -7,12 +7,17 @@ background-color: transparent; } +.highlighted { + box-shadow: black 0 0 2px; +} + .contentWrapper { display: flex; align-items: center; .handleLeft { position: relative; + z-index: 3; top: 25%; border: 0px; @@ -86,6 +91,7 @@ $height: 10px; .arrowLeft { + z-index: 2; width: 0; height: 0; border-top: $height solid transparent; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx index 34c0d57eb52dbddd0412474b2c765f321a0c1c8f..f1d4d8592cf367e719ac3db963948b5c06145ef1 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx @@ -1,6 +1,7 @@ import { handles } from '@graphpolaris/querybuilder/usecases'; import { useTheme } from '@mui/material'; import { Handle, Position } from 'react-flow-renderer'; +import cn from 'classnames'; import styles from './relationpill.module.scss'; @@ -54,7 +55,9 @@ export default function RelationRFPill({ data }: { data: any }) { }} /> <div - className={styles.contentWrapper} + className={cn(styles.contentWrapper, { + [styles.highlighted]: data.suggestedForConnection, + })} style={{ color: theme.palette.queryBuilder.text, background: theme.palette.queryBuilder.relation.background, diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx index 22a655d556152fb7281b0bb22908bb8c0f78e0a9..3870f76de645a2d47757c5e0a40e2c8bea89503b 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx @@ -1,14 +1,15 @@ import { createReactFlowElements, - DragAttributePill, - DragAttributesAlong, + dragPill, + dragPillStarted, + dragPillStopped, } from '@graphpolaris/querybuilder/usecases'; import { setQuerybuilderNodes, useAppDispatch, useQuerybuilderNodes, } from '@graphpolaris/shared/data-access/store'; -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import ReactFlow, { ReactFlowProvider, Background, @@ -38,6 +39,7 @@ const onLoad = (reactFlowInstance: any) => { const QueryBuilder = (props: {}) => { const nodes = useQuerybuilderNodes(); const dispatch = useAppDispatch(); + const isDraggingPill = useRef(false); const elements = useMemo(() => createReactFlowElements(nodes), [nodes]); @@ -45,27 +47,35 @@ const QueryBuilder = (props: {}) => { event: React.MouseEvent<Element, MouseEvent>, node: Node<any> ) => { + // Get the node in the elements list to get the previous location const pNode = elements.find((e) => e.id == node.id); if (!(pNode && isNode(pNode))) return; - + // This is then used to calculate the delta position const dx = node.position.x - pNode.position.x; const dy = node.position.y - pNode.position.y; - nodes.setNodeAttribute(node.id, 'x', node.position.x); - nodes.setNodeAttribute(node.id, 'y', node.position.y); - - switch (nodes.getNodeAttribute(node.id, 'type')) { - case 'attribute': - DragAttributePill(node.id, nodes, dx, dy); - break; - case 'entity': - DragAttributesAlong(node.id, nodes, dx, dy); - break; - case 'relation': - DragAttributesAlong(node.id, nodes, dx, dy); - break; + // Check if we started dragging, if so, call the drag started usecase + if (!isDraggingPill.current) { + dragPillStarted(node.id, nodes); + isDraggingPill.current = true; } + // Call the drag usecase + dragPill(node.id, nodes, dx, dy, node.position); + + // Dispatch the new graphology object, so reactflow will get rerendered + dispatch(setQuerybuilderNodes(nodes.export())); + }; + const onNodeDragStop = ( + event: React.MouseEvent<Element, MouseEvent>, + node: Node<any> + ) => { + isDraggingPill.current = false; + + // Call the drag pill stopped usecase + dragPillStopped(node.id, nodes); + + // Dispatch the new graphology object, so reactflow will get rerendered dispatch(setQuerybuilderNodes(nodes.export())); }; @@ -81,6 +91,7 @@ const QueryBuilder = (props: {}) => { connectionLineComponent={ConnectionDragLine} onLoad={onLoad} onNodeDrag={onNodeDrag} + onNodeDragStop={onNodeDragStop} className={styles.reactflow} > <Background gap={10} size={0.7} /> diff --git a/libs/querybuilder/usecases/src/index.ts b/libs/querybuilder/usecases/src/index.ts index fa59d4c1fd04854d57f7c8765d39de8a765054b9..32e0c81f2ecfe335cb61852728753c148b9c6c8b 100644 --- a/libs/querybuilder/usecases/src/index.ts +++ b/libs/querybuilder/usecases/src/index.ts @@ -2,6 +2,5 @@ export * from './lib/attribute/getAttributeBoolOperators'; export * from './lib/attribute/checkInput'; export * from './lib/createReactFlowElements'; export * from './lib/pillHandles'; -export * from './lib/dragging/dragAttribute'; -export * from './lib/dragging/dragAttributesAlong'; +export * from './lib/dragging/dragPill'; export * from './lib/addPill'; diff --git a/libs/querybuilder/usecases/src/lib/addPill.ts b/libs/querybuilder/usecases/src/lib/addPill.ts index 008127598cfee48f3f71793ba23a5e3062607621..e7b359d8f0fc77f1281fa57a7c200f13acfb820e 100644 --- a/libs/querybuilder/usecases/src/lib/addPill.ts +++ b/libs/querybuilder/usecases/src/lib/addPill.ts @@ -56,7 +56,7 @@ function calcWidthHeightOfPill(attributes: Attributes): { const widthOfPillWithoutText = 42.1164; // WARNING: depends on styling w += widthOfPillWithoutText; - h = 21; + h = 20; break; } case 'relation': { diff --git a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts index f48dd787c36ab5b8ceb4c2cdf24f9a8ba1c9d5a6..a1c29ab1f05d62105afaf80faaa405c30ce60535 100644 --- a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts +++ b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts @@ -11,7 +11,7 @@ function toBoolean(s: string): string { return 'false'; } -/** Checks if the provided value is the same as the datatype of the attribute. */ +/** Checks if the provided value has the same as the datatype of the attribute. */ export function CheckDatatypeConstraint(type: string, str: string): string { let res = ''; switch (type) { diff --git a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts index eea786d7fe5c6cd7f3888545af8b621e6b69fbfc..eba3bf06477f9bd53ac9e1c87ea13126ecd58e02 100644 --- a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts +++ b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts @@ -1,6 +1,6 @@ -import Graph from 'graphology'; +import Graph, { MultiGraph } from 'graphology'; import { Attributes } from 'graphology-types'; -import { Elements, Node, Edge } from 'react-flow-renderer'; +import { Elements, Node, Edge, XYPosition } from 'react-flow-renderer'; // Takes the querybuilder graph as an input and creates react flow elements for them. export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { @@ -8,6 +8,7 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { graph.forEachNode((node: string, attributes: Attributes): void => { let data; + let position = { x: attributes?.x || 0, y: attributes?.y || 0 }; switch (attributes.type) { case 'entity': @@ -41,21 +42,29 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { value: attributes.value, attributeOfA: attributeOfA, }; + // Get the position of the attribute, based on the connection to entity or relation + const p = getAttributePosition(node, graph); + if (p) position = p; break; } } // Each pill should have a name and type - data = { ...data, name: attributes.name }; + data = { + ...data, + name: attributes.name, + suggestedForConnection: attributes.suggestedForConnection, // Highlights the pill, with shadow or something + }; const RFNode: Node = { id: node, type: attributes.type, - position: { x: attributes?.x || 0, y: attributes?.y || 0 }, + position: position, data: data, }; elements.push(RFNode); }); + // 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; @@ -73,3 +82,49 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { return elements; } + +/** Gets the position of an attribute based on the connection to an entity or relation. + * It uses the position of the parent pill and what the index is of this attribute in all + * the connected attributes to the parent. + */ +function getAttributePosition( + id: string, + nodes: MultiGraph +): XYPosition | undefined { + const nbs = nodes.filterOutNeighbors(id, (_, { type }) => + ['entity', 'relation'].includes(type) + ); + + if (nbs.length > 1) + console.log( + 'WARNING: attribute connected to more than one entity or relation' + ); + else if (nbs.length == 1) { + const nb = nbs[0]; + const connectedAttributes = nodes.filterInNeighbors( + nb, + (_, { type }) => type == 'attribute' + ); + + // An entity can have more attributes, what is the attributes index in the attributes array of that entity? + let nthAttibute = -1; + for (let i = 0; i < connectedAttributes.length; i++) { + if (connectedAttributes[i] == id) { + nthAttibute = i; + break; + } + } + + const nbAttr = nodes.getNodeAttributes(nb); + + const pos = { x: nbAttr.x + 30, y: nbAttr.y + nbAttr.h }; + // ASSUMES THAT EACH ATTRIBUTE HAS THE SAME HEIGHT + const heightOfAttributes = nodes.getNodeAttribute(id, 'h') - 1; + pos.y += nthAttibute * heightOfAttributes; + + return pos; + } + + // If the attribute has no (attribute_)connection, don't position it. + return undefined; +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts b/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts index 332ca29cd3283c97d3876b9ca176068d97815e3e..df89ab57dcc526b42c151e868a50add049efbe7d 100644 --- a/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts +++ b/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts @@ -1,12 +1,30 @@ -import Graph from 'graphology'; +import { MultiGraph } from 'graphology'; +import { GetClosestPill } from './getClosestPill'; + +export function DragAttributePillStarted(id: string, nodes: MultiGraph) { + // if the attribute is still connected to an entity or relation pill, disconnect + const es = nodes.outEdges(id); + es.forEach((e) => nodes.dropEdge(e)); +} export function DragAttributePill( id: string, - nodes: Graph, + nodes: MultiGraph, dx: number, dy: number ) { - // if the attribute is still connected to an entity or relation pill, disconnect - const es = nodes.outEdges(id); - es.forEach((e) => nodes.dropEdge(e)); + // Get the closes entity or relation node + const closestNode = GetClosestPill(id, nodes, ['entity', 'relation']); + // If we found one, highlight it by adding an attribute + if (closestNode) + nodes.setNodeAttribute(closestNode, 'suggestedForConnection', true); +} + +export function DragAttibutePillStopped(id: string, nodes: MultiGraph) { + // If there is currently a node with the suggestedForConnection attribute + // connect this attribute to it + nodes.forEachNode((node, { suggestedForConnection }) => { + if (suggestedForConnection) + nodes.addEdge(id, node, { type: 'attribute_connection' }); + }); } diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts b/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts new file mode 100644 index 0000000000000000000000000000000000000000..171124ac9330a007b531311fec85dceeb4ccf945 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts @@ -0,0 +1,18 @@ +import { MultiGraph } from 'graphology'; + +export function DragEntityPillStarted(id: string, nodes: MultiGraph) { + // Started dragging entity usecase +} + +export function DragEntityPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number +) { + // Code for dragging an entity pill should go here +} + +export function DragEntityPillStopped(id: string, nodes: MultiGraph) { + // Stopped dragging entity pill +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts b/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts new file mode 100644 index 0000000000000000000000000000000000000000..3832c103d94fbb2d759edc082cd643051580712a --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts @@ -0,0 +1,90 @@ +import { MultiGraph } from 'graphology'; +import { XYPosition } from 'react-flow-renderer'; +import { + DragAttibutePillStopped, + DragAttributePill, + DragAttributePillStarted, +} from './dragAttribute'; +import { DragAttributesAlong } from './dragAttributesAlong'; +import { + DragEntityPill, + DragEntityPillStarted, + DragEntityPillStopped, +} from './dragEntity'; +import { + DragRelationPill, + DragRelationPillStarted, + DragRelationPillStopped, +} from './dragRelation'; + +export function dragPillStarted(id: string, nodes: MultiGraph) { + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttributePillStarted(id, nodes); + break; + case 'entity': + DragEntityPillStarted(id, nodes); + break; + case 'relation': + DragRelationPillStarted(id, nodes); + break; + } +} + +/** + * A general drag usecase for any pill, it will select the correct usecase for each pill + * @param id + * @param nodes The graphology query builder nodes object + * @param dx Delta x + * @param dy Delta y + * @param position The already updated positiong (dx dy are already applied) + */ +export function dragPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number, + position: XYPosition +) { + // Update the position of the node in the graphology object + nodes.setNodeAttribute(id, 'x', position.x); + nodes.setNodeAttribute(id, 'y', position.y); + + // Remove the highlighted attribute from each node + nodes.forEachNode((node) => + nodes.removeNodeAttribute(node, 'suggestedForConnection') + ); + + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttributePill(id, nodes, dx, dy); + break; + case 'entity': + DragAttributesAlong(id, nodes, dx, dy); + DragEntityPill(id, nodes, dx, dy); + break; + case 'relation': + DragAttributesAlong(id, nodes, dx, dy); + DragRelationPill(id, nodes, dx, dy); + break; + } +} + +export function dragPillStopped(id: string, nodes: MultiGraph) { + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttibutePillStopped(id, nodes); + break; + case 'entity': + DragEntityPillStopped(id, nodes); + break; + case 'relation': + DragRelationPillStopped(id, nodes); + break; + } + + // Remove all suggestedForConnection attributes + nodes.forEachNode((node) => + nodes.removeNodeAttribute(node, 'suggestedForConnection') + ); +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts b/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts new file mode 100644 index 0000000000000000000000000000000000000000..12bf51961ae8cff27113c73a14c81d1e6c346b91 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts @@ -0,0 +1,18 @@ +import { MultiGraph } from 'graphology'; + +export function DragRelationPillStarted(id: string, nodes: MultiGraph) { + // Started dragging relation usecase +} + +export function DragRelationPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number +) { + // Code for dragging an relation pill should go here +} + +export function DragRelationPillStopped(id: string, nodes: MultiGraph) { + // Stopped dragging relation pill +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts b/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a3fd44874fa8de1f5b3d1508d456dfae0848faa --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts @@ -0,0 +1,40 @@ +import { MultiGraph } from 'graphology'; + +/** + * Gets the closest node to id + * @param id + * @param nodes Graphology querybuilder MultiGraph object + * @param allowedNodeTypes An array of the node types which are included in the search + * @param maxDistance The maximum distance + * @returns the closest node if within range + */ +export function GetClosestPill( + id: string, + nodes: MultiGraph, + allowedNodeTypes: string[], + maxDistance = 150 +): string | undefined { + const { x, y, w, h } = nodes.getNodeAttributes(id); + const center: { x: number; y: number } = { x: x + w / 2, y: y + h / 2 }; + + let minDist = maxDistance * maxDistance; + let closestNode: string | undefined = undefined; + nodes.forEachNode((node, { x, y, w, h, type }) => { + if (allowedNodeTypes.includes(type)) { + const nodeCenter: { x: number; y: number } = { + x: x + w / 2, + y: y + h / 2, + }; + + const dx = center.x - nodeCenter.x; + const dy = center.y - nodeCenter.y; + const dist = dx * dx + dy * dy; + if (dist < minDist) { + minDist = dist; + closestNode = node; + } + } + }); + + return closestNode; +} diff --git a/package.json b/package.json index 3f7093bb5c59beccce637d66aefbbab914cb7188..9d07a7788766fc39c5af14ad5142bf853f2776fe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/cytoscape": "^3.19.4", "@types/react-grid-layout": "^1.3.0", "@types/styled-components": "^5.1.21", + "classnames": "^2.3.1", "color": "^4.2.1", "core-js": "^3.6.5", "cytoscape": "^3.21.0", diff --git a/yarn.lock b/yarn.lock index 39285815c8ccda88216897b21a703f99843949e0..ad3e6305a7719eb8dc8d0e2a9ad48f4ff0bc32e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6427,6 +6427,11 @@ classcat@^5.0.3: resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.3.tgz#38eaa0ec6eb1b10faf101bbcef2afb319c23c17b" integrity sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ== +classnames@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"