From b45b2dcbac5ff9439a0b420f7114825eea98dfa6 Mon Sep 17 00:00:00 2001
From: 2427021 <s.a.vink@students.uu.nl>
Date: Mon, 25 Mar 2024 09:26:45 +0100
Subject: [PATCH] feat(visManager): moved search to sidebar

---
 apps/web/src/components/navbar/navbar.tsx     |   1 -
 .../components/navbar/search/SearchBar.tsx    | 260 ------------------
 libs/shared/lib/info/infoPanel.tsx            |   4 +-
 libs/shared/lib/info/search.tsx               |   7 -
 libs/shared/lib/info/search/searchbar.tsx     | 183 ++++++++++++
 .../shared/lib/info}/search/similarity.ts     |   0
 6 files changed, 185 insertions(+), 270 deletions(-)
 delete mode 100644 apps/web/src/components/navbar/search/SearchBar.tsx
 delete mode 100644 libs/shared/lib/info/search.tsx
 create mode 100644 libs/shared/lib/info/search/searchbar.tsx
 rename {apps/web/src/components/navbar => libs/shared/lib/info}/search/similarity.ts (100%)

diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx
index 585ac8b82..15820f87e 100644
--- a/apps/web/src/components/navbar/navbar.tsx
+++ b/apps/web/src/components/navbar/navbar.tsx
@@ -12,7 +12,6 @@ import React, { useState, useRef, useEffect } from 'react';
 import logo_white from './gp-logo-white.svg';
 import logo from './gp-logo.svg';
 import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access';
-import { SearchBar } from './search/SearchBar';
 import DatabaseSelector from './DatabaseManagement/dbConnectionSelector';
 import { DropdownItem, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns';
 import ColorMode from '@graphpolaris/shared/lib/components/color-mode';
diff --git a/apps/web/src/components/navbar/search/SearchBar.tsx b/apps/web/src/components/navbar/search/SearchBar.tsx
deleted file mode 100644
index e5182032b..000000000
--- a/apps/web/src/components/navbar/search/SearchBar.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-import React from 'react';
-import {
-  useAppDispatch,
-  useGraphQueryResult,
-  useSchemaGraph,
-  useQuerybuilderGraph,
-  useSearchResult,
-  AppDispatch,
-  useRecentSearches,
-} from '@graphpolaris/shared/lib/data-access';
-import { filterData } from './similarity';
-import { Search as SearchIcon } from '@mui/icons-material';
-import {
-  addSearchResultData,
-  addSearchResultSchema,
-  addSearchResultQueryBuilder,
-  CATEGORY_KEYS,
-  addRecentSearch,
-} from '@graphpolaris/shared/lib/data-access/store/searchResultSlice';
-import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
-
-const SIMILARITY_THRESHOLD = 0.7;
-
-const CATEGORY_ACTIONS: {
-  [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void;
-} = {
-  data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
-    dispatch(addSearchResultData(payload));
-  },
-  schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
-    dispatch(addSearchResultSchema(payload));
-  },
-  querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
-    dispatch(addSearchResultQueryBuilder(payload));
-  },
-};
-
-const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[];
-
-export function SearchBar({}) {
-  const inputRef = React.useRef<HTMLInputElement>(null);
-  const searchbarRef = React.useRef<HTMLDivElement>(null);
-  const dispatch = useAppDispatch();
-  const results = useSearchResult();
-  const recentSearches = useRecentSearches();
-  const schema = useSchemaGraph();
-  const graphData = useGraphQueryResult();
-  const querybuilderData = useQuerybuilderGraph();
-  const [search, setSearch] = React.useState<string>('');
-  const [searchOpen, setSearchOpen] = React.useState<boolean>(false);
-
-  const dataSources: {
-    [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] };
-  } = {
-    data: graphData,
-    schema: schema,
-    querybuilder: querybuilderData as QueryMultiGraph,
-  };
-
-  const toggleSearch = () => {
-    setSearchOpen(true);
-    if (!searchOpen && inputRef.current) {
-      inputRef.current.focus();
-    }
-  };
-
-  React.useEffect(() => {
-    const handleKeyPress = (event: KeyboardEvent) => {
-      if (event.key === 'Enter') {
-        if (searchOpen && search !== '') {
-          dispatch(addRecentSearch(search));
-        }
-      }
-    };
-    window.addEventListener('keydown', handleKeyPress);
-    return () => window.removeEventListener('keydown', handleKeyPress);
-  }, [searchOpen, search]);
-
-  React.useEffect(() => {
-    const handleKeyPress = (event: KeyboardEvent) => {
-      if (event.key === '/') {
-        if (!searchOpen) {
-          setSearchOpen(true);
-          setSearch('');
-        }
-      }
-    };
-    window.addEventListener('keydown', handleKeyPress);
-    return () => window.removeEventListener('keydown', handleKeyPress);
-  }, [searchOpen]);
-
-  React.useEffect(() => {
-    if (searchOpen && inputRef.current) {
-      inputRef.current.focus();
-      setSearch('');
-    }
-  }, [searchOpen]);
-
-  React.useEffect(() => {
-    const handleKeyPress = (event: KeyboardEvent) => {
-      if (event.key === 'Escape') {
-        if (searchOpen) {
-          setSearchOpen(false);
-          setSearch('');
-        }
-      }
-    };
-    window.addEventListener('keydown', handleKeyPress);
-    return () => window.removeEventListener('keydown', handleKeyPress);
-  }, [searchOpen]);
-
-  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 as CATEGORY_KEYS];
-          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 = ({ target }: MouseEvent) => {
-      if (inputRef.current && target && !inputRef.current.contains(target as Node) && !searchbarRef?.current?.contains(target as Node)) {
-        setSearch('');
-        setSearchOpen(false);
-      }
-    };
-    document.addEventListener('click', handleClickOutside);
-    return () => {
-      document.removeEventListener('click', handleClickOutside);
-    };
-  }, []);
-
-  return (
-    <div className="searchbar">
-      {searchOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40"></div>}
-      <div className="mr-2" ref={searchbarRef}>
-        <div
-          className="flex items-center border border-secondary-300 hover:bg-secondary-50 px-2 rounded text-sm w-44 h-8 text-secondary-900 cursor-pointer"
-          onClick={toggleSearch}
-        >
-          <SearchIcon />
-          <span className="ml-1 text-secondary-900">
-            Type <span className="border border-secondary-900 rounded px-1">/</span> to search
-          </span>
-        </div>
-
-        {searchOpen && (
-          <div className="fixed flex flex-col left-1/2 -translate-x-1/2 w-9/12 max-h-1/2 top-2 items-start justify-center z-50 bg-secondary-200 rounded">
-            <div className="p-3 w-full">
-              {/* <label className="sr-only">Search</label> */}
-              <div className="relative">
-                <div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
-                  <SearchIcon className="text-secondary500" />
-                </div>
-                <input
-                  type="text"
-                  ref={inputRef}
-                  value={search}
-                  onChange={(e) => setSearch(e.target.value)}
-                  id="input-group-search"
-                  className="block w-full p-2 ps-10 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0"
-                  placeholder="Search database"
-                ></input>
-              </div>
-            </div>
-
-            {recentSearches.length !== 0 && (
-              <div className="px-3 pb-3">
-                <p className="text-sm">Recent searches</p>
-                {recentSearches.slice(0, 3).map((term) => (
-                  <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}>
-                    {term}
-                  </p>
-                ))}
-              </div>
-            )}
-            {search !== '' && (
-              <div className="z-10 rounded card-bordered w-full overflow-auto max-h-[60vh] px-3 pb-3">
-                <p className="font-bold text-sm">Results</p>
-                {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? (
-                  <div className="ml-1 text-sm">
-                    <p className="text-secondary-500">Found no matches...</p>
-                  </div>
-                ) : (
-                  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 text-lg">
-                            <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p>
-                            <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p>
-                          </div>
-                          <div className="h-[1px] w-full bg-secondary-200"></div>
-                          {Object.values(Object.values(results[category]))
-                            .flat()
-                            .map((item, index) => (
-                              <div
-                                key={index}
-                                className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2"
-                                title={JSON.stringify(item)}
-                                onClick={() => {
-                                  CATEGORY_ACTIONS[category](
-                                    {
-                                      nodes: results[category].nodes.includes(item) ? [item] : [],
-                                      edges: results[category].edges.includes(item) ? [item] : [],
-                                    },
-                                    dispatch,
-                                  );
-                                }}
-                              >
-                                <div className="font-bold text-sm">
-                                  {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)}
-                                </div>
-                                <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div>
-                              </div>
-                            ))}
-                        </div>
-                      );
-                    } else return <></>;
-                  })
-                )}
-              </div>
-            )}
-          </div>
-        )}
-      </div>
-    </div>
-  );
-}
diff --git a/libs/shared/lib/info/infoPanel.tsx b/libs/shared/lib/info/infoPanel.tsx
index 0392aa05d..52a08ca4c 100644
--- a/libs/shared/lib/info/infoPanel.tsx
+++ b/libs/shared/lib/info/infoPanel.tsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
 import { Button } from '../components';
 import { Addchart, Schema as SchemaIcon, Search as SearchIcon } from '@mui/icons-material';
 import Schema from '../schema/panel';
-import Search from './search';
+import Searchbar from './search/searchbar';
 import Settings from './settings';
 
 export default function InfoPanel({ auth, manager }: { auth: boolean; manager: any }) {
@@ -40,7 +40,7 @@ export default function InfoPanel({ auth, manager }: { auth: boolean; manager: a
         <div className="relative flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100  border-b border-secondary-200">
           <h1 className="text-xs font-semibold text-secondary-800">{tab}</h1>
         </div>
-        {tab === 'Search' && <Search />}
+        {tab === 'Search' && <Searchbar />}
         {tab === 'Schema' && <Schema auth={auth} />}
         {tab === 'Visualization' && <Settings manager={manager} />}
       </div>
diff --git a/libs/shared/lib/info/search.tsx b/libs/shared/lib/info/search.tsx
deleted file mode 100644
index 2ab6a4ae5..000000000
--- a/libs/shared/lib/info/search.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-type Props = {};
-
-export default function Search({}: Props) {
-  return <div>Search to be implemented here</div>;
-}
diff --git a/libs/shared/lib/info/search/searchbar.tsx b/libs/shared/lib/info/search/searchbar.tsx
new file mode 100644
index 000000000..abafc6acd
--- /dev/null
+++ b/libs/shared/lib/info/search/searchbar.tsx
@@ -0,0 +1,183 @@
+import React, { useRef, useState, useEffect } from 'react';
+import {
+  AppDispatch,
+  useAppDispatch,
+  useGraphQueryResult,
+  useQuerybuilderGraph,
+  useRecentSearches,
+  useSchemaGraph,
+  useSearchResult,
+} from '../../data-access';
+import { QueryMultiGraph } from '../../querybuilder';
+import {
+  CATEGORY_KEYS,
+  addRecentSearch,
+  addSearchResultData,
+  addSearchResultQueryBuilder,
+  addSearchResultSchema,
+} from '../../data-access/store/searchResultSlice';
+import { filterData } from './similarity';
+
+const SIMILARITY_THRESHOLD = 0.7;
+
+const CATEGORY_ACTIONS: {
+  [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void;
+} = {
+  data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
+    dispatch(addSearchResultData(payload));
+  },
+  schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
+    dispatch(addSearchResultSchema(payload));
+  },
+  querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => {
+    dispatch(addSearchResultQueryBuilder(payload));
+  },
+};
+
+const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[];
+
+export default function Searchbar() {
+  const inputRef = useRef<HTMLInputElement>(null);
+  const [search, setSearch] = useState<string>('');
+  const searchbarRef = useRef<HTMLDivElement>(null);
+  const dispatch = useAppDispatch();
+  const results = useSearchResult();
+  const recentSearches = useRecentSearches();
+  const schema = useSchemaGraph();
+  const graphData = useGraphQueryResult();
+  const querybuilderData = useQuerybuilderGraph();
+
+  const dataSources: {
+    [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] };
+  } = {
+    data: graphData,
+    schema: schema,
+    querybuilder: querybuilderData as QueryMultiGraph,
+  };
+
+  useEffect(() => {
+    const handleKeyPress = (event: KeyboardEvent) => {
+      if (event.key === 'Enter') {
+        if (search !== '') {
+          dispatch(addRecentSearch(search));
+        }
+      }
+    };
+    window.addEventListener('keydown', handleKeyPress);
+    return () => window.removeEventListener('keydown', handleKeyPress);
+  }, [search]);
+
+  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 as CATEGORY_KEYS];
+          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);
+      }
+    }
+  };
+
+  return (
+    <div className="flex flex-col w-full p-2">
+      <div className="w-full">
+        <input
+          type="text"
+          ref={inputRef}
+          value={search}
+          onChange={(e) => setSearch(e.target.value)}
+          id="input-group-search"
+          className="block w-full p-2 ps-10 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0"
+          placeholder="Search database"
+        ></input>
+      </div>
+      <div>
+        {recentSearches.length !== 0 && (
+          <div className="px-3 pb-3">
+            <p className="text-sm">Recent searches</p>
+            {recentSearches.slice(0, 3).map((term) => (
+              <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}>
+                {term}
+              </p>
+            ))}
+          </div>
+        )}
+        {search !== '' && (
+          <div className="z-10 rounded card-bordered w-full overflow-auto max-h-[60vh] px-3 pb-3">
+            <p className="font-bold text-sm">Results</p>
+            {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? (
+              <div className="ml-1 text-sm">
+                <p className="text-secondary-500">Found no matches...</p>
+              </div>
+            ) : (
+              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 text-lg">
+                        <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p>
+                        <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p>
+                      </div>
+                      <div className="h-[1px] w-full bg-secondary-200"></div>
+                      {Object.values(Object.values(results[category]))
+                        .flat()
+                        .map((item, index) => (
+                          <div
+                            key={index}
+                            className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2"
+                            title={JSON.stringify(item)}
+                            onClick={() => {
+                              CATEGORY_ACTIONS[category](
+                                {
+                                  nodes: results[category].nodes.includes(item) ? [item] : [],
+                                  edges: results[category].edges.includes(item) ? [item] : [],
+                                },
+                                dispatch,
+                              );
+                            }}
+                          >
+                            <div className="font-bold text-sm">
+                              {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)}
+                            </div>
+                            <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div>
+                          </div>
+                        ))}
+                    </div>
+                  );
+                } else return <></>;
+              })
+            )}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/apps/web/src/components/navbar/search/similarity.ts b/libs/shared/lib/info/search/similarity.ts
similarity index 100%
rename from apps/web/src/components/navbar/search/similarity.ts
rename to libs/shared/lib/info/search/similarity.ts
-- 
GitLab