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