Skip to content
Snippets Groups Projects
Commit 8f6bfc26 authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

Merge branch 'feature/MIN-96-open-drawer-after-search' into 'development'

Resolve MIN-96 "Feature/ open drawer after search"

Closes MIN-96

See merge request !30
parents 62cac953 8c466924
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...,!30Resolve MIN-96 "Feature/ open drawer after search"
Pipeline #79677 failed
Showing
with 164 additions and 88 deletions
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 { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { fireEvent, render, screen } from '@testing-library/react';
import { SearchBar } from './SearchBar.component';
const renderComponent = (): { store: ToolkitStoreWithSingleSlice<SearchState> } => {
const { Wrapper, store } = getReduxWrapperUsingSliceReducer('search', searchReducer);
const renderComponent = (): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore();
return (
render(
......@@ -23,8 +21,8 @@ const renderComponent = (): { store: ToolkitStoreWithSingleSlice<SearchState> }
describe('SearchBar - component', () => {
it('should let user type text', () => {
renderComponent();
const input = screen.getByTestId<HTMLInputElement>('search-input');
fireEvent.change(input, { target: { value: 'test value' } });
expect(input.value).toBe('test value');
......@@ -32,13 +30,12 @@ describe('SearchBar - component', () => {
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');
fireEvent.change(input, { target: { value: 'park7' } });
const button = screen.getByRole('button');
fireEvent.click(button);
expect(button).toBeDisabled();
......@@ -46,12 +43,9 @@ describe('SearchBar - component', () => {
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.change(input, { target: { value: 'park7' } });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });
expect(input).toBeDisabled();
......
import lensIcon from '@/assets/vectors/icons/lens.svg';
import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors';
import { openDrawer } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import {
isPendingSearchStatusSelector,
......@@ -16,17 +18,31 @@ export const SearchBar = (): JSX.Element => {
const dispatch = useAppDispatch();
const isPendingSearchStatus = useSelector(isPendingSearchStatusSelector);
const prevSearchValue = useSelector(searchValueSelector);
const isDrawerOpen = useSelector(isDrawerOpenSelector);
const isSameSearchValue = prevSearchValue === searchValue;
const openSearchDrawerIfClosed = (): void => {
if (!isDrawerOpen) {
dispatch(openDrawer('search'));
}
};
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 onSearchClick = (): void => {
if (!isSameSearchValue) {
dispatch(getSearchData(searchValue));
openSearchDrawerIfClosed();
}
};
const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>): void => {
if (!isSameSearchValue && event.code === ENTER_KEY_CODE) dispatch(getSearchData(searchValue));
if (!isSameSearchValue && event.code === ENTER_KEY_CODE) {
dispatch(getSearchData(searchValue));
openSearchDrawerIfClosed();
}
};
return (
......
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 { StoreType } from '@/redux/store';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { TopBar } from './TopBar.component';
const renderComponent = (): { store: ToolkitStoreWithSingleSlice<SearchState> } => {
const { Wrapper, store } = getReduxWrapperUsingSliceReducer('search', searchReducer);
const renderComponent = (): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore();
return (
render(
......
import { ToolkitStoreWithSingleSlice } from '@/utils/createStoreInstanceUsingSliceReducer';
import { DrawerState } from '@/redux/drawer/drawer.types';
import { getReduxWrapperUsingSliceReducer } from '@/utils/testing/getReduxWrapperUsingSliceReducer';
import { screen, render, act, fireEvent } from '@testing-library/react';
import drawerReducer, { openDrawer } from '@/redux/drawer/drawer.slice';
import { openDrawer } from '@/redux/drawer/drawer.slice';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { Drawer } from './Drawer.component';
const renderComponent = (): { store: ToolkitStoreWithSingleSlice<DrawerState> } => {
const { Wrapper, store } = getReduxWrapperUsingSliceReducer('drawer', drawerReducer);
const renderComponent = (): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore();
return (
render(
<Wrapper>
......
import dynamic from 'next/dynamic';
import { twMerge } from 'tailwind-merge';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { drawerDataSelector } from '@/redux/drawer/drawer.selectors';
import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants';
import { drawerSelector } from '@/redux/drawer/drawer.selectors';
const SearchDrawerContent = dynamic(
async () =>
......@@ -15,17 +15,17 @@ const SearchDrawerContent = dynamic(
);
export const Drawer = (): JSX.Element => {
const { open, drawerName } = useAppSelector(drawerDataSelector);
const { isOpen, drawerName } = useAppSelector(drawerSelector);
return (
<div
className={twMerge(
'absolute left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform bg-white-pearl text-font-500 transition-all duration-500',
open && 'translate-x-0',
isOpen && 'translate-x-0',
)}
role={DRAWER_ROLE}
>
{open && drawerName === 'search' && <SearchDrawerContent />}
{isOpen && drawerName === 'search' && <SearchDrawerContent />}
{/* other drawers comes here, should use dynamic import */}
</div>
);
......
import {
Accordion,
AccordionItem,
AccordionItemButton,
AccordionItemPanel,
......@@ -11,30 +10,28 @@ import { BioEntitiesSubmapItem } from '@/components/Map/Drawer/SearchDrawerConte
export const BioEntitiesAccordion = (): JSX.Element => {
const entity = { mapName: 'main map', numberOfEntities: 21 };
return (
<Accordion allowZeroExpanded>
<AccordionItem>
<AccordionItemHeading>
<AccordionItemButton>Content (2137)</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel className="">
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
</AccordionItemPanel>
</AccordionItem>
</Accordion>
<AccordionItem>
<AccordionItemHeading>
<AccordionItemButton>Content (2137)</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel className="">
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
<BioEntitiesSubmapItem
mapName={entity.mapName}
numberOfEntities={entity.numberOfEntities}
/>
</AccordionItemPanel>
</AccordionItem>
);
};
import { render, screen } from '@testing-library/react';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { drugsFixture } from '@/models/fixtures/drugFixtures';
import { Accordion } from '@/shared/Accordion';
import { DrugsAccordion } from './DrugsAccordion.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<Accordion>
<DrugsAccordion />
</Accordion>
</Wrapper>,
),
{
store,
}
);
};
describe('DrugsAccordion - component', () => {
it('should display drugs number after succesfull drug search', () => {
renderComponent({
drugs: { data: drugsFixture, loading: 'succeeded', error: { name: '', message: '' } },
});
expect(screen.getByText('Drugs (2)')).toBeInTheDocument();
});
it('should display loading indicator while waiting for drug search response', () => {
renderComponent({
drugs: { data: [], loading: 'pending', error: { name: '', message: '' } },
});
expect(screen.getByText('Drugs (Loading...)')).toBeInTheDocument();
});
});
import { loadingDrugsStatusSelector, numberOfDrugsSelector } from '@/redux/drugs/drugs.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { AccordionItem, AccordionItemHeading, AccordionItemButton } from '@/shared/Accordion';
export const DrugsAccordion = (): JSX.Element => {
const drugsNumber = useAppSelector(numberOfDrugsSelector);
const drugsState = useAppSelector(loadingDrugsStatusSelector);
return (
<AccordionItem>
<AccordionItemHeading>
<AccordionItemButton variant="non-expandable">
Drugs
{drugsState === 'pending' && ' (Loading...)'}
{drugsState === 'succeeded' && ` (${drugsNumber})`}
</AccordionItemButton>
</AccordionItemHeading>
</AccordionItem>
);
};
export { DrugsAccordion } from './DrugsAccordion.component';
import { BioEntitiesAccordion } from '@/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion';
import { DrugsAccordion } from '@/components/Map/Drawer/SearchDrawerContent/DrugsAccordion';
import { closeDrawer } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { IconButton } from '@/shared/IconButton';
import { Accordion } from '@/shared/Accordion';
export const CLOSE_BUTTON_ROLE = 'close-drawer-button';
......@@ -28,7 +30,10 @@ export const SearchDrawerContent = (): JSX.Element => {
/>
</div>
<div className="px-6">
<BioEntitiesAccordion />
<Accordion allowZeroExpanded>
<BioEntitiesAccordion />
<DrugsAccordion />
</Accordion>
</div>
</div>
);
......
......@@ -5,7 +5,7 @@ import drawerReducer, { openDrawer, closeDrawer } from './drawer.slice';
import type { DrawerState } from './drawer.types';
const INITIAL_STATE: DrawerState = {
open: false,
isOpen: false,
drawerName: 'none',
};
......@@ -38,19 +38,19 @@ describe('drawer reducer', () => {
it('should update the store when you click a project info button on the nav bar', async () => {
const { type } = await store.dispatch(openDrawer('project-info'));
const { open, drawerName } = store.getState().drawer;
const { isOpen, drawerName } = store.getState().drawer;
expect(type).toBe('drawer/openDrawer');
expect(open).toBe(true);
expect(isOpen).toBe(true);
expect(drawerName).toEqual('project-info');
});
it('should update the store when you click the close button on the drawer', async () => {
const { type } = await store.dispatch(closeDrawer());
const { open, drawerName } = store.getState().drawer;
const { isOpen, drawerName } = store.getState().drawer;
expect(type).toBe('drawer/closeDrawer');
expect(open).toBe(false);
expect(isOpen).toBe(false);
expect(drawerName).toEqual('none');
});
......
......@@ -3,10 +3,10 @@ import { DrawerState } from '@/redux/drawer/drawer.types';
import { PathName } from '@/types/pathName';
export const openDrawerReducer = (state: DrawerState, action: PayloadAction<PathName>): void => {
state.open = true;
state.isOpen = true;
state.drawerName = action.payload;
};
export const closeDrawerReducer = (state: DrawerState): void => {
state.open = false;
state.isOpen = false;
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '@/redux/root/root.selectors';
export const drawerDataSelector = createSelector(rootSelector, state => state.drawer);
export const drawerSelector = createSelector(rootSelector, state => state.drawer);
export const isDrawerOpenSelector = createSelector(drawerSelector, state => state.isOpen);
......@@ -3,7 +3,7 @@ import { DrawerState } from '@/redux/drawer/drawer.types';
import { openDrawerReducer, closeDrawerReducer } from './drawer.reducers';
const initialState: DrawerState = {
open: false,
isOpen: false,
drawerName: 'none',
};
......
export type DrawerState = {
open: boolean;
isOpen: boolean;
drawerName: 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend';
};
import { rootSelector } from '@/redux/root/root.selectors';
import { createSelector } from '@reduxjs/toolkit';
const SIZE_OF_EMPTY_ARRAY = 0;
export const drugsSelector = createSelector(rootSelector, state => state.drugs);
export const loadingDrugsStatusSelector = createSelector(drugsSelector, state => state.loading);
export const numberOfDrugsSelector = createSelector(drugsSelector, state =>
state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY,
);
......@@ -4,10 +4,6 @@ import { createSlice } from '@reduxjs/toolkit';
const initialState: SearchState = {
searchValue: '',
searchResult: {
content: '',
drugs: '',
},
loading: 'idle',
};
......
import { Loading } from '@/types/loadingState';
export interface SearchResult {
content: string;
drugs: string;
}
export interface SearchState {
searchValue: string;
searchResult: SearchResult;
loading: Loading;
}
......@@ -22,6 +22,7 @@ export const store = configureStore({
devTools: true,
});
export type StoreType = typeof store;
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
......
import { Icon } from '@/shared/Icon';
import { AccordionItemButton as AIB } from 'react-accessible-accordion';
import './AccordionItemButton.style.css';
import { Variant } from './AccordionItemButton.types';
import { getIcon } from './AccordionItemButton.utils';
interface AccordionItemButtonProps {
children: React.ReactNode;
variant?: Variant;
}
export const AccordionItemButton = ({ children }: AccordionItemButtonProps): JSX.Element => (
<AIB className="accordion-button flex flex-row flex-nowrap justify-between">
{children}
<Icon name="chevron-down" className="arrow-button h-6 w-6 fill-font-500" />
</AIB>
);
export const AccordionItemButton = ({
children,
variant = 'expandable',
}: AccordionItemButtonProps): JSX.Element => {
const ButtonIcon = getIcon(variant);
return (
<AIB className="accordion-button flex flex-row flex-nowrap justify-between">
{children}
{ButtonIcon}
</AIB>
);
};
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