diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx index 23d49ba3cb4fa448c16e54994646964857762c35..bde42e867c7ebd7a7ddbf2ff148c9b571853b0d8 100644 --- a/apps/web/src/app/App.tsx +++ b/apps/web/src/app/App.tsx @@ -81,7 +81,7 @@ export function App(props: App) { <Sidebar onTab={(tab) => setTab(tab)} tab={tab} /> <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> {tab !== undefined ? ( - <div className="flex flex-col border w-full h-full bg-light"> + <div className="flex flex-col w-full h-full"> {tab === 'Search' && <SearchBar onRemove={() => setTab(undefined)} />} {tab === 'Schema' && <Schema auth={authCheck} onRemove={() => setTab(undefined)} />} </div> diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 493b194692fb30ebb255733525859d19f1ef6be5..5b58d4e9def1b0d4318612131e222754e76abed7 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -45,7 +45,7 @@ "postcss.config.js", // excludes PostCSS configuration file "tsconfig.tsbuildinfo" // excludes TypeScript build info file ], - "include": ["vite.config.ts", "src/**/*", "../../libs/shared/lib/components/layout/Panel.tsx"], + "include": ["vite.config.ts", "src/**/*"], "files": ["vite.config.ts"], "references": [] } diff --git a/libs/shared/lib/components/buttons/index.tsx b/libs/shared/lib/components/buttons/index.tsx index cc5a3ae449607b7c2fe1d5a7ef0d481838ba3ff4..87cc57e16c905eaa3a80eab8cb7eaaf25d52f2ed 100644 --- a/libs/shared/lib/components/buttons/index.tsx +++ b/libs/shared/lib/components/buttons/index.tsx @@ -104,7 +104,10 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT const blockClass = useMemo(() => (block ? styles['btn-block'] : ''), [block, styles]); const roundedClass = useMemo(() => (rounded ? styles['btn-rounded'] : ''), [rounded, styles]); - const icon = useMemo(() => (iconComponent ? <Icon component={iconComponent} size={iconSize} /> : null), [iconComponent, iconSize]); + const icon = useMemo( + () => (iconComponent ? <Icon component={iconComponent} size={iconSize} className="ml-auto" /> : null), + [iconComponent, iconSize], + ); const iconOnlyClass = useMemo( () => (iconComponent && !label && !children ? styles['btn-icon-only'] : ''), diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index 2ef371c0cd0c7aec6e0bae851773c8cb630f6a0b..28a6c8ced0d66cfc1f29ef1a0f5fd6d9ed77e7b3 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -17,7 +17,7 @@ type SliderProps = { }; type TextProps = { - label: string; + label?: string; type: 'text'; placeholder?: string; value: string; @@ -27,6 +27,7 @@ type TextProps = { disabled?: boolean; tooltip?: string; info?: string; + className?: string; validate?: (value: any) => boolean; onChange?: (value: string) => void; }; @@ -151,24 +152,31 @@ export const TextInput = ({ onChange, tooltip, info, + className, }: TextProps) => { const [isValid, setIsValid] = React.useState<boolean>(true); return ( <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}> - <label className="label"> - <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> - {label} - </span> - {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} - {info && <Info tooltip={info} side={'left'} />} - </label> + {label && ( + <label className="label"> + <span + className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + > + {label} + </span> + {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} + {info && <Info tooltip={info} side={'left'} />} + </label> + )} <input type={visible ? 'text' : 'password'} placeholder={placeholder} - className={`px-3 py-2 bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ - isValid ? '' : 'input-error' - }`} + className={ + `px-3 py-2 bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ + isValid ? '' : 'input-error' + }` + (className ? ` ${className}` : '') + } value={value.toString()} onChange={(e) => { if (required && validate) { diff --git a/libs/shared/lib/components/layout/Panel.tsx b/libs/shared/lib/components/layout/Panel.tsx index ad77898ffed7289e910b53ae64c284f22c1f9bdb..191afbf50c19ce21a8eef874c4314db50e9337b9 100644 --- a/libs/shared/lib/components/layout/Panel.tsx +++ b/libs/shared/lib/components/layout/Panel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { ControlContainer } from '../controls'; +import { ControlContainer } from '..'; export type Panel = { title: string; diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index dc469a11c213b0a2f0798333b05b5bd2b1085a23..e9e6fc3abe98b63f33119c39f06fd4a8d855e227 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -1,4 +1,4 @@ -import { QueryBuilder } from './../../querybuilder/panel/querybuilder'; +import { QueryBuilder } from '../../querybuilder/panel/QueryBuilder'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; import Graph, { MultiGraph } from 'graphology'; @@ -49,25 +49,25 @@ export const querybuilderSlice = createSlice({ name: 'querybuilder', initialState, reducers: { - setQuerybuilderGraph: (state:QueryBuilderState, action: PayloadAction<QueryMultiGraph>) => { + setQuerybuilderGraph: (state: QueryBuilderState, action: PayloadAction<QueryMultiGraph>) => { // @ts-ignore state.graph = action.payload; state.ignoreReactivity = false; }, - setQuerybuilderNodes: (state:QueryBuilderState, action: PayloadAction<QueryBuilderState>) => { + setQuerybuilderNodes: (state: QueryBuilderState, action: PayloadAction<QueryBuilderState>) => { if (action.payload.graph?.nodes && action.payload.graph?.edges) { state.graph = action.payload.graph; state.settings = action.payload.settings; // state.ignoreReactivity = true; } }, - clearQB: (state:QueryBuilderState) => { + clearQB: (state: QueryBuilderState) => { state.graph = defaultGraph(); }, - setQuerybuilderSettings: (state:QueryBuilderState, action: PayloadAction<QueryBuilderSettings>) => { + setQuerybuilderSettings: (state: QueryBuilderState, action: PayloadAction<QueryBuilderSettings>) => { state.settings = action.payload; }, - setQueryText: (state:QueryBuilderState, action: PayloadAction<QueryBuilderText>) => { + setQueryText: (state: QueryBuilderState, action: PayloadAction<QueryBuilderText>) => { state.queryTranslation = action.payload; }, }, diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx similarity index 98% rename from libs/shared/lib/querybuilder/panel/querybuilder.tsx rename to libs/shared/lib/querybuilder/panel/QueryBuilder.tsx index 3e118b5dc67551d8469d176b9da4307bd055d4f9..5268683959e34d7e141106b93a8d15dcec0925e2 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx @@ -83,6 +83,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { const elements = useMemo(() => createReactFlowElements(graphologyGraph), [graph, qbHash]); const searchResults = useSearchResultQB(); const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); + const [allowZoom, setAllowZoom] = useState(true); useEffect(() => { const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]); @@ -280,12 +281,14 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { }; const onNodeMouseEnter = (event: React.MouseEvent, node: Node) => { - console.log(node); - console.log(schema.getNodeAttribute(node.type, 'attributes')); + // console.log(node); + // console.log(schema.getNodeAttribute(node.type, 'attributes')); + setAllowZoom(false); }; const onNodeMouseLeave = (event: React.MouseEvent, node: Node) => { - console.log(node); + // console.log(node); + setAllowZoom(true); }; const onConnect = useCallback( @@ -349,7 +352,8 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { clientX -= left; clientY -= top; } - const position = reactFlow.project({ x: clientX, y: clientY }); + + const position = reactFlow.screenToFlowPosition({ x: clientX, y: clientY }); if (connectingNodeId?.current) connectingNodeId.current.position = position; if (!connectingNodeId?.current?.params?.handleId) { @@ -620,6 +624,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { edges={elements.edges} nodes={elements.nodes} snapGrid={[10, 10]} + zoomOnScroll={allowZoom} snapToGrid nodeTypes={nodeTypes} edgeTypes={edgeTypes} diff --git a/libs/shared/lib/querybuilder/panel/index.ts b/libs/shared/lib/querybuilder/panel/index.ts index 561b9d24164a3eb6a13e3e6f75f041e4eb7e8c0e..916afbdaf0f525e20d863aca22cffd1e8baced2e 100644 --- a/libs/shared/lib/querybuilder/panel/index.ts +++ b/libs/shared/lib/querybuilder/panel/index.ts @@ -1 +1 @@ -export * from './querybuilder'; +export * from './QueryBuilder'; diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx index cd0530b4e35964f92b727b06743259789d01e2d8..e2f12695fb882a4bd596784d6f0adc3c96392f24 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx @@ -4,7 +4,7 @@ import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@grap import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import QueryBuilder from '../querybuilder'; +import QueryBuilder from '../QueryBuilder'; import { Handles, NodeAttribute, QueryElementTypes, QueryMultiGraphology } from '../../model'; import { SchemaUtils } from '../../../schema/schema-utils'; @@ -68,7 +68,7 @@ export const Simple = { y: 100, name: 'Airport 1', }, - schema.getNodeAttribute('entity', 'attributes') + schema.getNodeAttribute('entity', 'attributes'), ); const entity2 = graph.addPill2Graphology( { @@ -78,7 +78,7 @@ export const Simple = { y: 200, name: 'Airport 2', }, - schema.getNodeAttribute('entity', 'attributes') + schema.getNodeAttribute('entity', 'attributes'), ); // graph.addNode('0', { type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); @@ -92,7 +92,7 @@ export const Simple = { collection: 'Relation Pill', depth: { min: 0, max: 1 }, }, - schema.getEdgeAttribute('entity:entity_entityentity', 'attributes') + schema.getEdgeAttribute('entity:entity_entityentity', 'attributes'), ); // addPill2Graphology( // '2', diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx index 9eb58c7ba64a810f53b26b9fcb16c408af78ce28..791ac0922db477770b203f769052b14821e1f378 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx @@ -4,7 +4,7 @@ import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@grap import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import QueryBuilder from '../querybuilder'; +import QueryBuilder from '../QueryBuilder'; import { QueryElementTypes, QueryMultiGraphology } from '../../model'; const Component: Meta<typeof QueryBuilder> = { diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx index fe73dc689e1d2b02fd1d8275fc78e568f683ffff..e0e97bffe606232aaa1ad803a5da06e841ea8b0c 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx @@ -4,7 +4,7 @@ import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@grap import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import QueryBuilder from '../querybuilder'; +import QueryBuilder from '../QueryBuilder'; import { QueryElementTypes, QueryMultiGraphology } from '../../model'; const Component: Meta<typeof QueryBuilder> = { diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx index 14075769e33a768ab247802ac800839efab9a83d..0f842fa977a49e7cca653c3755b70831f466f5e4 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx @@ -54,7 +54,7 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { }; return ( - <div className="w-fit h-fit" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <div className="w-fit h-fit nowheel" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <EntityPill title={data.name || ''} withHandles="horizontal" diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx index bd0a4a33279842bca149c66e3b60ada3f4b25f22..7df04570621dfa47e2bfd4932b90b810d946322c 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx @@ -84,7 +84,7 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { }; return ( - <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <div className="p-3 bg-transparent nowheel" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <RelationPill title={ <div className="flex flex-row w-full"> diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index 75cb652b6dd2df87514e7d07ec2421f308ea90ee..a5c4e9cb59180b66ef9f20ccc651ceef12f3dd7c 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -1,10 +1,11 @@ -import { useMemo, ReactElement } from 'react'; +import { useMemo, ReactElement, useState } from 'react'; import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; -import { Handle, Position } from 'reactflow'; +import { Handle, Position, useUpdateNodeInternals } 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'; +import { Button, TextInput } from '../../..'; type PillDropdownProps = { node: SchemaReactflowEntityNode; @@ -29,64 +30,82 @@ const IconMap: IconMapType = { export const PillDropdown = (props: PillDropdownProps) => { const forceOpen = false; - const openNumbers: number[] = useMemo(() => { - if (forceOpen || props.hovered) return props.attributes.map((_, i) => i); + const [filter, setFilter] = useState<string>(''); - //collect connected nodes - const connected = props.attributes - .map((attribute) => { - const nodeID = toHandleId(handleDataFromReactflowToDataId(props.node, attribute)); - return props.attributeEdges.some((edge) => edge?.sourceHandleData && toHandleId(edge?.sourceHandleData) === nodeID); - }) - .map((connected, i) => (connected ? i : -1)) - .filter((i) => i !== -1); - - // add to it the node being dragged - if (props.handleBeingDraggedIdx !== -1 && !connected.includes(props.handleBeingDraggedIdx)) connected.push(props.handleBeingDraggedIdx); - return connected.sort(); - }, [props]); + const [attributesOfInterest, setAttributesOfInterest] = useState<Set<number>>(new Set()); return ( <> - <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) { - throw new Error('attribute.handleData.attributeName is undefined'); - } + <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200'}> + {attributesOfInterest && + [...attributesOfInterest].map((i) => { + const attribute = props.attributes[i]; + if (attribute.handleData.attributeName === undefined) { + throw new Error('attribute.handleData.attributeName is undefined'); + } - return ( - <div - 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 && <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} />} - <PillHandle - mr={-pillDropdownPadding} - handleTop="auto" - position={Position.Right} - className={'fill-accent-500 stroke-white'} - type="square" + return ( + <div + 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); + }} > - <Handle - id={toHandleId(handleDataFromReactflowToDataId(props.node, attribute))} - type="source" + <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p> + {attribute.handleData?.attributeDimension && ( + <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} /> + )} + <PillHandle + mr={-pillDropdownPadding} + handleTop="auto" position={Position.Right} - className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'} - ></Handle> - </PillHandle> + 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> + ); + })} + {(props.hovered || forceOpen) && ( + <> + <h4 className="p-1 bg-white border-t-[2px] font-semibold text-2xs">Available Attributes:</h4> + <TextInput type={'text'} placeholder="Filter" className="!p-0.5" value={filter} onChange={(v) => setFilter(v)} /> + <div className="max-h-28 overflow-auto flex flex-col bg-white"> + {props.attributes.map((attribute, i) => { + if (filter && !attribute.handleData.attributeName?.toLowerCase().includes(filter.toLowerCase())) return null; + if (attribute.handleData.attributeName === undefined) { + throw new Error('attribute.handleData.attributeName is undefined'); + } + + return ( + <Button + key={(attribute.handleData.attributeName || '') + i} + iconComponent={attribute?.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : undefined} + iconPosition="trailing" + additionalClasses={`w-full ${attributesOfInterest.has(i) ? 'bg-secondary-100' : 'bg-white'} justify-between rounded-none text-[0.7em] hover:cursor-copy`} + variant="ghost" + size={'xs'} + label={attribute.handleData.attributeName} + onClick={(event: React.MouseEvent) => { + if (attributesOfInterest.has(i)) { + setAttributesOfInterest(new Set([...attributesOfInterest].filter((x) => x !== i))); + } else { + setAttributesOfInterest(new Set([...attributesOfInterest, i])); + } + }} + ></Button> + ); + })} </div> - ); - })} + </> + )} </div> </> );