diff --git a/Group.svg b/Group.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c9f4a0e0768cd405fe0060c439a9f15a9766563 --- /dev/null +++ b/Group.svg @@ -0,0 +1,4 @@ +<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9 0C4.575 0 0 3.375 0 9C0 14.325 8.1 22.65 8.475 23.025C8.625 23.175 8.775 23.25 9 23.25C9.225 23.25 9.375 23.175 9.525 23.025C9.9 22.65 18 14.4 18 9C18 3.375 13.425 0 9 0ZM9 12C7.35 12 6 10.65 6 9C6 7.35 7.35 6 9 6C10.65 6 12 7.35 12 9C12 10.65 10.65 12 9 12Z" fill="#E17221"/> +<circle cx="9.0002" cy="8.99922" r="4.8" fill="#E17221"/> +</svg> diff --git a/package-lock.json b/package-lock.json index 5226c2f4253dc71a1351e5b2b7a8d6735914dd5a..c3791abe6160a76664edb97750677ee2ddf8ea1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "next": "13.4.19", "ol": "^8.1.0", "postcss": "8.4.29", + "query-string": "7.1.3", "react": "18.2.0", "react-accessible-accordion": "^5.0.0", "react-dom": "18.2.0", @@ -60,6 +61,7 @@ "jest-junit": "^16.0.0", "jest-watch-typeahead": "^2.2.2", "lint-staged": "^14.0.1", + "next-router-mock": "^0.9.10", "prettier": "^3.0.3", "prettier-2": "npm:prettier@^2", "prettier-plugin-tailwindcss": "^0.5.6", @@ -4320,6 +4322,14 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5828,6 +5838,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/find-node-modules": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", @@ -9494,6 +9512,16 @@ } } }, + "node_modules/next-router-mock": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.10.tgz", + "integrity": "sha512-bK6sRb/xGNFgHVUZuvuApn6KJBAKTPiP36A7a9mO77U4xQO5ukJx9WHlU67Tv8AuySd09pk0+Hu8qMVIAmLO6A==", + "dev": true, + "peerDependencies": { + "next": ">=10.0.0", + "react": ">=17.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -10467,6 +10495,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11316,6 +11361,14 @@ "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", "dev": true }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -11397,6 +11450,14 @@ "node": ">=10.0.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index 2192fbdfc7e619cd1bd53c4d249a720130bcc24a..1a9bd09169fddc2c35feb6141a1548d4567e11f8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "next": "13.4.19", "ol": "^8.1.0", "postcss": "8.4.29", + "query-string": "7.1.3", "react": "18.2.0", "react-accessible-accordion": "^5.0.0", "react-dom": "18.2.0", @@ -74,6 +75,7 @@ "jest-junit": "^16.0.0", "jest-watch-typeahead": "^2.2.2", "lint-staged": "^14.0.1", + "next-router-mock": "^0.9.10", "prettier": "^3.0.3", "prettier-2": "npm:prettier@^2", "prettier-plugin-tailwindcss": "^0.5.6", diff --git a/pages/redux-api-poc.tsx b/pages/redux-api-poc.tsx deleted file mode 100644 index f6add11e9d67d6f2fa1d73b3f6f89ac7a1ed6da2..0000000000000000000000000000000000000000 --- 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/setupTests.ts b/setupTests.ts index 7b0828bfa80fb3c4504510349e22dc4cf5bc0a7b..c74f9623495588d60aa54799a64cc29f91d2d56b 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -1 +1,3 @@ import '@testing-library/jest-dom'; + +jest.mock('next/router', () => require('next-router-mock')); diff --git a/src/assets/vectors/icons/Group.svg b/src/assets/vectors/icons/Group.svg new file mode 100644 index 0000000000000000000000000000000000000000..6d6e49d3b0756b321ce2ee1e317b6452f6cafc59 --- /dev/null +++ b/src/assets/vectors/icons/Group.svg @@ -0,0 +1,5 @@ +<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9 0C4.575 0 0 3.375 0 9C0 14.325 8.1 22.65 8.475 23.025C8.625 23.175 8.775 23.25 9 23.25C9.225 23.25 9.375 23.175 9.525 23.025C9.9 22.65 18 14.4 18 9C18 3.375 13.425 0 9 0ZM9 12C7.35 12 6 10.65 6 9C6 7.35 7.35 6 9 6C10.65 6 12 7.35 12 9C12 10.65 10.65 12 9 12Z" fill="#E17221"/> +<circle cx="9.0002" cy="8.99922" r="4.8" fill="#E17221"/> +<path d="M8.846 15V7.344L7.22 8.322V7.332L8.846 6.36H9.74V15H8.846Z" fill="white"/> +</svg> diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx index 8b38fda6785369dffa380a4849599412e8387f09..c5bff4c3eade40bfea3f82240ff36741467ab2e9 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/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx index 14af66353e1e7f94c1b79dbf41bf5f4b6dc424bd..6783d04b7a443687caba8e69a88d753c5a865738 100644 --- a/src/components/FunctionalArea/NavBar/NavBar.component.tsx +++ b/src/components/FunctionalArea/NavBar/NavBar.component.tsx @@ -1,9 +1,9 @@ -import Image from 'next/image'; -import { IconButton } from '@/shared/IconButton'; import logoImg from '@/assets/images/logo.png'; import luxembourgLogoImg from '@/assets/images/luxembourg-logo.png'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { openDrawer } from '@/redux/drawer/drawer.slice'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { IconButton } from '@/shared/IconButton'; +import Image from 'next/image'; export const NavBar = (): JSX.Element => { const dispatch = useAppDispatch(); diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx index f3db33ecd32d76c2d7318382b391de148348b23c..e61a7403b87cc2c30538e1946f58e5456be9b530 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx @@ -1,15 +1,105 @@ -import { screen, render, RenderResult, fireEvent } from '@testing-library/react'; +import { StoreType } from '@/redux/store'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { fireEvent, render, screen } from '@testing-library/react'; +import mockedRouter from 'next-router-mock'; import { SearchBar } from './SearchBar.component'; -const renderComponent = (): RenderResult => render(<SearchBar />); +const renderComponent = (): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(); + + return ( + render( + <Wrapper> + <SearchBar /> + </Wrapper>, + ), + { + store, + } + ); +}; describe('SearchBar - component', () => { it('should let user type text', () => { renderComponent(); + const input = screen.getByTestId<HTMLInputElement>('search-input'); - const input = screen.getByTestId('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' } }); + + 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' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(input).toBeDisabled(); + }); + + it('should set parameters on the url when the user enters a value in the search bar and clicks Enter', () => { + renderComponent(); + + const input = screen.getByTestId<HTMLInputElement>('search-input'); + fireEvent.change(input, { target: { value: 'park7' } }); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(button).toBeDisabled(); + + expect(mockedRouter).toMatchObject({ + asPath: '/?search=park7', + pathname: '/', + query: { search: 'park7' }, + }); + }); + + it('should set parameters on the url when the user enters a value in the search bar and clicks lens button', () => { + renderComponent(); + + const input = screen.getByTestId<HTMLInputElement>('search-input'); + fireEvent.change(input, { target: { value: 'park7' } }); + + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(input).toBeDisabled(); + + expect(mockedRouter).toMatchObject({ + asPath: '/?search=park7', + pathname: '/', + query: { search: 'park7' }, + }); + }); + + it('should set the value on the input filed when the user has query parameters in the url', () => { + renderComponent(); + + mockedRouter.push('/?search=park7'); + + const input = screen.getByTestId<HTMLInputElement>('search-input'); + + expect(input.value).toBe('park7'); + + expect(mockedRouter).toMatchObject({ + asPath: '/?search=park7', + pathname: '/', + query: { search: 'park7' }, + }); }); }); diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx index 0c7cdd153ad0db61420f0d1efbe0a6d069ee396e..a1c26e887b5e71d8076546b78a7f6322888970f5 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx @@ -1,12 +1,55 @@ -import Image from 'next/image'; -import { ChangeEvent, useState } from 'react'; import lensIcon from '@/assets/vectors/icons/lens.svg'; +import { useParamsQuery } from '@/components/FunctionalArea/TopBar/SearchBar/hooks/useParamsQuery'; +import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; +import { clearSearchDrawerState, openDrawer } from '@/redux/drawer/drawer.slice'; +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 isPendingSearchStatus = useSelector(isPendingSearchStatusSelector); + const prevSearchValue = useSelector(searchValueSelector); + const isDrawerOpen = useSelector(isDrawerOpenSelector); + + const { setSearchQueryInRouter, searchParams } = useParamsQuery(); + + const [searchValue, setSearchValue] = useState<string>((searchParams?.search as string) || ''); + const dispatch = useAppDispatch(); + + const isSameSearchValue = prevSearchValue === searchValue; - const onSearchChange = (event: ChangeEvent<HTMLInputElement>): void => { + const openSearchDrawerIfClosed = (): void => { + if (!isDrawerOpen) { + dispatch(openDrawer('search')); + dispatch(clearSearchDrawerState()); + } + }; + + const onSearchChange = (event: ChangeEvent<HTMLInputElement>): void => setSearchValue(event.target.value); + + const onSearchClick = (): void => { + if (!isSameSearchValue) { + dispatch(getSearchData(searchValue)); + setSearchQueryInRouter(searchValue); + openSearchDrawerIfClosed(); + } + }; + + const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>): void => { + if (!isSameSearchValue && event.code === ENTER_KEY_CODE) { + dispatch(getSearchData(searchValue)); + setSearchQueryInRouter(searchValue); + openSearchDrawerIfClosed(); + } }; return ( @@ -16,16 +59,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/SearchBar/hooks/useParamsQuery.ts b/src/components/FunctionalArea/TopBar/SearchBar/hooks/useParamsQuery.ts new file mode 100644 index 0000000000000000000000000000000000000000..c332beda9c2f6ce79499c2435ddca59b7c7fba19 --- /dev/null +++ b/src/components/FunctionalArea/TopBar/SearchBar/hooks/useParamsQuery.ts @@ -0,0 +1,38 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { getSearchData } from '@/redux/search/search.thunks'; +import { useRouter } from 'next/router'; +import type { ParsedQuery } from 'query-string'; +import qs from 'query-string'; +import { useEffect } from 'react'; + +type UseParamsQuery = { + setSearchQueryInRouter: (searchValue: string) => void; + searchParams: ParsedQuery<string>; +}; + +export const useParamsQuery = (): UseParamsQuery => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const path = router.asPath; + + // The number of the character from which to cut the characters from path. + const sliceFromCharNumber = 2; + // The number of the character at which to end the cut string from path. + const sliceToCharNumber = path.length; + const searchParams = qs.parse(path.slice(sliceFromCharNumber, sliceToCharNumber)); + + const setSearchQueryInRouter = (searchValue: string): void => { + const searchQuery = { + search: searchValue, + }; + + router.push(`?${qs.stringify(searchQuery)}`); + }; + + useEffect(() => { + if (searchParams?.search) dispatch(getSearchData(searchParams.search as string)); + }, [dispatch]); + + return { setSearchQueryInRouter, searchParams }; +}; diff --git a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx index a611ffd3fa009a1d98b6d663bd9af6dd9fb7a73a..e6e44ac13532f839d4b82fbde10dfdd537fc49ce 100644 --- a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx @@ -1,7 +1,22 @@ -import { screen, render, RenderResult } from '@testing-library/react'; +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 = (): RenderResult => render(<TopBar />); +const renderComponent = (): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(); + + return ( + render( + <Wrapper> + <TopBar /> + </Wrapper>, + ), + { + store, + } + ); +}; describe('TopBar - component', () => { it('Should contain user avatar, search bar', () => { diff --git a/src/components/Map/Drawer/Drawer.component.test.tsx b/src/components/Map/Drawer/Drawer.component.test.tsx index a0663188edef546d4eb44d1fde6591bbcdbaa1f5..1741384a561e63b950dfe8c63002fe99407033a0 100644 --- a/src/components/Map/Drawer/Drawer.component.test.tsx +++ b/src/components/Map/Drawer/Drawer.component.test.tsx @@ -1,13 +1,11 @@ -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 { StoreType } from '@/redux/store'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { act, fireEvent, render, screen } from '@testing-library/react'; 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> diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index 4e692262a8a87a4e45a2a079b6db8c3551b4b4af..52f4a1d53d3d1ebe9bcbc26259494e7b3253bbca 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -1,8 +1,8 @@ +import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants'; +import { drawerSelector } from '@/redux/drawer/drawer.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; 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'; 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> ); diff --git a/src/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx index ff006e6b0d2f9cc5ba046e81ec909ac018bf7995..6442eca6bb1de817ab1ee0dfdf8c343b5c24edea 100644 --- a/src/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx @@ -1,5 +1,4 @@ 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> ); }; diff --git a/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c8a4826a1446507681ca673059c8e1f5c24d2b6b --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; +import { Accordion } from '@/shared/Accordion'; +import { ChemicalsAccordion } from './ChemicalsAccordion.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <Accordion> + <ChemicalsAccordion /> + </Accordion> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('DrugsAccordion - component', () => { + it('should display drugs number after succesfull chemicals search', () => { + renderComponent({ + chemicals: { data: chemicalsFixture, loading: 'succeeded', error: { name: '', message: '' } }, + }); + expect(screen.getByText('Chemicals (2)')).toBeInTheDocument(); + }); + it('should display loading indicator while waiting for chemicals search response', () => { + renderComponent({ + chemicals: { data: [], loading: 'pending', error: { name: '', message: '' } }, + }); + expect(screen.getByText('Chemicals (Loading...)')).toBeInTheDocument(); + }); +}); diff --git a/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..46ee6f7865e555e2d825ba238941400d09887788 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/ChemicalsAccordion.component.tsx @@ -0,0 +1,23 @@ +import { + numberOfChemicalsSelector, + loadingChemicalsStatusSelector, +} from '@/redux/chemicals/chemicals.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { AccordionItem, AccordionItemHeading, AccordionItemButton } from '@/shared/Accordion'; + +export const ChemicalsAccordion = (): JSX.Element => { + const chemicalsNumber = useAppSelector(numberOfChemicalsSelector); + const drugsState = useAppSelector(loadingChemicalsStatusSelector); + + return ( + <AccordionItem> + <AccordionItemHeading> + <AccordionItemButton variant="non-expandable"> + Chemicals + {drugsState === 'pending' && ' (Loading...)'} + {drugsState === 'succeeded' && ` (${chemicalsNumber})`} + </AccordionItemButton> + </AccordionItemHeading> + </AccordionItem> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/index.ts b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fc44924d2be7c38dcb00d5a0a5289b7bf237a1a --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion/index.ts @@ -0,0 +1 @@ +export { ChemicalsAccordion } from './ChemicalsAccordion.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c1d1f6fe7bcb63e15e2d4b2d6f7f0b0c11e987b4 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.test.tsx @@ -0,0 +1,41 @@ +import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { StoreType } from '@/redux/store'; +import { Accordion } from '@/shared/Accordion'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +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(); + }); +}); diff --git a/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..af14387558dbfecd9b62eef326603c17dd49743c --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/DrugsAccordion.component.tsx @@ -0,0 +1,20 @@ +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> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/index.ts b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..36be6a7acb3a6c9fb32efdfa5cbb70efac87a909 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/DrugsAccordion/index.ts @@ -0,0 +1 @@ +export { DrugsAccordion } from './DrugsAccordion.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b3b10bf68e3f0478792e5e0add36aa5559fb70b6 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { mirnasFixture } from '@/models/fixtures/mirnasFixture'; +import { Accordion } from '@/shared/Accordion'; +import { MirnaAccordion } from './MirnaAccordion.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <Accordion> + <MirnaAccordion /> + </Accordion> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('DrugsAccordion - component', () => { + it('should display drugs number after succesfull chemicals search', () => { + renderComponent({ + mirnas: { data: mirnasFixture, loading: 'succeeded', error: { name: '', message: '' } }, + }); + expect(screen.getByText('MiRNA (2)')).toBeInTheDocument(); + }); + it('should display loading indicator while waiting for chemicals search response', () => { + renderComponent({ + mirnas: { data: [], loading: 'pending', error: { name: '', message: '' } }, + }); + expect(screen.getByText('MiRNA (Loading...)')).toBeInTheDocument(); + }); +}); diff --git a/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e23bf05659a15086e88b15bdeeb3e3e83b0bcd6e --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/MirnaAccordion.component.tsx @@ -0,0 +1,23 @@ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { + numberOfMirnasSelector, + loadingMirnasStatusSelector, +} from '@/redux/mirnas/mirnas.selectors'; +import { AccordionItem, AccordionItemHeading, AccordionItemButton } from '@/shared/Accordion'; + +export const MirnaAccordion = (): JSX.Element => { + const mirnaNumber = useAppSelector(numberOfMirnasSelector); + const mirnaState = useAppSelector(loadingMirnasStatusSelector); + + return ( + <AccordionItem> + <AccordionItemHeading> + <AccordionItemButton variant="non-expandable"> + MiRNA + {mirnaState === 'pending' && ' (Loading...)'} + {mirnaState === 'succeeded' && ` (${mirnaNumber})`} + </AccordionItemButton> + </AccordionItemHeading> + </AccordionItem> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/index.ts b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..69f2736c465dba1da3c0737fa07a3a8ac5268b8c --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/MirnaAccordion/index.ts @@ -0,0 +1 @@ +export { MirnaAccordion } from './MirnaAccordion.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2678ebb1ba45b1e314797e321038f52d1b031ce5 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.component.tsx @@ -0,0 +1,14 @@ +import { PinItem } from './PinsList.types'; +import { PinsListItem } from './PinsListItem'; + +interface PinsListProps { + pinsList: PinItem[]; +} + +export const PinsList = ({ pinsList }: PinsListProps): JSX.Element => ( + <ul className="px-6 py-2"> + {pinsList.map(pin => ( + <PinsListItem key={pin.name} name={pin.name} /> + ))} + </ul> +); diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.types.tsx b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.types.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ce02ca44317ef35bcaa08a1f816c2b4157a4d91b --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsList.types.tsx @@ -0,0 +1,3 @@ +export type PinItem = { + name: string; +}; diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/PinsListItem.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/PinsListItem.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6eba919fdc7c3dc7375bc62874cea9da02d8668 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/PinsListItem.component.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/shared/Icon'; + +interface PinsListItemProps { + name: string; +} + +export const PinsListItem = ({ name }: PinsListItemProps): JSX.Element => ( + <div className="flex flex-row justify-between pt-4"> + <Icon name="pin" className="mr-2 shrink-0" /> + <p className="w-full text-left">{name}</p> + <Icon name="chevron-right" className="h-6 w-6 shrink-0" /> + </div> +); diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/index.ts b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..89b9aebca900ed5e4282564c6b0221ff861325d5 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/PinsListItem/index.ts @@ -0,0 +1 @@ +export { PinsListItem } from './PinsListItem.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/index.ts b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9d388b05a673ede7c8f42b9611fabba0eae00d7 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/PinsList/index.ts @@ -0,0 +1 @@ +export { PinsList } from './PinsList.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/Results.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/Results/Results.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..89b9cd601d70e334669de335f5abb0acf793976a --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/Results.component.tsx @@ -0,0 +1,27 @@ +import { DrawerHeadingBackwardButton } from '@/shared/DrawerHeadingBackwardButton'; +import { PinsList } from './PinsList'; +import { PinItem } from './PinsList/PinsList.types'; + +const PINS_LIST: PinItem[] = [ + { name: 'Glyceraldehyde-3-phosphate dehydrogenase' }, + { name: 'D-3-phosphoglycerate dehydrogenase' }, + { name: 'Glutathione reductase, mitochondrial' }, + { name: 'NADH dehydrogenase [ubiquinone] iron-sulfur protein 8, mitochondrial' }, +]; + +export const Results = (): JSX.Element => { + const drugName = 'Aspirin'; + + const navigateToGroupedSearchResults = (): void => {}; + + return ( + <div> + <DrawerHeadingBackwardButton + title="Drugs" + value={drugName} + backwardFunction={navigateToGroupedSearchResults} + /> + <PinsList pinsList={PINS_LIST} /> + </div> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerContent/Results/index.ts b/src/components/Map/Drawer/SearchDrawerContent/Results/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b2da8f8d99a9b80340d6ba81a92990adb10029d --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerContent/Results/index.ts @@ -0,0 +1 @@ +export { Results } from './Results.component'; diff --git a/src/components/Map/Drawer/SearchDrawerContent/SearchDrawerContent.component.tsx b/src/components/Map/Drawer/SearchDrawerContent/SearchDrawerContent.component.tsx index ad1b350ce514b1077ec72147ee250400c3c1174f..7f0cc18beaeafb98364d31dedab045ab00c4d7d1 100644 --- a/src/components/Map/Drawer/SearchDrawerContent/SearchDrawerContent.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerContent/SearchDrawerContent.component.tsx @@ -1,7 +1,11 @@ import { BioEntitiesAccordion } from '@/components/Map/Drawer/SearchDrawerContent/BioEntitiesAccordion'; +import { DrugsAccordion } from '@/components/Map/Drawer/SearchDrawerContent/DrugsAccordion'; +import { ChemicalsAccordion } from '@/components/Map/Drawer/SearchDrawerContent/ChemicalsAccordion'; +import { MirnaAccordion } from '@/components/Map/Drawer/SearchDrawerContent/MirnaAccordion'; 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 +32,12 @@ export const SearchDrawerContent = (): JSX.Element => { /> </div> <div className="px-6"> - <BioEntitiesAccordion /> + <Accordion allowZeroExpanded> + <BioEntitiesAccordion /> + <DrugsAccordion /> + <ChemicalsAccordion /> + <MirnaAccordion /> + </Accordion> </div> </div> ); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..19c588d71caddd0b92fbadf301ff589931ef559c --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx @@ -0,0 +1,116 @@ +import { SearchDrawerWrapper } from '@/components/Map/Drawer/SearchDrawerWrapper'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <SearchDrawerWrapper /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('SearchDrawerWrapper - component', () => { + it('should display the first step for search', () => { + renderComponent({ + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { + currentStep: 1, + selectedValue: { + name: '', + valueType: 'none', + }, + }, + }, + }); + + expect(screen.getByTestId('search-first-step')).toBeInTheDocument(); + }); + + it('should display the second step for value type bioEntity', () => { + renderComponent({ + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { + currentStep: 2, + selectedValue: { + model: { name: 'test model bioEntity', id: 'test-id' }, + name: 'bio entity second step', + valueType: 'bioEntity', + }, + }, + }, + }); + + expect(screen.getByTestId('search-second-step')).toBeInTheDocument(); + }); + + it('should display the second step for value type chemicals', () => { + renderComponent({ + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { + currentStep: 2, + selectedValue: { + name: 'chemicals second step', + valueType: 'chemicals', + }, + }, + }, + }); + + expect(screen.getByTestId('search-second-step')).toBeInTheDocument(); + }); + + it('should display the third step for value type bioEntity', () => { + renderComponent({ + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { + currentStep: 3, + selectedValue: { + model: { name: 'test model bioEntity', id: 'test-id' }, + name: 'bio entity third step', + valueType: 'bioEntity', + }, + }, + }, + }); + + expect(screen.getByTestId('search-third-step')).toBeInTheDocument(); + }); + + it('should display the third step for value type chemicals', () => { + renderComponent({ + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { + currentStep: 3, + selectedValue: { + name: 'chemicals third step', + valueType: 'chemicals', + }, + }, + }, + }); + + expect(screen.getByTestId('search-third-step')).toBeInTheDocument(); + }); +}); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9ce500cf316b75bbc4616efe6afe1246d58b87e6 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx @@ -0,0 +1,38 @@ +import { STEP } from '@/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.constants'; +import { BIO_ENTITY, DRUGS_CHEMICALS_MIRNA } from '@/constants'; +import { + currentStepDrawerStateSelector, + valueTypeDrawerSelector, +} from '@/redux/drawer/drawer.selectors'; +import { useSelector } from 'react-redux'; + +export const SearchDrawerWrapper = (): JSX.Element => { + const currentStep = useSelector(currentStepDrawerStateSelector); + const valueType = useSelector(valueTypeDrawerSelector); + + const isBioEntityType = valueType === BIO_ENTITY; + const isChemicalsDrugsOrMirnaType = DRUGS_CHEMICALS_MIRNA.includes(valueType); + + return ( + <div> + {/* first step for displaying search results, drawers etc */} + {currentStep === STEP.FIRST && <div data-testid="search-first-step">The first step</div>} + {/* 2nd step for bioEntities aka content */} + {currentStep === STEP.SECOND && isBioEntityType && ( + <div data-testid="search-second-step">The second step</div> + )} + {/* 2nd step for drugs,chemicals,mirna */} + {currentStep === STEP.SECOND && isChemicalsDrugsOrMirnaType && ( + <div data-testid="search-second-step">The second step</div> + )} + {/* last step for bioentity */} + {currentStep === STEP.THIRD && isBioEntityType && ( + <div data-testid="search-third-step">The third step</div> + )} + {/* last step for drugs,chemicals,mirna */} + {currentStep === STEP.THIRD && isChemicalsDrugsOrMirnaType && ( + <div data-testid="search-third-step">The third step</div> + )} + </div> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.constants.ts b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc1b7da72a652c9b0b755f71034b61cd477c6ccd --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.constants.ts @@ -0,0 +1,5 @@ +export const STEP = { + FIRST: 1, + SECOND: 2, + THIRD: 3, +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/index.ts b/src/components/Map/Drawer/SearchDrawerWrapper/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee8e9189191c28b0933c04d1af693c2c22cd86a9 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/index.ts @@ -0,0 +1 @@ +export { SearchDrawerWrapper } from './SearchDrawerWrapper.component'; diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx index c4b61c47c268f3421b3e2fa908b7853e2b6ff8e6..c9b5381584b0a04df6a647779e15e82e4bff4962 100644 --- a/src/components/SPA/MinervaSPA.component.tsx +++ b/src/components/SPA/MinervaSPA.component.tsx @@ -2,6 +2,9 @@ import { Manrope } from '@next/font/google'; import { twMerge } from 'tailwind-merge'; import { Map } from '@/components/Map'; import { FunctionalArea } from '@/components/FunctionalArea'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useEffect } from 'react'; +import { getModels } from '@/redux/models/models.thunks'; const manrope = Manrope({ variable: '--font-manrope', @@ -10,9 +13,17 @@ const manrope = Manrope({ subsets: ['latin'], }); -export const MinervaSPA = (): JSX.Element => ( - <div className={twMerge('relative', manrope.variable)}> - <FunctionalArea /> - <Map /> - </div> -); +export const MinervaSPA = (): JSX.Element => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(getModels()); + }, [dispatch]); + + return ( + <div className={twMerge('relative', manrope.variable)}> + <FunctionalArea /> + <Map /> + </div> + ); +}; diff --git a/src/constants/common.ts b/src/constants/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee434dc7c1ebfa6b43b4f2b6004ef5d042a41861 --- /dev/null +++ b/src/constants/common.ts @@ -0,0 +1 @@ +export const SIZE_OF_EMPTY_ARRAY = 0; diff --git a/src/constants/index.ts b/src/constants/index.ts index 43423ae1b1478dfd342c37c5488a44cc09703006..16e892f07522e3d15e68ac4d0e802f290a406b8c 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,5 @@ export const BASE_API_URL = process.env.NEXT_PUBLIC_BASE_API_URL || ''; export const PROJECT_ID = process.env.NEXT_PUBLIC_PROJECT_ID || ''; export const ZOD_SEED = 997; +export const BIO_ENTITY = 'bioEntity'; +export const DRUGS_CHEMICALS_MIRNA = ['drugs', 'chemicals', 'mirna']; diff --git a/src/hooks/usePrevious.tsx b/src/hooks/usePrevious.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a5233144ca680a146795cc40f679cabd31ee0171 --- /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/models/authorSchema.ts b/src/models/authorSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dfa82ec918b797f2a3b1cd8816e197274cd2f1c --- /dev/null +++ b/src/models/authorSchema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const authorSchema = z.string(); diff --git a/src/models/fixtures/modelsFixture.ts b/src/models/fixtures/modelsFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d4e39e35e9ec6500ecf7759da7c3fa028f54c09 --- /dev/null +++ b/src/models/fixtures/modelsFixture.ts @@ -0,0 +1,10 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { z } from 'zod'; +import { ZOD_SEED } from '@/constants'; +import { modelSchema } from '@/models/modelSchema'; + +export const modelsFixture = createFixture(z.array(modelSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/modelSchema.ts b/src/models/modelSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..64752c8c1bd4043d1ff24cc0de26022f5f22daa2 --- /dev/null +++ b/src/models/modelSchema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { referenceSchema } from './referenceSchema'; +import { authorSchema } from './authorSchema'; + +export const modelSchema = z.object({ + /** name of the map */ + name: z.string(), + description: z.string(), + /** map id */ + idObject: z.number(), + /** map width */ + width: z.number(), + /** map height */ + height: z.number(), + /** size of the png tile used to visualize in frontend */ + tileSize: z.number(), + /** default x center used in frontend visualization */ + defaultCenterX: z.union([z.number(), z.null()]), + /** default y center used in frontend visualization */ + defaultCenterY: z.union([z.number(), z.null()]), + /** default zoom level used in frontend visualization */ + defaultZoomLevel: z.union([z.number(), z.null()]), + /** minimum zoom level availbale for the map */ + minZoom: z.number(), + /** maximum zoom level available for the map */ + maxZoom: z.number(), + authors: z.array(authorSchema), + references: z.array(referenceSchema), + creationDate: z.union([z.string(), z.null()]), + modificationDates: z.array(z.string()), +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 8469a3d16b0f7eb0e9144339ff69da900564e86c..11ce6f11c1e59f6e8f6a4bf38037ac62e2e501a2 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -7,6 +7,7 @@ export const apiPath = { `projects/${PROJECT_ID}/drugs:search?query=${searchQuery}`, getMirnasStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/miRnas:search?query=${searchQuery}`, + getModelsString: (): string => `projects/${PROJECT_ID}/models/`, getChemicalsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, }; diff --git a/src/redux/bioEntityContents/bioEntityContents.selectors.ts b/src/redux/bioEntityContents/bioEntityContents.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..80a88cb0bec55d0ebf43ba8dd85827b44b429121 --- /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 0000000000000000000000000000000000000000..0fef7a3275e97b1a501223003e06e4cfe05f3f23 --- /dev/null +++ b/src/redux/chemicals/chemicals.selectors.ts @@ -0,0 +1,14 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +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, +); + +export const numberOfChemicalsSelector = createSelector(chemicalsSelector, state => + state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY, +); diff --git a/src/redux/drawer/drawer.reducers.test.ts b/src/redux/drawer/drawer.reducers.test.ts index 7701a180c2bc0c148b050e96c0ffc2bc523f1bcd..d7bf3cedfe4bccb7fb41301f747220119f204573 100644 --- a/src/redux/drawer/drawer.reducers.test.ts +++ b/src/redux/drawer/drawer.reducers.test.ts @@ -1,12 +1,24 @@ -import { AnyAction } from '@reduxjs/toolkit'; import * as toolkitRaw from '@reduxjs/toolkit'; +import { AnyAction } from '@reduxjs/toolkit'; import type { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore'; -import drawerReducer, { openDrawer, closeDrawer } from './drawer.slice'; +import drawerReducer, { + clearSearchDrawerState, + closeDrawer, + openDrawer, + setSearchDrawerState, +} from './drawer.slice'; import type { DrawerState } from './drawer.types'; const INITIAL_STATE: DrawerState = { - open: false, + isOpen: false, drawerName: 'none', + searchDrawerState: { + currentStep: 0, + selectedValue: { + name: '', + valueType: 'none', + }, + }, }; type SliceReducerType = ToolkitStore< @@ -38,23 +50,67 @@ 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'); }); - it.skip('should update the store when you type in the search', async () => { - // TODO + it('should update the store when you choose from the drawer chemical`s first step', async () => { + const searchDrawerData: DrawerState['searchDrawerState'] = { + currentStep: 1, + selectedValue: { + name: 'chemicals frist step', + valueType: 'chemicals', + }, + }; + + const { type } = await store.dispatch(setSearchDrawerState(searchDrawerData)); + const { searchDrawerState } = store.getState().drawer; + const { currentStep, selectedValue } = searchDrawerState; + + const currentStepToBE = 1; + + expect(type).toBe('drawer/setSearchDrawerState'); + expect(currentStep).toBe(currentStepToBE); + expect(selectedValue).toEqual({ + name: 'chemicals frist step', + valueType: 'chemicals', + }); + }); + + it('should update the store when you clear the search drawer state', async () => { + const searchDrawerData: DrawerState['searchDrawerState'] = { + currentStep: 1, + selectedValue: { + name: 'chemicals frist step', + valueType: 'chemicals', + }, + }; + + await store.dispatch(setSearchDrawerState(searchDrawerData)); + const { type } = await store.dispatch(clearSearchDrawerState()); + + const { searchDrawerState } = store.getState().drawer; + const { currentStep, selectedValue } = searchDrawerState; + + const currentStepToBE = 0; + + expect(type).toBe('drawer/clearSearchDrawerState'); + expect(currentStep).toBe(currentStepToBE); + expect(selectedValue).toEqual({ + name: '', + valueType: 'none', + }); }); }); diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 47b8ef6dd7ff78df197ba96bd753bb80d99b186b..e205e9a44d2a73955b70020f07b827ac86333096 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -1,12 +1,30 @@ -import { PayloadAction } from '@reduxjs/toolkit'; -import { DrawerState } from '@/redux/drawer/drawer.types'; -import { PathName } from '@/types/pathName'; +import type { DrawerState } from '@/redux/drawer/drawer.types'; +import type { DrawerName } from '@/types/drawerName'; +import type { PayloadAction } from '@reduxjs/toolkit'; -export const openDrawerReducer = (state: DrawerState, action: PayloadAction<PathName>): void => { - state.open = true; +export const openDrawerReducer = (state: DrawerState, action: PayloadAction<DrawerName>): void => { + state.isOpen = true; state.drawerName = action.payload; }; export const closeDrawerReducer = (state: DrawerState): void => { - state.open = false; + state.isOpen = false; +}; + +export const setSearchDrawerStateReducer = ( + state: DrawerState, + action: PayloadAction<DrawerState['searchDrawerState']>, +): void => { + const { currentStep, selectedValue } = action.payload; + + state.searchDrawerState.currentStep = currentStep; + state.searchDrawerState.selectedValue = selectedValue; +}; + +export const clearSearchDrawerStateReducer = (state: DrawerState): void => { + state.searchDrawerState.currentStep = 0; + state.searchDrawerState.selectedValue = { + name: '', + valueType: 'none', + }; }; diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts index a49b98fffb4585ab3afef535d62b364377f20a6b..a622a1f6146c2ffbf97b32803f618661b1b2fc9b 100644 --- a/src/redux/drawer/drawer.selectors.ts +++ b/src/redux/drawer/drawer.selectors.ts @@ -1,4 +1,26 @@ -import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; +import { createSelector } from '@reduxjs/toolkit'; + +export const drawerSelector = createSelector(rootSelector, state => state.drawer); + +export const isDrawerOpenSelector = createSelector(drawerSelector, state => state.isOpen); + +export const searchDrawerStateSelector = createSelector( + drawerSelector, + state => state.searchDrawerState, +); + +export const currentStepDrawerStateSelector = createSelector( + searchDrawerStateSelector, + state => state.currentStep, +); + +export const selectedValueDrawerSelector = createSelector( + searchDrawerStateSelector, + state => state.selectedValue, +); -export const drawerDataSelector = createSelector(rootSelector, state => state.drawer); +export const valueTypeDrawerSelector = createSelector( + selectedValueDrawerSelector, + state => state.valueType, +); diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts index 4c71fda1792e323ce63c52292f06bf13cb1f044f..cbbe5a331f263507dd35c55d639846b9dd208714 100644 --- a/src/redux/drawer/drawer.slice.ts +++ b/src/redux/drawer/drawer.slice.ts @@ -1,10 +1,22 @@ -import { createSlice } from '@reduxjs/toolkit'; import { DrawerState } from '@/redux/drawer/drawer.types'; -import { openDrawerReducer, closeDrawerReducer } from './drawer.reducers'; +import { createSlice } from '@reduxjs/toolkit'; +import { + clearSearchDrawerStateReducer, + closeDrawerReducer, + openDrawerReducer, + setSearchDrawerStateReducer, +} from './drawer.reducers'; const initialState: DrawerState = { - open: false, + isOpen: false, drawerName: 'none', + searchDrawerState: { + currentStep: 0, + selectedValue: { + name: '', + valueType: 'none', + }, + }, }; const drawerSlice = createSlice({ @@ -13,9 +25,12 @@ const drawerSlice = createSlice({ reducers: { openDrawer: openDrawerReducer, closeDrawer: closeDrawerReducer, + setSearchDrawerState: setSearchDrawerStateReducer, + clearSearchDrawerState: clearSearchDrawerStateReducer, }, }); -export const { openDrawer, closeDrawer } = drawerSlice.actions; +export const { openDrawer, closeDrawer, setSearchDrawerState, clearSearchDrawerState } = + drawerSlice.actions; export default drawerSlice.reducer; diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index a0ad70b8b7e1cf4f7555eaf15ffc265dd9b19206..404d0fc2c1c6163b21fc69e6269b2ee14d49a503 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -1,4 +1,40 @@ +import type { DrawerName } from '@/types/drawerName'; + +type DrugValue = { + name: string; + valueType: 'drugs'; +}; + +type ChemicalsValue = { + name: string; + valueType: 'chemicals'; +}; + +type MirnaValue = { + name: string; + valueType: 'mirna'; +}; + +type BioEntityValue = { + model: { name: string; id: string }; + name: string; + valueType: 'bioEntity'; +}; + +type NoneValue = { + name: string; + valueType: 'none'; +}; + +export type SelectedValue = DrugValue | ChemicalsValue | MirnaValue | BioEntityValue | NoneValue; + +export type SearchDrawerState = { + currentStep: number; + selectedValue: SelectedValue; +}; + export type DrawerState = { - open: boolean; - drawerName: 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend'; + isOpen: boolean; + drawerName: DrawerName; + searchDrawerState: SearchDrawerState; }; diff --git a/src/redux/drugs/drugs.selectors.ts b/src/redux/drugs/drugs.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a1d3eacacbf0fee4cd7093f4c0255f4c910af51 --- /dev/null +++ b/src/redux/drugs/drugs.selectors.ts @@ -0,0 +1,10 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +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); +export const numberOfDrugsSelector = createSelector(drugsSelector, state => + state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY, +); diff --git a/src/redux/mirnas/mirnas.selectors.ts b/src/redux/mirnas/mirnas.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..51ac4dea078d807985a399bede164e6782e7ae9b --- /dev/null +++ b/src/redux/mirnas/mirnas.selectors.ts @@ -0,0 +1,10 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +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); +export const numberOfMirnasSelector = createSelector(mirnasSelector, state => + state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY, +); diff --git a/src/redux/models/models.reducers.test.ts b/src/redux/models/models.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c130ae0ee5a782380554a87b39e9a77f31ec3864 --- /dev/null +++ b/src/redux/models/models.reducers.test.ts @@ -0,0 +1,72 @@ +import { HttpStatusCode } from 'axios'; +import { modelsFixture } from '@/models/fixtures/modelsFixture'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { apiPath } from '@/redux/apiPath'; +import { getModels } from './models.thunks'; +import modelsReducer from './models.slice'; +import { ModelsState } from './models.types'; + +const mockedAxiosClient = mockNetworkResponse(); + +const INITIAL_STATE: ModelsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('models reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<ModelsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('models', modelsReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(modelsReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getModels query', async () => { + mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture); + + const { type } = await store.dispatch(getModels()); + const { data, loading, error } = store.getState().models; + + expect(type).toBe('project/getModels/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(modelsFixture); + }); + + it('should update store after failed getModels query', async () => { + mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.NotFound, []); + + const { type } = await store.dispatch(getModels()); + const { data, loading, error } = store.getState().models; + + expect(type).toBe('project/getModels/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getModels query', async () => { + mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture); + + const modelsPromise = store.dispatch(getModels()); + + const { data, loading } = store.getState().models; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + modelsPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().models; + + expect(dataPromiseFulfilled).toEqual(modelsFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/models/models.reducers.ts b/src/redux/models/models.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..28f71efbe06a26fde0b4f5cdb8b11599965f5713 --- /dev/null +++ b/src/redux/models/models.reducers.ts @@ -0,0 +1,17 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { ModelsState } from './models.types'; +import { getModels } from './models.thunks'; + +export const getModelsReducer = (builder: ActionReducerMapBuilder<ModelsState>): void => { + builder.addCase(getModels.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getModels.fulfilled, (state, action) => { + state.data = action.payload; + state.loading = 'succeeded'; + }); + builder.addCase(getModels.rejected, state => { + state.loading = 'failed'; + // TODO to discuss manage state of failure + }); +}; diff --git a/src/redux/models/models.slice.ts b/src/redux/models/models.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..61c765b6b8668424f2b518eb50f463c8629d769f --- /dev/null +++ b/src/redux/models/models.slice.ts @@ -0,0 +1,20 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { ModelsState } from '@/redux/models/models.types'; +import { getModelsReducer } from './models.reducers'; + +const initialState: ModelsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +export const modelsSlice = createSlice({ + name: 'models', + initialState, + reducers: {}, + extraReducers: builder => { + getModelsReducer(builder); + }, +}); + +export default modelsSlice.reducer; diff --git a/src/redux/models/models.thunks.test.ts b/src/redux/models/models.thunks.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d85a15e274e0d0ee6352353d25e504de8097259 --- /dev/null +++ b/src/redux/models/models.thunks.test.ts @@ -0,0 +1,36 @@ +import { HttpStatusCode } from 'axios'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { ModelsState } from '@/redux/models/models.types'; +import { apiPath } from '@/redux/apiPath'; +import { modelsFixture } from '@/models/fixtures/modelsFixture'; +import modelsReducer from './models.slice'; +import { getModels } from './models.thunks'; + +const mockedAxiosClient = mockNetworkResponse(); + +describe('models thunks', () => { + let store = {} as ToolkitStoreWithSingleSlice<ModelsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('models', modelsReducer); + }); + describe('getModels', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture); + + const { payload } = await store.dispatch(getModels()); + expect(payload).toEqual(modelsFixture); + }); + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(apiPath.getModelsString()) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getModels()); + expect(payload).toEqual(undefined); + }); + }); +}); diff --git a/src/redux/models/models.thunks.ts b/src/redux/models/models.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..430bafb0e14acf41302102c42eddcc54fe5ca72c --- /dev/null +++ b/src/redux/models/models.thunks.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { Model } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { apiPath } from '@/redux/apiPath'; +import { modelSchema } from '@/models/modelSchema'; + +export const getModels = createAsyncThunk( + 'project/getModels', + async (): Promise<Model[] | undefined> => { + const response = await axiosInstance.get<Model[]>(apiPath.getModelsString()); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(modelSchema)); + + return isDataValid ? response.data : undefined; + }, +); diff --git a/src/redux/models/models.types.ts b/src/redux/models/models.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3a6b9824eb8abc7a30d10137a1ae0f87ce4f5e7 --- /dev/null +++ b/src/redux/models/models.types.ts @@ -0,0 +1,4 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { Model } from '@/types/models'; + +export type ModelsState = FetchDataState<Model[]>; diff --git a/src/redux/search/search.reducers.test.ts b/src/redux/search/search.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..46495b268efc8f5c87e8da6a144117478dd65426 --- /dev/null +++ b/src/redux/search/search.reducers.test.ts @@ -0,0 +1,51 @@ +import { getSearchData } from '@/redux/search/search.thunks'; +import type { SearchState } from '@/redux/search/search.types'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import searchReducer from './search.slice'; + +const SEARCH_QUERY = 'Corticosterone'; + +const INITIAL_STATE: SearchState = { + searchValue: '', + loading: 'idle', +}; + +describe('search reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<SearchState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('search', searchReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(searchReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + + it('should update store after succesfull getSearchData query', async () => { + await store.dispatch(getSearchData(SEARCH_QUERY)); + + const { searchValue, loading } = store.getState().search; + expect(searchValue).toEqual(SEARCH_QUERY); + expect(loading).toEqual('succeeded'); + }); + + it('should update store on loading getSearchData query', async () => { + const searchPromise = store.dispatch(getSearchData(SEARCH_QUERY)); + + const { searchValue, loading } = store.getState().search; + expect(searchValue).toEqual(SEARCH_QUERY); + expect(loading).toEqual('pending'); + + searchPromise.then(() => { + const { searchValue: searchValueFulfilled, loading: promiseFulfilled } = + store.getState().search; + + expect(searchValueFulfilled).toEqual(SEARCH_QUERY); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/search/search.reducers.ts b/src/redux/search/search.reducers.ts index 4f6f747d357f19c9a48c43041c484c4052c21600..21e30af3a9bc50deca0e7800746589bb792ef7fd 100644 --- a/src/redux/search/search.reducers.ts +++ b/src/redux/search/search.reducers.ts @@ -1,7 +1,19 @@ // 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, action) => { + state.searchValue = action.meta.arg; + 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 c845eecd0c4220dc245e95335f575ece55ecb804..143488fe3fa882c94f86691cec3479d4590791b6 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 92357f8c943fea34aae15b7978266df4c89ae760..360bfa689ed19e83f6474176a4d02fe7c4512384 100644 --- a/src/redux/search/search.slice.ts +++ b/src/redux/search/search.slice.ts @@ -1,23 +1,19 @@ -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: '', - searchResult: { - 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 0000000000000000000000000000000000000000..2724826c65aa793de889176cc3fe75764509ec1c --- /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 6b6316a881f83aed58588d03b75c2f0ca0d5d9a1..45380f011abc19d556077abbbc62021f6c1fa016 100644 --- a/src/redux/search/search.types.ts +++ b/src/redux/search/search.types.ts @@ -1,9 +1,6 @@ -export interface SearchResult { - content: string; - drugs: string; -} +import { Loading } from '@/types/loadingState'; export interface SearchState { searchValue: string; - searchResult: SearchResult; + loading: Loading; } diff --git a/src/redux/store.ts b/src/redux/store.ts index 0136870f551d8ceb8695c6e54e45087e360f3ca6..4d16d81bf05937fca10c7390f19778fdfce73742 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,6 +1,10 @@ +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 mapReducer from '@/redux/map/map.slice'; +import mirnasReducer from '@/redux/mirnas/mirnas.slice'; +import modelsReducer from '@/redux/models/models.slice'; import projectSlice from '@/redux/project/project.slice'; import searchReducer from '@/redux/search/search.slice'; import { configureStore } from '@reduxjs/toolkit'; @@ -10,12 +14,17 @@ export const store = configureStore({ search: searchReducer, project: projectSlice, drugs: drugsReducer, + mirnas: mirnasReducer, + chemicals: chemicalsReducer, + bioEntityContents: bioEntityContentsReducer, drawer: drawerReducer, map: mapReducer, + models: modelsReducer, }, 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} diff --git a/src/shared/Accordion/AccordionItemButton/AccordionItemButton.component.tsx b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.component.tsx index 32942ae9a996c6f83c3402b9f904a4d27e1351a3..09603e0609e575f13e43dd5d4facdd18eb6763cf 100644 --- a/src/shared/Accordion/AccordionItemButton/AccordionItemButton.component.tsx +++ b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.component.tsx @@ -1,14 +1,23 @@ -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> + ); +}; diff --git a/src/shared/Accordion/AccordionItemButton/AccordionItemButton.types.ts b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae2054d23759807ce59d282febcc2c5a091ef045 --- /dev/null +++ b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.types.ts @@ -0,0 +1 @@ +export type Variant = 'expandable' | 'non-expandable'; diff --git a/src/shared/Accordion/AccordionItemButton/AccordionItemButton.utils.tsx b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.utils.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2d597a92b01655e0b73d72516d30717caa3c75c1 --- /dev/null +++ b/src/shared/Accordion/AccordionItemButton/AccordionItemButton.utils.tsx @@ -0,0 +1,11 @@ +import { Icon } from '@/shared/Icon'; +import { Variant } from './AccordionItemButton.types'; + +export const getIcon = (variant: Variant): JSX.Element => { + const variantsIcons: Record<Variant, JSX.Element> = { + expandable: <Icon name="chevron-down" className="arrow-button h-6 w-6 fill-font-500" />, + 'non-expandable': <Icon name="chevron-right" className="h-6 w-6 fill-font-500" />, + }; + + return variantsIcons[variant]; +}; diff --git a/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.test.tsx b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e35e20491cc5d1f1b2e16ed4bd12b0866ad41df0 --- /dev/null +++ b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.test.tsx @@ -0,0 +1,72 @@ +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +import { DrawerHeadingBackwardButton } from './DrawerHeadingBackwardButton.component'; + +const backwardFunction = jest.fn(); + +const renderComponent = ( + title: string, + value: string, + initialStoreState: InitialStoreState = {}, +): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <DrawerHeadingBackwardButton + title={title} + value={value} + backwardFunction={backwardFunction} + /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('DrawerHeadingBackwardButton - component', () => { + beforeEach(() => { + backwardFunction.mockReset(); + }); + + it('should render passed values', () => { + renderComponent('Title', 'value'); + + expect(screen.getByRole('back-button')).toBeInTheDocument(); + expect(screen.getByText('Title:')).toBeInTheDocument(); + expect(screen.getByText('value')).toBeInTheDocument(); + expect(screen.getByRole('close-drawer-button')).toBeInTheDocument(); + }); + + it('should call backward function on back button click', () => { + renderComponent('Title', 'value'); + + const backButton = screen.getByRole('back-button'); + backButton.click(); + + expect(backwardFunction).toBeCalled(); + }); + + it('should call class drawer on close button click', () => { + const { store } = renderComponent('Title', 'value', { + drawer: { + isOpen: true, + drawerName: 'search', + searchDrawerState: { currentStep: 0, selectedValue: { name: '', valueType: 'none' } }, + }, + }); + expect(store.getState().drawer.isOpen).toBe(true); + + const closeButton = screen.getByRole('close-drawer-button'); + closeButton.click(); + + expect(store.getState().drawer.isOpen).toBe(false); + }); +}); diff --git a/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.tsx b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4e39e76ac513958f91f90b29693bc88eb1f50d9e --- /dev/null +++ b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.component.tsx @@ -0,0 +1,51 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { closeDrawer } from '@/redux/drawer/drawer.slice'; +import { IconButton } from '@/shared/IconButton'; +import { BACK_BUTTON_ROLE, CLOSE_BUTTON_ROLE } from './DrawerHeadingBackwardButton.constants'; + +export interface DrawerHeadingBackwardButtonProps { + title: string; + value: string; + backwardFunction: () => void; +} + +export const DrawerHeadingBackwardButton = ({ + backwardFunction, + title, + value, +}: DrawerHeadingBackwardButtonProps): JSX.Element => { + const dispatch = useAppDispatch(); + + const handleCloseDrawer = (): void => { + dispatch(closeDrawer()); + }; + + const onBackwardClick = (): void => { + backwardFunction(); + }; + + return ( + <div className="flex items-center justify-between border-b border-b-divide pl-4 pr-5"> + <div className="flex flex-row flex-nowrap items-center"> + <IconButton + className="h-6 w-6 bg-white-pearl" + icon="chevron-left" + classNameIcon="fill-font-500 h-6 w-6" + onClick={onBackwardClick} + role={BACK_BUTTON_ROLE} + /> + <div className="ml-2 py-8 text-xl"> + <span className="font-normal">{title}: </span> + <span className="font-semibold">{value}</span> + </div> + </div> + <IconButton + className="bg-white-pearl" + classNameIcon="fill-font-500" + icon="close" + role={CLOSE_BUTTON_ROLE} + onClick={handleCloseDrawer} + /> + </div> + ); +}; diff --git a/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.constants.tsx b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.constants.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8422870530be28c4877b289d32e73754160af761 --- /dev/null +++ b/src/shared/DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.constants.tsx @@ -0,0 +1,2 @@ +export const CLOSE_BUTTON_ROLE = 'close-drawer-button'; +export const BACK_BUTTON_ROLE = 'back-button'; diff --git a/src/shared/DrawerHeadingBackwardButton/index.ts b/src/shared/DrawerHeadingBackwardButton/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3fb46743bc8c6aa582eb014c30eef2d0eb3ef88 --- /dev/null +++ b/src/shared/DrawerHeadingBackwardButton/index.ts @@ -0,0 +1 @@ +export { DrawerHeadingBackwardButton } from './DrawerHeadingBackwardButton.component'; diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index b23a3ad43cb93631acb252e238ae2531626c3ff3..b683ed15a8d2d574411618f5f9b8486c41b8319f 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -12,6 +12,7 @@ import { PageIcon } from '@/shared/Icon/Icons/PageIcon'; import { PluginIcon } from '@/shared/Icon/Icons/PluginIcon'; import { PlusIcon } from '@/shared/Icon/Icons/PlusIcon'; import { CloseIcon } from '@/shared/Icon/Icons/CloseIcon'; +import { Pin } from '@/shared/Icon/Icons/Pin'; import type { IconTypes } from '@/types/iconTypes'; @@ -25,6 +26,7 @@ const icons = { 'chevron-left': ChevronLeftIcon, 'chevron-up': ChevronUpIcon, 'chevron-down': ChevronDownIcon, + pin: Pin, plus: PlusIcon, arrow: ArrowIcon, dots: DotsIcon, diff --git a/src/shared/Icon/Icons/Pin.tsx b/src/shared/Icon/Icons/Pin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f0ee2edd45997180603d7ecedbab5fb24c78daaa --- /dev/null +++ b/src/shared/Icon/Icons/Pin.tsx @@ -0,0 +1,20 @@ +interface PinOrangeProps { + className: string; +} + +export const Pin = ({ className }: PinOrangeProps): JSX.Element => ( + <svg + width="18" + height="24" + viewBox="0 0 18 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <path + d="M9 0C4.575 0 0 3.375 0 9C0 14.325 8.1 22.65 8.475 23.025C8.625 23.175 8.775 23.25 9 23.25C9.225 23.25 9.375 23.175 9.525 23.025C9.9 22.65 18 14.4 18 9C18 3.375 13.425 0 9 0ZM9 12C7.35 12 6 10.65 6 9C6 7.35 7.35 6 9 6C10.65 6 12 7.35 12 9C12 10.65 10.65 12 9 12Z" + fill="currentColor" + /> + <circle cx="9.0002" cy="8.99922" r="4.8" fill="currentColor" /> + </svg> +); diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ee1478dcee864ab2339be99906cb6f583d84cf1 --- /dev/null +++ b/src/types/drawerName.ts @@ -0,0 +1 @@ +export type DrawerName = 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend'; diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 9b14f66b477ef940782e3c94b8d1079b8683af89..c37714cb64db3ae771784cd0978c5858bfdf5536 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -12,4 +12,5 @@ export type IconTypes = | 'legend' | 'page' | 'plugin' - | 'close'; + | 'close' + | 'pin'; diff --git a/src/types/models.ts b/src/types/models.ts index abadf02ac342719044fbc94260c65ed3704d8b71..8902849349a999a58661d14d73d5734574f96130 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -6,6 +6,7 @@ import { mirnaSchema } from '@/models/mirnaSchema'; import { organism } from '@/models/organism'; import { projectSchema } from '@/models/project'; import { z } from 'zod'; +import { modelSchema } from '@/models/modelSchema'; export type Project = z.infer<typeof projectSchema>; export type Organism = z.infer<typeof organism>; @@ -13,4 +14,5 @@ export type Disease = z.infer<typeof disease>; export type Drug = z.infer<typeof drugSchema>; export type Mirna = z.infer<typeof mirnaSchema>; export type BioEntityContent = z.infer<typeof bioEntityContentSchema>; +export type Model = z.infer<typeof modelSchema>; export type Chemical = z.infer<typeof chemicalSchema>; diff --git a/src/types/pathName.ts b/src/types/pathName.ts deleted file mode 100644 index 2b61453e4b4469bf51cc26f440f5b49985c302b6..0000000000000000000000000000000000000000 --- a/src/types/pathName.ts +++ /dev/null @@ -1 +0,0 @@ -export type PathName = 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend'; diff --git a/src/utils/renderComponentWithProvider.tsx b/src/utils/renderComponentWithProvider.tsx deleted file mode 100644 index c62bc5d9438bc3742549ae737db262e5567ca1dc..0000000000000000000000000000000000000000 --- 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/src/utils/testing/getReduxWrapperWithStore.tsx b/src/utils/testing/getReduxWrapperWithStore.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bfff374c2d6fa878ef1cae2ee7a75637c57d5a7e --- /dev/null +++ b/src/utils/testing/getReduxWrapperWithStore.tsx @@ -0,0 +1,48 @@ +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 mapReducer from '@/redux/map/map.slice'; +import mirnasReducer from '@/redux/mirnas/mirnas.slice'; +import modelsReducer from '@/redux/models/models.slice'; +import projectReducer from '@/redux/project/project.slice'; +import searchReducer from '@/redux/search/search.slice'; +import { RootState, StoreType } from '@/redux/store'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; + +interface WrapperProps { + children: React.ReactNode; +} + +export type InitialStoreState = Partial<RootState>; + +type GetReduxWrapperUsingSliceReducer = (initialState?: InitialStoreState) => { + Wrapper: ({ children }: WrapperProps) => JSX.Element; + store: StoreType; +}; + +export const getReduxWrapperWithStore: GetReduxWrapperUsingSliceReducer = ( + preloadedState: InitialStoreState = {}, +) => { + const testStore = configureStore({ + reducer: { + search: searchReducer, + project: projectReducer, + drugs: drugsReducer, + mirnas: mirnasReducer, + chemicals: chemicalsReducer, + bioEntityContents: bioEntityContentsReducer, + drawer: drawerReducer, + models: modelsReducer, + map: mapReducer, + }, + preloadedState, + }); + + const Wrapper = ({ children }: WrapperProps): JSX.Element => ( + <Provider store={testStore}>{children}</Provider> + ); + + return { Wrapper, store: testStore }; +}; diff --git a/tsconfig.json b/tsconfig.json index 338c730927fddced861320bac441ccaa9568de16..a44f42a56e52544084e6f465ad04da481689ef14 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" ],