diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index af45ce67c3ec86b003aeed5bbf56ec094668aded..e778a820bd1cfbd6fd04add06c431f8906553681 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -25,6 +25,7 @@ import { import { DatabaseMenu } from './databasemenu'; import { NewDatabaseForm } from './AddDatabaseForm/newdatabaseform'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import SearchBar from './search/searchBar'; /** NavbarComponentProps is an interface containing the NavbarViewModel. */ export interface NavbarComponentProps { @@ -75,6 +76,7 @@ export const Navbar = (props: NavbarComponentProps) => { <a href="https://graphpolaris.com/" className="w-full"> <img src={currentLogo} /> </a> + <SearchBar /> <div className="w-fit"> <div className=""> <button diff --git a/apps/web/src/components/navbar/search/SearchBar.tsx b/apps/web/src/components/navbar/search/SearchBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cf835d926ed7a2b3bec997beb2fc08dc6e374860 --- /dev/null +++ b/apps/web/src/components/navbar/search/SearchBar.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { + useAppDispatch, + useGraphQueryResult, + useSchemaGraph, + useQuerybuilderGraph, + useSearchResult, +} from '@graphpolaris/shared/lib/data-access'; +import { filterData } from './similarity'; +import { + addSearchResultData, + addSearchResultSchema, + addSearchResultQueryBuilder, + resetSearchResults, +} from '@graphpolaris/shared/lib/data-access/store/searchResultSlice'; +import { Close } from '@mui/icons-material'; + +const SIMILARITY_THRESHOLD = 0.7; + +const CATEGORY_ACTIONS = { + data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch) => { + dispatch(addSearchResultData(payload)); + }, + schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch) => { + dispatch(addSearchResultSchema(payload)); + }, + querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch) => { + dispatch(addSearchResultQueryBuilder(payload)); + }, +}; + +const SEARCH_CATEGORIES: string[] = Object.keys(CATEGORY_ACTIONS); + +export default function SearchBar({}) { + const inputRef = React.useRef<HTMLInputElement>(null); + const dispatch = useAppDispatch(); + const results = useSearchResult(); + const schema = useSchemaGraph(); + const graphData = useGraphQueryResult(); + const querybuilderData = useQuerybuilderGraph(); + const [search, setSearch] = React.useState<string>(''); + const [inputActive, setInputActive] = React.useState<boolean>(false); + + const dataSources = { + data: graphData, + schema: schema, + querybuilder: querybuilderData, + }; + + React.useEffect(() => { + handleSearch(); + }, [search]); + + const handleSearch = () => { + let query = search.toLowerCase(); + const categories = search.match(/@[^ ]+/g); + + if (categories) { + categories.map((category) => { + query = query.replace(category, '').trim(); + const cat = category.substring(1); + + if (cat in CATEGORY_ACTIONS) { + const categoryAction = CATEGORY_ACTIONS[cat]; + const data = dataSources[cat]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + + categoryAction(payload, dispatch); + } + }); + } else { + for (const category of SEARCH_CATEGORIES) { + const categoryAction = CATEGORY_ACTIONS[category]; + const data = dataSources[category]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + + categoryAction(payload, dispatch); + } + } + }; + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (inputRef.current && !inputRef.current.contains(event.target)) { + setSearch(''); + // dispatch(resetSearchResults()); + inputRef.current.blur(); + } + }; + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, []); + + return ( + <div className={`relative form-control mr-4 ${inputActive || search !== '' ? 'w-96' : 'w-56'} transition-width duration-300`}> + <div className="relative rounded-md shadow-sm w-full"> + <input + ref={inputRef} + value={search} + onChange={(e) => setSearch(e.target.value)} + type="text" + onFocus={() => setInputActive(true)} + onBlur={() => setInputActive(false)} + placeholder="Search…" + className="block w-full rounded-md border-0 py-1.5 pl-2 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6" + /> + {search !== '' && ( + <div + className="absolute inset-y-0 right-0 flex items-center cursor-pointer" + onClick={() => { + dispatch(resetSearchResults()); + setSearch(''); + }} + > + <div className="py-0 px-2"> + <span className="text-gray-400 text-xs"> + <Close /> + </span> + </div> + </div> + )} + </div> + {search !== '' && ( + <div className="absolute top-[calc(100%+0.5rem)] z-[1] bg-white rounded-none card-bordered w-full"> + {SEARCH_CATEGORIES.map((category, index) => { + if (results[category].nodes.length > 0 || results[category].edges.length > 0) { + return ( + <div key={index}> + <div className="flex justify-between p-2"> + <p className="text-entity-500">{category.charAt(0).toUpperCase() + category.slice(1)}</p> + <p className="font-thin text-slate-400">{results[category].nodes.length + results[category].edges.length} results</p> + </div> + <div className="h-[1px] w-full bg-offwhite-300"></div> + {[].concat(...Object.values(results[category])).map((item, index) => ( + <div + key={index} + className="flex flex-col hover:bg-slate-300 p-2 cursor-pointer" + title={JSON.stringify(item)} + onClick={() => { + CATEGORY_ACTIONS[category]( + { + nodes: results[category].nodes.includes(item) ? [item] : [], + edges: results[category].edges.includes(item) ? [item] : [], + }, + dispatch, + ); + }} + > + <p style={{ fontSize: '14px', fontWeight: '600' }}> + {item.key.slice(0, 18) || item.id.slice(0, 18) || Object.values(item)[0].slice(0, 18)} + </p> + <p className="font-thin text-slate-400" style={{ fontSize: '12px' }}> + {JSON.stringify(item).substring(0, 40)}... + </p> + </div> + ))} + </div> + ); + } + })} + </div> + )} + </div> + ); +} diff --git a/apps/web/src/components/navbar/search/similarity.ts b/apps/web/src/components/navbar/search/similarity.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5714c0aa60c00d8045bf43790078d56f0a32ced --- /dev/null +++ b/apps/web/src/components/navbar/search/similarity.ts @@ -0,0 +1,70 @@ +export const filterData = (query: string, data: Record<string, any>[], threshold: number): Record<string, any>[] => { + return data + .map((object) => { + const similarity = matches(query, object); + return { ...object, similarity }; + }) + .filter((object) => object.similarity >= threshold); +}; + +const matches = (query: string, object: Record<string, any>): number => { + let highestScore = 0; + + for (const key in object) { + if (object.hasOwnProperty(key)) { + const attributeValue = object[key]; + if (typeof attributeValue === 'object') { + const subScore = matches(query, attributeValue); + if (subScore > highestScore) { + highestScore = subScore; + } + } else if (typeof attributeValue === 'string') { + const value = attributeValue.toString().toLowerCase(); + const similarity = jaroSimilarity(query, value); + if (similarity > highestScore) { + highestScore = similarity; + } + } + } + } + return highestScore; +}; + +const jaroSimilarity = (s1: string, s2: string) => { + if (s1 == s2) return 1.0; + + const len1 = s1.length; + const len2 = s2.length; + const max_dist = Math.floor(Math.max(len1, len2) / 2) - 1; + + let match = 0; + + const hash_s1 = Array(s1.length).fill(0); + const hash_s2 = Array(s1.length).fill(0); + + for (var i = 0; i < len1; i++) { + for (var j = Math.max(0, i - max_dist); j < Math.min(len2, i + max_dist + 1); j++) + if (s1[i] == s2[j] && hash_s2[j] == 0) { + hash_s1[i] = 1; + hash_s2[j] = 1; + match++; + break; + } + } + + if (match == 0) return 0.0; + + let t = 0; + let point = 0; + + for (var i = 0; i < len1; i++) + if (hash_s1[i]) { + while (hash_s2[point] == 0) point++; + + if (s1[i] != s2[point++]) t++; + } + + t /= 2; + + return (match / len1 + match / len2 + (match - t) / match) / 3.0; +}; diff --git a/libs/shared/lib/components/forms/index.tsx b/libs/shared/lib/components/forms/index.tsx index 561852353bc47599c1c5b1d46e553e401ebd8393..ce3c383314f26258a94fa33d0e82fb4c912c0410 100644 --- a/libs/shared/lib/components/forms/index.tsx +++ b/libs/shared/lib/components/forms/index.tsx @@ -7,7 +7,7 @@ export const FormDiv = React.forwardRef<HTMLDivElement, PropsWithChildren<{ clas <div className={'absolute opacity-100 transition-opacity group-hover:opacity-100 z-50 ' + (props.className ? props.className : '')} ref={ref} - style={props.hAnchor === 'left' ? { left: props.offset || 0 } : { right: props.offset || '5rem' }} + // style={props.hAnchor === 'left' ? { left: props.offset || 0 } : { right: props.offset || '5rem' }} > {props.children} </div> diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 224170da3c1371265a4f721f2a073abeff6f0db8..3288c7c469cfcc9c6b1c0a0645854d3fe5770031 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -11,6 +11,7 @@ import { import { sessionCacheState } from './sessionSlice'; import { authState } from './authSlice'; import { allMLEnabled, selectML } from './mlSlice'; +import { searchResultState, searchResultData, searchResultSchema, searchResultQB } from './searchResultSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -38,3 +39,9 @@ export const useAuthorizationCache = () => useAppSelector(authState); // Machine Learning Slices export const useML = () => useAppSelector(selectML); export const useMLEnabledHash = () => useAppSelector(allMLEnabled); + +// Search Result Slices +export const useSearchResult = () => useAppSelector(searchResultState); +export const useSearchResultData = () => useAppSelector(searchResultData); +export const useSearchResultSchema = () => useAppSelector(searchResultSchema); +export const useSearchResultQB = () => useAppSelector(searchResultQB); diff --git a/libs/shared/lib/data-access/store/searchResultSlice.ts b/libs/shared/lib/data-access/store/searchResultSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..f15d1aecdf33b464278e969a9ffe1d75c5d51f08 --- /dev/null +++ b/libs/shared/lib/data-access/store/searchResultSlice.ts @@ -0,0 +1,78 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; + +// Define the initial state using that type +export const initialState: { + data: { + nodes: Record<string, any>[]; + edges: Record<string, any>[]; + }; + schema: { + nodes: Record<string, any>[]; + edges: Record<string, any>[]; + }; + querybuilder: { + nodes: Record<string, any>[]; + edges: Record<string, any>[]; + }; +} = { + data: { + nodes: [], + edges: [], + }, + schema: { + nodes: [], + edges: [], + }, + querybuilder: { + nodes: [], + edges: [], + }, +}; + +export const searchResultSlice = createSlice({ + name: 'search', + initialState, + reducers: { + addSearchResultData: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { + state.data = action.payload; + }, + addSearchResultSchema: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { + state.schema = action.payload; + }, + addSearchResultQueryBuilder: (state, action: PayloadAction<{ nodes: Record<string, any>[]; edges: Record<string, any>[] }>) => { + state.querybuilder = action.payload; + }, + addSearchResultSelected: (state, action: PayloadAction<{ category: string; value: Record<string, any> }>) => { + state.data = { nodes: [], edges: [] }; + state.schema = { nodes: [], edges: [] }; + state.querybuilder = { nodes: [], edges: [] }; + + const { category, value } = action.payload; + state[category] = value; + }, + resetSearchResults: (state) => { + return { + ...state, + data: initialState.data, + schema: initialState.schema, + querybuilder: initialState.querybuilder, + }; + }, + }, +}); + +export const { addSearchResultData, addSearchResultSchema, addSearchResultQueryBuilder, addSearchResultSelected, resetSearchResults } = + searchResultSlice.actions; + +// Other code such as selectors can use the imported `RootState` type +export const searchResultState = (state: RootState) => state.searchResults; +export const searchResultData = (state: RootState) => state.searchResults.data; +export const searchResultSchema = (state: RootState) => { + const nodes = state.searchResults.schema.nodes.map((node) => node.key); + const edges = state.searchResults.schema.edges.map((edge) => edge.key); + return [...nodes, ...edges]; +}; +export const searchResultQB = (state: RootState) => state.searchResults.querybuilder; + +export default searchResultSlice.reducer; diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts index 713410cabd3acf056d6def96289b06c7fa3f322b..ce9f4f555cde3ae83b004730c77bc11f39c1c5d0 100644 --- a/libs/shared/lib/data-access/store/store.ts +++ b/libs/shared/lib/data-access/store/store.ts @@ -6,6 +6,7 @@ import configSlice from './configSlice'; import sessionSlice from './sessionSlice'; import authSlice from './authSlice'; import mlSlice from './mlSlice'; +import searchResultSlice from './searchResultSlice'; export const store = configureStore({ reducer: { @@ -16,6 +17,7 @@ export const store = configureStore({ auth: authSlice, config: configSlice, ml: mlSlice, + searchResults: searchResultSlice, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts index 02b336deb8cf39c292610ff35934e5714047f4de..0125076ceb9b8d4af8f7f9d6b5cc91d2c6f7b652 100644 --- a/libs/shared/lib/querybuilder/model/graphology/model.ts +++ b/libs/shared/lib/querybuilder/model/graphology/model.ts @@ -30,6 +30,7 @@ export interface EntityData { name?: string; leftRelationHandleId?: QueryGraphEdgeHandle; rightRelationHandleId?: QueryGraphEdgeHandle; + selected?: boolean; } /** Interface for the data in an relation node. */ @@ -40,6 +41,7 @@ export interface RelationData { leftEntityHandleId?: QueryGraphEdgeHandle; rightEntityHandleId?: QueryGraphEdgeHandle; direction?: 'left' | 'right' | 'both'; + selected?: boolean; } export interface LogicData { diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index dc9049659b7eac9ea029770faa7b823cbefe005f..215be6b727045db682e20878554f73cf790ab99a 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -1,5 +1,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useConfig, useQuerybuilderGraph, useQuerybuilderSettings, useSchemaGraph } from '@graphpolaris/shared/lib/data-access/store'; +import { + useConfig, + useQuerybuilderGraph, + useQuerybuilderSettings, + useSchemaGraph, + useSearchResultQB, +} from '@graphpolaris/shared/lib/data-access/store'; import ReactFlow, { Background, Connection, @@ -87,6 +93,22 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { const isOnConnect = useRef(false); const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph]); const elements = useMemo(() => createReactFlowElements(graphologyGraph), [graph]); + const searchResults = useSearchResultQB(); + + useEffect(() => { + const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]); + + elements.nodes.forEach((node) => { + const isNodeInSearchResults = searchResultKeys.has(node?.id); + if (isNodeInSearchResults && !graphologyGraph.getNodeAttribute(node?.id, 'selected')) { + graphologyGraph.setNodeAttribute(node?.id, 'selected', true); + } else if (!isNodeInSearchResults && graphologyGraph.getNodeAttribute(node?.id, 'selected')) { + graphologyGraph.setNodeAttribute(node?.id, 'selected', false); + } + }); + + dispatch(setQuerybuilderGraphology(graphologyGraph)); + }, [searchResults]); const onInit = (reactFlowInstance: ReactFlowInstance) => { setTimeout(() => reactFlow.fitView(), 0); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index b2963dd7b98ca114ef110c5309f208a456ab7783..356e303e69ddfc39590d52cdf413a2aedae3ef42 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -93,7 +93,11 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => return ( <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <div className={`border-l-[3px] border-solid bg-entity-50 border-l-entity-600 font-bold text-xs min-w-[8rem] query_builder-entity`}> + <div + className={`border-l-[3px] border-solid ${ + data.selected ? 'bg-slate-400' : 'bg-entity-50' + } border-l-entity-600 font-bold text-xs min-w-[8rem] query_builder-entity`} + > <div> <Handle id={toHandleId(data.leftRelationHandleId)} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..bd38e97d08cb3a012c6a0ea798b56c6c8be879b5 --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss @@ -0,0 +1,117 @@ +@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-relation-50; + @apply border-l-relation-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-relation-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: 5; + } + + .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: 0px; + 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 new file mode 100644 index 0000000000000000000000000000000000000000..432ea72c9fc83e70e90bce2dbca2911362df4a5a --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module.scss.d.ts @@ -0,0 +1,34 @@ +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.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index d1f3975ff1ca96cd561a28edd8c53271b314bbc5..80b10a612501609ce94b3909055b797a942dcfe4 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -67,7 +67,11 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { }; return ( - <div className="text-center font-bold bg-relation-50 border-l-relation-600 border-l-[3px] text-[13px] min-w-[325px]"> + <div + className={`text-center font-bold ${ + data.selected ? 'bg-slate-400' : 'bg-relation-50' + } border-l-relation-600 border-l-[3px] text-[13px] min-w-[325px]`} + > <div> <span> {data.rightEntityHandleId && ( diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index e82aaa42957a93da55c8caa03fbd43a030f86dd2..d4c678d2cfc965ed03b22d86efeece706ff39244 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -1,6 +1,12 @@ import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/shared/lib/graph-layout'; import { schemaGraphology2Reactflow, schemaExpandRelation } from '@graphpolaris/shared/lib/schema/schema-utils'; -import { useSchemaGraph, useSchemaLayout, useSchemaSettings, useSessionCache } from '@graphpolaris/shared/lib/data-access/store'; +import { + useSchemaGraph, + useSchemaLayout, + useSchemaSettings, + useSearchResultSchema, + useSessionCache, +} from '@graphpolaris/shared/lib/data-access/store'; import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -13,6 +19,7 @@ import ReactFlow, { useNodesState, useEdgesState, ReactFlowInstance, + useReactFlow, } from 'reactflow'; import CachedIcon from '@mui/icons-material/Cached'; import SettingsIcon from '@mui/icons-material/Settings'; @@ -65,6 +72,8 @@ export const Schema = (props: Props) => { const session = useSessionCache(); const settings = useSchemaSettings(); + const searchResults = useSearchResultSchema(); + const [toggleSchemaSettings, setToggleSchemaSettings] = useState(false); const [nodes, setNodes, onNodeChanged] = useNodesState([] as Node[]); const [edges, setEdges, onEdgeChanged] = useEdgesState([] as Edge[]); @@ -127,7 +136,14 @@ export const Schema = (props: Props) => { // ); }, [schemaGraph, settings]); - // console.log(nodes, edges); + useEffect(() => { + setNodes((nds) => + nds.map((node) => ({ + ...node, + selected: searchResults.includes(node.id) || searchResults.includes(node.data.label), + })) + ); + }, [searchResults]); useEffect(() => { if (dialogRef.current && settingsIconRef.current) { diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx index b6b8c9a3f90e4dd87dc606f6ac496dccb7b34e8a..af97fd7cd9432ff42320a975e3fe05a64cd35857 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx @@ -22,7 +22,7 @@ import { Popup } from '@graphpolaris/shared/lib/components/Popup'; * 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, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { +export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); /** @@ -56,7 +56,7 @@ export const EntityNode = React.memo(({ id, data }: NodeProps<SchemaReactflowNod </Popup> )} <div - className="border-l-2 bg-offwhite-200 border-l-entity-600 min-w-[8rem] text-[0.8rem]" + className={`border-l-2 border-l-entity-600 min-w-[8rem] text-[0.8rem] ${selected ? 'bg-slate-400' : 'bg-offwhite-200'}`} onDragStart={(event) => onDragStart(event)} onDragStartCapture={(event) => onDragStart(event)} onMouseDownCapture={(event) => { diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index c902947195057fe6114e0a9daab868465907d505..6f42caf441a68c6b1c1d880fc75432cc608c56bf 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -21,7 +21,7 @@ import { SchemaRelationshipPopup } from './SchemaRelationshipPopup'; * 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, data }: NodeProps<SchemaReactflowRelationWithFunctions>) => { +export const RelationNode = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowRelationWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); /** @@ -76,7 +76,12 @@ export const RelationNode = React.memo(({ id, data }: NodeProps<SchemaReactflowR draggable // style={{ width: 100, height: 100 }} > - <div className="text-[0.8rem] border-l-2 bg-offwhite-200 border-l-relation-600 min-w-[8rem]"> + <div + className="text-[0.8rem] border-l-2 border-l-relation-600 min-w-[8rem]" + style={{ + backgroundColor: selected ? '#97a2b6' : '#f4f6f7', + }} + > <Handle style={{ pointerEvents: 'none' }} className={styles.handleTriangleTop}