Skip to content
Snippets Groups Projects
Commit 855f9c29 authored by mateuszmiko's avatar mateuszmiko
Browse files

feat: connect search with input field search triggered by lens click and enter (MIN-63)

parent 66410e7a
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!29feat: connect search with input field search triggered by lens click and enter (MIN-63)
Showing
with 233 additions and 76 deletions
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;
import { RenderResult, screen } from '@testing-library/react'; import drawerReducer from '@/redux/drawer/drawer.slice';
import { renderComponentWithProvider } from '@/utils/renderComponentWithProvider'; 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'; 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', () => { describe('NavBar - component', () => {
it('Should contain navigation buttons and logos with powered by info', () => { it('Should contain navigation buttons and logos with powered by info', () => {
......
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'; 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', () => { describe('SearchBar - component', () => {
it('should let user type text', () => { it('should let user type text', () => {
renderComponent(); renderComponent();
const input = screen.getByTestId('search-input'); const input = screen.getByTestId<HTMLInputElement>('search-input');
fireEvent.change(input, { target: { value: 'test value' } }); 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();
}); });
}); });
import Image from 'next/image';
import { ChangeEvent, useState } from 'react';
import lensIcon from '@/assets/vectors/icons/lens.svg'; 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 => { export const SearchBar = (): JSX.Element => {
const [searchValue, setSearchValue] = useState<string>(''); 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); 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 ( return (
...@@ -16,16 +36,25 @@ export const SearchBar = (): JSX.Element => { ...@@ -16,16 +36,25 @@ export const SearchBar = (): JSX.Element => {
name="search-input" name="search-input"
aria-label="search-input" aria-label="search-input"
data-testid="search-input" data-testid="search-input"
onKeyDown={handleKeyPress}
onChange={onSearchChange} 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" 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 <button
src={lensIcon} disabled={isPendingSearchStatus}
alt="lens icon" type="button"
height={16} className="bg-transparent"
width={16} onClick={onSearchClick}
className="absolute right-4 top-2.5" >
/> <Image
src={lensIcon}
alt="lens icon"
height={16}
width={16}
className="absolute right-4 top-2.5"
/>
</button>
</div> </div>
); );
}; };
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'; 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', () => { describe('TopBar - component', () => {
it('Should contain user avatar, search bar', () => { it('Should contain user avatar, search bar', () => {
......
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;
}
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,
);
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,
);
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);
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);
// updating state // updating state
import { getSearchData } from '@/redux/search/search.thunks';
import { SearchState } from '@/redux/search/search.types'; 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 => { export const getSearchDataReducer = (builder: ActionReducerMapBuilder<SearchState>): void => {
state.searchValue = action.payload; 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
});
}; };
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. const PENDING_STATUS = 'pending';
export const selectSearchValue = (state: RootState): string => state.search.searchValue;
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,
);
import { createSlice } from '@reduxjs/toolkit'; import { getSearchDataReducer } from '@/redux/search/search.reducers';
import { SearchState } from '@/redux/search/search.types'; import { SearchState } from '@/redux/search/search.types';
import { setSearchValueReducer } from '@/redux/search/search.reducers'; import { createSlice } from '@reduxjs/toolkit';
const initialState: SearchState = { const initialState: SearchState = {
searchValue: '', searchValue: '',
...@@ -8,16 +8,16 @@ const initialState: SearchState = { ...@@ -8,16 +8,16 @@ const initialState: SearchState = {
content: '', content: '',
drugs: '', drugs: '',
}, },
loading: 'idle',
}; };
export const searchSlice = createSlice({ export const searchSlice = createSlice({
name: 'search', name: 'search',
initialState, initialState,
reducers: { reducers: {},
setSearchValue: setSearchValueReducer, extraReducers(builder) {
getSearchDataReducer(builder);
}, },
}); });
export const { setSearchValue } = searchSlice.actions;
export default searchSlice.reducer; export default searchSlice.reducer;
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)),
]);
},
);
import { Loading } from '@/types/loadingState';
export interface SearchResult { export interface SearchResult {
content: string; content: string;
drugs: string; drugs: string;
...@@ -6,4 +8,5 @@ export interface SearchResult { ...@@ -6,4 +8,5 @@ export interface SearchResult {
export interface SearchState { export interface SearchState {
searchValue: string; searchValue: string;
searchResult: SearchResult; searchResult: SearchResult;
loading: Loading;
} }
import { configureStore } from '@reduxjs/toolkit'; import bioEntityContentsReducer from '@/redux/bioEntityContents/bioEntityContents.slice';
import searchReducer from '@/redux/search/search.slice'; import chemicalsReducer from '@/redux/chemicals/chemicals.slice';
import projectSlice from '@/redux/project/project.slice';
import drugsReducer from '@/redux/drugs/drugs.slice';
import drawerReducer from '@/redux/drawer/drawer.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({ export const store = configureStore({
reducer: { reducer: {
search: searchReducer, search: searchReducer,
project: projectSlice, project: projectSlice,
drugs: drugsReducer, drugs: drugsReducer,
mirnas: mirnasReducer,
chemicals: chemicalsReducer,
bioEntityContents: bioEntityContentsReducer,
drawer: drawerReducer, drawer: drawerReducer,
}, },
devTools: true, devTools: true,
......
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>);
...@@ -29,7 +29,6 @@ ...@@ -29,7 +29,6 @@
"**/*.tsx", "**/*.tsx",
".next/types/**/*.ts", ".next/types/**/*.ts",
"pages", "pages",
"@types/images.d.ts",
"jest.config.ts", "jest.config.ts",
"setupTests.ts" "setupTests.ts"
], ],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment