diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.test.ts index 7dcf389a3dd7f3e22337040b0b6dd783395dd20b..44b10f6834dad0e87c789718d99e9119ac584eb3 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.test.ts @@ -1,16 +1,22 @@ /* eslint-disable no-magic-numbers */ -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture'; import { findClosestBioEntityPoint } from './findClosestBioEntityPoint'; describe('findClosestBioEntityPoint', () => { const bioEntityContents = [ { - ...bioEntityContentFixture, - bioEntity: { ...bioEntityContentFixture.bioEntity, x: 10, y: 10, width: 20, height: 20 }, + ...bioEntityFixture, + x: 10, + y: 10, + width: 20, + height: 20, }, { - ...bioEntityContentFixture, - bioEntity: { ...bioEntityContentFixture.bioEntity, x: 50, y: 50, width: 30, height: 30 }, + ...bioEntityFixture, + x: 50, + y: 50, + width: 30, + height: 30, }, ]; diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.ts index c7150b506832aad15760e632091660191f009d09..329f8452d902f3fc58d18f9a6241b4d2dc9ac659 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/findClosestBioEntityPoint.ts @@ -1,18 +1,18 @@ import { Point as PointType } from '@/types/map'; -import { BioEntityContent } from '@/types/models'; +import { BioEntity } from '@/types/models'; import { getMaxClickDistance } from './getMaxClickDistance'; export const findClosestBioEntityPoint = ( - bioEntityContents: BioEntityContent[], + bioEntityContents: BioEntity[], searchDistance: string, maxZoom: number, zoom: number, point: PointType, -): BioEntityContent | undefined => { +): BioEntity | undefined => { const maxDistance = getMaxClickDistance(maxZoom, zoom, searchDistance); const matchingBioEntityFound = bioEntityContents.find(bio => { - const { x, y, width, height } = bio.bioEntity; + const { x, y, width, height } = bio; const minX = x - maxDistance; const maxX = x + width + maxDistance; diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts index f3136895e438517686533a5925b974a840bc6349..e00535c5e895bb9b34d0563fb0132387abe31dcb 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts @@ -8,6 +8,7 @@ import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetw import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { waitFor } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; +import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture'; import { handleAliasResults } from './handleAliasResults'; jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); @@ -45,12 +46,15 @@ describe('handleAliasResults - util', () => { it('should clear bio entities and do not close drawer if result drawer is not open', async () => { mockedAxiosClient .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id.toString(), - isPerfectMatch: true, - }), + apiPath.getElementById( + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id, + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.modelId, + ), ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + .reply(HttpStatusCode.Ok, { + ...bioEntityFixture, + idReaction: undefined, + }); const { store } = getReduxStoreWithActionsListener(); const { dispatch } = store; @@ -68,11 +72,12 @@ describe('handleAliasResults - util', () => { expect(actionTypes).toEqual([ 'project/getMultiBioEntity/pending', - 'project/getBioEntityContents/pending', - 'project/getBioEntityContents/fulfilled', + 'project/getBioEntityById/pending', + 'project/getCommentElement/pending', + 'project/getCommentElement/fulfilled', + 'entityNumber/addNumbersToEntityNumberData', + 'project/getBioEntityById/fulfilled', 'entityNumber/addNumbersToEntityNumberData', - 'reactions/getByIds/pending', - 'reactions/getByIds/fulfilled', 'project/getMultiBioEntity/fulfilled', 'bioEntityContents/clearBioEntitiesData', ]); @@ -81,12 +86,15 @@ describe('handleAliasResults - util', () => { it('should clear bio entities and close drawer if result drawer is already open', async () => { mockedAxiosClient .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id.toString(), - isPerfectMatch: true, - }), + apiPath.getElementById( + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id, + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.modelId, + ), ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + .reply(HttpStatusCode.Ok, { + ...bioEntityFixture, + idReaction: undefined, + }); const { store } = getReduxStoreWithActionsListener(); const { dispatch } = store; @@ -104,11 +112,12 @@ describe('handleAliasResults - util', () => { expect(actionTypes).toEqual([ 'project/getMultiBioEntity/pending', - 'project/getBioEntityContents/pending', - 'project/getBioEntityContents/fulfilled', + 'project/getBioEntityById/pending', + 'project/getCommentElement/pending', + 'project/getCommentElement/fulfilled', + 'entityNumber/addNumbersToEntityNumberData', + 'project/getBioEntityById/fulfilled', 'entityNumber/addNumbersToEntityNumberData', - 'reactions/getByIds/pending', - 'reactions/getByIds/fulfilled', 'project/getMultiBioEntity/fulfilled', 'drawer/closeDrawer', 'bioEntityContents/clearBioEntitiesData', @@ -120,27 +129,20 @@ describe('handleAliasResults - util', () => { it('should select tab and open bio entity drawer', async () => { mockedAxiosClient .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id.toString(), - isPerfectMatch: true, - }), + apiPath.getElementById( + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id, + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.modelId, + ), ) .reply(HttpStatusCode.Ok, { - ...bioEntityResponseFixture, - content: [ - { - ...bioEntityResponseFixture.content[0], - bioEntity: { - ...bioEntityResponseFixture.content[0].bioEntity, - x: 500, - y: 700, - width: 50, - height: 50, - idReaction: undefined, - }, - }, - ], + ...bioEntityFixture, + x: 500, + y: 700, + width: 50, + height: 50, + idReaction: undefined, }); + const { store } = getReduxStoreWithActionsListener(); const { dispatch } = store; @@ -158,8 +160,11 @@ describe('handleAliasResults - util', () => { expect(actionTypes).toEqual([ 'project/getMultiBioEntity/pending', - 'project/getBioEntityContents/pending', - 'project/getBioEntityContents/fulfilled', + 'project/getBioEntityById/pending', + 'project/getCommentElement/pending', + 'project/getCommentElement/fulfilled', + 'entityNumber/addNumbersToEntityNumberData', + 'project/getBioEntityById/fulfilled', 'entityNumber/addNumbersToEntityNumberData', 'project/getMultiBioEntity/fulfilled', 'drawer/selectTab', @@ -173,12 +178,15 @@ describe('handleAliasResults - util', () => { it('should select tab and open drawer without clearing bio entities', async () => { mockedAxiosClient .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id.toString(), - isPerfectMatch: true, - }), + apiPath.getElementById( + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.id, + ELEMENT_SEARCH_RESULT_MOCK_ALIAS.modelId, + ), ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + .reply(HttpStatusCode.Ok, bioEntityFixture); + mockedAxiosOldClient + .onGet(apiPath.getReactionsWithIds([Number(bioEntityFixture.id)])) + .reply(HttpStatusCode.Ok, bioEntityFixture); const { store } = getReduxStoreWithActionsListener(); const { dispatch } = store; @@ -195,8 +203,11 @@ describe('handleAliasResults - util', () => { expect(actionTypes).toEqual([ 'project/getMultiBioEntity/pending', - 'project/getBioEntityContents/pending', - 'project/getBioEntityContents/fulfilled', + 'project/getBioEntityById/pending', + 'project/getCommentElement/pending', + 'project/getCommentElement/fulfilled', + 'entityNumber/addNumbersToEntityNumberData', + 'project/getBioEntityById/fulfilled', 'entityNumber/addNumbersToEntityNumberData', 'reactions/getByIds/pending', 'reactions/getByIds/fulfilled', diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts index cd8fc849d4b02a287a06c6909dc2af9db1e68c44..02c8312ec556e43e4399fdc51860c17b18475610 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts @@ -1,4 +1,3 @@ -import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks'; import { closeDrawer, openBioEntityDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; @@ -6,6 +5,7 @@ import { ElementSearchResult } from '@/types/models'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { clearBioEntitiesData } from '@/redux/bioEntity/bioEntity.slice'; import { Point } from '@/types/map'; +import { getMultiBioEntityByIds } from '@/redux/bioEntity/thunks/getMultiBioEntity'; import { findClosestBioEntityPoint } from './findClosestBioEntityPoint'; type SearchConfig = { @@ -16,20 +16,21 @@ type SearchConfig = { hasFitBounds?: boolean; isResultDrawerOpen?: boolean; }; +/* prettier-ignore */ + /* prettier-ignore */ export const handleAliasResults = (dispatch: AppDispatch, closestSearchResult: ElementSearchResult, { hasFitBounds, maxZoom, point, searchDistance, zoom, isResultDrawerOpen }: SearchConfig) => - async ({ id }: ElementSearchResult): Promise<void> => { - const bioEntityContents = await dispatch( - getMultiBioEntity({ - searchQueries: [id.toString()], - isPerfectMatch: true + async ({ id, modelId, type }: ElementSearchResult): Promise<void> => { + const bioEntities = await dispatch( + getMultiBioEntityByIds({ + elementsToFetch: [{elementId: id, type, modelId, addNumbersToEntityNumber: true}] }), ).unwrap(); if (searchDistance) { - const matchingBioEntityFound = findClosestBioEntityPoint(bioEntityContents, searchDistance, maxZoom, zoom, point); + const matchingBioEntityFound = findClosestBioEntityPoint(bioEntities, searchDistance, maxZoom, zoom, point); if (!matchingBioEntityFound) { if (isResultDrawerOpen) { @@ -48,7 +49,7 @@ export const handleAliasResults = PluginsEventBus.dispatchEvent('onSearch', { type: 'bioEntity', searchValues: [closestSearchResult], - results: [bioEntityContents], + results: [bioEntities.map((bioEntity)=>{return {perfect: true, bioEntity};})], }); if (hasFitBounds) { diff --git a/src/models/bioEntitySchema.ts b/src/models/bioEntitySchema.ts index 4fe837ef05f6b9d18e18df2f4e87e940dcf36704..187b1982bf1f7273d6694aca1a6a725d097c6f1a 100644 --- a/src/models/bioEntitySchema.ts +++ b/src/models/bioEntitySchema.ts @@ -11,7 +11,7 @@ import { structuralStateSchema } from './structuralStateSchema'; import { submodelSchema } from './submodelSchema'; export const bioEntitySchema = z.object({ - id: z.union([z.number(), z.string()]), + id: z.union([z.number().int().positive(), z.string()]), stringType: z.string(), name: z.string(), elementId: z.string(), diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts index 46892945a2d1221206680c947b8fd8398b225fe7..3f68a154b171e3d8d869381dd82587777b2bbbe1 100644 --- a/src/redux/bioEntity/bioEntity.reducers.ts +++ b/src/redux/bioEntity/bioEntity.reducers.ts @@ -1,5 +1,6 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { getBioEntityById } from '@/redux/bioEntity/thunks/getBioEntity'; import { BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE } from './bioEntity.constants'; import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks'; import { BioEntityContentsState } from './bioEntity.types'; @@ -16,6 +17,14 @@ export const getBioEntityContentsReducer = ( error: DEFAULT_ERROR, }); }); + builder.addCase(getBioEntityById.pending, (state, action) => { + state.data.push({ + searchQueryElement: `${action.meta.arg.elementId}`, + data: undefined, + loading: 'pending', + error: DEFAULT_ERROR, + }); + }); builder.addCase(getBioEntity.fulfilled, (state, action) => { const bioEntities = state.data.find( bioEntity => bioEntity.searchQueryElement === action.meta.arg.searchQuery, @@ -25,12 +34,30 @@ export const getBioEntityContentsReducer = ( bioEntities.loading = 'succeeded'; } }); + builder.addCase(getBioEntityById.fulfilled, (state, action) => { + const bioEntities = state.data.find( + bioEntity => bioEntity.searchQueryElement === `${action.meta.arg.elementId}`, + ); + if (bioEntities) { + bioEntities.data = action.payload ? [{ perfect: true, bioEntity: action.payload }] : []; + bioEntities.loading = 'succeeded'; + } + }); builder.addCase(getBioEntity.rejected, (state, action) => { - const chemicals = state.data.find( - chemical => chemical.searchQueryElement === action.meta.arg.searchQuery, + const bioEntities = state.data.find( + bioEntity => bioEntity.searchQueryElement === action.meta.arg.searchQuery, + ); + if (bioEntities) { + bioEntities.loading = 'failed'; + // TODO: error management to be discussed in the team + } + }); + builder.addCase(getBioEntityById.rejected, (state, action) => { + const bioEntities = state.data.find( + bioEntity => bioEntity.searchQueryElement === `${action.meta.arg.elementId}`, ); - if (chemicals) { - chemicals.loading = 'failed'; + if (bioEntities) { + bioEntities.loading = 'failed'; // TODO: error management to be discussed in the team } }); diff --git a/src/redux/bioEntity/thunks/getBioEntity.ts b/src/redux/bioEntity/thunks/getBioEntity.ts index bf54870a8db007d3785bb07606516fa293044a6f..c89cc25d3b430f33179b4249e778888ae8a0f7ed 100644 --- a/src/redux/bioEntity/thunks/getBioEntity.ts +++ b/src/redux/bioEntity/thunks/getBioEntity.ts @@ -1,12 +1,13 @@ import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema'; import { apiPath } from '@/redux/apiPath'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { BioEntityContent, BioEntityResponse } from '@/types/models'; -import { PerfectSearchParams } from '@/types/search'; +import { BioEntity, BioEntityContent, BioEntityResponse } from '@/types/models'; +import { IdSearchQuery, PerfectSearchParams } from '@/types/search'; import { ThunkConfig } from '@/types/store'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; -import { createAsyncThunk } from '@reduxjs/toolkit'; +import { createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { getError } from '@/utils/error-report/getError'; +import { getCommentElement } from '@/redux/comment/thunks/getComments'; import { addNumbersToEntityNumberData } from '../../entityNumber/entityNumber.slice'; import { BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants'; @@ -40,3 +41,34 @@ export const getBioEntity = createAsyncThunk< } }, ); + +type GetBioEntityByIdProps = IdSearchQuery; +type GetBioEntityByIdAction = PayloadAction<BioEntity | null>; // if error thrown, string containing error message is returned + +export const getBioEntityById = createAsyncThunk< + BioEntity | null, + GetBioEntityByIdProps, + ThunkConfig +>( + 'project/getBioEntityById', + async ({ elementId, modelId, type, addNumbersToEntityNumber }, { dispatch }) => { + try { + if (type === 'ALIAS') { + const elementAction = (await dispatch( + getCommentElement({ elementId, modelId }), + )) as GetBioEntityByIdAction; + + const element = elementAction.payload; + + if (addNumbersToEntityNumber && element) { + dispatch(addNumbersToEntityNumberData([element.elementId])); + } + + return element; + } + throw new Error('Not implemented'); + } catch (error) { + return Promise.reject(getError({ error, prefix: BIO_ENTITY_FETCHING_ERROR_PREFIX })); + } + }, +); diff --git a/src/redux/bioEntity/thunks/getMultiBioEntity.ts b/src/redux/bioEntity/thunks/getMultiBioEntity.ts index 948156156324bc4cb4d9ff0e846f7dfc97fb8284..dad84c23999040857bbf4cae0cce37a6a6c923bb 100644 --- a/src/redux/bioEntity/thunks/getMultiBioEntity.ts +++ b/src/redux/bioEntity/thunks/getMultiBioEntity.ts @@ -1,13 +1,13 @@ import { ZERO } from '@/constants/common'; import type { AppDispatch, store } from '@/redux/store'; -import { BioEntityContent } from '@/types/models'; -import { PerfectMultiSearchParams } from '@/types/search'; +import { BioEntity, BioEntityContent } from '@/types/models'; +import { MultiSearchByIdParams, PerfectMultiSearchParams } from '@/types/search'; import { ThunkConfig } from '@/types/store'; import { PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { getError } from '@/utils/error-report/getError'; import { addNumbersToEntityNumberData } from '../../entityNumber/entityNumber.slice'; import { MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants'; -import { getBioEntity } from './getBioEntity'; +import { getBioEntity, getBioEntityById } from './getBioEntity'; import { fetchReactionsAndGetBioEntitiesIds } from './utils/fetchReactionsAndGetBioEntitiesIds'; type GetMultiBioEntityProps = PerfectMultiSearchParams; @@ -54,3 +54,48 @@ export const getMultiBioEntity = createAsyncThunk< } }, ); + +type GetMultiBioEntityByIdProps = MultiSearchByIdParams; +type GetMultiBioEntityByIdActions = PayloadAction<BioEntity[] | undefined>[]; // if error thrown, string containing error message is returned + +export const getMultiBioEntityByIds = createAsyncThunk< + BioEntity[], + GetMultiBioEntityByIdProps, + ThunkConfig +>( + 'project/getMultiBioEntity', + // eslint-disable-next-line consistent-return + async ({ elementsToFetch }, { dispatch, getState }) => { + try { + const asyncGetBioEntityFunctions = elementsToFetch.map(elementToFetch => + dispatch(getBioEntityById(elementToFetch)), + ); + + const bioEntityContentsActions = (await Promise.all( + asyncGetBioEntityFunctions, + )) as GetMultiBioEntityByIdActions; + + const bioEntities = bioEntityContentsActions + .map(bioEntityContentsAction => bioEntityContentsAction?.payload || []) + .flat(); + + const bioEntityIds = bioEntities.map(b => b.elementId); + dispatch(addNumbersToEntityNumberData(bioEntityIds)); + + const bioEntitiesIds = await fetchReactionsAndGetBioEntitiesIds({ + bioEntityContents: bioEntities.map(bioEntity => { + return { perfect: true, bioEntity }; + }), + dispatch: dispatch as AppDispatch, + getState: getState as typeof store.getState, + }); + if (bioEntitiesIds.length > ZERO) { + await dispatch(getMultiBioEntity({ searchQueries: bioEntitiesIds, isPerfectMatch: true })); + } + + return bioEntities; + } catch (error) { + return Promise.reject(getError({ error, prefix: MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX })); + } + }, +); diff --git a/src/redux/reactions/reactions.thunks.ts b/src/redux/reactions/reactions.thunks.ts index 5a650ebce706d23eecad1e8c1fdd72a95b2411d2..af0b8c057f8af9724fba767b6ee04cc974f23f70 100644 --- a/src/redux/reactions/reactions.thunks.ts +++ b/src/redux/reactions/reactions.thunks.ts @@ -31,6 +31,7 @@ export const getReactionsByIds = createAsyncThunk< try { const response = await axiosInstance.get<Reaction[]>(apiPath.getReactionsWithIds(ids)); + const isDataValid = validateDataUsingZodSchema(response.data, z.array(reactionSchema)); if (!isDataValid) { diff --git a/src/types/search.ts b/src/types/search.ts index 6e341820a1253b0a98618f755df91b1067234e09..3267083c5701bb146960ca58a0b4fcb889137ae4 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,3 +1,16 @@ +export type BioEntityType = 'ALIAS' | 'REACTION'; + +export type IdSearchQuery = { + elementId: number; + modelId: number; + type: BioEntityType; + addNumbersToEntityNumber?: boolean; +}; + +export type MultiSearchByIdParams = { + elementsToFetch: IdSearchQuery[]; +}; + export type PerfectMultiSearchParams = { searchQueries: string[]; isPerfectMatch: boolean;