diff --git a/libs/shared/lib/components/index.ts b/libs/shared/lib/components/index.ts index 7a58d1a1ae29e1d5ef28b847b3ba04da6a65525f..46862ade62b05133aaa731f0c93d9e43a6b45fe9 100644 --- a/libs/shared/lib/components/index.ts +++ b/libs/shared/lib/components/index.ts @@ -15,3 +15,4 @@ export * from './Legend'; export * from './LoadingSpinner'; export * from './Popup'; export * from './Resizable'; +export * from './pills'; diff --git a/libs/shared/lib/components/pills/Pill.tsx b/libs/shared/lib/components/pills/Pill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6045c582e14efced6e8c70007d848c15275cf27c --- /dev/null +++ b/libs/shared/lib/components/pills/Pill.tsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import { pillWidth, pillHeight, pillXPadding, pillInnerMargin, topLineHeight, pillBorderWidth, pillDropdownPadding } from './pill.const'; +import { Position } from 'reactflow'; +import { PillHandle } from './PillHandle'; + +export type PillI = { + onHovered?: (hovered: boolean) => void; + // onDragged: (dragged: boolean) => void; + title: string | React.ReactNode; + children?: React.ReactNode; + handles?: React.ReactNode; + style?: React.CSSProperties; + corner?: 'rounded' | 'square' | 'diamond'; + topColor: string; + handleUp?: React.ReactNode; + handleDown?: React.ReactNode; + handleLeft?: React.ReactNode; + handleRight?: React.ReactNode; +}; + +export const Pill = React.memo((props: PillI) => { + const [hovered, setHovered] = useState(false); + + const onMouseEnter = (event: React.MouseEvent) => { + if (!hovered) { + setHovered(true); + if (props.onHovered) props.onHovered(true); + } + }; + + const onMouseLeave = (event: React.MouseEvent) => { + if (hovered) { + setHovered(false); + if (props.onHovered) props.onHovered(false); + } + }; + + const width = pillWidth; + const corner = props.corner || 'rounded'; + + const innerContentStyle = { + minWidth: corner === 'diamond' ? width - pillBorderWidth * 2 : width - pillBorderWidth * 2, + maxWidth: corner === 'diamond' ? width - pillBorderWidth * 2 : width - pillBorderWidth * 2, + minHeight: pillHeight - pillBorderWidth * 2, + maxHeight: pillHeight - pillBorderWidth * 2, + margin: pillBorderWidth, + clipPath: corner === 'diamond' ? 'polygon(0.5% 50%, 4.9% 0%, 95.1% 0%, 99.5% 50%, 95.1% 100%, 4.9% 100%)' : '', + }; + + const outerContentStyle = { + minWidth: width, + maxWidth: width, + minHeight: pillHeight, + maxHeight: pillHeight, + clipPath: corner === 'diamond' ? 'polygon(0% 50%, 5% 0%, 95% 0%, 100% 50%, 95% 100%, 5% 100%)' : '', + }; + + return ( + <div className="flex flex-row flex-grow-0 text-xs"> + <div + className={'bg-secondary-200 ' + (corner !== 'square' ? 'rounded' : '')} + style={{ + ...outerContentStyle, + }} + > + <div + className="absolute -z-10" + style={{ + top: pillHeight - pillInnerMargin, + width: pillWidth, + paddingLeft: pillDropdownPadding, + paddingRight: pillDropdownPadding, + }} + > + {props.children} + </div> + <div + className={'bg-secondary-100 ' + (corner !== 'square' ? 'rounded-[3px]' : '')} + style={{ + ...innerContentStyle, + }} + > + {props.topColor && ( + <div className={(corner !== 'square' ? 'rounded-t ' : '') + props.topColor} style={{ height: topLineHeight }}></div> + )} + <div + className={'font-semibold bg-neutral-100 ' + (corner !== 'square' ? 'rounded-b-[3px]' : '')} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + > + <div + className="" + style={{ + transform: `translateY(${topLineHeight / 3}px)`, + paddingLeft: pillXPadding + (corner === 'diamond' ? 5 : 0), + paddingRight: pillXPadding + (corner === 'diamond' ? 5 : 0), + }} + > + {props.title} + </div> + </div> + </div> + </div> + <div className="absolute z-50 pointer-events-auto">{props.handles}</div> + </div> + ); +}); + +export const EntityPill = React.memo((props: Omit<PillI, 'topColor'> & { withHandles?: 'vertical' | 'horizontal' }) => { + const handles = !props.withHandles ? undefined : props.withHandles === 'horizontal' ? ( + <> + <PillHandle position={Position.Left} className={'fill-accent-500 stroke-white'} type="square"> + {props.handleLeft} + </PillHandle> + <PillHandle position={Position.Right} className={'fill-accent-500 stroke-white'} type="square"> + {props.handleRight} + </PillHandle> + </> + ) : ( + <> + <PillHandle position={Position.Top} className={'fill-accent-500 stroke-white stroke-1'} type="arrowUp"> + {props.handleUp} + </PillHandle> + <PillHandle position={Position.Bottom} className={'fill-accent-500 stroke-white stroke-1'} type="arrowDown"> + {props.handleDown} + </PillHandle> + </> + ); + return <Pill {...props} corner="rounded" topColor="bg-accent-500" handles={handles} />; +}); + +export const RelationPill = React.memo((props: Omit<PillI, 'topColor'> & { withHandles?: 'vertical' | 'horizontal' }) => { + const handles = !props.withHandles ? undefined : props.withHandles === 'horizontal' ? ( + <> + <PillHandle position={Position.Left} className={'fill-[#0676C1] stroke-white'} type="square"> + {props.handleLeft} + </PillHandle> + <PillHandle position={Position.Right} className={'fill-[#0676C1] stroke-white'} type="square" mr={-pillBorderWidth}> + {props.handleRight} + </PillHandle> + </> + ) : ( + <> + <PillHandle position={Position.Top} className={'fill-[#0676C1] stroke-white stroke-1'} type="arrowUp"> + {props.handleUp} + </PillHandle> + <PillHandle position={Position.Bottom} className={'fill-[#0676C1] stroke-white stroke-1'} type="arrowDown"> + {props.handleDown} + </PillHandle> + </> + ); + + return <Pill {...props} corner="diamond" topColor="bg-[#0676C1]" handles={handles} />; +}); + +export const LogicPill = React.memo((props: Omit<PillI, 'topColor'>) => { + return <Pill {...props} corner="square" topColor="bg-[#543719]" />; +}); diff --git a/libs/shared/lib/components/pills/PillHandle.tsx b/libs/shared/lib/components/pills/PillHandle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e2e04d0f906fd066312310a9ae8676788866c3ea --- /dev/null +++ b/libs/shared/lib/components/pills/PillHandle.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { Position } from 'reactflow'; +import { pillHeight, pillWidth, topLineHeight, pillBorderWidth } from './pill.const'; + +export const PillHandle = (props: { + handleTop?: 'auto' | 'fixed'; + hidden?: boolean; + isValidConnection?: boolean; + children?: React.ReactNode; + className?: string; + position: Position; + type: 'square' | 'arrowUp' | 'arrowDown'; + mr?: number; + outerSize?: number; + innerSize?: number; +}) => { + const outerSize = props.outerSize || (props.type === 'square' ? 6 : 4); + const innerSize = props.innerSize || (props.type === 'square' ? 4 : 6); + + const style: React.CSSProperties = { + width: outerSize * 2, + height: outerSize * 2, + }; + if (props.position === Position.Left) { + style.left = -outerSize; // round size + style.top = props.handleTop === 'auto' ? `auto` : pillHeight / 2 - outerSize; + } else if (props.position === Position.Right) { + style.left = pillWidth + (props.mr || 0) - outerSize; // width of pill + style.top = props.handleTop === 'auto' ? `auto` : pillHeight / 2 - outerSize; + } else if (props.position === Position.Top) { + style.left = pillWidth / 2 - outerSize; + style.top = -outerSize - innerSize / 2 + topLineHeight + pillBorderWidth; + } else if (props.position === Position.Bottom) { + style.left = pillWidth / 2 - outerSize; + style.top = pillHeight - outerSize + innerSize / 2 - topLineHeight; + } + + const innerStyle: React.CSSProperties = { width: innerSize * 2, height: innerSize * 2 }; + innerStyle.left = outerSize - innerSize; + innerStyle.top = outerSize - innerSize; + + const innerHandle = useMemo(() => { + if (props.type === 'square') + return ( + <svg className={'absolute pointer-events-none'} style={innerStyle} width="8" height="8" viewBox="0 0 8 8" fill="none"> + <rect x="0.5" y="0.5" width="7" height="7" className={props.className} /> + </svg> + ); + if (props.type === 'arrowUp') + return ( + <svg width="14" height="7" viewBox="0 0 14 7" fill="none" className={'absolute pointer-events-none'} style={innerStyle}> + <path d="M14 7H0L7 0L14 7Z" className={props.className} /> + </svg> + ); + if (props.type === 'arrowDown') + return ( + <svg width="14" height="7" viewBox="0 0 14 7" fill="none" className={'absolute pointer-events-none'} style={innerStyle}> + <path d="M14 0H0L7 7L14 0Z" className={props.className} /> + </svg> + ); + return <></>; + }, [props.type]); + + return ( + <> + <div + className="absolute z-40" + style={{ + ...style, + top: style.top as number, + left: style.left as number, + }} + > + {props.children} + </div> + <div className="absolute z-40 pointer-events-none" style={style}> + {innerHandle} + </div> + </> + ); +}; diff --git a/libs/shared/lib/components/pills/index.ts b/libs/shared/lib/components/pills/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c32b085637d3e880ee0cab0ddf716aa69174c086 --- /dev/null +++ b/libs/shared/lib/components/pills/index.ts @@ -0,0 +1 @@ +export * from './Pill'; diff --git a/libs/shared/lib/components/pills/pill.const.ts b/libs/shared/lib/components/pills/pill.const.ts new file mode 100644 index 0000000000000000000000000000000000000000..b888e15cd7334365b542bb0a92950174440ecb05 --- /dev/null +++ b/libs/shared/lib/components/pills/pill.const.ts @@ -0,0 +1,7 @@ +export const pillHeight = 26; +export const pillInnerMargin = 2; +export const pillXPadding = 10; +export const pillDropdownPadding = 2; +export const pillWidth = 154; +export const topLineHeight = 3; +export const pillBorderWidth = 1; diff --git a/libs/shared/lib/components/pills/stories/pill.stories.tsx b/libs/shared/lib/components/pills/stories/pill.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0e4e18dcfd9c5b3513ba0bc72a2417a0457a1ece --- /dev/null +++ b/libs/shared/lib/components/pills/stories/pill.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; + +import { Pill } from '../Pill'; + +const Component: Meta<typeof Pill> = { + title: 'Pills/Pill', + component: Pill, + decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], +}; + +export default Component; + +const Mockstore = configureStore({ + reducer: {}, +}); + +export const Default = { + args: { + title: 'TestEntity', + children: <div></div>, + }, +}; diff --git a/libs/shared/lib/components/pills/stories/pillEntity.stories.tsx b/libs/shared/lib/components/pills/stories/pillEntity.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d6984cb51c423dfde3dfed792901ed934de5bc4 --- /dev/null +++ b/libs/shared/lib/components/pills/stories/pillEntity.stories.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { Position } from 'reactflow'; + +import { EntityPill } from '../Pill'; +import { PillHandle } from '../PillHandle'; + +const Component: Meta<typeof EntityPill> = { + title: 'Pills/Pill', + component: EntityPill, + decorators: [ + (story) => ( + <Provider store={Mockstore}> + <div className="m-10">{story()}</div> + </Provider> + ), + ], +}; + +export default Component; + +const Mockstore = configureStore({ + reducer: {}, +}); + +export const EntityPillStory = { + args: { + onHovered: (hovered: boolean) => {}, + onDragged: (dragged: boolean) => {}, + title: 'TestEntity', + children: <div></div>, + }, +}; diff --git a/libs/shared/lib/components/pills/stories/pillLogic.stories.tsx b/libs/shared/lib/components/pills/stories/pillLogic.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4c2dc4321f7ec8e5e1cf3b8f327e7a40b96eaf05 --- /dev/null +++ b/libs/shared/lib/components/pills/stories/pillLogic.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; + +import { LogicPill } from '../Pill'; + +const Component: Meta<typeof LogicPill> = { + title: 'Pills/Pill', + component: LogicPill, + decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], +}; + +export default Component; + +const Mockstore = configureStore({ + reducer: {}, +}); + +export const LogicPillStory = { + args: { + onHovered: (hovered: boolean) => {}, + onDragged: (dragged: boolean) => {}, + title: 'TestEntity', + children: <div></div>, + }, +}; diff --git a/libs/shared/lib/components/pills/stories/pillRelation.stories.tsx b/libs/shared/lib/components/pills/stories/pillRelation.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..08f8a035c57e5bf0a3180cf331571ffe3ecc3232 --- /dev/null +++ b/libs/shared/lib/components/pills/stories/pillRelation.stories.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; + +import { RelationPill } from '../Pill'; +import { PillHandle } from '../PillHandle'; +import { Position } from 'reactflow'; + +const Component: Meta<typeof RelationPill> = { + title: 'Pills/Pill', + component: RelationPill, + decorators: [ + (story) => ( + <Provider store={Mockstore}> + <div className="m-10">{story()}</div> + </Provider> + ), + ], +}; + +export default Component; + +const Mockstore = configureStore({ + reducer: {}, +}); + +export const RelationPillStory = { + args: { + onHovered: (hovered: boolean) => {}, + onDragged: (dragged: boolean) => {}, + title: 'TestEntity', + children: <div></div>, + }, +}; diff --git a/libs/shared/lib/querybuilder/model/index.ts b/libs/shared/lib/querybuilder/model/index.ts index 4fadc5634d9b980bdf5eb38d591f0c7447ed1152..f5ea6888afd16838edeae0380bc4a8e4d0aa5103 100644 --- a/libs/shared/lib/querybuilder/model/index.ts +++ b/libs/shared/lib/querybuilder/model/index.ts @@ -8,7 +8,8 @@ export * from './logic'; export * from './reactflow'; type ExtraProps = { extra?: string; separator?: string }; -export function toHandleId(handleData: QueryGraphEdgeHandle, separator: string = '__'): string { +export function toHandleId(handleData?: QueryGraphEdgeHandle, separator: string = '__'): string { + if (!handleData) throw Error('handleData is not defined'); // if (!extra) extra = ''; if (!separator) separator = '__'; return [ @@ -26,7 +27,7 @@ export function toHandleId(handleData: QueryGraphEdgeHandle, separator: string = export function handleDataFromReactflowToDataId( node: SchemaReactflowNode, attribute: NodeAttribute, - options: ExtraProps = {} + options: ExtraProps = {}, ): QueryGraphEdgeHandle { if (!node.data.name) throw Error('node.data is not defined'); return { diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index 1491281f27be219fbae44bdd1b0e86a03b2397e1..e8224d3cc4cb48b636c5cdfa3972e464b2d53fa5 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -31,8 +31,8 @@ import { addError } from '../../data-access/store/configSlice'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { LayoutFactory } from '../../graph-layout'; import { AllLogicMap, QueryElementTypes, createReactFlowElements, isLogicHandle, toHandleData } from '../model'; -import { ConnectionDragLine, ConnectionLine, EntityFlowElement, RelationPill } from '../pills'; -import { LogicPill } from '../pills/customFlowPills/logicpill/logicpill'; +import { ConnectionDragLine, ConnectionLine, QueryEntityPill, QueryRelationPill } from '../pills'; +import { QueryLogicPill } from '../pills/customFlowPills/logicpill/QueryLogicPill'; import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; import styles from './querybuilder.module.scss'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; @@ -59,9 +59,9 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { var nodeTypes = useMemo( () => ({ - entity: EntityFlowElement, - relation: RelationPill, - logic: LogicPill, + entity: QueryEntityPill, + relation: QueryRelationPill, + logic: QueryLogicPill, }), [], ); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..43ac8efd166f8b94af3f045581ecc8b4810b8646 --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx @@ -0,0 +1,88 @@ +import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; +import React, { useMemo, useState } from 'react'; +import { Handle, Position } from 'reactflow'; +import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model'; +import { PillDropdown } from '../../pilldropdown/PillDropdown'; +import { EntityPill } from '@graphpolaris/shared/lib/components'; + +/** + * Component to render an entity flow element + * @param {NodeProps} param0 The data of an entity flow element. + */ +export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { + const data = node.data; + if (!data.leftRelationHandleId) throw new Error('EntityFlowElement: data.leftRelationHandleId is undefined'); + if (!data.rightRelationHandleId) throw new Error('EntityFlowElement: data.rightRelationHandleId is undefined'); + + const graph = useQuerybuilderGraph(); + const attributeEdges = useMemo( + () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), + [graph], + ); + + const [hovered, setHovered] = useState(false); + const [handleBeingDragged, setHandleBeingDragged] = useState(-1); + + const onMouseEnter = (event: React.MouseEvent) => { + if (!hovered) setHovered(true); + }; + + const onMouseLeave = (event: React.MouseEvent) => { + if (hovered) setHovered(false); + }; + + const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { + setHandleBeingDragged(i); + window.addEventListener('mouseup', onHandleMouseUp, true); + }; + + const onHandleMouseUp = () => { + setHandleBeingDragged(-1); + window.removeEventListener('mouseup', onHandleMouseUp, true); + }; + + const onConnect = (params: any) => { + console.log('EntityPill onConnect', params); + }; + + return ( + <div className="w-fit h-fit" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <EntityPill + title={data.name || ''} + withHandles="horizontal" + handleLeft={ + <Handle + id={toHandleId(data.leftRelationHandleId)} + position={Position.Left} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'} + type="target" + ></Handle> + } + handleRight={ + <Handle + id={toHandleId(data.rightRelationHandleId)} + // id="entitySourceRight" + position={Position.Right} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'} + type="source" + ></Handle> + } + > + {data?.attributes && ( + <PillDropdown + node={node} + attributes={data.attributes} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + hovered={hovered} + handleBeingDraggedIdx={handleBeingDragged} + onHandleMouseDown={onHandleMouseDown} + /> + )} + </EntityPill> + </div> + ); +}); + +QueryEntityPill.displayName = 'QueryEntityPill'; + +export default QueryEntityPill; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx deleted file mode 100644 index 9e5750185ee7e9a3bb7cbdd212b182b026770a36..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { querybuilderSlice, schemaSlice, setQuerybuilderGraph, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; - -import { configureStore } from '@reduxjs/toolkit'; -import { Meta } from '@storybook/react'; -import { Provider } from 'react-redux'; -import { QueryBuilder } from '../../../panel'; -import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; -import { QueryElementTypes } from '../../../model'; -import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; - -const Component: Meta<typeof QueryBuilder> = { - component: QueryBuilder, - title: 'Querybuilder/Pills/EntityPill', - decorators: [(story) => <Provider store={Mockstore}>{story()}</Provider>], -}; - -const Mockstore = configureStore({ - reducer: { - querybuilder: querybuilderSlice.reducer, - schema: schemaSlice.reducer, - searchResults: searchResultSlice.reducer, - }, -}); - -export const Flow = { - play: async () => { - const dispatch = Mockstore.dispatch; - - const graph = new QueryMultiGraphology(); - graph.addPill2Graphology({ id: '2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); - dispatch(setQuerybuilderGraph(graph.export())); - }, - args: {}, -}; - -export default Component; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx deleted file mode 100644 index 4d38dd88341c00f002df61815ed2301b0edd5917..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { Meta, StoryObj } from '@storybook/react'; -import EntityFlowElement from './entitypill'; -import { configureStore } from '@reduxjs/toolkit'; -import { Provider } from 'react-redux'; - -import { querybuilderSlice, schemaSlice, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; -import { ReactFlowProvider } from 'reactflow'; -import { EntityData, Handles, QueryElementTypes } from '../../../model'; - -const Component: Meta<typeof EntityFlowElement> = { - title: 'Querybuilder/Pills/EntityPill', - component: EntityFlowElement, - decorators: [ - (story) => ( - <Provider store={Mockstore}> - <ReactFlowProvider>{story()}</ReactFlowProvider> - </Provider> - ), - ], -}; - -export default Component; - -const Mockstore = configureStore({ - reducer: { - querybuilder: querybuilderSlice.reducer, - schema: schemaSlice.reducer, - searchResults: searchResultSlice.reducer, - }, -}); - -export const Default: StoryObj<{ data: EntityData }> = { - args: { - data: { - name: 'TestEntity', - leftRelationHandleId: { nodeId: '1', nodeName: 'string', nodeType: QueryElementTypes.Entity, handleType: Handles.EntityLeft }, - rightRelationHandleId: { nodeId: '2', nodeName: 'string2', nodeType: QueryElementTypes.Entity, handleType: Handles.EntityRight }, - selected: false, - }, - }, -}; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx deleted file mode 100644 index 7953484ba1c08a1f83d3a31b2c00853c3792e667..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// import { handles } from '@graphpolaris/shared/lib/querybuilder/usecases'; -import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; -import React, { useMemo, useState } from 'react'; -import { Position } from 'reactflow'; -import { NodeAttribute, SchemaReactflowEntityNode } from '../../../model'; -import { PillDropdown } from '../../pilldropdown/pilldropdown'; -import { FilterHandle } from '../../FilterHandle'; - -/** - * Component to render an entity flow element - * @param {NodeProps} param0 The data of an entity flow element. - */ -export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => { - const data = node.data; - if (!data.leftRelationHandleId) throw new Error('EntityFlowElement: data.leftRelationHandleId is undefined'); - if (!data.rightRelationHandleId) throw new Error('EntityFlowElement: data.rightRelationHandleId is undefined'); - - const graph = useQuerybuilderGraph(); - const attributeEdges = useMemo( - () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph], - ); - - const [hovered, setHovered] = useState(false); - const [handleBeingDragged, setHandleBeingDragged] = useState(-1); - - const onMouseEnter = (event: React.MouseEvent) => { - if (!hovered) setHovered(true); - }; - - const onMouseLeave = (event: React.MouseEvent) => { - if (hovered) setHovered(false); - }; - - const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { - setHandleBeingDragged(i); - window.addEventListener('mouseup', onHandleMouseUp, true); - }; - - const onHandleMouseUp = () => { - setHandleBeingDragged(-1); - window.removeEventListener('mouseup', onHandleMouseUp, true); - }; - - const onConnect = (params: any) => { - console.log('EntityPill onConnect', params); - }; - - return ( - <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <div className={`rounded-sm shadow min-w-[9rem] max-w-[9rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`}> - <div className={`pt-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> - <FilterHandle - handle={data.leftRelationHandleId} - type="target" - position={Position.Left} - // className={'!top-8 !left-2 !bg-danger-700 !rounded-none'} - // outerClassName={'!bg-blue-700 !rounded-none'} - className={'!bg-accent-700'} - /> - <FilterHandle - handle={data.rightRelationHandleId} - type="source" - position={Position.Right} - // outerClassName={'!bg-blue-700 !rounded-none'} - className={'!bg-accent-700'} - /> - <div className=""> - <div className="text-center py-1">{data.name}</div> - </div> - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - hovered={hovered} - handleBeingDraggedIdx={handleBeingDragged} - onHandleMouseDown={onHandleMouseDown} - /> - )} - </div> - </div> - </div> - ); -}); - -EntityFlowElement.displayName = 'EntityFlowElement'; - -export default EntityFlowElement; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/index.ts b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/index.ts index 8f909017f21464b2b42ba9cb57c7c4a40535601d..6a0697e6fe00968b4a567662620880df1bd41fbd 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/index.ts +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/index.ts @@ -1 +1 @@ -export * from './entitypill'; +export * from './QueryEntityPill'; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/LogicInput.tsx similarity index 86% rename from libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx rename to libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/LogicInput.tsx index e0087c7ff65730d855706d27fbf9861be8f63171..6d4b4a852dc7ff6ba956a8eddd11638469e1e309 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicInput.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/LogicInput.tsx @@ -15,8 +15,7 @@ export const LogicInput = (props: { value: string; type: string; onChange(value: return ( <input ref={ref} - className="px-0.5 m-2 mt-0 h-5 rounded-sm border-[1px]" - style={{ width: props.type === 'string' ? '9rem' : '4rem' }} + className="px-1 m-0 mx-1 p-0 h-5 w-full rounded-sm border-[1px]" placeholder="empty" value={props.value} onMouseDownCapture={(e) => { diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/QueryLogicPill.tsx similarity index 78% rename from libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx rename to libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/QueryLogicPill.tsx index bb3ec30967629b7d46bc790db044b7cbfc116fc8..d3e6f409bf2042adcaef7af5d6974709aef4105b 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/QueryLogicPill.tsx @@ -1,13 +1,3 @@ -/** - * This program has been developed by students from the bachelor Computer Science at - * Utrecht University within the Software Project course. - * © Copyright Utrecht University (Department of Information and Computing Sciences) - */ - -/* istanbul ignore file */ -/* The comment above was added so the code coverage wouldn't count this file towards code coverage. - * We do not test components/renderfunctions/styling files. - * See testing plan for more details.*/ import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderHash } from '@graphpolaris/shared/lib/data-access'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, Position } from 'reactflow'; @@ -15,13 +5,10 @@ import { Handles, LogicNodeAttributes, SchemaReactflowLogicNode, toHandleId } fr import { InputNode, InputNodeTypeTypes } from '../../../model/logic/general'; import { styleHandleMap } from '../../utils'; import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { LogicInput } from './logicInput'; +import { LogicInput } from './LogicInput'; +import { LogicPill } from '@graphpolaris/shared/lib/components'; -/** - * Component to render an entity flow element - * @param param0 Data of the flow element. - */ -export function LogicPill(node: SchemaReactflowLogicNode) { +export function QueryLogicPill(node: SchemaReactflowLogicNode) { const dispatch = useAppDispatch(); const data = node.data; const logic = data.logic; @@ -64,13 +51,11 @@ export function LogicPill(node: SchemaReactflowLogicNode) { }, [node.id]); return ( - <div className="rounded-sm shadow h-min-[3rem] text-[13px] bg-gradient-to-r pt-1 from-[#DEB68E] to-[#543719]"> - <div className={`py-1 h-fit ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> - { - <div className="m-1 mx-2 text-left"> - {connectionsToLeft.map((e) => e?.attributes?.sourceHandleData.attributeName)}.{output.name} - </div> - } + <LogicPill title="LogicPill"> + <div className={`py-1 h-fit border-[1px] border-secondary-200 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-100'}`}> + <div className="m-1 mx-2 text-left"> + {connectionsToLeft.map((e) => e?.attributes?.sourceHandleData.attributeName)}.{output.name} + </div> {node.data.logic.inputs.map((input, i) => { return ( <div key={i}> @@ -101,7 +86,7 @@ export function LogicPill(node: SchemaReactflowLogicNode) { handleType: Handles.LogicLeft, })} key={input.name + input.type} - style={{ top: `${((i + 0.8) / (node.data.logic.inputs.length + 0.6)) * 100}%` }} + style={{ top: `${((i + 0.7) / (node.data.logic.inputs.length + 0.4)) * 100}%` }} className={styleHandleMap[input.type] + ''} ></Handle> </div> @@ -121,6 +106,6 @@ export function LogicPill(node: SchemaReactflowLogicNode) { ></Handle> )} </div> - </div> + </LogicPill> ); } diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9747cd305c193c0dc00fa254cdadf91720ea5ee6 --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx @@ -0,0 +1,179 @@ +import { memo, useState, useMemo, useEffect } from 'react'; +import { NodeAttribute, RelationNodeAttributes, SchemaReactflowRelationNode, toHandleId } from '../../../model'; +import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderSettings } from '@graphpolaris/shared/lib/data-access'; +import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import { PillDropdown } from '../../pilldropdown/PillDropdown'; +import { RelationPill } from '@graphpolaris/shared/lib/components'; +import { Handle, Position } from 'reactflow'; + +export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { + const data = node.data; + const graph = useQuerybuilderGraph(); + const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph]); + const settings = useQuerybuilderSettings(); + const dispatch = useAppDispatch(); + const graphologyNodeAttributes = useMemo<RelationNodeAttributes | undefined>( + () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), + [node.id], + ); + const attributeEdges = useMemo( + () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), + [graph], + ); + const [hovered, setHovered] = useState(false); + const [handleBeingDragged, setHandleBeingDragged] = useState(-1); + + const [depth, setDepth] = useState<{ min: number; max: number }>({ + min: data.depth.min || settings.depth.min, + max: data.depth.max || settings.depth.max, + }); + + // TODO: must do this once design is chosen + // const [direction, setDirection] = useState<RelationshipHandleArrowType>('right'); + + useEffect(() => { + setDepth({ min: data.depth.min || settings.depth.min, max: data.depth.max || settings.depth.max }); + }, [data.depth]); + + const onNodeUpdated = () => { + if (depth.min < 0) { + dispatch(addWarning('The minimum depth cannot be smaller than 0')); + } else if (depth.max > 99) { + dispatch(addWarning('The maximum depth cannot be larger than 99')); + } else if (depth.min > depth.max) { + dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); + } else { + graphologyGraph.setNodeAttribute<any>(node.id, 'depth', depth); + dispatch(setQuerybuilderGraphology(graphologyGraph)); + } + }; + + // TODO: must do this once design is chosen + // 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'; + }; + + const onMouseEnter = (event: React.MouseEvent) => { + if (!hovered) setHovered(true); + }; + + const onMouseLeave = (event: React.MouseEvent) => { + if (hovered) setHovered(false); + }; + + const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { + setHandleBeingDragged(i); + window.addEventListener('mouseup', onHandleMouseUp, true); + }; + + const onHandleMouseUp = () => { + setHandleBeingDragged(-1); + window.removeEventListener('mouseup', onHandleMouseUp, true); + }; + + return ( + <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <RelationPill + title={ + <div className="flex flex-row w-full"> + <span className="flex-grow">{data?.name}</span> + <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" + handleLeft={ + <Handle + // onDoubleClick={onChangeDirection} + id={toHandleId(data.leftEntityHandleId)} + position={Position.Left} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !b-0 !right-0 !left-0'} + type="target" + ></Handle> + } + handleRight={ + <Handle + // onDoubleClick={onChangeDirection} + id={toHandleId(data.rightEntityHandleId)} + position={Position.Right} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'} + type="source" + ></Handle> + } + > + {data?.attributes && ( + <PillDropdown + node={node} + attributes={data.attributes} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + hovered={hovered} + handleBeingDraggedIdx={handleBeingDragged} + onHandleMouseDown={onHandleMouseDown} + /> + )} + </RelationPill> + </div> + ); +}); + +QueryRelationPill.displayName = 'RelationPill'; +export default QueryRelationPill; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/index.ts b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/index.ts index deef2e611ad7700adac469c821310a527f7a3989..c6c75c7e1762365e1f5036cc2cc1e688e00932f4 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/index.ts +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/index.ts @@ -1 +1 @@ -export * from './relationpill'; +export * from './QueryRelationPill'; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx deleted file mode 100644 index 5da8f51bcf87b9eb55c64c991eab36cb46f76b6c..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { querybuilderSlice, schemaSlice, setQuerybuilderGraph, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; - -import { configureStore } from '@reduxjs/toolkit'; -import { Meta } from '@storybook/react'; -import { Provider } from 'react-redux'; -import { QueryBuilder } from '../../../panel'; -import { QueryElementTypes, QueryMultiGraphology } from '../../../model'; - -const Component: Meta<typeof QueryBuilder> = { - component: QueryBuilder, - title: 'Querybuilder/Pills/relationPill', - decorators: [(story) => <Provider store={mockStore}>{story()}</Provider>], -}; - -// Mock palette store -const mockStore = configureStore({ - reducer: { - querybuilder: querybuilderSlice.reducer, - schema: schemaSlice.reducer, - searchResults: searchResultSlice.reducer, - }, -}); -const graph = new QueryMultiGraphology(); -graph.addPill2Graphology({ - id: '2', - type: QueryElementTypes.Relation, - x: 140, - y: 140, - name: 'Relation Pill', - depth: { min: 0, max: 1 }, -}); -console.log(graph.export()); - -mockStore.dispatch(setQuerybuilderGraph(graph.export())); - -export const Flow = { - args: {}, -}; - -export default Component; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx deleted file mode 100644 index d1942fbadef5233583f90546fb81c844375f3816..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Handle, HandleType, Position } from 'reactflow'; -import { QueryGraphEdgeHandle } from '../../..'; -import { tailwindColors } from 'config'; -import { FilterHandle } from '../../FilterHandle'; - -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 = { - handle: QueryGraphEdgeHandle; - type: HandleType; - point: RelationshipHandleArrowType; - onDoubleClick?: () => void; -}; - -export const LeftHandle = (props: Props) => { - return ( - <FilterHandle - handle={props.handle} - type={props.type} - position={Position.Left} - onDoubleClickCapture={(e) => { - e.preventDefault(); - e.stopPropagation(); - if (props.onDoubleClick) props.onDoubleClick(); - }} - > - <svg className="pointer-events-none" height={10} style={{ transform: `translate(3px, 4px)` }}> - <polygon points={getArrow[props.point]} className="fill-primary-600" /> - </svg> - </FilterHandle> - ); -}; - -export const RightHandle = (props: Props) => { - return ( - <FilterHandle - handle={props.handle} - type={props.type} - position={Position.Right} - onDoubleClickCapture={(e) => { - e.preventDefault(); - e.stopPropagation(); - if (props.onDoubleClick) props.onDoubleClick(); - }} - > - <svg height={10} className="pointer-events-none" style={{ transform: `translate(6px, 4px)` }}> - <polygon points={getArrow[props.point]} className="fill-primary-600" /> - </svg> - </FilterHandle> - ); -}; 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 2c087a1ef0ee7a060d9effcdc28d225aa2b16c21..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss +++ /dev/null @@ -1,119 +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-primary-50; - @apply border-l-primary-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-primary-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: 5px; - } - - .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: 0; - 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 432ea72c9fc83e70e90bce2dbca2911362df4a5a..0000000000000000000000000000000000000000 --- 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 deleted file mode 100644 index 3fbbdd1c485dc77ef7d9b8c82772c605c4593be0..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { Meta, StoryObj } from '@storybook/react'; -import RelationPill from './relationpill'; -import { configureStore } from '@reduxjs/toolkit'; -import { Provider } from 'react-redux'; - -import { querybuilderSlice, schemaSlice, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; -import { ReactFlowProvider } from 'reactflow'; -import { RelationData } from '../../../model'; - -const Component: Meta<typeof RelationPill> = { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ - title: 'Querybuilder/Pills/RelationPill', - component: RelationPill, - decorators: [ - (story) => ( - <Provider store={Mockstore}> - <ReactFlowProvider>{story()}</ReactFlowProvider> - </Provider> - ), - ], -}; - -export default Component; - -// A super-simple mock of a redux store -const Mockstore = configureStore({ - reducer: { - querybuilder: querybuilderSlice.reducer, - schema: schemaSlice.reducer, - searchResults: searchResultSlice.reducer, - }, -}); - -export const Default: StoryObj<{ data: RelationData }> = { - args: { - data: { - name: 'TestEntity', - collection: 'test', - depth: { min: 0, max: 1 }, - direction: 'right', - }, - }, -}; - -// Default.decorators = [ -// (story) => ( -// <Provider store={Mockstore}> -// {story()} -// </Provider> -// ), -// ]; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx deleted file mode 100644 index 7280803b61d09120a0432b9b9aa0e43e29c02a73..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { memo, useState, useMemo, useEffect } from 'react'; -import { NodeAttribute, RelationNodeAttributes, SchemaReactflowRelationNode } from '../../../model'; -import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderSettings } from '@graphpolaris/shared/lib/data-access'; -import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice'; -import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { LeftHandle, RelationshipHandleArrowType, RightHandle } from './relation-handles'; -import { PillDropdown } from '../../pilldropdown/pilldropdown'; - -/** - * Component to render a relation flow element - * @param { FlowElement<RelationData>} param0 The data of a relation flow element. - */ -export const RelationPill = memo((node: SchemaReactflowRelationNode) => { - const data = node.data; - const graph = useQuerybuilderGraph(); - const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph]); - const settings = useQuerybuilderSettings(); - const dispatch = useAppDispatch(); - const graphologyNodeAttributes = useMemo<RelationNodeAttributes | undefined>( - () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), - [node.id] - ); - const attributeEdges = useMemo( - () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph] - ); - const [hovered, setHovered] = useState(false); - const [handleBeingDragged, setHandleBeingDragged] = useState(-1); - - const [depth, setDepth] = useState<{ min: number; max: number }>({ - min: data.depth.min || settings.depth.min, - 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]); - - const onNodeUpdated = () => { - if (depth.min < 0) { - dispatch(addWarning('The minimum depth cannot be smaller than 0')); - } else if (depth.max > 99) { - dispatch(addWarning('The maximum depth cannot be larger than 99')); - } else if (depth.min > depth.max) { - dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); - } else { - graphologyGraph.setNodeAttribute<any>(node.id, 'depth', depth); - dispatch(setQuerybuilderGraphology(graphologyGraph)); - } - }; - - 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'; - }; - - const onMouseEnter = (event: React.MouseEvent) => { - if (!hovered) setHovered(true); - }; - - const onMouseLeave = (event: React.MouseEvent) => { - if (hovered) setHovered(false); - }; - - const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { - setHandleBeingDragged(i); - window.addEventListener('mouseup', onHandleMouseUp, true); - }; - - const onHandleMouseUp = () => { - setHandleBeingDragged(-1); - window.removeEventListener('mouseup', onHandleMouseUp, true); - }; - - return ( - <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <div className={`rounded-sm shadow min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`}> - <div className={`py-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> - <div className="px-10"> - <div className="text-center py-1"> - {data?.name} - <span> - <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> - </div> - <span> - {data.leftEntityHandleId && ( - <LeftHandle handle={data.leftEntityHandleId} type="target" point={'left'} onDoubleClick={onChangeDirection} /> - )} - </span> - - <span> - {data.rightEntityHandleId && ( - <RightHandle handle={data.rightEntityHandleId} type="source" point={direction} onDoubleClick={onChangeDirection} /> - )} - </span> - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - hovered={hovered} - handleBeingDraggedIdx={handleBeingDragged} - onHandleMouseDown={onHandleMouseDown} - /> - )} - </div> - </div> - </div> - ); -}); - -RelationPill.displayName = 'RelationPill'; -export default RelationPill; diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx similarity index 65% rename from libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.tsx rename to libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index 312a11d0553563f8b2a3d6f009a330a0d7a53778..156337cd65b6a2bfca9c9a57a2c173a4787914ac 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -1,10 +1,10 @@ -import { useMemo, ReactNode, ReactElement } from 'react'; +import { useMemo, ReactElement } from 'react'; import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; -import { Position } from 'reactflow'; -import styles from './pilldropdown.module.scss'; -import { FilterHandle } from '../FilterHandle'; +import { Handle, Position } from 'reactflow'; 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'; type PillDropdownProps = { node: SchemaReactflowEntityNode; @@ -28,7 +28,7 @@ const IconMap: IconMapType = { }; export const PillDropdown = (props: PillDropdownProps) => { - const forceOpen = false; + const forceOpen = true; const openNumbers: number[] = useMemo(() => { if (forceOpen || props.hovered) return props.attributes.map((_, i) => i); @@ -48,7 +48,12 @@ export const PillDropdown = (props: PillDropdownProps) => { return ( <> - <div className={'divide-y divide-secondary-100' + (openNumbers.length > 0 ? 'animate-openmenu ' : 'animate-closemenu ')}> + <div + className={ + 'border-[1px] border-secondary-200 divide-y divide-secondary-200 ' + + (openNumbers.length > 0 ? 'animate-openmenu ' : 'animate-closemenu ') + } + > {openNumbers.map((i) => { const attribute = props.attributes[i]; if (attribute.handleData.attributeName === undefined) { @@ -57,26 +62,28 @@ export const PillDropdown = (props: PillDropdownProps) => { return ( <div - className="px-2 py-1 bg-white flex justify-between items-center" + className="px-2 py-1 bg-secondary-100 flex justify-between items-center" key={(attribute.handleData.attributeName || '') + i} onMouseDown={(event: React.MouseEvent) => { props.onHandleMouseDown(attribute, i, event); }} > <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p> - {attribute.handleData?.attributeDimension && ( - // <div className="!text-xs text-secondary-500">{IconMap[attribute.handleData.attributeDimension]}</div> - // <div className="!text-xs text-secondary-500"> - <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} /> - // </div> - )} - <FilterHandle - handle={handleDataFromReactflowToDataId(props.node, attribute)} - type="source" - position={Position.Right} - className={styles.handle + ' bg-secondary-500 rounded-full'} + {attribute.handleData?.attributeDimension && <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} />} + <PillHandle + mr={-pillDropdownPadding} handleTop="auto" - ></FilterHandle> + position={Position.Right} + className={'fill-accent-500 stroke-white'} + type="square" + > + <Handle + id={toHandleId(handleDataFromReactflowToDataId(props.node, attribute))} + type="source" + 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/pills/pilldropdown/pilldropdown.module.scss b/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.module.scss deleted file mode 100644 index 0386e7d220304e8f57038cc8abaf15d91f8c2a54..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.module.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../querypills.module.scss'; diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.module.scss.d.ts b/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.module.scss.d.ts deleted file mode 100644 index e478e565f42129a12498bfde643731c7c978d6fc..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/pilldropdown.module.scss.d.ts +++ /dev/null @@ -1,23 +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'; -}; -export = classNames; diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index b396083e7b1eed374bb3f33f5b7b66979c2e759e..533948832c2410119618633b45cd21d6287ef538 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -7,8 +7,8 @@ import { useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../ import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { NodeEdge } from '../pills/edges/node-edge'; import { SelfEdge } from '../pills/edges/self-edge'; -import { EntityNode } from '../pills/nodes/entity/entity-node'; -import { RelationNode } from '../pills/nodes/relation/relation-node'; +import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill'; +import { SchemaRelationPill } from '../pills/nodes/relation/SchemaRelationPill'; import { SchemaDialog } from './schemaDialog'; import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; @@ -25,8 +25,8 @@ const onInit = (reactFlowInstance: ReactFlowInstance) => { }; const nodeTypes = { - entity: EntityNode, - relation: RelationNode, + entity: SchemaEntityPill, + relation: SchemaRelationPill, }; const edgeTypes = { nodeEdge: NodeEdge, diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx similarity index 50% rename from libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx rename to libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx index 4e343b81782c229efaa6fad40782e0bf9ca0cee8..3afed3f5e2796be843e7a13b2d854f88547b515c 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx @@ -1,29 +1,13 @@ -/** - * This program has been developed by students from the bachelor Computer Science at - * Utrecht University within the Software Project course. - * © Copyright Utrecht University (Department of Information and Computing Sciences) - */ - -/* istanbul ignore file */ -/* The comment above was added so the code coverage wouldn't count this file towards code coverage. - * We do not test components/renderfunctions/styling files. - * See testing plan for more details.*/ import React, { useState } from 'react'; -import { Node, Handle, Position, NodeProps } from 'reactflow'; -import styles from './entity.module.scss'; +import { Handle, Position, NodeProps } from 'reactflow'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; import { SchemaEntityPopup } from './SchemaEntityPopup'; import { Popup } from '@graphpolaris/shared/lib/components/Popup'; import { SchemaNode } from '../../../model'; +import { EntityPill } from '@graphpolaris/shared/lib/components'; -/** - * EntityNode is the node that represents the database entities. - * It has four handles on the top, bottom, right and left to connect edges. - * It also has a text to display the entity name and show the amount of attributes it contains. - * @param {NodeProps} param0 The data of an entity flow element. - */ -export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { +export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); /** @@ -61,8 +45,9 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe <SchemaEntityPopup data={data} onClose={() => setOpenPopup(false)} /> </Popup> )} + <div - className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[4rem] max-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`} + className="w-fit h-fit" onDragStart={(event) => onDragStart(event)} onDragStartCapture={(event) => onDragStart(event)} onMouseDownCapture={(event) => { @@ -73,28 +58,31 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe }} draggable > - <div className={`py-1 ${selected ? 'bg-secondary-300' : 'bg-secondary-100'}`}> - <Handle - style={{ pointerEvents: 'none' }} - id="entityTargetLeft" - position={Position.Left} - className={styles.handleTriangleLeft} - type="target" - ></Handle> - <Handle - style={{ pointerEvents: 'none' }} - id="entitySourceRight" - position={Position.Right} - className={styles.handleTriangleRight} - type="source" - ></Handle> - <div style={{ pointerEvents: 'none' }} className="p-2 py-1 text-center truncate"> - <span className="">{id}</span> - </div> - </div> + <EntityPill + title={id} + withHandles="vertical" + handleUp={ + <Handle + style={{ pointerEvents: 'none' }} + id="entityTargetLeft" + position={Position.Top} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !b-0 !top-0 !bottom-0'} + type="target" + ></Handle> + } + handleDown={ + <Handle + style={{ pointerEvents: 'none' }} + id="entitySourceRight" + position={Position.Bottom} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !top-0 !bottom-0'} + type="source" + ></Handle> + } + /> </div> </> ); }); -EntityNode.displayName = 'EntityNode'; +SchemaEntityPill.displayName = 'EntityNode'; diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx deleted file mode 100644 index 9bf1fe02b34174e7525cffe7d693ddd481c2a818..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; -import { Meta } from '@storybook/react'; - -import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; -import { schemaSlice, setSchema } from '@graphpolaris/shared/lib/data-access/store'; - -import { configureStore } from '@reduxjs/toolkit'; -import { Provider } from 'react-redux'; - -import { Schema } from '../../../panel'; - -const Component: Meta<typeof Schema> = { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ - title: 'Schema/Pills/EntityNode', - - component: Schema, - decorators: [ - (story) => ( - <Provider store={Mockstore}> - <div - style={{ - width: '100%', - height: '100vh', - }} - > - {story()} - </div> - </Provider> - ), - ], -}; - -// Mock the schema and palette store -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - }, -}); - -export const Default = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ - nodes: [ - { - name: 'NodeDefault', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - ], - edges: [], - }); - - dispatch(setSchema(schema.export())); - }, -}; - -export const TooLongTextLabel = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ - nodes: [ - { - name: 'NodeDefaultNodeDefaultNodeDefaultNodeDefaultNodeDefaultNodeDefault', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - ], - edges: [], - }); - - dispatch(setSchema(schema.export())); - }, -}; - -export const ShortTextLabel = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ - nodes: [ - { - name: 'N', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - ], - edges: [], - }); - - dispatch(setSchema(schema.export())); - }, -}; - -export default Component; diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss b/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss deleted file mode 100644 index d33f2bd0fffde5c771a2f0064ed718e7ec2c6477..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import '../schema-pills.module.scss'; - -.entityNodeAttributesBox { - height: 20px; - position: absolute; - left: 115px; - top: -33%; - transform: translateX(50%); - border-radius: 1px; - box-shadow: -1px 2px 5px #888888; - text-align: right; -} - -.handleTriangle { - 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; - //todo: color - //@apply border-b-secondary-300; -} - -.handleTriangleLeft { - @extend .handleTriangle; - transform: rotate(-90deg) translate(50%, -55%) scale(0.65) !important; -} - -.handleTriangleRight { - @extend .handleTriangle; - transform: rotate(90deg) translate(-50%, -45%) scale(0.65) !important; - @apply border-b-accent-300; -} diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss.d.ts b/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss.d.ts deleted file mode 100644 index 6856f4d0e503955a34ce6cbe754655c32a5e41b6..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/entity/entity.module.scss.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const classNames: { - readonly entityNodeAttributesBox: 'entityNodeAttributesBox'; - readonly handleTriangle: 'handleTriangle'; - readonly handleTriangleRight: 'handleTriangleRight'; - readonly handleTriangleLeft: 'handleTriangleLeft'; -}; -export = classNames; diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx similarity index 59% rename from libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx rename to libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx index 5cb30ac7f1cbfd11a272f35f83e0d7b0eb56ae69..c7a79c38a0927f9f71f67db74e0cb0bcaaede703 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx @@ -1,28 +1,13 @@ -/** - * This program has been developed by students from the bachelor Computer Science at - * Utrecht University within the Software Project course. - * © Copyright Utrecht University (Department of Information and Computing Sciences) - */ - -/* istanbul ignore file */ -/* The comment above was added so the code coverage wouldn't count this file towards code coverage. - * We do not test components/renderfunctions/styling files. - * See testing plan for more details.*/ import React, { useState } from 'react'; -import { Node, Handle, Position, NodeProps } from 'reactflow'; -import styles from './relation.module.scss'; -import { SchemaReactflowRelation, SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; +import { Handle, Position, NodeProps } from 'reactflow'; +import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; import { Popup } from '@graphpolaris/shared/lib/components/Popup'; import { SchemaRelationshipPopup } from './SchemaRelationshipPopup'; import { SchemaEdge } from '../../../model'; +import { RelationPill } from '@graphpolaris/shared/lib/components'; -/** - * Relation node component that renders a relation node for the schema. - * Can be dragged and dropped to the query builder. - * @param {NodeProps} param0 The data of an entity flow element. - */ -export const RelationNode = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { +export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); /** @@ -67,6 +52,7 @@ export const RelationNode = React.memo(({ id, selected, data, ...props }: NodePr </Popup> )} <div + className="w-fit h-fit" onDragStart={(event) => onDragStart(event)} onDragStartCapture={(event) => onDragStart(event)} onMouseDownCapture={(event) => { @@ -77,32 +63,31 @@ export const RelationNode = React.memo(({ id, selected, data, ...props }: NodePr }} draggable > - <div - className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[4rem] max-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} - > - <div className={`py-1 ${selected ? 'bg-secondary-300' : 'bg-secondary-100'}`}> + <RelationPill + title={data.collection} + withHandles="vertical" + handleUp={ <Handle style={{ pointerEvents: 'none' }} - className={styles.handleTriangleTop} id="entitySourceLeft" position={Position.Top} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !b-0 !top-0 !bottom-0'} type="target" ></Handle> - <div className="p-2 py-1 text-center truncate"> - <span className="">{data.collection}</span> - </div> - + } + handleDown={ <Handle - className={styles.handleTriangleBottom} style={{ pointerEvents: 'none' }} + id="entitySourceRight" position={Position.Bottom} + className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !top-0 !bottom-0'} type="source" ></Handle> - </div> - </div> + } + /> </div> </> ); }); -RelationNode.displayName = 'RelationNode'; +SchemaRelationPill.displayName = 'RelationNode'; diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx deleted file mode 100644 index c6274574729840c84e3a95b5737e578b6df78a86..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { Meta, Story, ComponentStory } from '@storybook/react'; - -import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; -import { schemaSlice, setSchema } from '@graphpolaris/shared/lib/data-access/store'; - -import { configureStore } from '@reduxjs/toolkit'; -import { Provider } from 'react-redux'; - -import { Schema } from '../../../panel'; - -const Component: Meta<typeof Schema> = { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ - title: 'Schema/Pills/RelationNode', - - component: Schema, - decorators: [ - (story) => ( - <Provider store={Mockstore}> - <div - style={{ - width: '100%', - height: '100vh', - }} - > - {story()} - </div> - </Provider> - ), - ], -}; - -// Mock the schema and palette store -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - }, -}); - -export const Default = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ - nodes: [ - { - name: 'Node', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - ], - edges: [ - { - name: 'Node:Node', - label: 'Node:Node', - from: 'Node', - to: 'Node', - collection: 'Flights', - attributes: [ - { name: 'arrivalTime', type: 'int' }, - { name: 'departureTime', type: 'int' }, - ], - }, - ], - }); - - dispatch(setSchema(schema.export())); - }, -}; - -export const TooLongLabel = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ - nodes: [ - { - name: 'Node', - attributes: [ - { name: 'city', type: 'string' }, - { name: 'vip', type: 'bool' }, - { name: 'state', type: 'string' }, - ], - }, - ], - edges: [ - { - name: 'Node:Node', - label: 'Node:Node', - from: 'Node', - to: 'Node', - collection: 'FlightsFlightsFlightsFlightsFlightsFlightsFlightsFlights', - attributes: [ - { name: 'arrivalTime', type: 'int' }, - { name: 'departureTime', type: 'int' }, - ], - }, - ], - }); - - dispatch(setSchema(schema.export())); - }, -}; - -export default Component; diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss b/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss deleted file mode 100644 index 3ec5fdd99e303a9959fbd51fb7cca16d0de360e6..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss +++ /dev/null @@ -1,50 +0,0 @@ -@import '../schema-pills.module.scss'; - -$width: 145; - -.handleTriangle { - border-radius: 0px !important; - background: transparent !important; - width: 0 !important; - height: 0 !important; - border-left: 4px solid transparent !important; - border-right: 4px solid transparent !important; - border-bottom: 6px solid !important; - //todo: color - //@apply border-b-secondary-200; -} - -.handleTriangleTop { - @extend .handleTriangle; - transform: translate(0, -40%) !important; -} - -.handleTriangleBottom { - @extend .handleTriangle; - transform: rotate(-180deg) translate(0, -40%) !important; - @apply border-b-primary-700; -} - -.controls { - left: auto !important; - bottom: auto !important; - top: 10px; - right: 20px; - width: auto !important; -} - -.exportButton { - left: auto !important; - bottom: auto !important; - top: 10px; - right: 20px; - - & svg { - transform: scale(1.4); - } -} - -.menuText { - font-size: small; - font-family: Poppins, sans-serif; -} diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss.d.ts b/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss.d.ts deleted file mode 100644 index 24d00a2bac1bd61b189a1f4b2ebcfc204233e8db..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/relation/relation.module.scss.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const classNames: { - readonly handleTriangle: 'handleTriangle'; - readonly handleTriangleBottom: 'handleTriangleBottom'; - readonly handleTriangleTop: 'handleTriangleTop'; - readonly controls: 'controls'; - readonly exportButton: 'exportButton'; - readonly menuText: 'menuText'; -}; -export = classNames;