diff --git a/package.json b/package.json index 54dfdef6f81b5365f64141c7b74a92f419bdae94..72f51b8d83f11523c2886d9e81c7961c52d4206f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/react-dom": "18.2.7", "autoprefixer": "10.4.15", "axios": "^1.5.1", + "axios-cache-interceptor": "^1.5.1", "axios-hooks": "^5.0.0", "crypto-js": "^4.2.0", "downshift": "^8.2.3", 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 c5e4e4029fa87e2bed4477c4666c8d41729959e6..7a3f8c17ece6aebd1a04e89877b45d13458d0f42 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 @@ -15,6 +15,13 @@ import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component'; import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity; +const INITIAL_STORE_WITH_ENTITY_NUMBER: InitialStoreState = { + entityNumber: { + data: { + [BIO_ENTITY.elementId]: 123, + }, + }, +}; const renderComponent = ( name: string, @@ -56,14 +63,14 @@ const renderComponentWithActionListener = ( describe('BioEntitiesPinsListItem - component ', () => { it('should display name of bio entity element', () => { - renderComponent(BIO_ENTITY.name, BIO_ENTITY); + renderComponent(BIO_ENTITY.name, BIO_ENTITY, INITIAL_STORE_WITH_ENTITY_NUMBER); const bioEntityName = BIO_ENTITY.fullName || ''; expect(screen.getByText(bioEntityName, { exact: false })).toBeInTheDocument(); }); it('should display symbol of bio entity element', () => { - renderComponent(BIO_ENTITY.name, BIO_ENTITY); + renderComponent(BIO_ENTITY.name, BIO_ENTITY, INITIAL_STORE_WITH_ENTITY_NUMBER); const bioEntitySymbol = BIO_ENTITY.symbol || ''; @@ -80,14 +87,14 @@ describe('BioEntitiesPinsListItem - component ', () => { expect(screen.queryAllByTestId('bio-entity-symbol')).toHaveLength(0); }); it('should display string type of bio entity element', () => { - renderComponent(BIO_ENTITY.name, BIO_ENTITY); + renderComponent(BIO_ENTITY.name, BIO_ENTITY, INITIAL_STORE_WITH_ENTITY_NUMBER); const bioEntityStringType = BIO_ENTITY.stringType; expect(screen.getByText(bioEntityStringType, { exact: false })).toBeInTheDocument(); }); it('should display synonyms of bio entity element', () => { - renderComponent(BIO_ENTITY.name, BIO_ENTITY); + renderComponent(BIO_ENTITY.name, BIO_ENTITY, INITIAL_STORE_WITH_ENTITY_NUMBER); const firstBioEntitySynonym = BIO_ENTITY.synonyms[0]; const secondBioEntitySynonym = BIO_ENTITY.synonyms[1]; @@ -96,7 +103,7 @@ describe('BioEntitiesPinsListItem - component ', () => { expect(screen.getByText(secondBioEntitySynonym, { exact: false })).toBeInTheDocument(); }); it('should display list of references for pin', () => { - renderComponent(BIO_ENTITY.name, BIO_ENTITY); + renderComponent(BIO_ENTITY.name, BIO_ENTITY, INITIAL_STORE_WITH_ENTITY_NUMBER); const firstPinReferenceType = BIO_ENTITY.references[0].type; const firstPinReferenceResource = BIO_ENTITY.references[0].resource; @@ -110,6 +117,7 @@ describe('BioEntitiesPinsListItem - component ', () => { }); it('should center map to pin coordinates after click on pin icon', async () => { const { store } = renderComponent(BIO_ENTITY.name, BIO_ENTITY, { + ...INITIAL_STORE_WITH_ENTITY_NUMBER, map: { ...MAP_INITIAL_STATE, data: { @@ -160,6 +168,7 @@ describe('BioEntitiesPinsListItem - component ', () => { y: undefined, }, { + ...INITIAL_STORE_WITH_ENTITY_NUMBER, map: { ...MAP_INITIAL_STATE, data: { @@ -211,6 +220,7 @@ describe('BioEntitiesPinsListItem - component ', () => { y: undefined, }, { + ...INITIAL_STORE_WITH_ENTITY_NUMBER, map: { ...MAP_INITIAL_STATE, data: { 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 ceff8eb02308d091e466e63055f2a09211bbd4a5..77e7d4edb35b70342bfb522b7b4d2331dde6d9e6 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 @@ -1,17 +1,20 @@ +/* eslint-disable @next/next/no-img-element */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { getDefaultSearchTab, getSearchValuesArrayAndTrimToSeven, } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils'; +import { getCanvasIcon } from '@/components/Map/MapViewer/utils/config/getCanvasIcon'; +import { PINS_COLORS } from '@/constants/canvas'; import { DEFAULT_MAX_ZOOM } from '@/constants/map'; import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice'; +import { numberByEntityNumberIdSelector } from '@/redux/entityNumber/entityNumber.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { setMapPosition } from '@/redux/map/map.slice'; import { getSearchData } from '@/redux/search/search.thunks'; -import { Icon } from '@/shared/Icon'; import { twMerge } from 'tailwind-merge'; -import { getPinColor } from '../../../ResultsList/PinsList/PinsListItem/PinsListItem.component.utils'; import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; import { isPinWithCoordinates } from './BioEntitiesPinsListItem.utils'; @@ -26,6 +29,13 @@ export const BioEntitiesPinsListItem = ({ }: BioEntitiesPinsListItemProps): JSX.Element => { const dispatch = useAppDispatch(); const pinHasCoords = isPinWithCoordinates(pin); + const pinIconValue = useAppSelector(state => + numberByEntityNumberIdSelector(state, pin.elementId || ''), + ); + const pinIconCanvas = getCanvasIcon({ + color: PINS_COLORS.bioEntity, + value: pinIconValue, + }); const handleCenterMapToPin = (): void => { if (!pinHasCoords) { @@ -56,7 +66,7 @@ export const BioEntitiesPinsListItem = ({ className={twMerge('mr-2 shrink-0', !pinHasCoords && 'cursor-default')} data-testid="center-to-pin-button" > - <Icon name="pin" className={getPinColor('bioEntity')} /> + <img src={pinIconCanvas.toDataURL()} alt="pin icon" /> </button> <p> {pin.stringType ? `${pin.stringType}: ` : ''} diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts index e1e6cce5b1aa6d7af4cf29fde5c4626ebe230335..4fe4314ac40d53fd644441e26ef332cb3c7c4d5d 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts @@ -6,6 +6,7 @@ export type PinListBioEntity = Pick<BioEntity, 'synonyms' | 'references'> & { fullName?: BioEntity['fullName']; x?: BioEntity['x']; y?: BioEntity['y']; + elementId?: BioEntity['elementId']; }; export type PinListBioEntityWithCoords = PinListBioEntity & { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx index 6e91ddbfc171362a1fece6eb2ac85673645c519d..3a734b7d7dadfa5ea848dbf04657d1e9793a9dcb 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx @@ -1,7 +1,10 @@ +import { entityNumberDataSelector } from '@/redux/entityNumber/entityNumber.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { assertNever } from '@/utils/assertNever'; import { AccordionsDetails } from '../AccordionsDetails/AccordionsDetails.component'; import { PinItem, PinTypeWithNone } from './PinsList.types'; import { PinsListItem } from './PinsListItem'; +import { getTargetElementsUniqueSorted } from './utils/getTargetElementsUniqueSorted'; interface PinsListProps { pinsList: PinItem[]; @@ -9,35 +12,33 @@ interface PinsListProps { } export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => { + const entityNumber = useAppSelector(entityNumberDataSelector); + switch (type) { case 'drugs': + case 'chemicals': { + const targetElements = getTargetElementsUniqueSorted(pinsList, { entityNumber }); + return ( <div className="h-[calc(100%-214px)] max-h-[calc(100%-214px)] overflow-auto"> <AccordionsDetails pinsList={pinsList} type={type} /> <ul className="px-6 py-2" data-testid="pins-list"> - {pinsList.map(result => { - return result.data.targets.map(pin => ( - <PinsListItem key={pin.name} name={pin.name} type={type} pin={pin} /> - )); - })} + {targetElements.map(({ target, element }) => ( + <PinsListItem + key={element.elementId} + name={target.name} + type={type} + pin={target} + element={element} + number={entityNumber[element.elementId]} + /> + ))} </ul> </div> ); + } case 'bioEntity': return <div />; - case 'chemicals': - return ( - <div className="h-[calc(100%-214px)] max-h-[calc(100%-214px)] overflow-auto"> - <AccordionsDetails pinsList={pinsList} type={type} /> - <ul className="px-6 py-2" data-testid="pins-list"> - {pinsList.map(result => { - return result.data.targets.map(pin => ( - <PinsListItem key={pin.name} name={pin.name} type={type} pin={pin} /> - )); - })} - </ul> - </div> - ); case 'none': return <div />; default: diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.types.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.types.tsx index e0bdafa37a72ba495598c4aa8c333cf005c958cd..a72f8629e2eb718e43e56b5471183f837f785aba 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.types.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.types.tsx @@ -1,4 +1,4 @@ -import { Chemical, Drug } from '@/types/models'; +import { BioEntity, Chemical, Drug, PinDetailsItem } from '@/types/models'; import { PinType } from '@/types/pin'; export type PinItem = { @@ -14,3 +14,8 @@ export type AvailableSubmaps = { modelId: number; name: string; }; + +export type TargetElement = { + target: PinDetailsItem; + element: BioEntity; +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx index cff97d8b721a5e7023a8e26504afcc747174e606..c1ed276c8a3efaf6727c2b790cd33080a2e84680 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx @@ -2,35 +2,17 @@ import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; import { drugsFixture } from '@/models/fixtures/drugFixtures'; -import { StoreType } from '@/redux/store'; -import { PinDetailsItem } from '@/types/models'; -import { - InitialStoreState, - getReduxWrapperWithStore, -} from '@/utils/testing/getReduxWrapperWithStore'; +import { AppDispatch, RootState } from '@/redux/store'; +import { BioEntity, PinDetailsItem } from '@/types/models'; +import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore'; import { render, screen } from '@testing-library/react'; // import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; -import { act } from 'react-dom/test-utils'; +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; +import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; +import { MockStoreEnhanced } from 'redux-mock-store'; import { PinTypeWithNone } from '../PinsList.types'; import { PinsListItem } from './PinsListItem.component'; -import { useVisiblePinsPolygonCoordinates } from './hooks/useVisiblePinsPolygonCoordinates'; - -const setBounds = jest.fn(); - -setBounds.mockImplementation(() => {}); -jest.mock('../../../../../../../utils/map/useSetBounds', () => ({ - _esModule: true, - useSetBounds: (): jest.Mock => setBounds, -})); - -const useVisiblePinsPolygonCoordinatesMock = useVisiblePinsPolygonCoordinates as jest.Mock; - -jest.mock('./hooks/useVisiblePinsPolygonCoordinates', () => ({ - _esModule: true, - useVisiblePinsPolygonCoordinates: jest.fn(), -})); - -setBounds.mockImplementation(() => {}); const DRUGS_PIN = { name: drugsFixture[0].targets[0].name, @@ -42,18 +24,35 @@ const CHEMICALS_PIN = { pin: chemicalsFixture[0].targets[0], }; +const PIN_NUMBER = 10; +const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity; + +const INITIAL_STORE_STATE: InitialStoreState = { + models: MODELS_DATA_MOCK_WITH_MAIN_MAP, + map: { + data: { + ...initialMapDataFixture, + modelId: 5053, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, +}; + const renderComponent = ( name: string, pin: PinDetailsItem, type: PinTypeWithNone, + element: BioEntity, initialStoreState: InitialStoreState = {}, -): { store: StoreType } => { - const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); +): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { + const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState); return ( render( <Wrapper> - <PinsListItem name={name} type={type} pin={pin} /> + <PinsListItem name={name} type={type} pin={pin} element={element} number={PIN_NUMBER} /> </Wrapper>, ), { @@ -64,14 +63,14 @@ const renderComponent = ( describe('PinsListItem - component ', () => { it('should display full name of pin', () => { - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs'); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); const drugName = drugsFixture[0].targets[0].name; expect(screen.getByText(drugName)).toBeInTheDocument(); }); it('should display list of elements for pin for drugs', () => { - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs'); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); const firstPinElementType = drugsFixture[0].targets[0].targetParticipants[0].type; const firstPinElementResource = drugsFixture[0].targets[0].targetParticipants[0].resource; @@ -84,7 +83,7 @@ describe('PinsListItem - component ', () => { expect(screen.getByText(secondPinElementResource, { exact: false })).toBeInTheDocument(); }); it('should display list of references for pin', () => { - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs'); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); const firstPinReferenceType = drugsFixture[0].targets[0].references[0].type; const firstPinReferenceResource = drugsFixture[0].targets[0].references[0].resource; @@ -97,7 +96,13 @@ describe('PinsListItem - component ', () => { expect(screen.getByText(secondPinReferenceResource, { exact: false })).toBeInTheDocument(); }); it('should display list of elements for pin for chemicals', () => { - renderComponent(CHEMICALS_PIN.name, CHEMICALS_PIN.pin, 'chemicals'); + renderComponent( + CHEMICALS_PIN.name, + CHEMICALS_PIN.pin, + 'chemicals', + BIO_ENTITY, + INITIAL_STORE_STATE, + ); const firstPinElementType = chemicalsFixture[0].targets[0].targetParticipants[0].type; const firstPinElementResource = chemicalsFixture[0].targets[0].targetParticipants[0].resource; @@ -112,7 +117,7 @@ describe('PinsListItem - component ', () => { // TODO - it's probably flacky test it.skip('should not display list of elements for pin for bioentities', () => { - renderComponent(CHEMICALS_PIN.name, CHEMICALS_PIN.pin, 'drugs'); + renderComponent(CHEMICALS_PIN.name, CHEMICALS_PIN.pin, 'drugs', BIO_ENTITY); const bioEntityName = bioEntitiesContentFixture[2].bioEntity.fullName ? bioEntitiesContentFixture[2].bioEntity.fullName @@ -126,41 +131,87 @@ describe('PinsListItem - component ', () => { targetElements: [], }; - renderComponent(CHEMICALS_PIN.name, chemicalWithoutSubmaps, 'chemicals'); + renderComponent( + CHEMICALS_PIN.name, + chemicalWithoutSubmaps, + 'chemicals', + BIO_ENTITY, + INITIAL_STORE_STATE, + ); expect(screen.queryByText('Available in submaps:')).toBeNull(); }); - it('should not call setBounds if coordinates do not exist', () => { - useVisiblePinsPolygonCoordinatesMock.mockImplementation(() => undefined); - - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs'); - - const buttonCenterMapToPin = screen.getByTestId('center-to-pin'); - - expect(buttonCenterMapToPin).toBeInTheDocument(); - - act(() => { - buttonCenterMapToPin.click(); - }); - - expect(setBounds).not.toHaveBeenCalled(); - }); - it('should call setBounds if coordinates exist', () => { - useVisiblePinsPolygonCoordinatesMock.mockImplementation(() => [ - [292, 333], - [341, 842], + it('should call setMapPosition if coordinates exist in bioentity element', () => { + const { store } = renderComponent( + DRUGS_PIN.name, + DRUGS_PIN.pin, + 'drugs', + { + ...BIO_ENTITY, + x: 1000, + y: 500, + }, + { + models: MODELS_DATA_MOCK_WITH_MAIN_MAP, + map: { + data: { + ...initialMapDataFixture, + modelId: 5053, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }, + ); + + const centerToPinButton = screen.getByTestId('center-to-pin'); + centerToPinButton.click(); + + expect(store.getActions()).toStrictEqual([ + { payload: { x: 1000, y: 500, z: 8 }, type: 'map/setMapPosition' }, ]); + }); - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs'); - - const buttonCenterMapToPin = screen.getByTestId('center-to-pin'); - - expect(buttonCenterMapToPin).toBeInTheDocument(); - - act(() => { - buttonCenterMapToPin.click(); - }); - - expect(setBounds).toHaveBeenCalled(); + it('should call setMapPosition and onSubmapOpen if bioentity element model is different from current', () => { + const { store } = renderComponent( + DRUGS_PIN.name, + DRUGS_PIN.pin, + 'drugs', + { + ...BIO_ENTITY, + x: 1000, + y: 500, + model: 52, + }, + { + models: MODELS_DATA_MOCK_WITH_MAIN_MAP, + map: { + data: { + ...initialMapDataFixture, + modelId: 5053, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }, + ); + + const centerToPinButton = screen.getByTestId('center-to-pin'); + centerToPinButton.click(); + + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { + payload: { modelId: 52, modelName: 'Core PD map' }, + type: 'map/openMapAndSetActive', + }, + ]), + ); + + expect(store.getActions()).toEqual( + expect.arrayContaining([{ payload: { x: 1000, y: 500, z: 8 }, type: 'map/setMapPosition' }]), + ); }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx index 394dbf49d764ef073aaf0ca5e2e0e3f151cbcadb..254d0bc3197d2042db7280d7519517a8e0a70cc3 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx @@ -1,36 +1,48 @@ -import { Icon } from '@/shared/Icon'; -import { PinDetailsItem } from '@/types/models'; -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { modelsDataSelector } from '@/redux/models/models.selectors'; +/* eslint-disable @next/next/no-img-element */ +import { getCanvasIcon } from '@/components/Map/MapViewer/utils/config/getCanvasIcon'; +import { PINS_COLOR_WITH_NONE } from '@/constants/canvas'; import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { DEFAULT_MAX_ZOOM } from '@/constants/map'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors'; +import { openMapAndSetActive, setActiveMap, setMapPosition } from '@/redux/map/map.slice'; +import { modelsDataSelector, modelsNameMapSelector } from '@/redux/models/models.selectors'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { useSetBounds } from '@/utils/map/useSetBounds'; -import { getListOfAvailableSubmaps, getPinColor } from './PinsListItem.component.utils'; +import { BioEntity, PinDetailsItem } from '@/types/models'; import { AvailableSubmaps, PinTypeWithNone } from '../PinsList.types'; -import { useVisiblePinsPolygonCoordinates } from './hooks/useVisiblePinsPolygonCoordinates'; +import { getListOfAvailableSubmaps } from './PinsListItem.component.utils'; interface PinsListItemProps { name: string; type: PinTypeWithNone; pin: PinDetailsItem; + element: BioEntity; + number: number; } -export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Element => { +export const PinsListItem = ({ + name, + type, + pin, + element, + number, +}: PinsListItemProps): JSX.Element => { const dispatch = useAppDispatch(); const openedMaps = useAppSelector(mapOpenedMapsSelector); const models = useAppSelector(modelsDataSelector); const availableSubmaps = getListOfAvailableSubmaps(pin, models); const currentModelId = useAppSelector(mapModelIdSelector); - const coordinates = useVisiblePinsPolygonCoordinates(pin.targetElements); - const setBounds = useSetBounds(); + const modelsNames = useAppSelector(modelsNameMapSelector); + const pinIconCanvas = getCanvasIcon({ + color: PINS_COLOR_WITH_NONE[type], + value: number, + }); const isMapAlreadyOpened = (modelId: number): boolean => openedMaps.some(map => map.modelId === modelId); - const onSubmapClick = (map: AvailableSubmaps): void => { + const onSubmapOpen = (map: AvailableSubmaps): void => { if (isMapAlreadyOpened(map.modelId)) { dispatch(setActiveMap({ modelId: map.modelId })); } else { @@ -43,8 +55,21 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen }; const handleCenterMapToPin = (): void => { - if (!coordinates) return; - setBounds(coordinates); + if (currentModelId !== element.model) { + onSubmapOpen({ + id: element.model, + modelId: element.model, + name: modelsNames[element.model], + }); + } + + dispatch( + setMapPosition({ + x: element.x, + y: element.y, + z: DEFAULT_MAX_ZOOM, + }), + ); }; return ( @@ -56,7 +81,7 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen onClick={handleCenterMapToPin} data-testid="center-to-pin" > - <Icon name="pin" className={getPinColor(type)} /> + <img src={pinIconCanvas.toDataURL()} alt={`${number}`} title={`${number}`} /> </button> <p> Full name: <span className="w-full font-bold">{name}</span> @@ -65,15 +90,16 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen <ul className="leading-6"> <div className="font-bold">Elements:</div> {'targetParticipants' in pin && - pin.targetParticipants.map(element => { + pin.targetParticipants.map(participant => { return ( - <li key={element.id} className="my-2 px-2"> + // participant.id is almost always = 0 + <li key={`${participant.id}-${participant.link}`} className="my-2 px-2"> <a - href={element.link} + href={participant.link} target="_blank" className="cursor-pointer text-primary-500 underline" > - {element.type} ({element.resource}) + {participant.type} ({participant.resource}) </a> </li> ); @@ -83,7 +109,8 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen <div className="font-bold">References:</div> {pin.references.map(reference => { return ( - <li key={reference.id} className="my-2 px-2"> + // reference.id is almost always = 0 + <li key={`${reference.id}-${reference.resource}`} className="my-2 px-2"> <a href={reference.article?.link} target="_blank" @@ -101,7 +128,7 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen {availableSubmaps.map(submap => { return ( <button - onClick={(): void => onSubmapClick(submap)} + onClick={(): void => onSubmapOpen(submap)} className="mb-2 mr-2 rounded border border-solid border-greyscale-500 p-2 font-normal text-[#6A6977] hover:border-[#6A6977]" type="button" key={submap.id} diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.test.ts b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..af3b1747f52d753ebb92b68938edd776e2ce3294 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.test.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-magic-numbers */ +import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; +import { drugFixture } from '@/models/fixtures/drugFixtures'; +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { PinItem } from '../PinsList.types'; +import { getTargetElementsUniqueSorted } from './getTargetElementsUniqueSorted'; + +const BASE_TARGET_ELEMENT = + drugFixture.targets[FIRST_ARRAY_ELEMENT].targetElements[FIRST_ARRAY_ELEMENT]; +const TARGET_ELEMENT_ID_DUPLICATED = 'el_duplicated'; +const TARGET_ELEMENT_ID_UNIQUE = 'el_unique'; +const TARGET_ELEMENT_ID_UNIQUE_2 = 'el_unique_2'; +const NUMBER_OF_UNIQUE_ELEMENTS = 3; // 4 elements, but 1 is an duplicate + +const PIN_ITEM: PinItem = { + id: 1001, + name: 'pin name', + data: { + ...drugFixture, + targets: [ + { + ...drugFixture.targets[FIRST_ARRAY_ELEMENT], + targetElements: [ + { + ...BASE_TARGET_ELEMENT, + elementId: TARGET_ELEMENT_ID_DUPLICATED, + }, + { + ...BASE_TARGET_ELEMENT, + elementId: TARGET_ELEMENT_ID_UNIQUE, + }, + { + ...BASE_TARGET_ELEMENT, + elementId: TARGET_ELEMENT_ID_UNIQUE_2, + }, + ], + }, + { + ...drugFixture.targets[FIRST_ARRAY_ELEMENT], + targetElements: [ + { + ...BASE_TARGET_ELEMENT, + elementId: TARGET_ELEMENT_ID_DUPLICATED, + }, + ], + }, + ], + }, +}; + +const ENTITY_NUMBER: EntityNumber = { + [TARGET_ELEMENT_ID_UNIQUE]: 1, + [TARGET_ELEMENT_ID_DUPLICATED]: 2, + [TARGET_ELEMENT_ID_UNIQUE_2]: 3, +}; + +describe('getTargetElementsUniqueSorted - util', () => { + it('should return sorted by entityNumber unique target elements', () => { + const result = getTargetElementsUniqueSorted([PIN_ITEM], { entityNumber: ENTITY_NUMBER }); + + expect(result.length).toEqual(NUMBER_OF_UNIQUE_ELEMENTS); + expect(result[0].element.elementId).toEqual(TARGET_ELEMENT_ID_UNIQUE); + expect(result[1].element.elementId).toEqual(TARGET_ELEMENT_ID_DUPLICATED); + expect(result[2].element.elementId).toEqual(TARGET_ELEMENT_ID_UNIQUE_2); + }); +}); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.ts b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.ts new file mode 100644 index 0000000000000000000000000000000000000000..05a7ba6839fa6edd2855aa49cacf92a3f1e9e9d0 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/utils/getTargetElementsUniqueSorted.ts @@ -0,0 +1,24 @@ +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { PinItem, TargetElement } from '../PinsList.types'; + +interface Options { + entityNumber: EntityNumber; +} + +export const getTargetElementsUniqueSorted = ( + pinsList: PinItem[], + { entityNumber }: Options, +): TargetElement[] => { + const targets = pinsList.map(p => p.data.targets).flat(); + const targetsElementsKeyValue: [string, TargetElement][] = targets + .map((target): [string, TargetElement][] => + target.targetElements.map(element => [element.elementId, { target, element }]), + ) + .flat(); + const targetElementsDict = Object.fromEntries(targetsElementsKeyValue); + const targetElementsUnique = Object.values(targetElementsDict); + + return targetElementsUnique.sort( + (a, b) => entityNumber[a.element.elementId] - entityNumber[b.element.elementId], + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx index 24af3ef7543bba17574ae3df30766b182d405346..4601d1aa7decd26af61741f6162fcf85f504512a 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx @@ -72,8 +72,11 @@ describe('ResultsList - component ', () => { const fristDrugName = drugsFixture[0].targets[0].name; const secondDrugName = drugsFixture[0].targets[1].name; - expect(screen.getByText(fristDrugName)).toBeInTheDocument(); - expect(screen.getByText(secondDrugName)).toBeInTheDocument(); + const fristDrugTargetElementsNumber = drugsFixture[0].targets[0].targetElements.length; + const secondDrugTargetElementsNumber = drugsFixture[0].targets[1].targetElements.length; + + expect(screen.queryAllByText(fristDrugName).length).toEqual(fristDrugTargetElementsNumber); + expect(screen.queryAllByText(secondDrugName).length).toEqual(secondDrugTargetElementsNumber); }); it('should navigate to grouped search results after backward button click', async () => { const { store } = renderComponent(INITIAL_STATE); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts index 674813c32e9490a733afec28ae0b6df6b691de63..f45b6ea6a613880d52c1e155924bdb49f7e410cd 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts @@ -1,4 +1,4 @@ -import { ONE } from '@/constants/common'; +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; import { BioEntity } from '@/types/models'; import { PinType } from '@/types/pin'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; @@ -10,17 +10,19 @@ export const getBioEntitiesFeatures = ( { pointToProjection, type, + entityNumber, }: { pointToProjection: UsePointToProjectionResult; type: PinType; + entityNumber: EntityNumber; }, ): Feature[] => { - return bioEntites.map((bioEntity, index) => + return bioEntites.map(bioEntity => getBioEntitySingleFeature(bioEntity, { pointToProjection, type, // pin's index number - value: index + ONE, + value: entityNumber?.[bioEntity.elementId], }), ); }; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts index 04f1f7930d7cb2bb98da50ae8cf5588726561017..35d959b35da79436078e72c1e90da0b302eff389 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts @@ -35,6 +35,7 @@ describe('getBioEntitiesFeatures - subUtil', () => { const result = getBioEntitiesFeatures(bioEntities, { pointToProjection, type, + entityNumber: {}, }); result.forEach(resultElement => { diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index 2e98660497995ef8871b39a322ddfa147a213c89..b7b05f6cb4e3cc30837d59c31504824021cb9ed2 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -2,6 +2,7 @@ import { searchedBioEntitesSelectorOfCurrentMap } from '@/redux/bioEntity/bioEntity.selectors'; import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '@/redux/chemicals/chemicals.selectors'; import { searchedDrugsBioEntitesOfCurrentMapSelector } from '@/redux/drugs/drugs.selectors'; +import { entityNumberDataSelector } from '@/redux/entityNumber/entityNumber.selectors'; import { markersPinsOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import Feature from 'ol/Feature'; @@ -19,16 +20,36 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> const chemicalsBioEntities = useSelector(searchedChemicalsBioEntitesOfCurrentMapSelector); const drugsBioEntities = useSelector(searchedDrugsBioEntitesOfCurrentMapSelector); const markersEntities = useSelector(markersPinsOfCurrentMapDataSelector); + const entityNumber = useSelector(entityNumberDataSelector); const elementsFeatures = useMemo( () => [ - getBioEntitiesFeatures(contentBioEntites, { pointToProjection, type: 'bioEntity' }), - getBioEntitiesFeatures(chemicalsBioEntities, { pointToProjection, type: 'chemicals' }), - getBioEntitiesFeatures(drugsBioEntities, { pointToProjection, type: 'drugs' }), + getBioEntitiesFeatures(contentBioEntites, { + pointToProjection, + type: 'bioEntity', + entityNumber, + }), + getBioEntitiesFeatures(chemicalsBioEntities, { + pointToProjection, + type: 'chemicals', + entityNumber, + }), + getBioEntitiesFeatures(drugsBioEntities, { + pointToProjection, + type: 'drugs', + entityNumber, + }), getMarkersFeatures(markersEntities, { pointToProjection }), ].flat(), - [contentBioEntites, drugsBioEntities, chemicalsBioEntities, pointToProjection, markersEntities], + [ + contentBioEntites, + drugsBioEntities, + chemicalsBioEntities, + pointToProjection, + markersEntities, + entityNumber, + ], ); const vectorSource = useMemo(() => { diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.test.ts index bb5cd7f87d05025a2a4336fd242e1b4b64cfaa08..904522c2251dc6d5e75fb7adbec882e58029c6b8 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.test.ts @@ -15,6 +15,7 @@ describe('handleDataReset', () => { 'drugs/clearDrugsData', 'chemicals/clearChemicalsData', 'contextMenu/closeContextMenu', + 'entityNumber/clearEntityNumberData', ]; expect(actions.map(a => a.type)).toStrictEqual(actionsTypes); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.ts index 3f4afbc730371e6d9218f419ea98b3d91cc1657e..f3b7e3b47d8c4d2ff9dcd30f5465a1d457c234f6 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset.ts @@ -1,6 +1,7 @@ import { clearChemicalsData } from '@/redux/chemicals/chemicals.slice'; import { closeContextMenu } from '@/redux/contextMenu/contextMenu.slice'; import { clearDrugsData } from '@/redux/drugs/drugs.slice'; +import { clearEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { clearSearchData } from '@/redux/search/search.slice'; import { AppDispatch } from '@/redux/store'; @@ -16,4 +17,5 @@ export const handleDataReset = (dispatch: AppDispatch): void => { dispatch(clearDrugsData()); dispatch(clearChemicalsData()); dispatch(closeContextMenu()); + dispatch(clearEntityNumberData()); }; 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 020f0c6480a1298b8799319fab20ca3e3f9c77b1..58ee8bf0e88ac60f71eb84361c6c67a55aed8c4e 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts @@ -7,6 +7,7 @@ import { ELEMENT_SEARCH_RESULT_MOCK_REACTION, } from '@/models/mocks/elementSearchResultMock'; import { apiPath } from '@/redux/apiPath'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetworkResponse'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { HttpStatusCode } from 'axios'; @@ -16,7 +17,9 @@ const mockedAxiosOldClient = mockNetworkResponse(); const mockedAxiosNewClient = mockNetworkNewAPIResponse(); describe('handleReactionResults - util', () => { - const { store } = getReduxStoreWithActionsListener(); + const { store } = getReduxStoreWithActionsListener({ + ...INITIAL_STORE_STATE_MOCK, + }); const { dispatch } = store; mockedAxiosNewClient @@ -30,10 +33,22 @@ describe('handleReactionResults - util', () => { mockedAxiosOldClient .onGet(apiPath.getReactionsWithIds([ELEMENT_SEARCH_RESULT_MOCK_REACTION.id])) - .reply(HttpStatusCode.Ok, reactionsFixture); + .reply(HttpStatusCode.Ok, [ + { + ...reactionsFixture[0], + reactants: [], + products: [], + modifiers: [ + { + ...reactionsFixture[0].modifiers[0], + aliasId: ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id, + }, + ], + }, + ]); - beforeAll(async () => { - handleReactionResults( + beforeEach(async () => { + await handleReactionResults( dispatch, ELEMENT_SEARCH_RESULT_MOCK_REACTION, )(ELEMENT_SEARCH_RESULT_MOCK_REACTION); @@ -59,9 +74,21 @@ describe('handleReactionResults - util', () => { expect(actions[3].type).toEqual('project/getMultiBioEntity/pending'); }); - it('should run getBioEntity as fourth action', () => { + 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'); }); + + it('should run getBioEntityContents fullfilled as fourth action', () => { + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); + expect(actions[5].type).toEqual('project/getBioEntityContents/fulfilled'); + }); + + it('should run addNumbersToEntityNumberData as fifth action', () => { + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); + expect(actions[6].type).toEqual('entityNumber/addNumbersToEntityNumberData'); + }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts index bcadc4a1e476956a6c62d7a190f502924294251b..564f106be15fbee31cff1611ae95a0228542f89a 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts @@ -4,9 +4,9 @@ import { openReactionDrawerById } 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'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { ElementSearchResult, Reaction } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; -import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; /* prettier-ignore */ export const handleReactionResults = diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts index 3c033ad551f3537ca8eb81b83279af39265f816f..0bcb270865a790daac5e54d1e3561b9aaaecaff7 100644 --- a/src/constants/canvas.ts +++ b/src/constants/canvas.ts @@ -1,3 +1,4 @@ +import { PinTypeWithNone } from '@/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.types'; import { PinType } from '@/types/pin'; export const PIN_PATH2D = @@ -14,6 +15,11 @@ export const PINS_COLORS: Record<PinType, string> = { bioEntity: '#106AD7', }; +export const PINS_COLOR_WITH_NONE: Record<PinTypeWithNone, string> = { + ...PINS_COLORS, + none: '#000000', +}; + export const LINE_COLOR = '#00AAFF'; export const LINE_WIDTH = 6; diff --git a/src/constants/map.ts b/src/constants/map.ts index ae669ac9cae9e68cb574d6146438918f9814f88c..ba730d08a28b991817343a63a787cb97ddd722e6 100644 --- a/src/constants/map.ts +++ b/src/constants/map.ts @@ -4,7 +4,7 @@ import { HALF_SECOND_MS, ONE_HUNDRED_MS } from './time'; export const DEFAULT_TILE_SIZE = 256; export const DEFAULT_MIN_ZOOM = 2; -export const DEFAULT_MAX_ZOOM = 9; +export const DEFAULT_MAX_ZOOM = 8; export const DEFAULT_ZOOM = 4; export const DEFAULT_CENTER_X = 0; export const DEFAULT_CENTER_Y = 0; diff --git a/src/redux/bioEntity/bioEntity.thunks.ts b/src/redux/bioEntity/bioEntity.thunks.ts index ddb9d790d2e9efc981372ff33f5bdd3deae83855..783735232b4016cd7823ae5805755b1a8a8dc9d8 100644 --- a/src/redux/bioEntity/bioEntity.thunks.ts +++ b/src/redux/bioEntity/bioEntity.thunks.ts @@ -7,6 +7,7 @@ import { ThunkConfig } from '@/types/store'; import { getErrorMessage } from '@/utils/getErrorMessage'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; +import { addNumbersToEntityNumberData } from '../entityNumber/entityNumber.slice'; import { getReactionsByIds } from '../reactions/reactions.thunks'; import { BIO_ENTITY_FETCHING_ERROR_PREFIX, @@ -21,7 +22,10 @@ export const getBioEntity = createAsyncThunk< ThunkConfig >( 'project/getBioEntityContents', - async ({ searchQuery, isPerfectMatch }, { rejectWithValue, dispatch }) => { + async ( + { searchQuery, isPerfectMatch, addNumbersToEntityNumber = true }, + { rejectWithValue, dispatch }, + ) => { try { const response = await axiosInstanceNewAPI.get<BioEntityResponse>( apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch }), @@ -44,6 +48,11 @@ export const getBioEntity = createAsyncThunk< }), ); + if (addNumbersToEntityNumber && response.data.content) { + const bioEntityIds = response.data.content.map(b => b.bioEntity.elementId); + dispatch(addNumbersToEntityNumberData(bioEntityIds)); + } + return isDataValidBioEnity ? response.data.content : undefined; } catch (error) { const errorMessage = getErrorMessage({ @@ -56,7 +65,7 @@ export const getBioEntity = createAsyncThunk< ); type GetMultiBioEntityProps = PerfectMultiSearchParams; -type GetMultiBioEntityActions = PayloadAction<BioEntityContent[] | undefined>[]; +type GetMultiBioEntityActions = PayloadAction<BioEntityContent[] | undefined | string>[]; // if error thrown, string containing error message is returned export const getMultiBioEntity = createAsyncThunk< BioEntityContent[], @@ -68,7 +77,7 @@ export const getMultiBioEntity = createAsyncThunk< async ({ searchQueries, isPerfectMatch }, { dispatch, rejectWithValue }) => { try { const asyncGetBioEntityFunctions = searchQueries.map(searchQuery => - dispatch(getBioEntity({ searchQuery, isPerfectMatch })), + dispatch(getBioEntity({ searchQuery, isPerfectMatch, addNumbersToEntityNumber: false })), ); const bioEntityContentsActions = (await Promise.all( @@ -76,8 +85,13 @@ export const getMultiBioEntity = createAsyncThunk< )) as GetMultiBioEntityActions; const bioEntityContents = bioEntityContentsActions - .map(bioEntityContentsAction => bioEntityContentsAction.payload || []) - .flat(); + .map(bioEntityContentsAction => bioEntityContentsAction?.payload || []) + .flat() + .filter((payload): payload is BioEntityContent => typeof payload !== 'string') + .filter(payload => 'bioEntity' in payload || {}); + + const bioEntityIds = bioEntityContents.map(b => b.bioEntity.elementId); + dispatch(addNumbersToEntityNumberData(bioEntityIds)); return bioEntityContents; } catch (error) { diff --git a/src/redux/chemicals/chemicals.thunks.ts b/src/redux/chemicals/chemicals.thunks.ts index 5b25951d7bf7e4ebdaefecdca4afa4bd54501346..8a2f9878eb66b14c781d3ad69e484cf83efe6c6d 100644 --- a/src/redux/chemicals/chemicals.thunks.ts +++ b/src/redux/chemicals/chemicals.thunks.ts @@ -2,11 +2,12 @@ import { chemicalSchema } from '@/models/chemicalSchema'; import { apiPath } from '@/redux/apiPath'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { Chemical } from '@/types/models'; +import { ThunkConfig } from '@/types/store'; import { getErrorMessage } from '@/utils/getErrorMessage'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { z } from 'zod'; -import { ThunkConfig } from '@/types/store'; +import { addNumbersToEntityNumberData } from '../entityNumber/entityNumber.slice'; import { CHEMICALS_FETCHING_ERROR_PREFIX, MULTI_CHEMICALS_FETCHING_ERROR_PREFIX, @@ -39,7 +40,20 @@ export const getMultiChemicals = createAsyncThunk<void, string[], ThunkConfig>( dispatch(getChemicals(searchQuery)), ); - await Promise.all(asyncGetChemicalsFunctions); + const chemicalsDataActions = await Promise.all(asyncGetChemicalsFunctions); + + const chemicalsTargetsData = chemicalsDataActions + .map(chemicalsDataAction => + typeof chemicalsDataAction.payload === 'string' ? [] : chemicalsDataAction.payload || [], + ) + .flat() + .map(drug => drug.targets) + .flat() + .map(target => target.targetElements) + .flat(); + + const chemicalsIds = chemicalsTargetsData.map(d => d.elementId); + dispatch(addNumbersToEntityNumberData(chemicalsIds)); } catch (error) { const errorMessage = getErrorMessage({ error, diff --git a/src/redux/drugs/drugs.thunks.ts b/src/redux/drugs/drugs.thunks.ts index 6bd3b532665b877cdd8a7a95263ebdd9ac2bb5de..f837f49d662ef6bfe3b7fef5558a43d0caf3434a 100644 --- a/src/redux/drugs/drugs.thunks.ts +++ b/src/redux/drugs/drugs.thunks.ts @@ -2,11 +2,12 @@ import { drugSchema } from '@/models/drugSchema'; import { apiPath } from '@/redux/apiPath'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { Drug } from '@/types/models'; +import { ThunkConfig } from '@/types/store'; import { getErrorMessage } from '@/utils/getErrorMessage'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { z } from 'zod'; -import { ThunkConfig } from '@/types/store'; +import { addNumbersToEntityNumberData } from '../entityNumber/entityNumber.slice'; import { DRUGS_FETCHING_ERROR_PREFIX, MULTI_DRUGS_FETCHING_ERROR_PREFIX } from './drugs.constants'; export const getDrugs = createAsyncThunk<Drug[] | undefined, string, ThunkConfig>( @@ -36,7 +37,20 @@ export const getMultiDrugs = createAsyncThunk<void, string[], ThunkConfig>( dispatch(getDrugs(searchQuery)), ); - await Promise.all(asyncGetDrugsFunctions); + const drugsDataActions = await Promise.all(asyncGetDrugsFunctions); + + const drugsTargetsData = drugsDataActions + .map(drugsDataAction => + typeof drugsDataAction.payload === 'string' ? [] : drugsDataAction.payload || [], + ) + .flat() + .map(drug => drug.targets) + .flat() + .map(target => target.targetElements) + .flat(); + + const drugsIds = drugsTargetsData.map(d => d.elementId); + dispatch(addNumbersToEntityNumberData(drugsIds)); } catch (error) { const errorMessage = getErrorMessage({ error, prefix: MULTI_DRUGS_FETCHING_ERROR_PREFIX }); diff --git a/src/redux/entityNumber/entityNumber.constants.ts b/src/redux/entityNumber/entityNumber.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f56f664b101e29c221f2a58e1bea671c1ea73e0 --- /dev/null +++ b/src/redux/entityNumber/entityNumber.constants.ts @@ -0,0 +1,5 @@ +import { EntityNumberState } from './entityNumber.types'; + +export const ENTITY_NUMBER_INITIAL_STATE: EntityNumberState = { + data: {}, +}; diff --git a/src/redux/entityNumber/entityNumber.mock.ts b/src/redux/entityNumber/entityNumber.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..adab69c7c4a77f63bbc0e27d343fdc2a3f60456b --- /dev/null +++ b/src/redux/entityNumber/entityNumber.mock.ts @@ -0,0 +1,5 @@ +import { EntityNumberState } from './entityNumber.types'; + +export const ENTITY_NUMBER_INITIAL_STATE_MOCK: EntityNumberState = { + data: {}, +}; diff --git a/src/redux/entityNumber/entityNumber.reducers.ts b/src/redux/entityNumber/entityNumber.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..56ce666551d9068af457ce07122f74bebea6f791 --- /dev/null +++ b/src/redux/entityNumber/entityNumber.reducers.ts @@ -0,0 +1,26 @@ +import { ONE } from '@/constants/common'; +import { + AddNumbersToEntityNumberDataAction, + EntityNumber, + EntityNumberState, +} from './entityNumber.types'; + +export const clearEntityNumberDataReducer = (state: EntityNumberState): void => { + state.data = {}; +}; + +export const addNumbersToEntityNumberDataReducer = ( + state: EntityNumberState, + action: AddNumbersToEntityNumberDataAction, +): void => { + const { payload: ids } = action; + const lastNumber = Object.keys(state.data).length || ONE; // min num = 1 + const newEntityNumber: EntityNumber = Object.fromEntries( + ids.map((id, index) => [id, lastNumber + index]), + ); + + state.data = { + ...newEntityNumber, + ...state.data, + }; +}; diff --git a/src/redux/entityNumber/entityNumber.selectors.ts b/src/redux/entityNumber/entityNumber.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..64576d502818bba454ea65edc2a29707f07610f8 --- /dev/null +++ b/src/redux/entityNumber/entityNumber.selectors.ts @@ -0,0 +1,14 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { rootSelector } from '../root/root.selectors'; + +export const entityNumberSelector = createSelector(rootSelector, state => state.entityNumber); + +export const entityNumberDataSelector = createSelector( + entityNumberSelector, + entityNumber => entityNumber.data, +); + +export const numberByEntityNumberIdSelector = createSelector( + [entityNumberDataSelector, (_state, id: string): string => id], + (entityNumber, id): number | undefined => entityNumber?.[id], +); diff --git a/src/redux/entityNumber/entityNumber.slice.ts b/src/redux/entityNumber/entityNumber.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..50e87210160f627a3718246a3681f8ccbb4a0082 --- /dev/null +++ b/src/redux/entityNumber/entityNumber.slice.ts @@ -0,0 +1,20 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { ENTITY_NUMBER_INITIAL_STATE } from './entityNumber.constants'; +import { + addNumbersToEntityNumberDataReducer, + clearEntityNumberDataReducer, +} from './entityNumber.reducers'; + +export const entityNumberSlice = createSlice({ + name: 'entityNumber', + initialState: ENTITY_NUMBER_INITIAL_STATE, + reducers: { + addNumbersToEntityNumberData: addNumbersToEntityNumberDataReducer, + clearEntityNumberData: clearEntityNumberDataReducer, + }, + extraReducers: () => {}, +}); + +export const { addNumbersToEntityNumberData, clearEntityNumberData } = entityNumberSlice.actions; + +export default entityNumberSlice.reducer; diff --git a/src/redux/entityNumber/entityNumber.types.ts b/src/redux/entityNumber/entityNumber.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..909ae6f08fda82d88a6e0131dacf6f5ceb6cb2fe --- /dev/null +++ b/src/redux/entityNumber/entityNumber.types.ts @@ -0,0 +1,12 @@ +import { PayloadAction } from '@reduxjs/toolkit'; + +export type EntityNumberId = string; +export type EntityNumberValue = number; +export type EntityNumber = Record<EntityNumberId, EntityNumberValue>; + +export interface EntityNumberState { + data: EntityNumber; +} + +export type AddNumbersToEntityNumberDataPayload = EntityNumberId[]; +export type AddNumbersToEntityNumberDataAction = PayloadAction<AddNumbersToEntityNumberDataPayload>; diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index 66b6151cdd5494a54e3dd6c70cc3f03fd1d9acfb..5ff21f0b80bfa11ef48b15914aff4304615bb16f 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -7,6 +7,7 @@ import { CONTEXT_MENU_INITIAL_STATE } from '../contextMenu/contextMenu.constants import { COOKIE_BANNER_INITIAL_STATE_MOCK } from '../cookieBanner/cookieBanner.mock'; import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture'; import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock'; +import { ENTITY_NUMBER_INITIAL_STATE_MOCK } from '../entityNumber/entityNumber.mock'; import { EXPORT_INITIAL_STATE_MOCK } from '../export/export.mock'; import { LEGEND_INITIAL_STATE_MOCK } from '../legend/legend.mock'; import { initialMapStateFixture } from '../map/map.fixtures'; @@ -49,4 +50,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { export: EXPORT_INITIAL_STATE_MOCK, plugins: PLUGINS_INITIAL_STATE_MOCK, markers: MARKERS_INITIAL_STATE_MOCK, + entityNumber: ENTITY_NUMBER_INITIAL_STATE_MOCK, }; diff --git a/src/redux/store.ts b/src/redux/store.ts index 3fb15d8bb7a91d5e21285b67ba6fcf586ae93a2b..82042d987bf7638f1d8ffe8cc7f33610475c07bf 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -23,13 +23,14 @@ import { configureStore, } from '@reduxjs/toolkit'; import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice'; +import entityNumberReducer from './entityNumber/entityNumber.slice'; import exportReducer from './export/export.slice'; import legendReducer from './legend/legend.slice'; import { mapListenerMiddleware } from './map/middleware/map.middleware'; import markersReducer from './markers/markers.slice'; +import { errorListenerMiddleware } from './middlewares/error.middleware'; import pluginsReducer from './plugins/plugins.slice'; import publicationsReducer from './publications/publications.slice'; -import { errorListenerMiddleware } from './middlewares/error.middleware'; import statisticsReducer from './statistics/statistics.slice'; export const reducers = { @@ -57,6 +58,7 @@ export const reducers = { export: exportReducer, plugins: pluginsReducer, markers: markersReducer, + entityNumber: entityNumberReducer, }; export const middlewares = [mapListenerMiddleware.middleware, errorListenerMiddleware.middleware]; diff --git a/src/types/search.ts b/src/types/search.ts index 4ec4a95387f4f77f607a419a7a8ab75ffa030d65..6e341820a1253b0a98618f755df91b1067234e09 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -6,4 +6,5 @@ export type PerfectMultiSearchParams = { export type PerfectSearchParams = { searchQuery: string; isPerfectMatch: boolean; + addNumbersToEntityNumber?: boolean; };