From 30ef93fc90291ad55d637ad10c5d6327722c6b42 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Wed, 4 Sep 2024 10:35:35 +0200
Subject: [PATCH] autocomplete for search queries

---
 .../TopBar/SearchBar/SearchBar.component.tsx  | 22 ++++++++++-----
 .../TopBar/TopBar.component.test.tsx          |  3 ++
 src/constants/common.ts                       |  2 ++
 src/models/autocompleteSchema.ts              |  3 ++
 src/redux/apiPath.ts                          |  3 ++
 .../autocomplete/autocomplete.constants.ts    |  6 ++++
 .../autocomplete/autocomplete.reducers.ts     | 19 +++++++++++++
 .../autocomplete/autocomplete.selectors.ts    |  4 +++
 src/redux/autocomplete/autocomplete.slice.ts  | 14 ++++++++++
 src/redux/autocomplete/autocomplete.thunks.ts | 28 +++++++++++++++++++
 src/redux/autocomplete/autocomplete.types.ts  |  6 ++++
 src/redux/root/init.thunks.ts                 |  4 +++
 src/redux/root/root.fixtures.ts               |  2 ++
 src/redux/store.ts                            |  2 ++
 14 files changed, 111 insertions(+), 7 deletions(-)
 create mode 100644 src/models/autocompleteSchema.ts
 create mode 100644 src/redux/autocomplete/autocomplete.constants.ts
 create mode 100644 src/redux/autocomplete/autocomplete.reducers.ts
 create mode 100644 src/redux/autocomplete/autocomplete.selectors.ts
 create mode 100644 src/redux/autocomplete/autocomplete.slice.ts
 create mode 100644 src/redux/autocomplete/autocomplete.thunks.ts
 create mode 100644 src/redux/autocomplete/autocomplete.types.ts

diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
index d5a4c4a5..8ef41e48 100644
--- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
+++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
@@ -1,3 +1,4 @@
+import { autocompleteSelector } from '@/redux/autocomplete/autocomplete.selectors';
 import {
   currentSelectedSearchElement,
   searchDrawerOpenSelector,
@@ -16,7 +17,7 @@ import Image from 'next/image';
 import { useRouter } from 'next/router';
 import { useCallback, KeyboardEvent, useEffect, useState } from 'react';
 import { useSelector } from 'react-redux';
-import { ONE, ZERO } from '@/constants/common';
+import { FIVE, ONE, ZERO } from '@/constants/common';
 import Autosuggest from 'react-autosuggest';
 import { clearEntityNumberData } from '@/redux/entityNumber/entityNumber.slice';
 import { getDefaultSearchTab, getSearchValuesArrayAndTrimToSeven } from './SearchBar.utils';
@@ -35,6 +36,7 @@ export const SearchBar = (): JSX.Element => {
   const isSearchDrawerOpen = useSelector(searchDrawerOpenSelector);
   const isPerfectMatch = useSelector(perfectMatchSelector);
   const searchValueState = useSelector(searchValueSelector);
+  const searchAutocompleteState = useSelector(autocompleteSelector);
   const dispatch = useAppDispatch();
   const router = useRouter();
   const currentTab = useSelector(currentSelectedSearchElement);
@@ -85,15 +87,21 @@ export const SearchBar = (): JSX.Element => {
     openSearchDrawerIfClosed(currentTab);
   };
 
-  const suggestions = [{ name: 'alpha' }, { name: 'amigo' }, { name: 'beta' }, { name: 'omega' }];
+  // eslint-disable-next-line no-console
+  console.log(searchAutocompleteState.searchValues);
+  const suggestions = searchAutocompleteState.searchValues.map(entry => {
+    return { name: entry };
+  });
 
-  const getSuggestions = function (value: string): Suggestion[] {
+  const getSuggestions = (value: string): Suggestion[] => {
     const inputValue = value.trim().toLowerCase();
     const inputLength = inputValue.length;
-
-    return inputLength === ZERO
-      ? []
-      : suggestions.filter(lang => lang.name.toLowerCase().slice(ZERO, inputLength) === inputValue);
+    if (inputLength === ZERO) {
+      return [];
+    }
+    return suggestions
+      .filter(lang => lang.name.toLowerCase().slice(ZERO, inputLength) === inputValue)
+      .slice(ZERO, FIVE);
   };
 
   const renderSuggestion = (suggestion: Suggestion): JSX.Element => {
diff --git a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
index 09a15505..d5428a92 100644
--- a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
+++ b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
@@ -20,6 +20,7 @@ import { SEARCH_STATE_INITIAL_MOCK } from '@/redux/search/search.mock';
 import { ZOD_SEED } from '@/constants';
 import { createFixture } from 'zod-fixture';
 import { overviewImageView } from '@/models/overviewImageView';
+import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants';
 import { TopBar } from './TopBar.component';
 
 const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
@@ -117,6 +118,7 @@ describe('TopBar - component', () => {
       user: USER_INITIAL_STATE_MOCK,
       map: initialMapStateFixture,
       search: SEARCH_STATE_INITIAL_MOCK,
+      autocomplete: AUTOCOMPLETE_INITIAL_STATE,
       backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
     });
 
@@ -133,6 +135,7 @@ describe('TopBar - component', () => {
     renderComponentWithActionListener({
       user: USER_INITIAL_STATE_MOCK,
       search: SEARCH_STATE_INITIAL_MOCK,
+      autocomplete: AUTOCOMPLETE_INITIAL_STATE,
       drawer: initialStateFixture,
       project: {
         ...PROJECT_STATE_INITIAL_MOCK,
diff --git a/src/constants/common.ts b/src/constants/common.ts
index 9bdcd648..3480fb27 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -12,6 +12,8 @@ export const SECOND_ARRAY_ELEMENT = 1;
 
 export const TWO = 2;
 
+export const FIVE = 5;
+
 export const THIRD_ARRAY_ELEMENT = 2;
 
 export const NOOP = (): void => {};
diff --git a/src/models/autocompleteSchema.ts b/src/models/autocompleteSchema.ts
new file mode 100644
index 00000000..1f8e1095
--- /dev/null
+++ b/src/models/autocompleteSchema.ts
@@ -0,0 +1,3 @@
+import { z } from 'zod';
+
+export const autocompleteSchema = z.array(z.string());
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 29aa4174..b8002874 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -109,4 +109,7 @@ export const apiPath = {
   getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`,
   addComment: (modelId: number, x: number, y: number): string =>
     `projects/${PROJECT_ID}/comments/models/${modelId}/points/${x},${y}/`,
+
+  getSearchAutocomplete: (): string =>
+    `projects/${PROJECT_ID}/models/*/bioEntities/suggestedQueryList`,
 };
diff --git a/src/redux/autocomplete/autocomplete.constants.ts b/src/redux/autocomplete/autocomplete.constants.ts
new file mode 100644
index 00000000..486a494b
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.constants.ts
@@ -0,0 +1,6 @@
+import { AutocompleteState } from './autocomplete.types';
+
+export const AUTOCOMPLETE_INITIAL_STATE: AutocompleteState = {
+  searchValues: [''],
+  loading: 'idle',
+};
diff --git a/src/redux/autocomplete/autocomplete.reducers.ts b/src/redux/autocomplete/autocomplete.reducers.ts
new file mode 100644
index 00000000..8fb6ce0e
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.reducers.ts
@@ -0,0 +1,19 @@
+import { AutocompleteState } from '@/redux/autocomplete/autocomplete.types';
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { getSearchAutocomplete } from '@/redux/autocomplete/autocomplete.thunks';
+
+export const getSearchAutocompleteReducer = (
+  builder: ActionReducerMapBuilder<AutocompleteState>,
+): void => {
+  builder.addCase(getSearchAutocomplete.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(getSearchAutocomplete.fulfilled, (state, action) => {
+    state.searchValues = action.payload ? action.payload : [];
+    state.loading = 'succeeded';
+  });
+  builder.addCase(getSearchAutocomplete.rejected, state => {
+    state.loading = 'failed';
+    // TODO: error management to be discussed in the team
+  });
+};
diff --git a/src/redux/autocomplete/autocomplete.selectors.ts b/src/redux/autocomplete/autocomplete.selectors.ts
new file mode 100644
index 00000000..6d6b97bc
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.selectors.ts
@@ -0,0 +1,4 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const autocompleteSelector = createSelector(rootSelector, state => state.autocomplete);
diff --git a/src/redux/autocomplete/autocomplete.slice.ts b/src/redux/autocomplete/autocomplete.slice.ts
new file mode 100644
index 00000000..4cb0f5b1
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.slice.ts
@@ -0,0 +1,14 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants';
+import { getSearchAutocompleteReducer } from '@/redux/autocomplete/autocomplete.reducers';
+
+export const autocompleteSlice = createSlice({
+  name: 'autocomplete',
+  initialState: AUTOCOMPLETE_INITIAL_STATE,
+  reducers: {},
+  extraReducers(builder) {
+    getSearchAutocompleteReducer(builder);
+  },
+});
+
+export default autocompleteSlice.reducer;
diff --git a/src/redux/autocomplete/autocomplete.thunks.ts b/src/redux/autocomplete/autocomplete.thunks.ts
new file mode 100644
index 00000000..6397ea42
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.thunks.ts
@@ -0,0 +1,28 @@
+import { ThunkConfig } from '@/types/store';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { getError } from '@/utils/error-report/getError';
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { apiPath } from '@/redux/apiPath';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { autocompleteSchema } from '@/models/autocompleteSchema';
+import type { RootState } from '../store';
+
+export const getSearchAutocomplete = createAsyncThunk<
+  string[] | undefined,
+  void,
+  { state: RootState } & ThunkConfig
+>(
+  'project/getSearchAutocomplete',
+  // eslint-disable-next-line consistent-return
+  async () => {
+    try {
+      const response = await axiosInstance.get<string[]>(apiPath.getSearchAutocomplete());
+
+      const isDataValid = validateDataUsingZodSchema(response.data, autocompleteSchema);
+
+      return isDataValid ? response.data : undefined;
+    } catch (error) {
+      return Promise.reject(getError({ error }));
+    }
+  },
+);
diff --git a/src/redux/autocomplete/autocomplete.types.ts b/src/redux/autocomplete/autocomplete.types.ts
new file mode 100644
index 00000000..4d359029
--- /dev/null
+++ b/src/redux/autocomplete/autocomplete.types.ts
@@ -0,0 +1,6 @@
+import { Loading } from '@/types/loadingState';
+
+export interface AutocompleteState {
+  searchValues: string[];
+  loading: Loading;
+}
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index 18dabadf..476e5b40 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -7,6 +7,7 @@ import { PluginsManager } from '@/services/pluginsManager';
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { ZERO } from '@/constants/common';
 import { getConstant } from '@/redux/constant/constant.thunks';
+import { getSearchAutocomplete } from '@/redux/autocomplete/autocomplete.thunks';
 import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
 import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
 import {
@@ -86,6 +87,9 @@ export const fetchInitialAppData = createAsyncThunk<
   // Fetch plugins list
   dispatch(getAllPlugins());
 
+  // autocomplete
+  dispatch(getSearchAutocomplete());
+
   /** Trigger search */
   if (queryData.searchValue) {
     dispatch(setPerfectMatch(queryData.perfectMatch));
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index 421151bd..3c9189d5 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -2,6 +2,7 @@ import { CONSTANT_INITIAL_STATE } from '@/redux/constant/constant.adapter';
 import { PROJECTS_STATE_INITIAL_MOCK } from '@/redux/projects/projects.mock';
 import { OAUTH_INITIAL_STATE_MOCK } from '@/redux/oauth/oauth.mock';
 import { COMMENT_INITIAL_STATE_MOCK } from '@/redux/comment/comment.mock';
+import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants';
 import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock';
 import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock';
 import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock';
@@ -30,6 +31,7 @@ import { RootState } from '../store';
 import { USER_INITIAL_STATE_MOCK } from '../user/user.mock';
 
 export const INITIAL_STORE_STATE_MOCK: RootState = {
+  autocomplete: AUTOCOMPLETE_INITIAL_STATE,
   search: SEARCH_STATE_INITIAL_MOCK,
   project: PROJECT_STATE_INITIAL_MOCK,
   projects: PROJECTS_STATE_INITIAL_MOCK,
diff --git a/src/redux/store.ts b/src/redux/store.ts
index ebc65642..fedbb126 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -18,6 +18,7 @@ import projectsReducer from '@/redux/projects/projects.slice';
 import reactionsReducer from '@/redux/reactions/reactions.slice';
 import searchReducer from '@/redux/search/search.slice';
 import userReducer from '@/redux/user/user.slice';
+import autocompleteReducer from '@/redux/autocomplete/autocomplete.slice';
 import {
   AnyAction,
   ListenerEffectAPI,
@@ -38,6 +39,7 @@ import publicationsReducer from './publications/publications.slice';
 import statisticsReducer from './statistics/statistics.slice';
 
 export const reducers = {
+  autocomplete: autocompleteReducer,
   search: searchReducer,
   project: projectReducer,
   projects: projectsReducer,
-- 
GitLab