From 855f9c29a23251a27eadc0dfa0b0b548c6a46ce5 Mon Sep 17 00:00:00 2001
From: mateuszmiko <dmastah92@gmail.com>
Date: Tue, 10 Oct 2023 15:55:26 +0200
Subject: [PATCH] feat: connect search with input field search triggered by
 lens click and enter (MIN-63)

---
 pages/redux-api-poc.tsx                       | 34 ------------
 .../NavBar/NavBar.component.test.tsx          | 22 ++++++--
 .../SearchBar/SearchBar.component.test.tsx    | 52 +++++++++++++++++--
 .../TopBar/SearchBar/SearchBar.component.tsx  | 49 +++++++++++++----
 .../TopBar/TopBar.component.test.tsx          | 21 +++++++-
 src/hooks/usePrevious.tsx                     | 11 ++++
 .../bioEntityContents.selectors.ts            | 12 +++++
 src/redux/chemicals/chemicals.selectors.ts    |  9 ++++
 src/redux/drugs/drugs.selectors.ts            |  6 +++
 src/redux/mirnas/mirnas.selectors.ts          |  6 +++
 src/redux/search/search.reducers.ts           | 17 ++++--
 src/redux/search/search.selectors.ts          | 17 ++++--
 src/redux/search/search.slice.ts              | 12 ++---
 src/redux/search/search.thunks.ts             | 17 ++++++
 src/redux/search/search.types.ts              |  3 ++
 src/redux/store.ts                            | 14 +++--
 src/utils/renderComponentWithProvider.tsx     |  6 ---
 tsconfig.json                                 |  1 -
 18 files changed, 233 insertions(+), 76 deletions(-)
 delete mode 100644 pages/redux-api-poc.tsx
 create mode 100644 src/hooks/usePrevious.tsx
 create mode 100644 src/redux/bioEntityContents/bioEntityContents.selectors.ts
 create mode 100644 src/redux/chemicals/chemicals.selectors.ts
 create mode 100644 src/redux/drugs/drugs.selectors.ts
 create mode 100644 src/redux/mirnas/mirnas.selectors.ts
 create mode 100644 src/redux/search/search.thunks.ts
 delete mode 100644 src/utils/renderComponentWithProvider.tsx

diff --git a/pages/redux-api-poc.tsx b/pages/redux-api-poc.tsx
deleted file mode 100644
index f6add11e..00000000
--- a/pages/redux-api-poc.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { getBioEntityContents } from '@/redux/bioEntityContents/bioEntityContents.thunks';
-import { getChemicals } from '@/redux/chemicals/chemicals.thunks';
-import { getDrugs } from '@/redux/drugs/drugs.thunks';
-import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
-import { getMirnas } from '@/redux/mirnas/mirnas.thunks';
-import { selectSearchValue } from '@/redux/search/search.selectors';
-import { setSearchValue } from '@/redux/search/search.slice';
-import { useSelector } from 'react-redux';
-
-const ReduxPage = (): JSX.Element => {
-  const dispatch = useAppDispatch();
-  const searchValue = useSelector(selectSearchValue);
-
-  const triggerSyncUpdate = (): void => {
-    // eslint-disable-next-line prefer-template
-    const newValue = searchValue + 'test';
-    dispatch(setSearchValue(newValue));
-    dispatch(getDrugs('aspirin'));
-    dispatch(getMirnas('hsa-miR-302b-3p'));
-    dispatch(getBioEntityContents('park7'));
-    dispatch(getChemicals('Corticosterone'));
-  };
-
-  return (
-    <div>
-      {searchValue}
-      <button type="button" onClick={triggerSyncUpdate}>
-        sync update
-      </button>
-    </div>
-  );
-};
-
-export default ReduxPage;
diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
index 8b38fda6..c5bff4c3 100644
--- a/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
+++ b/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
@@ -1,8 +1,24 @@
-import { RenderResult, screen } from '@testing-library/react';
-import { renderComponentWithProvider } from '@/utils/renderComponentWithProvider';
+import drawerReducer from '@/redux/drawer/drawer.slice';
+import type { DrawerState } from '@/redux/drawer/drawer.types';
+import { ToolkitStoreWithSingleSlice } from '@/utils/createStoreInstanceUsingSliceReducer';
+import { getReduxWrapperUsingSliceReducer } from '@/utils/testing/getReduxWrapperUsingSliceReducer';
+import { render, screen } from '@testing-library/react';
 import { NavBar } from './NavBar.component';
 
-const renderComponent = (): RenderResult => renderComponentWithProvider(<NavBar />);
+const renderComponent = (): { store: ToolkitStoreWithSingleSlice<DrawerState> } => {
+  const { Wrapper, store } = getReduxWrapperUsingSliceReducer('drawer', drawerReducer);
+
+  return (
+    render(
+      <Wrapper>
+        <NavBar />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
 
 describe('NavBar - component', () => {
   it('Should contain navigation buttons and logos with powered by info', () => {
diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx
index f3db33ec..f5ac55b0 100644
--- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx
+++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx
@@ -1,15 +1,59 @@
-import { screen, render, RenderResult, fireEvent } from '@testing-library/react';
+import searchReducer from '@/redux/search/search.slice';
+import type { SearchState } from '@/redux/search/search.types';
+import { ToolkitStoreWithSingleSlice } from '@/utils/createStoreInstanceUsingSliceReducer';
+import { getReduxWrapperUsingSliceReducer } from '@/utils/testing/getReduxWrapperUsingSliceReducer';
+import { fireEvent, render, screen } from '@testing-library/react';
 import { SearchBar } from './SearchBar.component';
 
-const renderComponent = (): RenderResult => render(<SearchBar />);
+const renderComponent = (): { store: ToolkitStoreWithSingleSlice<SearchState> } => {
+  const { Wrapper, store } = getReduxWrapperUsingSliceReducer('search', searchReducer);
+
+  return (
+    render(
+      <Wrapper>
+        <SearchBar />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
 
 describe('SearchBar - component', () => {
   it('should let user type text', () => {
     renderComponent();
 
-    const input = screen.getByTestId('search-input');
+    const input = screen.getByTestId<HTMLInputElement>('search-input');
     fireEvent.change(input, { target: { value: 'test value' } });
 
-    expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
+    expect(input.value).toBe('test value');
+  });
+
+  it('should disable button when the user clicks the lens button', () => {
+    renderComponent();
+
+    const input = screen.getByTestId<HTMLInputElement>('search-input');
+    fireEvent.change(input, { target: { value: 'park7' } });
+
+    expect(input.value).toBe('park7');
+
+    const button = screen.getByRole('button');
+    fireEvent.click(button);
+
+    expect(button).toBeDisabled();
+  });
+
+  it('should disable input when the user clicks the Enter', () => {
+    renderComponent();
+
+    const input = screen.getByTestId<HTMLInputElement>('search-input');
+    fireEvent.change(input, { target: { value: 'park7' } });
+
+    expect(input.value).toBe('park7');
+
+    fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+    expect(input).toBeDisabled();
   });
 });
diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
index 0c7cdd15..1932b5a2 100644
--- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
+++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx
@@ -1,12 +1,32 @@
-import Image from 'next/image';
-import { ChangeEvent, useState } from 'react';
 import lensIcon from '@/assets/vectors/icons/lens.svg';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import {
+  isPendingSearchStatusSelector,
+  searchValueSelector,
+} from '@/redux/search/search.selectors';
+import { getSearchData } from '@/redux/search/search.thunks';
+import Image from 'next/image';
+import { ChangeEvent, KeyboardEvent, useState } from 'react';
+import { useSelector } from 'react-redux';
+
+const ENTER_KEY_CODE = 'Enter';
 
 export const SearchBar = (): JSX.Element => {
   const [searchValue, setSearchValue] = useState<string>('');
+  const dispatch = useAppDispatch();
+  const isPendingSearchStatus = useSelector(isPendingSearchStatusSelector);
+  const prevSearchValue = useSelector(searchValueSelector);
+
+  const isSameSearchValue = prevSearchValue === searchValue;
 
-  const onSearchChange = (event: ChangeEvent<HTMLInputElement>): void => {
+  const onSearchChange = (event: ChangeEvent<HTMLInputElement>): void =>
     setSearchValue(event.target.value);
+
+  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+  const onSearchClick = () => !isSameSearchValue && dispatch(getSearchData(searchValue));
+
+  const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>): void => {
+    if (!isSameSearchValue && event.code === ENTER_KEY_CODE) dispatch(getSearchData(searchValue));
   };
 
   return (
@@ -16,16 +36,25 @@ export const SearchBar = (): JSX.Element => {
         name="search-input"
         aria-label="search-input"
         data-testid="search-input"
+        onKeyDown={handleKeyPress}
         onChange={onSearchChange}
+        disabled={isPendingSearchStatus}
         className="h-9 w-72 rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none  hover:border-greyscale-600 focus:border-greyscale-600"
       />
-      <Image
-        src={lensIcon}
-        alt="lens icon"
-        height={16}
-        width={16}
-        className="absolute right-4 top-2.5"
-      />
+      <button
+        disabled={isPendingSearchStatus}
+        type="button"
+        className="bg-transparent"
+        onClick={onSearchClick}
+      >
+        <Image
+          src={lensIcon}
+          alt="lens icon"
+          height={16}
+          width={16}
+          className="absolute right-4 top-2.5"
+        />
+      </button>
     </div>
   );
 };
diff --git a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
index a611ffd3..6ff1d849 100644
--- a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
+++ b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx
@@ -1,7 +1,24 @@
-import { screen, render, RenderResult } from '@testing-library/react';
+import searchReducer from '@/redux/search/search.slice';
+import type { SearchState } from '@/redux/search/search.types';
+import { ToolkitStoreWithSingleSlice } from '@/utils/createStoreInstanceUsingSliceReducer';
+import { getReduxWrapperUsingSliceReducer } from '@/utils/testing/getReduxWrapperUsingSliceReducer';
+import { render, screen } from '@testing-library/react';
 import { TopBar } from './TopBar.component';
 
-const renderComponent = (): RenderResult => render(<TopBar />);
+const renderComponent = (): { store: ToolkitStoreWithSingleSlice<SearchState> } => {
+  const { Wrapper, store } = getReduxWrapperUsingSliceReducer('search', searchReducer);
+
+  return (
+    render(
+      <Wrapper>
+        <TopBar />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
 
 describe('TopBar - component', () => {
   it('Should contain user avatar, search bar', () => {
diff --git a/src/hooks/usePrevious.tsx b/src/hooks/usePrevious.tsx
new file mode 100644
index 00000000..a5233144
--- /dev/null
+++ b/src/hooks/usePrevious.tsx
@@ -0,0 +1,11 @@
+import { useEffect, useRef } from 'react';
+
+export default function usePrevious<T>(state: T): T | undefined {
+  const ref = useRef<T>();
+
+  useEffect(() => {
+    ref.current = state;
+  });
+
+  return ref.current;
+}
diff --git a/src/redux/bioEntityContents/bioEntityContents.selectors.ts b/src/redux/bioEntityContents/bioEntityContents.selectors.ts
new file mode 100644
index 00000000..80a88cb0
--- /dev/null
+++ b/src/redux/bioEntityContents/bioEntityContents.selectors.ts
@@ -0,0 +1,12 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const bioEntityContentsSelector = createSelector(
+  rootSelector,
+  state => state.bioEntityContents,
+);
+
+export const loadingBioEntityStatusSelector = createSelector(
+  bioEntityContentsSelector,
+  state => state.loading,
+);
diff --git a/src/redux/chemicals/chemicals.selectors.ts b/src/redux/chemicals/chemicals.selectors.ts
new file mode 100644
index 00000000..b6c7cb1c
--- /dev/null
+++ b/src/redux/chemicals/chemicals.selectors.ts
@@ -0,0 +1,9 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const chemicalsSelector = createSelector(rootSelector, state => state.chemicals);
+
+export const loadingChemicalsStatusSelector = createSelector(
+  chemicalsSelector,
+  state => state.loading,
+);
diff --git a/src/redux/drugs/drugs.selectors.ts b/src/redux/drugs/drugs.selectors.ts
new file mode 100644
index 00000000..b67a8cb0
--- /dev/null
+++ b/src/redux/drugs/drugs.selectors.ts
@@ -0,0 +1,6 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const drugsSelector = createSelector(rootSelector, state => state.drugs);
+
+export const loadingDrugsStatusSelector = createSelector(drugsSelector, state => state.loading);
diff --git a/src/redux/mirnas/mirnas.selectors.ts b/src/redux/mirnas/mirnas.selectors.ts
new file mode 100644
index 00000000..5344f037
--- /dev/null
+++ b/src/redux/mirnas/mirnas.selectors.ts
@@ -0,0 +1,6 @@
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const mirnasSelector = createSelector(rootSelector, state => state.mirnas);
+
+export const loadingMirnasStatusSelector = createSelector(mirnasSelector, state => state.loading);
diff --git a/src/redux/search/search.reducers.ts b/src/redux/search/search.reducers.ts
index 4f6f747d..28226ca9 100644
--- a/src/redux/search/search.reducers.ts
+++ b/src/redux/search/search.reducers.ts
@@ -1,7 +1,18 @@
 // updating state
+import { getSearchData } from '@/redux/search/search.thunks';
 import { SearchState } from '@/redux/search/search.types';
-import { PayloadAction } from '@reduxjs/toolkit';
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
 
-export const setSearchValueReducer = (state: SearchState, action: PayloadAction<string>): void => {
-  state.searchValue = action.payload;
+export const getSearchDataReducer = (builder: ActionReducerMapBuilder<SearchState>): void => {
+  builder.addCase(getSearchData.pending, (state, action) => {
+    state.searchValue = action.meta.arg;
+    state.loading = 'pending';
+  });
+  builder.addCase(getSearchData.fulfilled, state => {
+    state.loading = 'succeeded';
+  });
+  builder.addCase(getSearchData.rejected, state => {
+    state.loading = 'failed';
+    // TODO: error management to be discussed in the team
+  });
 };
diff --git a/src/redux/search/search.selectors.ts b/src/redux/search/search.selectors.ts
index c845eecd..143488fe 100644
--- a/src/redux/search/search.selectors.ts
+++ b/src/redux/search/search.selectors.ts
@@ -1,4 +1,15 @@
-import type { RootState } from '@/redux/store';
+import { rootSelector } from '@/redux/root/root.selectors';
+import { createSelector } from '@reduxjs/toolkit';
 
-// THIS IS EXAMPLE, it's not memoised!!!! Check redux-tookit docs.
-export const selectSearchValue = (state: RootState): string => state.search.searchValue;
+const PENDING_STATUS = 'pending';
+
+export const searchSelector = createSelector(rootSelector, state => state.search);
+
+export const searchValueSelector = createSelector(searchSelector, state => state.searchValue);
+
+export const loadingSearchStatusSelector = createSelector(searchSelector, state => state.loading);
+
+export const isPendingSearchStatusSelector = createSelector(
+  loadingSearchStatusSelector,
+  state => state === PENDING_STATUS,
+);
diff --git a/src/redux/search/search.slice.ts b/src/redux/search/search.slice.ts
index 92357f8c..73930e33 100644
--- a/src/redux/search/search.slice.ts
+++ b/src/redux/search/search.slice.ts
@@ -1,6 +1,6 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { getSearchDataReducer } from '@/redux/search/search.reducers';
 import { SearchState } from '@/redux/search/search.types';
-import { setSearchValueReducer } from '@/redux/search/search.reducers';
+import { createSlice } from '@reduxjs/toolkit';
 
 const initialState: SearchState = {
   searchValue: '',
@@ -8,16 +8,16 @@ const initialState: SearchState = {
     content: '',
     drugs: '',
   },
+  loading: 'idle',
 };
 
 export const searchSlice = createSlice({
   name: 'search',
   initialState,
-  reducers: {
-    setSearchValue: setSearchValueReducer,
+  reducers: {},
+  extraReducers(builder) {
+    getSearchDataReducer(builder);
   },
 });
 
-export const { setSearchValue } = searchSlice.actions;
-
 export default searchSlice.reducer;
diff --git a/src/redux/search/search.thunks.ts b/src/redux/search/search.thunks.ts
new file mode 100644
index 00000000..2724826c
--- /dev/null
+++ b/src/redux/search/search.thunks.ts
@@ -0,0 +1,17 @@
+import { getBioEntityContents } from '@/redux/bioEntityContents/bioEntityContents.thunks';
+import { getChemicals } from '@/redux/chemicals/chemicals.thunks';
+import { getDrugs } from '@/redux/drugs/drugs.thunks';
+import { getMirnas } from '@/redux/mirnas/mirnas.thunks';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+export const getSearchData = createAsyncThunk(
+  'project/getSearchData',
+  async (searchQuery: string, { dispatch }): Promise<void> => {
+    await Promise.all([
+      dispatch(getDrugs(searchQuery)),
+      dispatch(getBioEntityContents(searchQuery)),
+      dispatch(getChemicals(searchQuery)),
+      dispatch(getMirnas(searchQuery)),
+    ]);
+  },
+);
diff --git a/src/redux/search/search.types.ts b/src/redux/search/search.types.ts
index 6b6316a8..1faf1b72 100644
--- a/src/redux/search/search.types.ts
+++ b/src/redux/search/search.types.ts
@@ -1,3 +1,5 @@
+import { Loading } from '@/types/loadingState';
+
 export interface SearchResult {
   content: string;
   drugs: string;
@@ -6,4 +8,5 @@ export interface SearchResult {
 export interface SearchState {
   searchValue: string;
   searchResult: SearchResult;
+  loading: Loading;
 }
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 081acb38..a095912b 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,14 +1,20 @@
-import { configureStore } from '@reduxjs/toolkit';
-import searchReducer from '@/redux/search/search.slice';
-import projectSlice from '@/redux/project/project.slice';
-import drugsReducer from '@/redux/drugs/drugs.slice';
+import bioEntityContentsReducer from '@/redux/bioEntityContents/bioEntityContents.slice';
+import chemicalsReducer from '@/redux/chemicals/chemicals.slice';
 import drawerReducer from '@/redux/drawer/drawer.slice';
+import drugsReducer from '@/redux/drugs/drugs.slice';
+import mirnasReducer from '@/redux/mirnas/mirnas.slice';
+import projectSlice from '@/redux/project/project.slice';
+import searchReducer from '@/redux/search/search.slice';
+import { configureStore } from '@reduxjs/toolkit';
 
 export const store = configureStore({
   reducer: {
     search: searchReducer,
     project: projectSlice,
     drugs: drugsReducer,
+    mirnas: mirnasReducer,
+    chemicals: chemicalsReducer,
+    bioEntityContents: bioEntityContentsReducer,
     drawer: drawerReducer,
   },
   devTools: true,
diff --git a/src/utils/renderComponentWithProvider.tsx b/src/utils/renderComponentWithProvider.tsx
deleted file mode 100644
index c62bc5d9..00000000
--- a/src/utils/renderComponentWithProvider.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { RenderResult, render } from '@testing-library/react';
-import { AppWrapper } from '@/components/AppWrapper';
-import type { ReactNode } from 'react';
-
-export const renderComponentWithProvider = (children: ReactNode): RenderResult =>
-  render(<AppWrapper>{children}</AppWrapper>);
diff --git a/tsconfig.json b/tsconfig.json
index 338c7309..a44f42a5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,7 +29,6 @@
     "**/*.tsx",
     ".next/types/**/*.ts",
     "pages",
-    "@types/images.d.ts",
     "jest.config.ts",
     "setupTests.ts"
   ],
-- 
GitLab