diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index e6500c336107407611d0c6b1646b67dccce12202..2570bc5fab1e1cff71e9010cc2ad321d78a9a182 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -1,11 +1,11 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { modalSelector } from '@/redux/modal/modal.selector'; import dynamic from 'next/dynamic'; +import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; +import { ModalLayout } from './ModalLayout'; import { OverviewImagesModal } from './OverviewImagesModal'; import { PublicationsModal } from './PublicationsModal'; -import { ModalLayout } from './ModalLayout'; -import { EditOverlayModal } from './EditOverlayModal'; const MolArtModal = dynamic( () => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal), diff --git a/src/components/FunctionalArea/Modal/MolArtModal/MolArtModal.component.tsx b/src/components/FunctionalArea/Modal/MolArtModal/MolArtModal.component.tsx index a7cdd7eb9b9aa871613e8d8cf1e39250973104d9..73a3e0f00a6a92c1374cc84bd2b554e5e181404f 100644 --- a/src/components/FunctionalArea/Modal/MolArtModal/MolArtModal.component.tsx +++ b/src/components/FunctionalArea/Modal/MolArtModal/MolArtModal.component.tsx @@ -1,5 +1,9 @@ +import { MOLART_ERRORS } from '@/constants/errors'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { currentSelectedBioEntityIdSelector } from '@/redux/modal/modal.selector'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { showToast } from '@/utils/showToast'; import * as MolArt from 'molart'; import * as React from 'react'; import { useEffect } from 'react'; @@ -8,12 +12,25 @@ const containerId = 'molart-container'; export const MolArtModal: React.FC = () => { const uniprot = useAppSelector(currentSelectedBioEntityIdSelector); + const dispatch = useAppDispatch(); useEffect(() => { - const molart = new MolArt({ - uniprotId: uniprot, - containerId, - }); + let molart: typeof MolArt; + + try { + molart = new MolArt({ + uniprotId: uniprot, + containerId, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + dispatch(closeModal()); + showToast({ + type: 'error', + message: MOLART_ERRORS.UNEXPECTED_MOLART_ERROR, + }); + } return () => { try { @@ -24,7 +41,7 @@ export const MolArtModal: React.FC = () => { console.error(e); } }; - }, [uniprot]); + }, [uniprot, dispatch]); return ( <div className="flex h-full w-full items-center justify-center bg-white" id={containerId} /> diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx index 4925b9c9b892ab79626a1f0832aeb5f31c6c6fca..53b8716b44e52f463718690853c455dfed68eb92 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.test.tsx @@ -1,11 +1,16 @@ +import { reactionsFixture } from '@/models/fixtures/reactionFixture'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { StoreType } from '@/redux/store'; -import { useRouter } from 'next/router'; -import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; import { fireEvent, render, screen } from '@testing-library/react'; +import { useRouter } from 'next/router'; import { SearchBar } from './SearchBar.component'; -const renderComponent = (): { store: StoreType } => { - const { Wrapper, store } = getReduxWrapperWithStore(); +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); return ( render( @@ -89,4 +94,24 @@ describe('SearchBar - component', () => { expect(selectedSearchElement).toBe('nadh'); }); + + it('should reset reactions data on search click', () => { + const { store } = renderComponent({ + reactions: { + ...INITIAL_STORE_STATE_MOCK.reactions, + data: reactionsFixture, + }, + }); + const input = screen.getByTestId<HTMLInputElement>('search-input'); + + fireEvent.change(input, { target: { value: 'nadh' } }); + + const button = screen.getByRole('button'); + + fireEvent.click(button); + + const { reactions } = store.getState(); + + expect(reactions.data).toStrictEqual([]); + }); }); diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx index ee5d31369ed9062989ef97c07c4d762c1933cb1c..5671f01fdd381b6c182e666094c2a1d535795502 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx @@ -2,14 +2,15 @@ import lensIcon from '@/assets/vectors/icons/lens.svg'; import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; import { openSearchDrawerWithSelectedTab, selectTab } from '@/redux/drawer/drawer.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { isPendingSearchStatusSelector, perfectMatchSelector, } from '@/redux/search/search.selectors'; import { getSearchData } from '@/redux/search/search.thunks'; import Image from 'next/image'; -import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { getDefaultSearchTab, getSearchValuesArrayAndTrimToSeven } from './SearchBar.utils'; @@ -45,6 +46,7 @@ export const SearchBar = (): JSX.Element => { const onSearchClick = (): void => { const searchValues = getSearchValuesArrayAndTrimToSeven(searchValue); + dispatch(resetReactionsData()); dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch })); openSearchDrawerIfClosed(getDefaultSearchTab(searchValues)); }; @@ -53,6 +55,7 @@ export const SearchBar = (): JSX.Element => { const searchValues = getSearchValuesArrayAndTrimToSeven(searchValue); if (event.code === ENTER_KEY_CODE) { + dispatch(resetReactionsData()); dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch })); openSearchDrawerIfClosed(getDefaultSearchTab(searchValues)); } diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils.ts b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils.ts index 9b2eceb4c2ecec3144cc4899e6872ae9d9aa55b8..de535a1ffeeb102dc7075e6945bc1264f1f71f8e 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils.ts +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils.ts @@ -1,8 +1,9 @@ const BEGINNING = 0; const END = 6; +const SEPARATOR = ';'; export const getSearchValuesArrayAndTrimToSeven = (searchString: string): string[] => - searchString.split(';').slice(BEGINNING, END); + searchString.split(SEPARATOR).slice(BEGINNING, END); export const getDefaultSearchTab = (searchValues: string[]): string => { const FIRST = 0; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts index a72087891e2d9625c718d3993e448f03c4cfb230..a14476cfb9650068b07a5185a0337441675bf09f 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts @@ -1,5 +1,7 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { isActiveLegendSelector } from '@/redux/legend/legend.selectors'; +import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice'; import { isPluginActiveSelector, isPluginLoadingSelector, @@ -7,11 +9,10 @@ import { } from '@/redux/plugins/plugins.selectors'; import { removePlugin } from '@/redux/plugins/plugins.slice'; import { PluginsManager } from '@/services/pluginsManager'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; +import { getErrorMessage } from '@/utils/getErrorMessage'; import { showToast } from '@/utils/showToast'; import axios from 'axios'; -import { getErrorMessage } from '@/utils/getErrorMessage'; -import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice'; -import { isActiveLegendSelector } from '@/redux/legend/legend.selectors'; import { PLUGIN_LOADING_ERROR_PREFIX } from '../../AvailablePluginsDrawer.constants'; type UseLoadPluginReturnType = { @@ -83,6 +84,7 @@ export const useLoadPlugin = ({ handleRemoveLegend(); PluginsManager.removePluginContent({ hash }); + PluginsEventBus.dispatchEvent('onPluginUnload', { hash }); }; const handleReloadPlugin = async (): Promise<void> => { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx index 7a3f8c17ece6aebd1a04e89877b45d13458d0f42..084bf61124bcb3a98391ce21c10b578ba44a9a5f 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx @@ -1,7 +1,9 @@ /* eslint-disable no-magic-numbers */ import { DEFAULT_MAX_ZOOM } from '@/constants/map'; import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { reactionsFixture } from '@/models/fixtures/reactionFixture'; import { MAP_INITIAL_STATE } from '@/redux/map/map.constants'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { AppDispatch, RootState, StoreType } from '@/redux/store'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { @@ -276,4 +278,57 @@ describe('BioEntitiesPinsListItem - component ', () => { ]), ); }); + + it('should reset reactions on fullName click', async () => { + const { store } = renderComponent( + BIO_ENTITY.name, + { + ...BIO_ENTITY, + x: undefined, + y: undefined, + }, + { + map: { + ...MAP_INITIAL_STATE, + data: { + ...MAP_INITIAL_STATE.data, + modelId: 5052, + size: { + width: 256, + height: 256, + tileSize: 256, + minZoom: 1, + maxZoom: 1, + }, + position: { + initial: { + x: 0, + y: 0, + z: 2, + }, + last: { + x: 1, + y: 1, + z: 3, + }, + }, + }, + }, + reactions: { + ...INITIAL_STORE_STATE_MOCK.reactions, + data: reactionsFixture, + }, + }, + ); + const button = screen.getByText(BIO_ENTITY.name); + expect(button).toBeInTheDocument(); + + act(() => { + button.click(); + }); + + const state = store.getState(); + + expect(state.reactions.data).toStrictEqual([]); + }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx index 77e7d4edb35b70342bfb522b7b4d2331dde6d9e6..fbc712831868f42dc694bfbddd8d4796ca9e477d 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx @@ -13,6 +13,7 @@ import { numberByEntityNumberIdSelector } from '@/redux/entityNumber/entityNumbe import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { setMapPosition } from '@/redux/map/map.slice'; +import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { getSearchData } from '@/redux/search/search.thunks'; import { twMerge } from 'tailwind-merge'; import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; @@ -53,6 +54,7 @@ export const BioEntitiesPinsListItem = ({ const handleSearchMapForPin = (): void => { const searchValues = getSearchValuesArrayAndTrimToSeven(name); + dispatch(resetReactionsData()); dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch: true })); dispatch(openSearchDrawerWithSelectedTab(getDefaultSearchTab(searchValues))); }; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.test.tsx index 2631f91b3e40514399ee92ab44cc6c2ccf1f5cc7..c08187dc9d36cbc6197a92f9dc4ea29711aadbca 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.test.tsx @@ -1,11 +1,14 @@ -import { act } from 'react-dom/test-utils'; +import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, THIRD_ARRAY_ELEMENT } from '@/constants/common'; +import { SEARCH_STATE_INITIAL_MOCK } from '@/redux/search/search.mock'; +import { AppDispatch, RootState, StoreType } from '@/redux/store'; +import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { StoreType } from '@/redux/store'; import { render, screen } from '@testing-library/react'; -import { SEARCH_STATE_INITIAL_MOCK } from '@/redux/search/search.mock'; +import { act } from 'react-dom/test-utils'; +import { MockStoreEnhanced } from 'redux-mock-store'; import { PerfectMatchSwitch } from './PerfectMatchSwitch.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -23,6 +26,23 @@ const renderComponent = (initialStoreState: InitialStoreState = {}): { store: St ); }; +const renderComponentWithActionListener = ( + initialStoreState: InitialStoreState = {}, +): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { + const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState); + + return ( + render( + <Wrapper> + <PerfectMatchSwitch /> + </Wrapper>, + ), + { + store, + } + ); +}; + describe('PerfectMatchSwitch - component', () => { it('should initialy be set to false when perfectMatch is not in query or set to false', () => { renderComponent({ search: SEARCH_STATE_INITIAL_MOCK }); @@ -62,4 +82,32 @@ describe('PerfectMatchSwitch - component', () => { expect(store.getState().search.perfectMatch).toBe(false); }); + it('should trigger get search data and reset reactions on checked value change', async () => { + const { store } = renderComponentWithActionListener({ + search: { ...SEARCH_STATE_INITIAL_MOCK, perfectMatch: false, searchValue: ['nadh', 'scna'] }, + }); + + const checkbox = screen.getByRole<HTMLInputElement>('checkbox'); + act(() => { + checkbox.click(); + }); + + const actions = store.getActions(); + + expect(actions[FIRST_ARRAY_ELEMENT]).toStrictEqual({ + payload: true, + type: 'search/setPerfectMatch', + }); + + expect(actions[SECOND_ARRAY_ELEMENT]).toStrictEqual({ + payload: undefined, + type: 'reactions/resetReactionsData', + }); + + expect(actions[THIRD_ARRAY_ELEMENT].meta.arg).toStrictEqual({ + isPerfectMatch: true, + searchQueries: ['nadh', 'scna'], + }); + expect(actions[THIRD_ARRAY_ELEMENT].type).toEqual('project/getSearchData/pending'); + }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.tsx index 1e38a26326a0d6e59b01de2302bac725f3be7320..c58d239020e0a17ef7839d779bfca8dba8de7a58 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerHeader/PerfectMatchSwitch/PerfectMatchSwitch.component.tsx @@ -1,16 +1,27 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { perfectMatchSelector } from '@/redux/search/search.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { resetReactionsData } from '@/redux/reactions/reactions.slice'; +import { perfectMatchSelector, searchValueSelector } from '@/redux/search/search.selectors'; import { setPerfectMatch } from '@/redux/search/search.slice'; +import { getSearchData } from '@/redux/search/search.thunks'; import { ChangeEvent } from 'react'; export const PerfectMatchSwitch = (): JSX.Element => { const dispatch = useAppDispatch(); const isChecked = useAppSelector(perfectMatchSelector); + const searchValue = useAppSelector(searchValueSelector); + + const setChecked = (value: boolean): void => { + dispatch(setPerfectMatch(value)); + }; + + const setCheckedAndRefreshSearch = (event: ChangeEvent<HTMLInputElement>): void => { + const isCheckedNewValue = event.target.checked; - const setChecked = (event: ChangeEvent<HTMLInputElement>): void => { - dispatch(setPerfectMatch(event.target.checked)); + setChecked(isCheckedNewValue); + dispatch(resetReactionsData()); + dispatch(getSearchData({ searchQueries: searchValue, isPerfectMatch: isCheckedNewValue })); }; return ( @@ -22,7 +33,7 @@ export const PerfectMatchSwitch = (): JSX.Element => { value="" className="peer sr-only" checked={isChecked} - onChange={setChecked} + onChange={setCheckedAndRefreshSearch} /> <div className="peer h-6 w-11 rounded-full bg-greyscale-500 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-med-sea-green peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none rtl:peer-checked:after:-translate-x-full" /> </label> diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts index a2f23be1f53f13cfe3e5aea6ee8bd0674db12f26..b50e4b9e98070f52cf2406dcbd2e5dd9edc7ba21 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts @@ -26,9 +26,9 @@ describe('handleFeaturesClick - util', () => { expect(dispatchEventSpy).toHaveBeenCalledWith('onPinIconClick', { id: featureId }); }); - it('should return shouldBlockCoordSearch=false', () => { + it('should return shouldBlockCoordSearch=true', () => { expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ - shouldBlockCoordSearch: false, + shouldBlockCoordSearch: true, }); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts index 3c1e5923d2290df828598a4b46d8c175979f020c..ff191109e84629da4c797cc8875789f8d37fef15 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts @@ -1,3 +1,4 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { FEATURE_TYPE, PIN_ICON_ANY, SURFACE_ANY } from '@/constants/features'; import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice'; import { clearSearchData } from '@/redux/search/search.slice'; @@ -13,9 +14,9 @@ export const handleFeaturesClick = ( features: FeatureLike[], dispatch: AppDispatch, ): HandleFeaturesClickResult => { - let shouldBlockCoordSearch = false; const pinFeatures = features.filter(feature => PIN_ICON_ANY.includes(feature.get('type'))); const surfaceFeatures = features.filter(feature => SURFACE_ANY.includes(feature.get('type'))); + const shouldBlockCoordSearch = pinFeatures.length > SIZE_OF_EMPTY_ARRAY; pinFeatures.forEach(pin => { const pinId = pin.get('id') as string | number; @@ -24,7 +25,6 @@ export const handleFeaturesClick = ( if (pin.get('type') === FEATURE_TYPE.PIN_ICON_BIOENTITY) { dispatch(clearSearchData()); dispatch(openBioEntityDrawerById(pinId)); - shouldBlockCoordSearch = true; } }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts index 58ee8bf0e88ac60f71eb84361c6c67a55aed8c4e..4daa661f7202dc3f6137bb1d579267e0b45aefd2 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts @@ -68,16 +68,22 @@ describe('handleReactionResults - util', () => { expect(actions[2].payload).toEqual(reactionsFixture[FIRST_ARRAY_ELEMENT].id); }); + it('should run select tab as third action', () => { + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); + expect(actions[3].type).toEqual('drawer/selectTab'); + }); + it('should run setBioEntityContent to empty array as third action', () => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[3].type).toEqual('project/getMultiBioEntity/pending'); + expect(actions[4].type).toEqual('project/getMultiBioEntity/pending'); }); it('should run getBioEntityContents as fourth action', () => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[4].type).toEqual('project/getBioEntityContents/pending'); + expect(actions[5].type).toEqual('project/getBioEntityContents/pending'); }); it('should run getBioEntityContents fullfilled as fourth action', () => { diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts index 564f106be15fbee31cff1611ae95a0228542f89a..bee6326b53484b58942b8ac77cf78e68a1d112f1 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts @@ -1,6 +1,6 @@ import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks'; -import { openReactionDrawerById } from '@/redux/drawer/drawer.slice'; +import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { getReactionsByIds } from '@/redux/reactions/reactions.thunks'; import { AppDispatch } from '@/redux/store'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; @@ -26,6 +26,7 @@ export const handleReactionResults = const bioEntitiesIds = [...productsIds, ...reactantsIds, ...modifiersIds].map(identifier => String(identifier)); dispatch(openReactionDrawerById(reaction.id)); + dispatch(selectTab('')); await dispatch( getMultiBioEntity({ searchQueries: bioEntitiesIds, diff --git a/src/constants/errors.ts b/src/constants/errors.ts index cba5c0d0c5954b0a36f8b40c6188afbb20117dd1..d21cfea4d64f473e8a10fba56593bb7d0e1dafff 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -10,3 +10,7 @@ export const ZOOM_ERRORS = { ZOOM_VALUE_TOO_LOW: (minZoom: number): string => `Provided zoom value exceeds min zoom of ${minZoom}`, }; + +export const MOLART_ERRORS = { + UNEXPECTED_MOLART_ERROR: 'UNEXPECTED_MOLART_ERROR', +}; diff --git a/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts index e804a294f9ad4f681661e008f15897543d1db1d9..b4beac3eee835d5984b8849c6c9c5be171ed41c5 100644 --- a/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts @@ -1,14 +1,15 @@ -import { RootState, store } from '@/redux/store'; -import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; -import { apiPath } from '@/redux/apiPath'; -import { HttpStatusCode } from 'axios'; import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { drugsFixture } from '@/models/fixtures/drugFixtures'; import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; +import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { apiPath } from '@/redux/apiPath'; import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; -import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; import { MAP_INITIAL_STATE } from '@/redux/map/map.constants'; +import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; +import { resetReactionsData } from '@/redux/reactions/reactions.slice'; +import { RootState, store } from '@/redux/store'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { waitFor } from '@testing-library/react'; +import { HttpStatusCode } from 'axios'; import { searchByQuery } from './searchByQuery'; import { searchFitBounds } from './searchFitBounds'; @@ -116,4 +117,32 @@ describe('searchByQuery', () => { expect(searchFitBounds).not.toHaveBeenCalled(); }); }); + + it('should reset reactions data on every trigger', async () => { + dispatchSpy.mockImplementation(() => ({ + unwrap: (): Promise<void> => Promise.resolve(), + })); + + getStateSpy.mockImplementation(() => MOCK_SEARCH_BY_QUERY_STORE as RootState); + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + mockedAxiosClient + .onGet(apiPath.getDrugsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, drugsFixture); + + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, chemicalsFixture); + + searchByQuery(SEARCH_QUERY, false, true); + + expect(dispatchSpy).toHaveBeenCalledWith(resetReactionsData()); + }); }); diff --git a/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts b/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts index e29c50f97532b73a77fe158f1d4e2571ff58bb4b..c195ee6cbce0094646e719ef91b840105fb40d8d 100644 --- a/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts +++ b/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts @@ -1,4 +1,5 @@ import { getSearchValuesArrayAndTrimToSeven } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils'; +import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { getSearchData } from '@/redux/search/search.thunks'; import { store } from '@/redux/store'; import { displaySearchDrawerWithSelectedDefaultTab } from './displaySearchDrawerWithSelectedDefaultTab'; @@ -13,6 +14,8 @@ export const searchByQuery = ( const searchValues = getSearchValuesArrayAndTrimToSeven(query); const isPerfectMatch = !!perfectSearch; + dispatch(resetReactionsData()); + dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch })) ?.unwrap() .then(() => { diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts index 1b522bb065723a9ef78cf2d24e01869960c3c00a..707fb16246d0749bdeba2dbada9517b4d6228e69 100644 --- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts @@ -1,16 +1,16 @@ /* eslint-disable no-magic-numbers */ -import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetworkResponse'; -import { apiPath } from '@/redux/apiPath'; -import { HttpStatusCode } from 'axios'; -import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { handleSearchResultAction } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction'; +import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; -import { RootState, store } from '@/redux/store'; +import { drugsFixture } from '@/models/fixtures/drugFixtures'; import { ELEMENT_SEARCH_RESULT_MOCK_ALIAS } from '@/models/mocks/elementSearchResultMock'; -import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { apiPath } from '@/redux/apiPath'; +import { RootState, store } from '@/redux/store'; +import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetworkResponse'; import { waitFor } from '@testing-library/react'; -import { handleSearchResultAction } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction'; -import { triggerSearch } from './triggerSearch'; +import { HttpStatusCode } from 'axios'; import { ERROR_INVALID_MODEL_ID_TYPE } from '../../errorMessages'; +import { triggerSearch } from './triggerSearch'; const mockedAxiosClient = mockNetworkNewAPIResponse(); const mockedAxiosOldClient = mockNetworkResponse(); @@ -70,7 +70,7 @@ describe('triggerSearch', () => { }), ).resolves.toBe(undefined); - expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledTimes(3); expect(store.dispatch).not.toHaveBeenCalledWith({ payload: SEARCH_QUERY, @@ -117,7 +117,7 @@ describe('triggerSearch', () => { ).resolves.toBe(undefined); expect(getState).toHaveBeenCalled(); - expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledTimes(3); expect(store.dispatch).not.toHaveBeenLastCalledWith({ payload: SEARCH_QUERY, type: 'drawer/openSearchDrawerWithSelectedTab', diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts index 2969f8ab7be3e86ef9f44f871c4eace00d0e97a4..e34911324f81a8824d842d7f4f4955d9ca4765ad 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts @@ -20,6 +20,9 @@ const PLUGINS_EVENTS = { search: { onSearch: 'onSearch', }, + plugins: { + onPluginUnload: 'onPluginUnload', + }, }; export const ALLOWED_PLUGINS_EVENTS = Object.values(PLUGINS_EVENTS).flatMap(obj => diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts index 66d4ab43fd2d6dd73422f714ada6c8a897592d0e..7ca8a74b6529806d7c81de339b756892698c2283 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts @@ -8,11 +8,13 @@ import type { ClickedSurfaceOverlay, Events, EventsData, + PluginUnloaded, PluginsEventBusType, SearchData, ZoomChanged, } from './pluginsEventBus.types'; +export function dispatchEvent(type: 'onPluginUnload', data: PluginUnloaded): void; export function dispatchEvent(type: 'onAddDataOverlay', createdOverlay: CreatedOverlay): void; export function dispatchEvent(type: 'onRemoveDataOverlay', overlayId: number): void; export function dispatchEvent(type: 'onShowOverlay', overlay: MapOverlay): void; diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts index 0fdec91300559d50089301f264031c2d069f5fc4..9e5a86aaf26dfb6169edb7cffcb2e31d3c7bf692 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts @@ -24,7 +24,9 @@ export type SubmapEvents = | 'onPinIconClick'; export type SearchEvents = 'onSearch'; -export type Events = OverlayEvents | BackgroundEvents | SubmapEvents | SearchEvents; +export type PluginEvents = 'onPluginUnload'; + +export type Events = OverlayEvents | BackgroundEvents | SubmapEvents | SearchEvents | PluginEvents; export type ZoomChanged = { zoom: number; @@ -69,6 +71,10 @@ export type SearchDataChemicals = { results: Chemical[][]; }; +export type PluginUnloaded = { + hash: string; +}; + export type SearchData = SearchDataBioEntity | SearchDataDrugs | SearchDataChemicals; export type EventsData = @@ -80,7 +86,8 @@ export type EventsData = | ClickedBioEntity | ClickedPinIcon | ClickedSurfaceOverlay - | SearchData; + | SearchData + | PluginUnloaded; export type PluginsEventBusType = { events: {