diff --git a/docs/plugins/errors.md b/docs/plugins/errors.md new file mode 100644 index 0000000000000000000000000000000000000000..2795ec513330d084028e1cfe6cc7daee6c522687 --- /dev/null +++ b/docs/plugins/errors.md @@ -0,0 +1,17 @@ +# Error Documentation + +## Map Errors + +- **Map with provided id does not exist**: This error occurs when the provided map id does not correspond to any existing map. + +## Search Errors + +- **Invalid query type. The query should be of string type**: This error occurs when the query parameter is not of string type. + +- **Invalid coordinates type or values**: This error occurs when the coordinates parameter is missing keys, or its values are not of number type. + +- **Invalid model id type. The model should be of number type**: This error occurs when the modelId parameter is not of number type. + +## Project Errors + +- **Project does not exist**: This error occurs when the project data is not available. diff --git a/docs/plugins/submaps.md b/docs/plugins/submaps.md new file mode 100644 index 0000000000000000000000000000000000000000..54c6a340857d14e1e45fc1cdbd7d70c2d11c7f62 --- /dev/null +++ b/docs/plugins/submaps.md @@ -0,0 +1,21 @@ +### Submaps + +#### Get Models + +To get data about all available submaps, plugins can use the `getModels` method defined in `window.minerva.map.data`. This method returns array with data about all submaps. + +##### Example of getModels usage: + +```javascript +window.minerva.map.data.getModels(); +``` + +#### Open Map + +To open map, plugins can use the `openMap` method defined in `window.minerva.map`. This method takes one argument: an object with an `id` property that indicates the map ID. + +##### Example of openMap usage: + +```javascript +window.minerva.map.openMap({ id: 51 }); +``` diff --git a/index.d.ts b/index.d.ts index fc84128215560bd0af912dfc6ba6a3773d60ffc7..db769bd296186e97e8d09c6adc0460de5f166242 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,8 @@ +import { getModels } from '@/services/pluginsManager/map/models/getModels'; +import { OpenMapArgs, openMap } from '@/services/pluginsManager/map/openMap'; import { triggerSearch } from '@/services/pluginsManager/map/triggerSearch'; import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager'; +import { MapModel } from '@/types/models'; import { getDisease } from '@/services/pluginsManager/project/data/getDisease'; import { getName } from '@/services/pluginsManager/project/data/getName'; import { getOrganism } from '@/services/pluginsManager/project/data/getOrganism'; @@ -32,6 +35,10 @@ declare global { bioEntities: BioEntitiesMethods; }; map: { + data: { + getModels: typeof getModels; + }; + openMap: typeof openMap; triggerSearch: typeof triggerSearch; }; project: { diff --git a/src/services/pluginsManager/errorMessages.ts b/src/services/pluginsManager/errorMessages.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d35649435f1f99a73a8f6b84b267ac76b01143e --- /dev/null +++ b/src/services/pluginsManager/errorMessages.ts @@ -0,0 +1,6 @@ +export const ERROR_MAP_NOT_FOUND = 'Map with provided id does not exist'; +export const ERROR_INVALID_QUERY_TYPE = 'Invalid query type. The query should be of string type'; +export const ERROR_INVALID_COORDINATES = 'Invalid coordinates type or values'; +export const ERROR_INVALID_MODEL_ID_TYPE = + 'Invalid model id type. The model should be of number type'; +export const ERROR_PROJECT_NOT_FOUND = 'Project does not exist'; diff --git a/src/services/pluginsManager/map/models/getModels.test.ts b/src/services/pluginsManager/map/models/getModels.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e357fe936dbb9b98045759c594f550c5ce18aded --- /dev/null +++ b/src/services/pluginsManager/map/models/getModels.test.ts @@ -0,0 +1,51 @@ +import { RootState, store } from '@/redux/store'; +import { modelsFixture } from '@/models/fixtures/modelsFixture'; +import { getModels } from './getModels'; + +jest.mock('../../../../redux/store'); + +describe('getModels', () => { + const getStateSpy = jest.spyOn(store, 'getState'); + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should return models when data is valid', () => { + getStateSpy.mockImplementation( + () => + ({ + models: { + data: modelsFixture, + }, + }) as RootState, + ); + + expect(getModels()).toEqual(modelsFixture); + }); + + it('should return empty array when data is invalid', () => { + getStateSpy.mockImplementation( + () => + ({ + models: { + data: 'invalid', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ); + + expect(getModels()).toEqual([]); + }); + + it('should return empty array when data is empty', () => { + getStateSpy.mockImplementation( + () => + ({ + models: { + data: [], + }, + }) as RootState, + ); + + expect(getModels()).toEqual([]); + }); +}); diff --git a/src/services/pluginsManager/map/models/getModels.ts b/src/services/pluginsManager/map/models/getModels.ts new file mode 100644 index 0000000000000000000000000000000000000000..4bd5ddec44908359c53c413b58cc6fd5908776d2 --- /dev/null +++ b/src/services/pluginsManager/map/models/getModels.ts @@ -0,0 +1,13 @@ +import { mapModelSchema } from '@/models/modelSchema'; +import { store } from '@/redux/store'; +import { MapModel } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { z } from 'zod'; + +export const getModels = (): MapModel[] => { + const models = store.getState().models.data; + + const isDataValid = validateDataUsingZodSchema(models, z.array(mapModelSchema)); + + return isDataValid ? models : []; +}; diff --git a/src/services/pluginsManager/map/openMap.test.ts b/src/services/pluginsManager/map/openMap.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..152f81b5f25ca97a71fde8292b315cb26f9af7ca --- /dev/null +++ b/src/services/pluginsManager/map/openMap.test.ts @@ -0,0 +1,93 @@ +/* eslint-disable no-magic-numbers */ +import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; +import { RootState, store } from '@/redux/store'; +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { MODELS_MOCK, MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; +import { PluginsEventBus } from '../pluginsEventBus'; +import { openMap } from './openMap'; + +jest.mock('../../../redux/store'); + +describe('openMap', () => { + const getStateSpy = jest.spyOn(store, 'getState'); + const dispatchSpy = jest.spyOn(store, 'dispatch'); + const pluginDispatchEvent = jest.spyOn(PluginsEventBus, 'dispatchEvent'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should set active map when map with provided id is already opened', () => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { ...initialMapDataFixture, modelId: 5052 }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + models: { + data: MODELS_MOCK_SHORT, + loading: 'succeeded', + error: { name: '', message: '' }, + }, + }) as RootState, + ); + + openMap({ id: 5053 }); + + expect(dispatchSpy).toHaveBeenCalledWith(setActiveMap({ modelId: 5053 })); + expect(pluginDispatchEvent).toHaveBeenCalledWith('onSubmapClose', 5052); + expect(pluginDispatchEvent).toHaveBeenCalledWith('onSubmapOpen', 5053); + }); + + it('should open map and set active map when map with provided id is not already opened', () => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { ...initialMapDataFixture, modelId: 5052 }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + models: { + data: MODELS_MOCK, + loading: 'succeeded', + error: { name: '', message: '' }, + }, + }) as RootState, + ); + + openMap({ id: 5061 }); + + expect(dispatchSpy).toHaveBeenCalledWith( + openMapAndSetActive({ modelId: 5061, modelName: 'Wnt signaling' }), + ); + expect(pluginDispatchEvent).toHaveBeenCalledWith('onSubmapClose', 5052); + expect(pluginDispatchEvent).toHaveBeenCalledWith('onSubmapOpen', 5061); + }); + + it('should throw an error when map with provided id does not exist', () => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { ...initialMapDataFixture, modelId: 5052 }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + models: { + data: MODELS_MOCK, + loading: 'succeeded', + error: { name: '', message: '' }, + }, + }) as RootState, + ); + + expect(() => openMap({ id: 3 })).toThrow('Map with provided id does not exist'); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(PluginsEventBus.dispatchEvent).not.toHaveBeenCalled(); + }); +}); diff --git a/src/services/pluginsManager/map/openMap.ts b/src/services/pluginsManager/map/openMap.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec6a9149d04a6503d03d5574f31e6727faf01514 --- /dev/null +++ b/src/services/pluginsManager/map/openMap.ts @@ -0,0 +1,33 @@ +import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors'; +import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; +import { modelsDataSelector } from '@/redux/models/models.selectors'; +import { store } from '@/redux/store'; +import { PluginsEventBus } from '../pluginsEventBus'; +import { ERROR_MAP_NOT_FOUND } from '../errorMessages'; + +export type OpenMapArgs = { + id: number; +}; + +export const openMap = ({ id }: OpenMapArgs): void => { + const { getState, dispatch } = store; + const models = modelsDataSelector(getState()); + const openedMaps = mapOpenedMapsSelector(getState()); + const mapToOpen = models.find(model => model.idObject === id); + const currentModelId = mapModelIdSelector(getState()); + + if (!mapToOpen) throw new Error(ERROR_MAP_NOT_FOUND); + + const isMapAlreadyOpened = openedMaps.some(map => map.modelId === mapToOpen.idObject); + + if (isMapAlreadyOpened) { + dispatch(setActiveMap({ modelId: mapToOpen.idObject })); + } else { + dispatch(openMapAndSetActive({ modelId: mapToOpen.idObject, modelName: mapToOpen.name })); + } + + if (currentModelId !== mapToOpen.idObject) { + PluginsEventBus.dispatchEvent('onSubmapClose', currentModelId); + PluginsEventBus.dispatchEvent('onSubmapOpen', mapToOpen.idObject); + } +}; diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts index 4a747095a9d1fdf446b603ea06e0760b40c9a906..8f1b42fe3f895d34ee15ea2beb690b16a8d2f202 100644 --- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts +++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts @@ -1,11 +1,16 @@ import { SearchParams } from './triggerSearch.types'; import { searchByQuery } from './searchByQuery'; import { searchByCoordinates } from './searchByCoordinates'; +import { + ERROR_INVALID_COORDINATES, + ERROR_INVALID_MODEL_ID_TYPE, + ERROR_INVALID_QUERY_TYPE, +} from '../../errorMessages'; export async function triggerSearch(params: SearchParams): Promise<void> { if ('query' in params) { if (typeof params.query !== 'string') { - throw new Error('Invalid query type. The query should be of string type'); + throw new Error(ERROR_INVALID_QUERY_TYPE); } searchByQuery(params.query, params.perfectSearch); } else { @@ -16,11 +21,11 @@ export async function triggerSearch(params: SearchParams): Promise<void> { typeof params.coordinates.x !== 'number' || typeof params.coordinates.y !== 'number'; if (areCoordinatesInvalidType || areCoordinatesMissingKeys || areCoordinatesValuesInvalid) { - throw new Error('Invalid coordinates type or values'); + throw new Error(ERROR_INVALID_COORDINATES); } if (typeof params.modelId !== 'number') { - throw new Error('Invalid model id type. The model should be of number type'); + throw new Error(ERROR_INVALID_MODEL_ID_TYPE); } searchByCoordinates(params.coordinates, params.modelId); diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts index 63e46d12074f7c001dd653b196be2983e62b13ef..23a78ff89353268cccf150657ebb644f4c850259 100644 --- a/src/services/pluginsManager/pluginsManager.ts +++ b/src/services/pluginsManager/pluginsManager.ts @@ -2,6 +2,8 @@ import { PLUGINS_CONTENT_ELEMENT_ATTR_NAME, PLUGINS_CONTENT_ELEMENT_ID } from '@ import { registerPlugin } from '@/redux/plugins/plugins.thunks'; import { store } from '@/redux/store'; import md5 from 'crypto-js/md5'; +import { getModels } from './map/models/getModels'; +import { openMap } from './map/openMap'; import { bioEntitiesMethods } from './bioEntities'; import { triggerSearch } from './map/triggerSearch'; import { PluginsEventBus } from './pluginsEventBus'; @@ -31,6 +33,10 @@ export const PluginsManager: PluginsManagerType = { bioEntities: bioEntitiesMethods, }, map: { + data: { + getModels, + }, + openMap, triggerSearch, }, project: { diff --git a/src/services/pluginsManager/project/data/getDisease.ts b/src/services/pluginsManager/project/data/getDisease.ts index 6d7e0ad775daa972825d4299714389f93698eb84..24c975fc08ba74790b6d6a3923e7f9b445e97b26 100644 --- a/src/services/pluginsManager/project/data/getDisease.ts +++ b/src/services/pluginsManager/project/data/getDisease.ts @@ -2,13 +2,14 @@ import { projectSchema } from '@/models/projectSchema'; import { store } from '@/redux/store'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { ERROR_PROJECT_NOT_FOUND } from '../../errorMessages'; type GetDiseaseReturnType = Project['disease'] | undefined; export const getDisease = (): GetDiseaseReturnType => { const project = store.getState().project.data; - if (!project) throw new Error('Project does not exist'); + if (!project) throw new Error(ERROR_PROJECT_NOT_FOUND); const isDataValid = validateDataUsingZodSchema(project, projectSchema); diff --git a/src/services/pluginsManager/project/data/getName.ts b/src/services/pluginsManager/project/data/getName.ts index c024ee22791fafd863d91270ef023df644c6395d..427d3c4de3f41450a3ff70dc88719abcfac148d6 100644 --- a/src/services/pluginsManager/project/data/getName.ts +++ b/src/services/pluginsManager/project/data/getName.ts @@ -2,13 +2,14 @@ import { projectSchema } from '@/models/projectSchema'; import { store } from '@/redux/store'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { ERROR_PROJECT_NOT_FOUND } from '../../errorMessages'; type GetNameReturnType = Project['name'] | undefined; export const getName = (): GetNameReturnType => { const project = store.getState().project.data; - if (!project) throw new Error('Project does not exist'); + if (!project) throw new Error(ERROR_PROJECT_NOT_FOUND); const isDataValid = validateDataUsingZodSchema(project, projectSchema); diff --git a/src/services/pluginsManager/project/data/getOrganism.ts b/src/services/pluginsManager/project/data/getOrganism.ts index a19fe476206e39e5034eb0792e149d6d50a67053..33dd628c0105b5aa3bb6542fbc8961e7bdc31c81 100644 --- a/src/services/pluginsManager/project/data/getOrganism.ts +++ b/src/services/pluginsManager/project/data/getOrganism.ts @@ -2,13 +2,14 @@ import { projectSchema } from '@/models/projectSchema'; import { store } from '@/redux/store'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { ERROR_PROJECT_NOT_FOUND } from '../../errorMessages'; type GetOrganismReturnType = Project['organism'] | undefined; export const getOrganism = (): GetOrganismReturnType => { const project = store.getState().project.data; - if (!project) throw new Error('Project does not exist'); + if (!project) throw new Error(ERROR_PROJECT_NOT_FOUND); const isDataValid = validateDataUsingZodSchema(project, projectSchema); diff --git a/src/services/pluginsManager/project/data/getProjectId.ts b/src/services/pluginsManager/project/data/getProjectId.ts index 2a8fd53f5f8de2d5b03d7114e2d35965ef0ef072..b790fc939d8dfe1b1871c4ebbffc1a80d21c135a 100644 --- a/src/services/pluginsManager/project/data/getProjectId.ts +++ b/src/services/pluginsManager/project/data/getProjectId.ts @@ -2,13 +2,14 @@ import { projectSchema } from '@/models/projectSchema'; import { store } from '@/redux/store'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { ERROR_PROJECT_NOT_FOUND } from '../../errorMessages'; type GetProjectIdReturnType = Project['projectId'] | undefined; export const getProjectId = (): GetProjectIdReturnType => { const project = store.getState().project.data; - if (!project) throw new Error('Project does not exist'); + if (!project) throw new Error(ERROR_PROJECT_NOT_FOUND); const isDataValid = validateDataUsingZodSchema(project, projectSchema); diff --git a/src/services/pluginsManager/project/data/getVersion.ts b/src/services/pluginsManager/project/data/getVersion.ts index 882735f46b6c1ab1328cfcd53d42104682a37ebd..ead2d68f2cbe00a1ef3bea236795b2bccc138458 100644 --- a/src/services/pluginsManager/project/data/getVersion.ts +++ b/src/services/pluginsManager/project/data/getVersion.ts @@ -2,13 +2,14 @@ import { projectSchema } from '@/models/projectSchema'; import { store } from '@/redux/store'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { ERROR_PROJECT_NOT_FOUND } from '../../errorMessages'; type GetVersionReturnType = Project['version'] | undefined; export const getVersion = (): GetVersionReturnType => { const project = store.getState().project.data; - if (!project) throw new Error('Project does not exist'); + if (!project) throw new Error(ERROR_PROJECT_NOT_FOUND); const isDataValid = validateDataUsingZodSchema(project, projectSchema);