Skip to content
Snippets Groups Projects
Commit 0544a36b authored by mateuszmiko's avatar mateuszmiko
Browse files

Merge branch...

Merge branch 'feature/MIN-63-Connect-search-with-input-field-search-triggered-by-lens-click-and-enter' into 'development'

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

See merge request !29
parents 66410e7a 855f9c29
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)
Pipeline #79609 passed
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 { 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', () => {
......
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();
});
});
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>
);
};
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', () => {
......
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
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
});
};
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,
);
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;
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 {
content: string;
drugs: string;
......@@ -6,4 +8,5 @@ export interface SearchResult {
export interface SearchState {
searchValue: string;
searchResult: SearchResult;
loading: Loading;
}
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,
......
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 @@
"**/*.tsx",
".next/types/**/*.ts",
"pages",
"@types/images.d.ts",
"jest.config.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