diff --git a/package-lock.json b/package-lock.json index a019d479eb4ff5e3f186e1a549ef58e12027c17d..5c864d3aa9f8baaea4c51709434681d0b1747845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "react-dropzone": "14.2.3", "react-redux": "8.1.3", "react-select": "5.9.0", + "react-use-websocket": "4.11.1", "sonner": "1.4.3", "tailwind-merge": "1.14.0", "tailwindcss": "3.4.13", @@ -12275,6 +12276,11 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz", + "integrity": "sha512-39e8mK2a2A1h8uY3ePF45b2q0vwMOmaEy7J5qEhQg4n7vYa5oDLmqutG36kZQgAQ/3KCZS0brlGRbbZJ0+zfKQ==" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -14457,9 +14463,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -23504,6 +23510,11 @@ "prop-types": "^15.6.2" } }, + "react-use-websocket": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz", + "integrity": "sha512-39e8mK2a2A1h8uY3ePF45b2q0vwMOmaEy7J5qEhQg4n7vYa5oDLmqutG36kZQgAQ/3KCZS0brlGRbbZJ0+zfKQ==" + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -25133,9 +25144,9 @@ } }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 0e93c457de270cc0bef882b9c4648d061254b59f..5d0cb06b70c7feaecd6439110106d5eaaf512c68 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react-dropzone": "14.2.3", "react-redux": "8.1.3", "react-select": "5.9.0", + "react-use-websocket": "4.11.1", "sonner": "1.4.3", "tailwind-merge": "1.14.0", "tailwindcss": "3.4.13", @@ -64,7 +65,6 @@ "zod-to-json-schema": "3.22.4" }, "devDependencies": { - "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.6.1", "@commitlint/cli": "17.8.1", "@commitlint/config-conventional": "17.8.1", "@testing-library/jest-dom": "6.1.6", diff --git a/src/components/AppWrapper/AppWrapper.component.tsx b/src/components/AppWrapper/AppWrapper.component.tsx index 39a65352f62dfc4a51e0c0a198333ef4f7b8ab17..686bd455a6b2bf901287f467ebb053525a8d3257 100644 --- a/src/components/AppWrapper/AppWrapper.component.tsx +++ b/src/components/AppWrapper/AppWrapper.component.tsx @@ -4,6 +4,7 @@ import { ReactNode } from 'react'; import { Provider } from 'react-redux'; import { Toaster } from 'sonner'; import { Modal } from '@/components/FunctionalArea/Modal'; +import { WebSocketEntityUpdatesProvider } from '@/utils/websocket-entity-updates/webSocketEntityUpdatesProvider'; interface AppWrapperProps { children: ReactNode; @@ -13,17 +14,19 @@ export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => { return ( <MapInstanceProvider> <Provider store={store}> - <> - <Modal /> - <Toaster - position="top-center" - visibleToasts={1} - style={{ - width: '700px', - }} - /> - {children} - </> + <WebSocketEntityUpdatesProvider> + <> + <Modal /> + <Toaster + position="top-center" + visibleToasts={1} + style={{ + width: '700px', + }} + /> + {children} + </> + </WebSocketEntityUpdatesProvider> </Provider> </MapInstanceProvider> ); diff --git a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.test.tsx b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.test.tsx index 0e83701bc5e0d32ad44f6d980693168e645a5254..c0051a1399061d0eec637ee05873454cbb50f27d 100644 --- a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.test.tsx +++ b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.test.tsx @@ -8,6 +8,8 @@ import { act, render, screen } from '@testing-library/react'; import { CONTEXT_MENU_INITIAL_STATE } from '@/redux/contextMenu/contextMenu.constants'; import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { PluginsContextMenu } from '@/services/pluginsManager/pluginContextMenu/pluginsContextMenu'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { DEFAULT_ERROR } from '@/constants/errors'; import { ContextMenu } from './ContextMenu.component'; const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { @@ -97,31 +99,40 @@ describe('ContextMenu - Component', () => { it('should display uniprot id as option if it is provided', () => { renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: bioEntityContentFixture.bioEntity.id.toString(), + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: 'BioEntity Full Name', - references: [ - { - ...bioEntityContentFixture.bioEntity.references[0], - type: 'UNIPROT', - }, - ], - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: modelElementFixture.id.toString(), + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: 'BioEntity Full Name', + references: [ + { + ...bioEntityContentFixture.bioEntity.references[0], + type: 'UNIPROT', + }, + ], + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, contextMenu: { ...CONTEXT_MENU_INITIAL_STATE, @@ -144,31 +155,40 @@ describe('ContextMenu - Component', () => { it('should open molart modal when clicking on uniprot', async () => { const { store } = renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: bioEntityContentFixture.bioEntity.id.toString(), + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: 'BioEntity Full Name', - references: [ - { - ...bioEntityContentFixture.bioEntity.references[0], - type: 'UNIPROT', - }, - ], - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: modelElementFixture.id.toString(), + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: 'BioEntity Full Name', + references: [ + { + ...bioEntityContentFixture.bioEntity.references[0], + type: 'UNIPROT', + }, + ], + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, contextMenu: { ...CONTEXT_MENU_INITIAL_STATE, diff --git a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx index 1e4d612b0297062a993227bdd18389bc5b9050b8..fe738b5116942a2141f039b5605d665425716360 100644 --- a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx +++ b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx @@ -1,4 +1,3 @@ -import { searchedBioEntityElementUniProtIdSelector } from '@/redux/bioEntity/bioEntity.selectors'; import { contextMenuSelector } from '@/redux/contextMenu/contextMenu.selector'; import { closeContextMenu } from '@/redux/contextMenu/contextMenu.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; @@ -9,12 +8,13 @@ import { twMerge } from 'tailwind-merge'; import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, ZERO } from '@/constants/common'; import { PluginsContextMenu } from '@/services/pluginsManager/pluginContextMenu/pluginsContextMenu'; -import { BioEntity, NewReaction } from '@/types/models'; +import { ModelElement, NewReaction } from '@/types/models'; import { ClickCoordinates } from '@/services/pluginsManager/pluginContextMenu/pluginsContextMenu.types'; import { currentModelSelector } from '@/redux/models/models.selectors'; import { mapDataLastPositionSelector } from '@/redux/map/map.selectors'; import { DEFAULT_ZOOM } from '@/constants/map'; import { OutsideClickWrapper } from '@/shared/OutsideClickWrapper'; +import { searchedModelElementUniProtIdSelector } from '@/redux/modelElements/modelElements.selector'; export const ContextMenu = (): React.ReactNode => { const pluginContextMenu = PluginsContextMenu.menuItems; @@ -22,7 +22,7 @@ export const ContextMenu = (): React.ReactNode => { const lastPosition = useAppSelector(mapDataLastPositionSelector); const dispatch = useAppDispatch(); const { isOpen, coordinates } = useAppSelector(contextMenuSelector); - const unitProtId = useAppSelector(searchedBioEntityElementUniProtIdSelector); + const unitProtId = useAppSelector(searchedModelElementUniProtIdSelector); const isUnitProtIdAvailable = (): boolean => unitProtId !== undefined; @@ -49,7 +49,10 @@ export const ContextMenu = (): React.ReactNode => { const modelId = model ? model.id : ZERO; const handleCallback = ( - callback: (coordinates: ClickCoordinates, element: BioEntity | NewReaction | undefined) => void, + callback: ( + coordinates: ClickCoordinates, + element: ModelElement | NewReaction | undefined, + ) => void, ) => { return () => { closeContextMenuFunction(); diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx index fe837db83b68d3baa2417df4dbe88ed583b0479f..c7f3f156cbe91b17d1f84a2dba35ab70d25ca150 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx @@ -98,6 +98,7 @@ describe('EditOverlayModal - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -136,6 +137,7 @@ describe('EditOverlayModal - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -175,6 +177,7 @@ describe('EditOverlayModal - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -220,6 +223,7 @@ describe('EditOverlayModal - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts index b2be1b21d0dc6969fd4d7dfb834b2dde359ea219..ac20ca6dbb4bff2878871d3fa430d70c397a4454 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts +++ b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts @@ -15,6 +15,7 @@ describe('useEditOverlay', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -54,6 +55,7 @@ describe('useEditOverlay', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -96,6 +98,7 @@ describe('useEditOverlay', () => { login: null, role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -135,6 +138,7 @@ describe('useEditOverlay', () => { login: 'test', role: 'user', userData: null, + token: null, }, modal: { isOpen: true, @@ -174,6 +178,7 @@ describe('useEditOverlay', () => { login: null, role: 'user', userData: null, + token: null, }, modal: { isOpen: true, diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx index dee287dcd03c2d23e5006f160b0f74c5cce9ee87..0c0973f2bb3ea4843ff5788e9c6e36863681c94d 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx @@ -43,7 +43,7 @@ const renderComponent = ( ...LAYERS_STATE_INITIAL_LAYER_MOCK, data: { ...LAYER_STATE_DEFAULT_DATA, - activeLayer: 1, + activeLayers: [1], }, }, }, @@ -136,6 +136,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { y: 1, width: 1, height: 1, + layer: 1, glyph: 1, z: 1, }; diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx index 655bb8d9c70c13f7e103b19c6103809e4afd99d8..e4dfcdd01a2d83fe449f515152ec47c68b592ce1 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx @@ -5,7 +5,6 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; @@ -22,7 +21,6 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const { mapInstance } = useMapInstance(); const currentModelId = useAppSelector(currentModelIdSelector); - const activeLayer = useAppSelector(layersActiveLayerSelector); const dispatch = useAppDispatch(); const [selectedGlyph, setSelectedGlyph] = useState<number | null>( @@ -32,7 +30,7 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const [isSending, setIsSending] = useState<boolean>(false); const handleSubmit = async (): Promise<void> => { - if (!layerImageObject || !activeLayer) { + if (!layerImageObject) { return; } setIsSending(true); @@ -49,15 +47,17 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const layerImage = await dispatch( updateLayerImageObject({ modelId: currentModelId, - layerId: activeLayer, + layerId: layerImageObject.layer, ...layerImageObject, glyph: glyphId, }), ).unwrap(); if (layerImage) { - dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage })); + dispatch( + layerUpdateImage({ modelId: currentModelId, layerId: layerImage.layer, layerImage }), + ); dispatch(mapEditToolsSetLayerObject(layerImage)); - updateGlyph(mapInstance, activeLayer, layerImage); + updateGlyph(mapInstance, layerImage.layer, layerImage); } showToast({ type: 'success', diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx index 6a4760962f2cf3bdba4561803db45153673fff32..0d0de1fe5c6508f1e307a9bb7999a21bb980bba7 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx @@ -36,7 +36,8 @@ const renderComponent = (): { store: StoreType } => { ...LAYERS_STATE_INITIAL_LAYER_MOCK, data: { ...LAYER_STATE_DEFAULT_DATA, - activeLayer: 1, + activeLayers: [1], + drawLayer: 1, }, }, }, diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx index 740a101c63d8bfc439e120a41ac6cdde852dca34..1efb7d8874cc8b998f5edf00a05ce2d40084d2d5 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; +import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; import { addLayerImageObject } from '@/redux/layers/layers.thunks'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { SerializedError } from '@reduxjs/toolkit'; @@ -15,10 +15,11 @@ import { useMapInstance } from '@/utils/context/mapInstanceContext'; import { layerAddImage } from '@/redux/layers/layers.slice'; import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; export const LayerImageObjectFactoryModal: React.FC = () => { const currentModelId = useAppSelector(currentModelIdSelector); - const activeLayer = useAppSelector(layersActiveLayerSelector); + const drawLayer = useAppSelector(layersDrawLayerSelector); const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector); const dispatch = useAppDispatch(); const highestZIndex = useAppSelector(highestZIndexSelector); @@ -29,7 +30,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { const [isSending, setIsSending] = useState<boolean>(false); const handleSubmit = async (): Promise<void> => { - if (!layerImageObjectFactoryState || !activeLayer) { + if (!layerImageObjectFactoryState || !drawLayer) { return; } setIsSending(true); @@ -45,7 +46,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { const imageData = await dispatch( addLayerImageObject({ modelId: currentModelId, - layerId: activeLayer, + layerId: drawLayer, x: layerImageObjectFactoryState.x, y: layerImageObjectFactoryState.y, z: highestZIndex + 1, @@ -62,11 +63,11 @@ export const LayerImageObjectFactoryModal: React.FC = () => { return; } dispatch( - layerAddImage({ modelId: currentModelId, layerId: activeLayer, layerImage: imageData }), + layerAddImage({ modelId: currentModelId, layerId: drawLayer, layerImage: imageData }), ); drawElementOnLayer({ mapInstance, - activeLayer, + activeLayer: drawLayer, object: imageData, drawFunctionKey: 'drawImage', }); @@ -75,6 +76,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { message: 'A new image has been successfully added', }); dispatch(closeModal()); + dispatch(mapEditToolsSetActiveAction(null)); } catch (error) { const typedError = error as SerializedError; showToast({ diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx index 4a2c58eba00154caee6eb1a2f91009f3338bfac7..c739fb38104b2a4c91fbff35c657dea106843d10 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx @@ -41,7 +41,8 @@ const renderComponent = (): { store: StoreType } => { ...LAYERS_STATE_INITIAL_LAYER_MOCK, data: { ...LAYER_STATE_DEFAULT_DATA, - activeLayer: 1, + activeLayers: [1], + drawLayer: 1, }, }, }, diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx index e715c0934030fea1e66da1f4209d1e540e107706..e1ccf6122d31395dca1a127417f02bcc672da7bf 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx @@ -13,7 +13,7 @@ import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTex import { Color } from '@/types/models'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerTextFactoryStateSelector } from '@/redux/modal/modal.selector'; -import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; +import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { showToast } from '@/utils/showToast'; @@ -23,10 +23,11 @@ import { addLayerText } from '@/redux/layers/layers.thunks'; import { layerAddText } from '@/redux/layers/layers.slice'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; -import { BLACK_COLOR, WHITE_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; +import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; +import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; export const LayerTextFactoryModal: React.FC = () => { - const activeLayer = useAppSelector(layersActiveLayerSelector); + const drawLayer = useAppSelector(layersDrawLayerSelector); const currentModelId = useAppSelector(currentModelIdSelector); const layerTextFactoryState = useAppSelector(layerTextFactoryStateSelector); const dispatch = useAppDispatch(); @@ -40,18 +41,18 @@ export const LayerTextFactoryModal: React.FC = () => { horizontalAlign: DEFAULT_HORIZONTAL_ALIGNMENT, verticalAlign: DEFAULT_VERTICAL_ALIGNMENT, color: BLACK_COLOR, - borderColor: { ...WHITE_COLOR, alpha: 0 }, + borderColor: BLACK_COLOR, }); const handleSubmit = async (): Promise<void> => { - if (!layerTextFactoryState || !activeLayer) { + if (!layerTextFactoryState || !drawLayer) { return; } try { const textData = await dispatch( addLayerText({ modelId: currentModelId, - layerId: activeLayer, + layerId: drawLayer, boundingBox: layerTextFactoryState, textData: data, z: highestZIndex + 1, @@ -64,12 +65,10 @@ export const LayerTextFactoryModal: React.FC = () => { }); return; } - dispatch( - layerAddText({ modelId: currentModelId, layerId: activeLayer, layerText: textData }), - ); + dispatch(layerAddText({ modelId: currentModelId, layerId: drawLayer, layerText: textData })); drawElementOnLayer({ mapInstance, - activeLayer, + activeLayer: drawLayer, object: textData, drawFunctionKey: 'drawText', }); @@ -78,6 +77,7 @@ export const LayerTextFactoryModal: React.FC = () => { message: 'A new text has been successfully added', }); dispatch(closeModal()); + dispatch(mapEditToolsSetActiveAction(null)); } catch (error) { const typedError = error as SerializedError; showToast({ @@ -87,10 +87,6 @@ export const LayerTextFactoryModal: React.FC = () => { } finally { setIsSending(false); } - setIsSending(true); - setTimeout(() => { - setIsSending(false); - }, 5000); }; const changeValues = (value: string | number | Color, key: string): void => { diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx index b2b7bb4582ae790c7e0523425483ccce740deffb..22bcde3c96feae8b26f9957cfa54d9e61ebe8206 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx @@ -58,11 +58,15 @@ export const LayerTextForm = ({ data, onChange }: LayerTextFormProps): React.JSX </div> <div> <span>Color:</span> - <ColorTilePicker colorChange={color => onChange(hexToRgbIntAlpha(color), 'color')} /> + <ColorTilePicker + initialColor={data.color} + colorChange={color => onChange(hexToRgbIntAlpha(color), 'color')} + /> </div> <div> <span>Border color:</span> <ColorTilePicker + initialColor={data.borderColor} colorChange={color => onChange(hexToRgbIntAlpha(color), 'borderColor')} /> </div> diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx index c13cc7a9112ce05bb872d21ae56e39fd17fa8893..af7fd74229d8aa4e09684dc7a8b6953a2e8f08e2 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx @@ -18,6 +18,8 @@ jest.mock('./utils/useOverviewImageSize', () => ({ })), })); +const PROJECT_DIRECTORY = 'directory'; + const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); @@ -42,6 +44,7 @@ describe('OverviewImagesModal - component', () => { ...projectFixture, overviewImageViews: [], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -70,6 +73,7 @@ describe('OverviewImagesModal - component', () => { ...projectFixture, overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -91,7 +95,7 @@ describe('OverviewImagesModal - component', () => { it('should render image with valid src', () => { const imageElement = screen.getByAltText('overview'); - const result = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; + const result = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_DIRECTORY}/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; expect(imageElement.getAttribute('src')).toBe(result); }); diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts index 702a3d48826f3b61aec54a7df6ca01b7ee885fd7..77e90faa86f0554645dd3075927d12f5e417620f 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts @@ -7,6 +7,8 @@ import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithSto import { renderHook } from '@testing-library/react'; import { useOverviewImage } from './useOverviewImage'; +const PROJECT_DIRECTORY = 'directory'; + describe('useOverviewImage - hook', () => { describe('when image data is invalid', () => { const { Wrapper } = getReduxWrapperWithStore({ @@ -15,6 +17,7 @@ describe('useOverviewImage - hook', () => { ...projectFixture, overviewImageViews: [], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -48,6 +51,7 @@ describe('useOverviewImage - hook', () => { ...projectFixture, overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -66,7 +70,7 @@ describe('useOverviewImage - hook', () => { }); it('should return default size of image and valid imageUrl', () => { - const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; + const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_DIRECTORY}/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; expect(result.current).toMatchObject({ imageUrl, @@ -88,6 +92,7 @@ describe('useOverviewImage - hook', () => { }, ], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -109,7 +114,7 @@ describe('useOverviewImage - hook', () => { ); it('should return size of image and valid imageUrl', () => { - const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; + const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_DIRECTORY}/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; expect(result.current).toMatchObject({ imageUrl, diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts index da3d6a6ab93353a6b3e38f456e291e0cf11652ed..cdd9aae42303c21439d39d6c63781c8fd625077e 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts @@ -6,6 +6,8 @@ import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithSto import { renderHook } from '@testing-library/react'; import { useOverviewImageUrl } from './useOverviewImageUrl'; +const PROJECT_DIRECTORY = 'directory'; + describe('useOverviewImageUrl - hook', () => { describe('when currentImage data is valid', () => { const { Wrapper } = getReduxWrapperWithStore({ @@ -14,6 +16,7 @@ describe('useOverviewImageUrl - hook', () => { ...projectFixture, overviewImageViews: [], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -41,6 +44,7 @@ describe('useOverviewImageUrl - hook', () => { ...projectFixture, overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + directory: PROJECT_DIRECTORY, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -58,7 +62,7 @@ describe('useOverviewImageUrl - hook', () => { const { result } = renderHook(() => useOverviewImageUrl(), { wrapper: Wrapper }); expect(result.current).toBe( - `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`, + `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_DIRECTORY}/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`, ); }); }); diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts index af9e09fe05b7d6ec6b6bab6fca489b438354066e..053e784ca91a3730e0acc13f9a629eb88373dd32 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts @@ -1,13 +1,16 @@ import { BASE_MAP_IMAGES_URL } from '@/constants'; -import { currentOverviewImageSelector } from '@/redux/project/project.selectors'; +import { + currentOverviewImageSelector, + projectDirectorySelector, +} from '@/redux/project/project.selectors'; import { useSelector } from 'react-redux'; export const useOverviewImageUrl = (): string => { const currentImage = useSelector(currentOverviewImageSelector); - - if (!currentImage) { + const directory = useSelector(projectDirectorySelector); + if (!currentImage || !directory) { return ''; } - return `${BASE_MAP_IMAGES_URL}/map_images/${currentImage.filename}`; + return `${BASE_MAP_IMAGES_URL}/map_images/${directory}/${currentImage.filename}`; }; diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx index ea4fe1f6f76cc3fcfec45cb21decbbb4b02d693d..22beff1a724b8eb4d7da732ed1624e8deefe9179 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx @@ -5,7 +5,7 @@ import { apiPath } from '@/redux/apiPath'; import { DEFAULT_POSITION } from '@/redux/map/map.constants'; import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { AppDispatch, RootState } from '@/redux/store'; -import { BioEntity, MapModel } from '@/types/models'; +import { MapModel, PublicationElement } from '@/types/models'; import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { InitialStoreState, @@ -14,12 +14,12 @@ import { import { render, screen, waitFor } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; import { MockStoreEnhanced } from 'redux-mock-store'; -import { isReactionBioEntity } from '@/redux/reactions/isReactionBioentity'; +import { isReactionElement } from '@/redux/reactions/isReactionElement'; import { ElementLink } from './ElementLink.component'; const mockedAxiosNewClient = mockNetworkNewAPIResponse(); -const TARGET_ELEMENT: BioEntity = { +const TARGET_ELEMENT: PublicationElement = { ...bioEntityResponseFixture.content[FIRST_ARRAY_ELEMENT].bioEntity, id: 123, model: 52, @@ -36,20 +36,20 @@ const OTHER_MODEL: MapModel = { }; interface Props { - target: BioEntity; + target: PublicationElement; } -const getElementText = (bioEntity: BioEntity): string => { - const isReaction = isReactionBioEntity(bioEntity); +const getElementText = (element: PublicationElement): string => { + const isReaction = isReactionElement(element); const prefix = isReaction ? 'Reaction: ' : 'Element: '; - return prefix + bioEntity.elementId; + return prefix + element.elementId; }; -const getSearchQuery = (bioEntity: BioEntity): string => { - const isReaction = isReactionBioEntity(bioEntity); +const getSearchQuery = (element: PublicationElement): string => { + const isReaction = isReactionElement(element); - return (isReaction ? 'reaction:' : 'element:') + bioEntity.id; + return (isReaction ? 'reaction:' : 'element:') + element.id; }; const renderComponent = ( @@ -86,10 +86,10 @@ describe('ElementLink - component', () => { }); it('should should show element id', async () => { - const bioEntity = TARGET_ELEMENT; + const element = TARGET_ELEMENT; await waitFor(() => { - expect(screen.getByText(getElementText(bioEntity))).toBeInTheDocument(); + expect(screen.getByText(getElementText(element))).toBeInTheDocument(); }); }); }); @@ -116,10 +116,10 @@ describe('ElementLink - component', () => { }, ); - const bioEntity = TARGET_ELEMENT; + const element = TARGET_ELEMENT; await waitFor(() => { - const link = screen.getByText(getElementText(bioEntity)); + const link = screen.getByText(getElementText(element)); link.click(); const actions = store.getActions(); @@ -148,7 +148,7 @@ describe('ElementLink - component', () => { expect(actions).toEqual( expect.arrayContaining([ expect.objectContaining({ - payload: getSearchQuery(bioEntity), + payload: getSearchQuery(element), type: 'drawer/openSearchDrawerWithSelectedTab', }), ]), @@ -206,10 +206,10 @@ describe('ElementLink - component', () => { }, ); - const bioEntity = TARGET_ELEMENT; + const element = TARGET_ELEMENT; await waitFor(() => { - const link = screen.getByText(getElementText(bioEntity)); + const link = screen.getByText(getElementText(element)); link.click(); const actions = store.getActions(); @@ -238,7 +238,7 @@ describe('ElementLink - component', () => { expect(actions).toEqual( expect.arrayContaining([ expect.objectContaining({ - payload: getSearchQuery(bioEntity), + payload: getSearchQuery(element), type: 'drawer/openSearchDrawerWithSelectedTab', }), ]), diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.tsx index 8bf24727e7f2a7f8ba8254f7ffaed63fd9a635af..baad3d083e77076a7f428b5b9aaa2af099e6ee17 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.tsx @@ -13,11 +13,11 @@ import { closeModal } from '@/redux/modal/modal.slice'; import { modelsNameMapSelector } from '@/redux/models/models.selectors'; import { getSearchData } from '@/redux/search/search.thunks'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { BioEntity } from '@/types/models'; -import { isReactionBioEntity } from '@/redux/reactions/isReactionBioentity'; +import { PublicationElement } from '@/types/models'; +import { isReactionElement } from '@/redux/reactions/isReactionElement'; interface Props { - target: BioEntity; + target: PublicationElement; } export const ElementLink = ({ target }: Props): JSX.Element => { @@ -26,7 +26,7 @@ export const ElementLink = ({ target }: Props): JSX.Element => { const currentModelId = useAppSelector(mapModelIdSelector); const mapsNames = useAppSelector(modelsNameMapSelector); - const isReaction = isReactionBioEntity(target); + const isReaction = isReactionElement(target); const isMapAlreadyOpened = (modelId: number): boolean => openedMaps.some(map => map.modelId === modelId); diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.test.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.test.tsx index 2e7349c6eed66cc238eacfdfa4ccb8fcf9dd4950..84798d95e4a28641e9882ca2767116580ffc3984 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.test.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.test.tsx @@ -1,18 +1,18 @@ import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { AppDispatch, RootState } from '@/redux/store'; -import { BioEntity } from '@/types/models'; +import { PublicationElement } from '@/types/models'; import { InitialStoreState, getReduxStoreWithActionsListener, } from '@/utils/testing/getReduxStoreActionsListener'; import { render, screen, waitFor } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; -import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture'; -import { isReactionBioEntity } from '@/redux/reactions/isReactionBioentity'; +import { isReactionElement } from '@/redux/reactions/isReactionElement'; +import { publicationElementFixture } from '@/models/fixtures/publicationElementFixture'; import { ElementsOnMapCell } from './ElementsOnMapCell.component'; interface Props { - targets: BioEntity[]; + targets: PublicationElement[]; } const renderComponent = ( @@ -33,8 +33,8 @@ const renderComponent = ( ); }; -const elementFixture = { ...bioEntityFixture, idReaction: undefined }; -const reactionFixture = { ...bioEntityFixture, idReaction: '123' }; +const elementFixture = { ...publicationElementFixture, idReaction: undefined }; +const reactionFixture = { ...publicationElementFixture, idReaction: '123' }; const mockTargets = [{ ...elementFixture }, { ...reactionFixture }]; @@ -49,7 +49,7 @@ describe('ElementsOnMapCell - component', () => { await waitFor(() => { // type as elementId - const isReaction = isReactionBioEntity(bioEntity); + const isReaction = isReactionElement(bioEntity); const prefix = isReaction ? 'Reaction: ' : 'Element: '; expect(screen.getByText(prefix + bioEntity.elementId)).toBeInTheDocument(); }); diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.tsx index 13c1269f4bf4685edc700a21a4be05eaf9f69067..36d7a247d1f7f559b4e532ea8db386a5cc8947dd 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementsOnMapCell.component.tsx @@ -1,9 +1,9 @@ import { ONE } from '@/constants/common'; -import { BioEntity } from '@/types/models'; +import { PublicationElement } from '@/types/models'; import { ElementLink } from './ElementLink'; interface Props { - targets: BioEntity[]; + targets: PublicationElement[]; } export const ElementsOnMapCell = ({ targets }: Props): JSX.Element => { diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx index cc0db919dcf46f8e33488d78755cedd014562755..562ccefac2268bb29b50aa78e5006ee1a2668e2a 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx @@ -23,7 +23,7 @@ import { } from '@tanstack/react-table'; import { useRef, useState } from 'react'; import { z } from 'zod'; -import { bioEntitySchema } from '@/models/bioEntitySchema'; +import { publicationElementSchema } from '@/models/publicationElementSchema'; import { ElementsOnMapCell } from './ElementsOnMapCell'; import { FilterBySubmapHeader } from './FilterBySubmapHeader/FilterBySubmapHeader.component'; import { DEFAULT_PAGE_SIZE } from './PublicationsTable.constants'; @@ -74,9 +74,7 @@ const columns = [ cell: ({ getValue }): JSX.Element => { try { const valueObject: unknown = JSON.parse(getValue()); - // eslint-disable-next-line no-console - console.log(valueObject); - const targets = z.array(bioEntitySchema).parse(valueObject); + const targets = z.array(publicationElementSchema).parse(valueObject); return <ElementsOnMapCell targets={targets} />; } catch (error) { diff --git a/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.test.tsx b/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.test.tsx index b8bc885aedc1f6e0382d6355baf871510edc85cd..494e4e379f625b796d6f54af9a31a27f2b2453b8 100644 --- a/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.test.tsx @@ -49,7 +49,7 @@ describe('ClearAnchorsButton - component', () => { { payload: undefined, type: 'contextMenu/closeContextMenu' }, { payload: undefined, type: 'reactions/resetReactionsData' }, { payload: undefined, type: 'search/clearSearchData' }, - { payload: undefined, type: 'bioEntityContents/clearBioEntities' }, + { payload: undefined, type: 'modelElements/clearSearchModelElements' }, { payload: undefined, type: 'drugs/clearDrugsData' }, { payload: undefined, type: 'chemicals/clearChemicalsData' }, ]); @@ -75,7 +75,7 @@ describe('ClearAnchorsButton - component', () => { { payload: undefined, type: 'contextMenu/closeContextMenu' }, { payload: undefined, type: 'reactions/resetReactionsData' }, { payload: undefined, type: 'search/clearSearchData' }, - { payload: undefined, type: 'bioEntityContents/clearBioEntities' }, + { payload: undefined, type: 'modelElements/clearSearchModelElements' }, { payload: undefined, type: 'drugs/clearDrugsData' }, { payload: undefined, type: 'chemicals/clearChemicalsData' }, ]); diff --git a/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.tsx b/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.tsx index 0cb3417872af8ff8ebd0541c3f04d2e838c8fef1..c370829b6e042d715612ade79e56e127e0c2e300 100644 --- a/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.tsx +++ b/src/components/FunctionalArea/TopBar/ClearAnchorsButton/ClearAnchorsButton.component.tsx @@ -1,4 +1,3 @@ -import { clearBioEntities } from '@/redux/bioEntity/bioEntity.slice'; import { clearChemicalsData } from '@/redux/chemicals/chemicals.slice'; import { closeContextMenu } from '@/redux/contextMenu/contextMenu.slice'; import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors'; @@ -11,6 +10,7 @@ import { Button } from '@/shared/Button'; import { Icon } from '@/shared/Icon'; import React from 'react'; import { useSelector } from 'react-redux'; +import { clearSearchModelElements } from '@/redux/modelElements/modelElements.slice'; export const ClearAnchorsButton = (): React.ReactNode => { const dispatch = useAppDispatch(); @@ -31,7 +31,7 @@ export const ClearAnchorsButton = (): React.ReactNode => { dispatch(clearSearchData()); // Reset old pins data - dispatch(clearBioEntities()); + dispatch(clearSearchModelElements()); dispatch(clearDrugsData()); dispatch(clearChemicalsData()); }; diff --git a/src/components/FunctionalArea/TopBar/User/User.component.test.tsx b/src/components/FunctionalArea/TopBar/User/User.component.test.tsx index 5a1ec9b5b884b2330ed91aa4faab3f490441ee70..f724d52af0d34f45eef0d580e1f43793e3d28a2c 100644 --- a/src/components/FunctionalArea/TopBar/User/User.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/User/User.component.test.tsx @@ -126,6 +126,7 @@ describe('AuthenticatedUser component', () => { login: null, role: null, userData: null, + token: null, }); }); diff --git a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx index 73231d8d0cb131410be7d075427705b50ce7459b..ed18ca49cc39a77540e24b73937955492c0cc989 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx @@ -1,11 +1,6 @@ import { SIZE_OF_ARRAY_WITH_ONE_ELEMENT, ZERO } from '@/constants/common'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; -import { - BIOENTITY_INITIAL_STATE_MOCK, - BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, - BIO_ENTITY_LINKING_TO_SUBMAP, -} from '@/redux/bioEntity/bioEntity.mock'; +import { BIOENTITY_INITIAL_STATE_MOCK } from '@/redux/bioEntity/bioEntity.mock'; import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; import { initialMapDataFixture, @@ -21,6 +16,12 @@ import { import { act, render, screen } from '@testing-library/react'; import { HISTAMINE_MAP_ID, MAIN_MAP_ID } from '@/constants/mocks'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { + MODEL_ELEMENT_LINKING_TO_SUBMAP, + MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK, +} from '@/redux/modelElements/modelElements.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { AssociatedSubmap } from './AssociatedSubmap.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -43,21 +44,27 @@ describe('AssociatedSubmap - component', () => { renderComponent({ bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, - data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { - bioentityId: bioEntityContentFixture.bioEntity.id, + bioentityId: modelElementFixture.id, drugs: {}, chemicals: {}, }, }, modelElements: { - [MAIN_MAP_ID]: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + [MAIN_MAP_ID]: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: { + data: MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK, + loading: 'idle', + error: DEFAULT_ERROR, }, } as ModelElementsState, models: { @@ -81,21 +88,27 @@ describe('AssociatedSubmap - component', () => { }, bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, - data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { - bioentityId: bioEntityContentFixture.bioEntity.id, + bioentityId: modelElementFixture.id, drugs: {}, chemicals: {}, }, }, modelElements: { - [MAIN_MAP_ID]: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + [MAIN_MAP_ID]: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: { + data: MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK, + loading: 'idle', + error: DEFAULT_ERROR, }, } as ModelElementsState, models: { @@ -118,19 +131,25 @@ describe('AssociatedSubmap - component', () => { }, bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, - data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, }, modelElements: { - 0: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: { + data: MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK, + loading: 'idle', + error: DEFAULT_ERROR, }, } as ModelElementsState, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { - bioentityId: bioEntityContentFixture.bioEntity.id, + bioentityId: modelElementFixture.id, drugs: {}, chemicals: {}, }, @@ -151,7 +170,6 @@ describe('AssociatedSubmap - component', () => { modelName: 'Histamine signaling', lastPosition: { x: 0, y: 0, z: 0 }, }); - const openSubmapButton = screen.getByRole('button', { name: 'Open submap' }); await act(() => { openSubmapButton.click(); @@ -184,19 +202,25 @@ describe('AssociatedSubmap - component', () => { }, bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, - data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, }, modelElements: { - [MAIN_MAP_ID]: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + [MAIN_MAP_ID]: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: { + data: MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK, + loading: 'idle', + error: DEFAULT_ERROR, }, } as ModelElementsState, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { - bioentityId: bioEntityContentFixture.bioEntity.id, + bioentityId: modelElementFixture.id, drugs: {}, chemicals: {}, }, diff --git a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx index ae4216066e9de25a9254ce5ddcc165a6e218728e..22c6c44926b303b080d720d9aa50369da179c525 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.tsx @@ -1,10 +1,10 @@ import { useOpenSubmap } from '@/hooks/useOpenSubmaps'; -import { currentDrawerBioEntityRelatedSubmapSelector } from '@/redux/bioEntity/bioEntity.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { Button } from '@/shared/Button'; +import { currentDrawerModelElementRelatedSubmapSelector } from '@/redux/modelElements/modelElements.selector'; export const AssociatedSubmap = (): React.ReactNode => { - const relatedSubmap = useAppSelector(currentDrawerBioEntityRelatedSubmapSelector); + const relatedSubmap = useAppSelector(currentDrawerModelElementRelatedSubmapSelector); const { openSubmap } = useOpenSubmap({ modelId: relatedSubmap?.id, modelName: relatedSubmap?.name, diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx index 7ae2a243928cae82231f3c7e4c0756a0d9018925..c0bb50583b1e19999dafd406aa4cadf0e9a7bbf5 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx @@ -1,6 +1,5 @@ /* eslint-disable no-magic-numbers */ import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; -import { BIO_ENTITY_LINKING_TO_SUBMAP } from '@/redux/bioEntity/bioEntity.mock'; import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; import { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock'; import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; @@ -10,8 +9,11 @@ import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore'; import { act, render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; import { getTypeBySBOTerm } from '@/utils/bioEntity/getTypeBySBOTerm'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { + MODEL_ELEMENT_LINKING_TO_SUBMAP, + MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, +} from '@/redux/modelElements/modelElements.mock'; import { BioEntityDrawer } from './BioEntityDrawer.component'; const renderComponent = ( @@ -43,10 +45,15 @@ describe('BioEntityDrawer - component', () => { describe("when there's NO matching bioEntity", () => { beforeEach(() => renderComponent({ - bioEntity: { - data: [], - loading: 'succeeded', - error: { message: '', name: '' }, + modelElements: { + data: { + 0: { + data: [], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, }, drawer: DRAWER_INITIAL_STATE, }), @@ -67,12 +74,15 @@ describe('BioEntityDrawer - component', () => { it('should show drawer header', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -91,12 +101,15 @@ describe('BioEntityDrawer - component', () => { it('should show drawer bioEntity full name', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: bioEntityFullName }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: bioEntityFullName }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -114,12 +127,15 @@ describe('BioEntityDrawer - component', () => { it("should not show drawer bioEntity full name if it doesn't exists", () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -136,12 +152,15 @@ describe('BioEntityDrawer - component', () => { it('should show list of annotations ', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -164,16 +183,19 @@ describe('BioEntityDrawer - component', () => { it('should display associated submaps if bio entity links to submap', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...BIO_ENTITY_LINKING_TO_SUBMAP, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...MODEL_ELEMENT_LINKING_TO_SUBMAP, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { - bioentityId: BIO_ENTITY_LINKING_TO_SUBMAP.id, + bioentityId: MODEL_ELEMENT_LINKING_TO_SUBMAP.id, drugs: {}, chemicals: {}, }, @@ -190,12 +212,15 @@ describe('BioEntityDrawer - component', () => { it('should display chemicals list header', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -212,12 +237,15 @@ describe('BioEntityDrawer - component', () => { it('should display drugs list header', () => { renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -234,12 +262,15 @@ describe('BioEntityDrawer - component', () => { it('should fetch chemicals on chemicals for target click', () => { const { store } = renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { @@ -263,12 +294,15 @@ describe('BioEntityDrawer - component', () => { it('should fetch drugs on drugs for target click', () => { const { store } = renderComponent({ modelElements: { - 0: { - data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [{ ...modelElementFixture, name: bioEntityName, fullName: null }], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, drawer: { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx index 1cf3684dcbb82d21be5758154abdec567e6a9d6b..259e7ac69bc3abc1420022fa0440f892d2f5c67d 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -1,8 +1,4 @@ import { ZERO } from '@/constants/common'; -import { - currentDrawerBioEntityRelatedSubmapSelector, - currentDrawerElementCommentsSelector, -} from '@/redux/bioEntity/bioEntity.selectors'; import { getChemicalsForBioEntityDrawerTarget, getDrugsForBioEntityDrawerTarget, @@ -18,6 +14,8 @@ import React from 'react'; import { AnnotationItemList } from '@/components/Map/Drawer/BioEntityDrawer/AnnotationItem/AnnotationItemList.component'; import { compartmentNameByIdSelector, + currentDrawerElementCommentsSelector, + currentDrawerModelElementRelatedSubmapSelector, currentDrawerModelElementSelector, } from '@/redux/modelElements/modelElements.selector'; import { CollapsibleSection } from '../ExportDrawer/CollapsibleSection'; @@ -32,7 +30,7 @@ export const BioEntityDrawer = (): React.ReactNode => { const dispatch = useAppDispatch(); const modelElement = useAppSelector(currentDrawerModelElementSelector); const commentsData = useAppSelector(currentDrawerElementCommentsSelector); - const relatedSubmap = useAppSelector(currentDrawerBioEntityRelatedSubmapSelector); + const relatedSubmap = useAppSelector(currentDrawerModelElementRelatedSubmapSelector); const currentTargetId = modelElement?.id ? `${TARGET_PREFIX}:${modelElement.id}` : ''; const fetchChemicalsForTarget = (): void => { dispatch(getChemicalsForBioEntityDrawerTarget(currentTargetId)); diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx index d57a9db9bde9e9a4b72258cf31a96713382f94cf..da1eb1425fc706d08b92bdc9c604815d61420b56 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx @@ -1,6 +1,5 @@ /* eslint-disable no-magic-numbers */ import { ZERO } from '@/constants/common'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { overlayFixture } from '@/models/fixtures/overlaysFixture'; import { GENE_VARIANTS_MOCK } from '@/models/mocks/geneVariantsMock'; import { CORE_PD_MODEL_MOCK } from '@/models/mocks/modelsMock'; @@ -61,26 +60,6 @@ describe('OverlayData - component', () => { ...INITIAL_STORE_STATE_MOCK.overlays, data: [{ ...overlayFixture, name: 'axis name' }], }, - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'pending', - error: { name: '', message: '' }, - data: [ - { - ...bioEntitiesContentFixture[0], - bioEntity: { - ...bioEntitiesContentFixture[0].bioEntity, - id: BIO_ENTITY.id, - }, - }, - ], - }, - ], - loading: 'pending', - error: { name: '', message: '' }, - }, overlayBioEntity: { ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, overlaysId: [OVERLAY_ID], @@ -133,26 +112,6 @@ describe('OverlayData - component', () => { ...INITIAL_STORE_STATE_MOCK.overlays, data: [{ ...overlayFixture, name: 'overlay name' }], }, - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'pending', - error: { name: '', message: '' }, - data: [ - { - ...bioEntitiesContentFixture[0], - bioEntity: { - ...bioEntitiesContentFixture[0].bioEntity, - id: BIO_ENTITY.id, - }, - }, - ], - }, - ], - loading: 'pending', - error: { name: '', message: '' }, - }, overlayBioEntity: { ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, overlaysId: [OVERLAY_ID], @@ -209,26 +168,6 @@ describe('OverlayData - component', () => { ...INITIAL_STORE_STATE_MOCK.overlays, data: [{ ...overlayFixture, name: 'overlay name' }], }, - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'pending', - error: { name: '', message: '' }, - data: [ - { - ...bioEntitiesContentFixture[0], - bioEntity: { - ...bioEntitiesContentFixture[0].bioEntity, - id: BIO_ENTITY.id, - }, - }, - ], - }, - ], - loading: 'pending', - error: { name: '', message: '' }, - }, overlayBioEntity: { ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, overlaysId: [OVERLAY_ID], diff --git a/src/components/Map/Drawer/Drawer.component.test.tsx b/src/components/Map/Drawer/Drawer.component.test.tsx index b677519d0651aab559f7658225e9fd21f0188ea8..200c4fc417fcbbe907174934befc3e380599bd0c 100644 --- a/src/components/Map/Drawer/Drawer.component.test.tsx +++ b/src/components/Map/Drawer/Drawer.component.test.tsx @@ -1,4 +1,3 @@ -import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { openBioEntityDrawerById, openReactionDrawerById, @@ -12,10 +11,11 @@ import { getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import type {} from 'redux-thunk/extend-redux'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { newReactionFixture } from '@/models/fixtures/newReactionFixture'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; import { Drawer } from './Drawer.component'; +import type {} from 'redux-thunk/extend-redux'; const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStore); @@ -121,20 +121,18 @@ describe('Drawer - component', () => { describe('bioEntity drawer', () => { it.skip('should open drawer and display bioEntity', async () => { - const { id } = bioEntitiesContentFixture[FIRST_ARRAY_ELEMENT].bioEntity; + const { id } = modelElementFixture; const { store } = renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, }, }); diff --git a/src/components/Map/Drawer/ExportDrawer/Elements/Elements.component.test.tsx b/src/components/Map/Drawer/ExportDrawer/Elements/Elements.component.test.tsx index d0e0d9b6d40bcf3dd53303748a8779b0d6fba0db..12684db74a3fae1700e1d1423e24b85d5acb6be5 100644 --- a/src/components/Map/Drawer/ExportDrawer/Elements/Elements.component.test.tsx +++ b/src/components/Map/Drawer/ExportDrawer/Elements/Elements.component.test.tsx @@ -16,6 +16,7 @@ import { MockStoreEnhanced } from 'redux-mock-store'; import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { COMPARTMENT_SBO_TERM } from '@/components/Map/MapViewer/MapViewer.constants'; +import { MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; import { ELEMENTS_COLUMNS } from '../ExportCompound/ExportCompound.constant'; import { Elements } from './Elements.component'; @@ -133,24 +134,27 @@ describe('Elements - component', () => { }, }, modelElements: { - 0: { - data: [ - { - ...modelElementFixture, - id: FIRST_COMPARMENT_PATHWAY_ID, - name: FIRST_COMPARMENT_PATHWAY_NAME, - sboTerm: COMPARTMENT_SBO_TERM, - }, - { - ...modelElementFixture, - id: SECOND_COMPARMENT_PATHWAY_ID, - name: SECOND_COMPARMENT_PATHWAY_NAME, - sboTerm: COMPARTMENT_SBO_TERM, - }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [ + { + ...modelElementFixture, + id: FIRST_COMPARMENT_PATHWAY_ID, + name: FIRST_COMPARMENT_PATHWAY_NAME, + sboTerm: COMPARTMENT_SBO_TERM, + }, + { + ...modelElementFixture, + id: SECOND_COMPARMENT_PATHWAY_ID, + name: SECOND_COMPARMENT_PATHWAY_NAME, + sboTerm: COMPARTMENT_SBO_TERM, + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, } as ModelElementsState, }); diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/ExportCompound.component.tsx b/src/components/Map/Drawer/ExportDrawer/ExportCompound/ExportCompound.component.tsx index 1a60d63dedff3a165c424ddf54228e8d5dc98b44..6becb21e3d311b60fae3cb53c5723dcbcb8b38b5 100644 --- a/src/components/Map/Drawer/ExportDrawer/ExportCompound/ExportCompound.component.tsx +++ b/src/components/Map/Drawer/ExportDrawer/ExportCompound/ExportCompound.component.tsx @@ -24,7 +24,7 @@ import { ImageFormat } from './ImageFormat'; import { ImageSize } from './ImageSize'; import { DEFAULT_IMAGE_SIZE } from './ImageSize/ImageSize.constants'; import { ImageSize as ImageSizeType } from './ImageSize/ImageSize.types'; -import { IncludedCompartmentPathways } from './IncludedCompartmentPathways '; +import { IncludedCompartmentPathways } from './IncludedCompartmentPathways'; import { Submap } from './Submap'; import { getDownloadElementsBodyRequest } from './utils/getDownloadElementsBodyRequest'; import { getGraphicsDownloadUrl } from './utils/getGraphicsDownloadUrl'; @@ -82,7 +82,6 @@ export const Export = ({ children }: ExportProps): JSX.Element => { const model = currentModels.find(currentModel => currentModel.id === Number(modelId)); const url = getGraphicsDownloadUrl({ - backgroundId: background, modelId: models?.[FIRST_ARRAY_ELEMENT]?.id, handler: imageFormats?.[FIRST_ARRAY_ELEMENT]?.id, zoom: getModelExportZoom(imageSize.width, model), @@ -97,7 +96,6 @@ export const Export = ({ children }: ExportProps): JSX.Element => { const handleDownloadCurrentView = useCallback(async () => { const url = getGraphicsDownloadUrl({ - backgroundId: background, modelId: `${selectedModelId}`, handler: imageFormats?.[FIRST_ARRAY_ELEMENT]?.id, zoom: getZoom(), diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /IncludedCompartmentPathways.component.test.tsx b/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/IncludedCompartmentPathways.component.test.tsx similarity index 100% rename from src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /IncludedCompartmentPathways.component.test.tsx rename to src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/IncludedCompartmentPathways.component.test.tsx diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /IncludedCompartmentPathways.component.tsx b/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/IncludedCompartmentPathways.component.tsx similarity index 100% rename from src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /IncludedCompartmentPathways.component.tsx rename to src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/IncludedCompartmentPathways.component.tsx diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /index.ts b/src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/index.ts similarity index 100% rename from src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways /index.ts rename to src/components/Map/Drawer/ExportDrawer/ExportCompound/IncludedCompartmentPathways/index.ts diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.test.ts b/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.test.ts index a4e4c59df4b2be89286280718a4100991f797fba..cfcda6d04eceef75f2157941091ca710fbcbbf16 100644 --- a/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.test.ts +++ b/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.test.ts @@ -6,14 +6,12 @@ describe('getGraphicsDownloadUrl - util', () => { [{ overlayIds: [] }, undefined], [ { - backgroundId: 50, overlayIds: [], }, undefined, ], [ { - backgroundId: 50, modelId: '30', overlayIds: [], }, @@ -21,7 +19,6 @@ describe('getGraphicsDownloadUrl - util', () => { ], [ { - backgroundId: 50, modelId: '30', handler: 'any.handler.image', overlayIds: [], @@ -30,13 +27,12 @@ describe('getGraphicsDownloadUrl - util', () => { ], [ { - backgroundId: 50, modelId: '30', handler: 'any.handler.image', zoom: 7, overlayIds: ['107'], }, - `${BASE_API_URL}/projects/${PROJECT_ID}/models/30:downloadImage?backgroundOverlayId=50&handlerClass=any.handler.image&zoomLevel=7&overlayIds=107`, + `${BASE_API_URL}/projects/${PROJECT_ID}/models/30:downloadImage?handlerClass=any.handler.image&zoomLevel=7&overlayIds=107`, ], ]; diff --git a/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.ts b/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.ts index 2844899535138beda38a96fc209db517bb13c12e..972e2540acac89ec824ca3e17e818a947800901a 100644 --- a/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.ts +++ b/src/components/Map/Drawer/ExportDrawer/ExportCompound/utils/getGraphicsDownloadUrl.ts @@ -2,7 +2,6 @@ import { BASE_API_URL, PROJECT_ID } from '@/constants'; import { getBounds } from '@/services/pluginsManager/map/data/getBounds'; export interface GetGraphicsDownloadUrlProps { - backgroundId?: number; modelId?: string; handler?: string; zoom?: number; @@ -11,14 +10,13 @@ export interface GetGraphicsDownloadUrlProps { } export const getGraphicsDownloadUrl = ({ - backgroundId, modelId, handler, zoom, overlayIds, currentView, }: GetGraphicsDownloadUrlProps): string | undefined => { - const isAllElementsTruthy = [backgroundId, modelId, handler, zoom].reduce( + const isAllElementsTruthy = [modelId, handler, zoom].reduce( (a, b) => Boolean(a) && Boolean(b), true, ); @@ -38,5 +36,5 @@ export const getGraphicsDownloadUrl = ({ } } - return `${BASE_API_URL}/projects/${PROJECT_ID}/models/${modelId}:downloadImage?backgroundOverlayId=${backgroundId}&handlerClass=${handler}&zoomLevel=${zoom}&overlayIds=${overlays}${polygonSuffix}`; + return `${BASE_API_URL}/projects/${PROJECT_ID}/models/${modelId}:downloadImage?handlerClass=${handler}&zoomLevel=${zoom}&overlayIds=${overlays}${polygonSuffix}`; }; diff --git a/src/components/Map/Drawer/ExportDrawer/Network/Network.component.test.tsx b/src/components/Map/Drawer/ExportDrawer/Network/Network.component.test.tsx index 88243df1272535cd78fa5a3a9dbd582963d58375..6241bcd266c87a42f2c64c1b6245e53221ad57ef 100644 --- a/src/components/Map/Drawer/ExportDrawer/Network/Network.component.test.tsx +++ b/src/components/Map/Drawer/ExportDrawer/Network/Network.component.test.tsx @@ -16,6 +16,7 @@ import { MockStoreEnhanced } from 'redux-mock-store'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { COMPARTMENT_SBO_TERM } from '@/components/Map/MapViewer/MapViewer.constants'; +import { MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; import { NETWORK_COLUMNS } from '../ExportCompound/ExportCompound.constant'; import { Network } from './Network.component'; @@ -136,24 +137,27 @@ describe('Network - component', () => { }, }, modelElements: { - 0: { - data: [ - { - ...modelElementFixture, - id: FIRST_COMPARMENT_PATHWAY_ID, - name: FIRST_COMPARMENT_PATHWAY_NAME, - sboTerm: COMPARTMENT_SBO_TERM, - }, - { - ...modelElementFixture, - id: SECOND_COMPARMENT_PATHWAY_ID, - name: SECOND_COMPARMENT_PATHWAY_NAME, - sboTerm: COMPARTMENT_SBO_TERM, - }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [ + { + ...modelElementFixture, + id: FIRST_COMPARMENT_PATHWAY_ID, + name: FIRST_COMPARMENT_PATHWAY_NAME, + sboTerm: COMPARTMENT_SBO_TERM, + }, + { + ...modelElementFixture, + id: SECOND_COMPARMENT_PATHWAY_ID, + name: SECOND_COMPARMENT_PATHWAY_NAME, + sboTerm: COMPARTMENT_SBO_TERM, + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, } as ModelElementsState, }); diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4906b2bda2797221e50cc80ce9c79ac1f16bbc65 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx @@ -0,0 +1,73 @@ +import React, { useState, JSX, useRef, useEffect } from 'react'; +import { IconButton } from '@/shared/IconButton'; +import { LayerDrawerLayerContextMenuItems } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component'; + +type LayerDrawerLayerContextMenuProps = { + removeLayer: () => void; + editLayer: () => void; + addImage: () => void; + addText: () => void; +}; + +export const LayerDrawerLayerContextMenu = ({ + removeLayer, + editLayer, + addImage, + addText, +}: LayerDrawerLayerContextMenuProps): JSX.Element => { + const [menuVisible, setMenuVisible] = useState(false); + const menuRef = useRef<HTMLUListElement>(null); + const dotsRef = useRef<HTMLDivElement>(null); + + const toggleMenu = (): void => { + setMenuVisible(!menuVisible); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + if (!dotsRef.current || dotsRef.current.contains(event.target as Node)) { + return; + } + if (!menuRef.current || menuRef.current.contains(event.target as Node)) { + return; + } + setMenuVisible(false); + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( + <div className="relative inline-block"> + <div ref={dotsRef}> + <IconButton + icon="dots" + title="Switch visibility" + className="h-auto w-auto bg-transparent p-0" + onClick={toggleMenu} + /> + </div> + {menuVisible && ( + <LayerDrawerLayerContextMenuItems + ref={menuRef} + removeLayer={() => { + setMenuVisible(false); + removeLayer(); + }} + editLayer={() => { + setMenuVisible(false); + editLayer(); + }} + addImage={() => { + setMenuVisible(false); + addImage(); + }} + addText={() => { + setMenuVisible(false); + addText(); + }} + /> + )} + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..12a7959c686d19332358bbb886f6dd9bba463aa3 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -0,0 +1,18 @@ +import { JSX } from 'react'; +import { LayerText } from '@/types/models'; +import { Icon } from '@/shared/Icon'; + +interface LayersDrawerTextItemProps { + layerText: LayerText; +} + +export const LayersDrawerTextItem = ({ + layerText, +}: LayersDrawerTextItemProps): JSX.Element | null => { + return ( + <div className="flex min-h-[24px] gap-2"> + <Icon name="text" className="shrink-0" /> + <span className="truncate">{layerText.notes}</span> + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index bbed3ad9ffe89dbdd02ad611b6b23ae56ce81208..df941aca918455a3b099da92866c2e5d9748f7a5 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -1,104 +1,81 @@ +/* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { - layersForCurrentModelSelector, - layersVisibilityForCurrentModelSelector, -} from '@/redux/layers/layers.selectors'; -import { setLayerVisibility } from '@/redux/layers/layers.slice'; -import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { layersForCurrentModelSelector } from '@/redux/layers/layers.selectors'; import { Button } from '@/shared/Button'; +import { JSX, useEffect, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; -import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; -import { useState } from 'react'; -import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks'; -import { showToast } from '@/utils/showToast'; -import { SerializedError } from '@reduxjs/toolkit'; -import { LayersDrawerLayerActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; +import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; +import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; export const LayersDrawer = (): JSX.Element => { const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); - const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); - const currentModelId = useAppSelector(currentModelIdSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); const dispatch = useAppDispatch(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [layerId, setLayerId] = useState<number | null>(null); + const layersDrawerRef = useRef<HTMLDivElement>(null); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); const addNewLayer = (): void => { dispatch(openLayerFactoryModal()); }; - const editLayer = (layerIdToEdit: number): void => { - dispatch(openLayerFactoryModal(layerIdToEdit)); - }; - - const rejectRemove = (): void => { - setIsModalOpen(false); - }; - - const confirmRemove = async (): Promise<void> => { - if (!layerId) { + useEffect(() => { + if (!mapEditToolsLayerImageObject || !layersDrawerRef.current) { return; } - try { - await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap(); - showToast({ - type: 'success', - message: 'The layer has been successfully removed', - }); - setIsModalOpen(false); - dispatch(getLayersForModel(currentModelId)); - } catch (error) { - const typedError = error as SerializedError; - showToast({ - type: 'error', - message: typedError.message || 'An error occurred while removing the layer', + const layerObjectElement = document.getElementById( + `layer-image-item-${mapEditToolsLayerImageObject.id}`, + ); + if (!layerObjectElement) { + return; + } + + const container = layersDrawerRef.current; + const extraPadding = 20; + + const elementRect = layerObjectElement.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + if (elementRect.top < containerRect.top) { + container.scrollTo({ + top: container.scrollTop + (elementRect.top - containerRect.top) - extraPadding, + behavior: 'smooth', }); } - }; - const onRemoveLayer = (layerIdToRemove: number): void => { - setLayerId(layerIdToRemove); - setIsModalOpen(true); - }; + if (elementRect.bottom > containerRect.bottom) { + container.scrollTo({ + top: container.scrollTop + (elementRect.bottom - containerRect.bottom) + extraPadding, + behavior: 'smooth', + }); + } + }, [mapEditToolsLayerImageObject]); return ( <div data-testid="layers-drawer" className="h-full max-h-full"> - <QuestionModal - isOpen={isModalOpen} - onClose={rejectRemove} - onConfirm={confirmRemove} - question="Are you sure you want to remove the layer?" - /> <DrawerHeading title="Layers" /> - <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6"> - <div className="flex justify-start pt-2"> - <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}> - Add new layer - </Button> - </div> - {layersForCurrentModel.map(layer => ( - <div - key={layer.details.id} - className="flex items-center justify-between gap-3 border-b py-4" - > - <h1 className="truncate">{layer.details.name}</h1> - <LayersDrawerLayerActions - isChecked={layersVisibilityForCurrentModel[layer.details.id]} - editLayer={() => editLayer(layer.details.id)} - removeLayer={() => onRemoveLayer(layer.details.id)} - toggleVisibility={value => - dispatch( - setLayerVisibility({ - modelId: currentModelId, - visible: value, - layerId: layer.details.id, - }), - ) - } - /> + <div + className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6 pb-4" + ref={layersDrawerRef} + > + {hasPrivilegeToWriteProject && ( + <div className="flex justify-start pt-2"> + <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}> + Add layer + </Button> </div> - ))} + )} + <div className="flex flex-col gap-4"> + {layersForCurrentModel.map(layer => ( + <LayersDrawerLayer + key={layer.details.id} + layerId={layer.details.id} + layerName={layer.details.name} + /> + ))} + </div> </div> </div> ); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..11ccb9b15737ce8c8e1826e491701725c5d10994 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -0,0 +1,81 @@ +import React, { JSX, useMemo } from 'react'; +import { LayerImage } from '@/types/models'; +import { Icon } from '@/shared/Icon'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { glyphFileNameByIdSelector } from '@/redux/glyphs/glyphs.selectors'; +import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; +import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; + +interface LayersDrawerImageItemProps { + layerImage: LayerImage; + bringToFront: () => void; + bringToBack: () => void; + removeObject: () => void; + centerObject: () => void; + isLayerVisible: boolean; + isLayerActive: boolean; +} + +export const LayersDrawerImageItem = ({ + layerImage, + bringToFront, + bringToBack, + removeObject, + centerObject, + isLayerVisible, + isLayerActive, +}: LayersDrawerImageItemProps): JSX.Element | null => { + const dispatch = useAppDispatch(); + const activeLayerImage = useAppSelector(mapEditToolsLayerImageObjectSelector); + const fileName = useAppSelector(state => glyphFileNameByIdSelector(state, layerImage.glyph)); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + + const showActions = useMemo(() => { + return activeLayerImage?.id === layerImage.id; + }, [activeLayerImage?.id, layerImage.id]); + + const canSelectItem = useMemo(() => { + return isLayerVisible && isLayerActive && hasPrivilegeToWriteProject; + }, [isLayerVisible, isLayerActive, hasPrivilegeToWriteProject]); + + const selectItem = useMemo(() => { + return (): void => { + if (canSelectItem) { + dispatch(mapEditToolsSetLayerObject(layerImage)); + } + }; + }, [canSelectItem, dispatch, layerImage]); + + const handleKeyPress = (): void => {}; + + return ( + <div + className="flex min-h-[24px] items-center justify-between gap-2" + id={`layer-image-item-${layerImage.id}`} + > + <div + className={`flex gap-2 ${canSelectItem ? 'cursor-pointer' : 'cursor-default'}`} + onClick={selectItem} + tabIndex={0} + onKeyDown={handleKeyPress} + role="button" + > + <Icon className="shrink-0" name="image" /> + <span className={`min-w-0 flex-1 truncate ${showActions ? 'max-w-[205px]' : ''}`}> + {fileName} + </span> + </div> + {showActions && ( + <LayersDrawerObjectActions + bringToFront={bringToFront} + bringToBack={bringToBack} + removeObject={removeObject} + centerObject={centerObject} + /> + )} + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49b8536fea4d643cc4158a6889d46539c51f858f --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -0,0 +1,134 @@ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { + layersActiveLayersSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; +import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; +import { useState, JSX, useMemo } from 'react'; +import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks'; +import { showToast } from '@/utils/showToast'; +import { SerializedError } from '@reduxjs/toolkit'; +import { LayersDrawerLayerActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component'; +import { + setDrawLayer, + setLayerToActive, + setLayerToInactive, + setLayerVisibility, +} from '@/redux/layers/layers.slice'; +import { LayersDrawerObjectsList } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component'; +import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; +import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; + +interface LayersDrawerLayerProps { + layerId: number; + layerName: string; +} + +export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps): JSX.Element => { + const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); + const activeLayers = useAppSelector(layersActiveLayersSelector); + const currentModelId = useAppSelector(currentModelIdSelector); + const dispatch = useAppDispatch(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const isLayerVisible = useMemo(() => { + return layersVisibilityForCurrentModel[layerId]; + }, [layerId, layersVisibilityForCurrentModel]); + + const isLayerActive = useMemo(() => { + return activeLayers.includes(layerId); + }, [activeLayers, layerId]); + + const editLayer = (): void => { + dispatch(openLayerFactoryModal(layerId)); + }; + + const addImage = (): void => { + dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)); + }; + + const addText = (): void => { + dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_TEXT)); + }; + + const rejectRemove = (): void => { + setIsModalOpen(false); + }; + + const confirmRemove = async (): Promise<void> => { + if (!layerId) { + return; + } + try { + await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap(); + showToast({ + type: 'success', + message: 'The layer has been successfully removed', + }); + setIsModalOpen(false); + dispatch(getLayersForModel(currentModelId)); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while removing the layer', + }); + } + }; + + const onRemoveLayer = (): void => { + setIsModalOpen(true); + }; + + const toggleActiveLayer = (value: boolean): void => { + if (value) { + dispatch(setLayerToActive({ modelId: currentModelId, layerId })); + } else { + dispatch(setLayerToInactive({ modelId: currentModelId, layerId })); + } + }; + + return ( + <div> + <QuestionModal + isOpen={isModalOpen} + onClose={rejectRemove} + onConfirm={confirmRemove} + question="Are you sure you want to remove the layer?" + /> + <div className="flex items-center justify-between py-3"> + <span className={`font-semibold ${isLayerVisible ? 'opacity-100' : 'opacity-40'}`}> + {layerName} + </span> + <LayersDrawerLayerActions + toggleVisibility={() => + dispatch( + setLayerVisibility({ + modelId: currentModelId, + visible: !isLayerVisible, + layerId, + }), + ) + } + toggleActiveLayer={toggleActiveLayer} + editLayer={editLayer} + removeLayer={onRemoveLayer} + addImage={addImage} + addText={addText} + isVisible={isLayerVisible} + isActive={isLayerActive} + /> + </div> + <LayersDrawerObjectsList + layerId={layerId} + isLayerVisible={isLayerVisible} + isLayerActive={isLayerActive} + /> + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx index 02ea7ed9246cde4aa02b163288a561fedd8b4a5e..18627b1325e8ed5afed261cbf82e2c4c501f9152 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx @@ -1,26 +1,56 @@ -import { Button } from '@/shared/Button'; -import { Switch } from '@/shared/Switch'; +import { IconButton } from '@/shared/IconButton'; +import { JSX } from 'react'; +import { LayerDrawerLayerContextMenu } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; type LayersDrawerLayerActionsProps = { editLayer: () => void; removeLayer: () => void; - isChecked: boolean; - toggleVisibility: (value: boolean) => void; + isVisible: boolean; + isActive: boolean; + toggleVisibility: () => void; + toggleActiveLayer: (value: boolean) => void; + addImage: () => void; + addText: () => void; }; export const LayersDrawerLayerActions = ({ editLayer, removeLayer, - isChecked, + isVisible, + isActive, toggleVisibility, + toggleActiveLayer, + addImage, + addText, }: LayersDrawerLayerActionsProps): JSX.Element => { + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + return ( - <div className="flex items-center gap-2"> - <Switch isChecked={isChecked} onToggle={value => toggleVisibility(value)} /> - <Button onClick={() => editLayer()}>Edit</Button> - <Button onClick={() => removeLayer()} color="error" variantStyles="remove"> - Remove - </Button> + <div className="flex gap-2"> + <IconButton + icon={isVisible ? 'eye' : 'crossed-eye'} + title="Switch visibility" + className="h-auto w-auto bg-transparent p-0" + onClick={toggleVisibility} + /> + {hasPrivilegeToWriteProject && ( + <> + <IconButton + icon={isActive ? 'padlock-open' : 'padlock-locked'} + title="Lock" + className="h-auto w-auto bg-transparent p-0" + onClick={() => toggleActiveLayer(!isActive)} + /> + <LayerDrawerLayerContextMenu + removeLayer={removeLayer} + editLayer={editLayer} + addImage={addImage} + addText={addText} + /> + </> + )} </div> ); }; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0696efca46a92cb2d68db4f365697fbb12f0caf0 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx @@ -0,0 +1,72 @@ +import React, { forwardRef, JSX } from 'react'; +import { Icon } from '@/shared/Icon'; + +type LayerDrawerLayerContextMenuProps = { + removeLayer: () => void; + editLayer: () => void; + addImage: () => void; + addText: () => void; +}; + +export const LayerDrawerLayerContextMenuItems = forwardRef< + HTMLUListElement, + LayerDrawerLayerContextMenuProps +>( + ( + { removeLayer, editLayer, addImage, addText }: LayerDrawerLayerContextMenuProps, + ref, + ): JSX.Element => { + const handleKeyPress = (): void => {}; + + return ( + <ul + ref={ref} + className="absolute right-[24px] top-[-14px] z-[1] mt-2 w-[11rem] rounded border bg-white p-2 shadow-md" + > + <li + className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" + tabIndex={0} + onClick={addImage} + onKeyDown={handleKeyPress} + role="menuitem" + > + <Icon name="image" /> + <span>Add glyph</span> + </li> + <li + className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" + tabIndex={0} + onClick={addText} + onKeyDown={handleKeyPress} + role="menuitem" + > + <Icon name="text" /> + <span>Add text</span> + </li> + <li + className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" + tabIndex={0} + onClick={editLayer} + onKeyDown={handleKeyPress} + role="menuitem" + > + <Icon name="edit-image" /> + <span>Edit layer</span> + </li> + <hr /> + <li + className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" + tabIndex={0} + onClick={removeLayer} + onKeyDown={handleKeyPress} + role="menuitem" + > + <Icon name="trash" className="fill-red-600" /> + <span>Delete layer</span> + </li> + </ul> + ); + }, +); + +LayerDrawerLayerContextMenuItems.displayName = 'LayerDrawerLayerContextMenuItems'; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..11f5e89496964cdb1c23d74e855f80155275df39 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx @@ -0,0 +1,46 @@ +import { JSX } from 'react'; +import { IconButton } from '@/shared/IconButton'; + +interface LayersDrawerObjectActionsProps { + bringToFront: () => void; + bringToBack: () => void; + removeObject: () => void; + centerObject: () => void; +} + +export const LayersDrawerObjectActions = ({ + bringToFront, + bringToBack, + removeObject, + centerObject, +}: LayersDrawerObjectActionsProps): JSX.Element | null => { + return ( + <div className="flex shrink-0 gap-2"> + <IconButton + icon="center" + className="h-auto w-auto bg-transparent p-0" + title="Center" + onClick={centerObject} + /> + <IconButton + icon="bring-front" + className="h-auto w-auto bg-transparent p-0" + title="Bring to front" + onClick={bringToFront} + /> + <IconButton + icon="bring-back" + className="h-auto w-auto bg-transparent p-0" + title="Bring to back" + onClick={bringToBack} + /> + <IconButton + icon="trash" + className="h-auto w-auto bg-transparent p-0 " + classNameIcon="group-hover:fill-red-700 group-active:fill-red-700 fill-red-600" + title="Remove" + onClick={removeObject} + /> + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..445574da375287606bb7a47766a5c0545816a4a3 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -0,0 +1,174 @@ +/* eslint-disable no-magic-numbers */ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { + highestZIndexSelector, + layerByIdSelector, + lowestZIndexSelector, +} from '@/redux/layers/layers.selectors'; +import { JSX, useState } from 'react'; +import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; +import { LayersDrawerTextItem } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component'; +import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; +import { removeLayerImage, updateLayerImageObject } from '@/redux/layers/layers.thunks'; +import { layerDeleteImage, layerUpdateImage } from '@/redux/layers/layers.slice'; +import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; +import { showToast } from '@/utils/showToast'; +import { SerializedError } from '@reduxjs/toolkit'; +import { LayerImage } from '@/types/models'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useMapInstance } from '@/utils/context/mapInstanceContext'; +import { mapModelIdSelector } from '@/redux/map/map.selectors'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import { useSetBounds } from '@/utils/map/useSetBounds'; +import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { usePointToProjection } from '@/utils/map/usePointToProjection'; +import { Coordinate } from 'ol/coordinate'; + +interface LayersDrawerObjectsListProps { + layerId: number; + isLayerVisible: boolean; + isLayerActive: boolean; +} + +export const LayersDrawerObjectsList = ({ + layerId, + isLayerVisible, + isLayerActive, +}: LayersDrawerObjectsListProps): JSX.Element | null => { + const currentModelId = useAppSelector(mapModelIdSelector); + const highestZIndex = useAppSelector(highestZIndexSelector); + const lowestZIndex = useAppSelector(lowestZIndexSelector); + const layer = useAppSelector(state => layerByIdSelector(state, layerId)); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const [isImageRemoveModalOpen, setIsImageRemoveModalOpen] = useState(false); + const [layerImageToRemove, setLayerImageToRemove] = useState<LayerImage | null>(null); + const dispatch = useAppDispatch(); + const setBounds = useSetBounds(); + const pointToProjection = usePointToProjection(); + const { mapInstance } = useMapInstance(); + + const removeImage = (layerImage: LayerImage): void => { + setLayerImageToRemove(layerImage); + setIsImageRemoveModalOpen(true); + }; + + const rejectImageRemove = (): void => { + setIsImageRemoveModalOpen(false); + }; + + const confirmImageRemove = async (): Promise<void> => { + if (!layerImageToRemove) { + return; + } + try { + await dispatch( + removeLayerImage({ + modelId: currentModelId, + layerId: layerImageToRemove.layer, + imageId: layerImageToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteImage({ + modelId: currentModelId, + layerId: layerImageToRemove.layer, + imageId: layerImageToRemove.id, + }), + ); + removeElementFromLayer({ + mapInstance, + layerId: layerImageToRemove.layer, + featureId: layerImageToRemove.id, + }); + showToast({ + type: 'success', + message: 'The layer image has been successfully removed', + }); + setIsImageRemoveModalOpen(false); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while removing the layer image', + }); + } + }; + + const updateImageZIndex = async ({ + zIndex, + layerImage, + }: { + zIndex: number; + layerImage: LayerImage; + }): Promise<void> => { + const newLayerImage = await dispatch( + updateLayerImageObject({ + modelId: currentModelId, + layerId: layerImage.layer, + ...layerImage, + z: zIndex, + }), + ).unwrap(); + if (newLayerImage) { + dispatch( + layerUpdateImage({ + modelId: currentModelId, + layerId: newLayerImage.layer, + layerImage: newLayerImage, + }), + ); + dispatch(mapEditToolsSetLayerObject(newLayerImage)); + updateGlyph(mapInstance, newLayerImage.layer, newLayerImage); + } + }; + + const bringImageToFront = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: highestZIndex + 1, layerImage }); + }; + + const bringImageToBack = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); + }; + + const centerObject = (layerImage: LayerImage): void => { + if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerImage.id) { + const point1 = pointToProjection({ x: layerImage.x, y: layerImage.y }); + const point2 = pointToProjection({ + x: layerImage.x + layerImage.width, + y: layerImage.y + layerImage.height, + }); + setBounds([point1, point2] as Coordinate[]); + } + }; + + if (!layer) { + return null; + } + + return ( + <div className={`${isLayerVisible ? 'opacity-100' : 'opacity-40'} flex flex-col gap-1 ps-3`}> + <QuestionModal + isOpen={isImageRemoveModalOpen} + onClose={rejectImageRemove} + onConfirm={confirmImageRemove} + question="Are you sure you want to remove the image?" + /> + {Object.values(layer.texts).map(layerText => ( + <LayersDrawerTextItem layerText={layerText} key={layerText.id} /> + ))} + {Object.values(layer.images).map(layerImage => ( + <LayersDrawerImageItem + layerImage={layerImage} + key={layerImage.id} + bringToFront={() => bringImageToFront(layerImage)} + bringToBack={() => bringImageToBack(layerImage)} + removeObject={() => removeImage(layerImage)} + centerObject={() => centerObject(layerImage)} + isLayerVisible={isLayerVisible} + isLayerActive={isLayerActive} + /> + ))} + </div> + ); +}; diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx index 6c34319e26351bcdb07db049cfe3bf06206b67b1..a1af19ffd602c567bb30051189ab9c801cc7f808 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx @@ -236,6 +236,7 @@ describe('UserOverlayForm - Component', () => { loading: 'succeeded', role: 'user', userData: null, + token: null, }, project: { data: projectFixture, @@ -302,6 +303,7 @@ describe('UserOverlayForm - Component', () => { loading: 'succeeded', role: 'user', userData: null, + token: null, }, project: { data: projectFixture, diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.test.tsx index 82c38758c84f11afeded8f09960e37938defc335..b8b77087743d0ffa2c207fe8d6234e7059245edc 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.test.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.test.tsx @@ -30,6 +30,7 @@ describe('UserOverlays component', () => { login: null, role: 'user', userData: null, + token: null, }, }); @@ -45,6 +46,7 @@ describe('UserOverlays component', () => { login: 'test', role: 'user', userData: null, + token: null, }, }); @@ -59,6 +61,7 @@ describe('UserOverlays component', () => { login: 'test', role: 'user', userData: null, + token: null, }, }); diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/UserOverlaysGroup.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/UserOverlaysGroup.component.test.tsx index 68208c809b724c2f97ba6a79acb1cabe7640dcd6..916f02ea4fa1e68b2376b47fe5a30e2eda9e9111 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/UserOverlaysGroup.component.test.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/UserOverlaysGroup.component.test.tsx @@ -47,6 +47,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -85,6 +86,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -108,6 +110,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -159,6 +162,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -198,6 +202,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -250,6 +255,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -293,6 +299,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -343,6 +350,7 @@ describe('UserOverlaysGroup - component', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/hooks/useUserOverlays.test.ts b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/hooks/useUserOverlays.test.ts index 3f34c159e06463686eb677a2c8c3b5a4d85a78a4..a1f55c97fbbb56e83d2ebf89563054296eeccbb0 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/hooks/useUserOverlays.test.ts +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysGroup/hooks/useUserOverlays.test.ts @@ -22,6 +22,7 @@ describe('useUserOverlays', () => { login: null, role: 'user', userData: null, + token: null, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -61,6 +62,7 @@ describe('useUserOverlays', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -110,6 +112,7 @@ describe('useUserOverlays', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, @@ -161,6 +164,7 @@ describe('useUserOverlays', () => { login: 'test', role: 'user', userData: null, + token: null, }, overlays: { ...OVERLAYS_INITIAL_STATE_MOCK, diff --git a/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.test.tsx b/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.test.tsx index 0fe3a588ecd06ed39cb31a05821f63f31d6a3ad4..96eea1f9f89db99ef53c8c45ee13bc593a86b217 100644 --- a/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.test.tsx +++ b/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.test.tsx @@ -3,8 +3,8 @@ import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithSto import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; import { StoreType } from '@/redux/store'; import { render, screen } from '@testing-library/react'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { BIOENTITY_INITIAL_STATE_MOCK } from '@/redux/bioEntity/bioEntity.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { ConnectedBioEntitiesList } from './ConnectedBioEntitiesList.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -27,11 +27,21 @@ describe('ConnectedBioEntitiesList', () => { jest.clearAllMocks(); }); - it('renders loading indicator when bioEntityLoading is pending', () => { + it('renders loading indicator when searchModelElementLoading is pending', () => { renderComponent({ - bioEntity: { - ...BIOENTITY_INITIAL_STATE_MOCK, - loading: 'pending', + modelElements: { + data: { + 0: { + data: [modelElementFixture], + loading: 'pending', + error: { message: '', name: '' }, + }, + }, + search: { + data: [], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, }); @@ -41,23 +51,31 @@ describe('ConnectedBioEntitiesList', () => { }); it('renders list of bio entities when bioEntityData is available', () => { - const bioEntityData = [bioEntitiesContentFixture[0]]; - renderComponent({ - bioEntity: { - ...BIOENTITY_INITIAL_STATE_MOCK, - data: [ - { - searchQueryElement: '', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntityData, + error: { message: '', name: '' }, }, - ], + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'idle', + error: DEFAULT_ERROR, + data: [{ modelElement: modelElementFixture, perfect: true }], + }, + ], + loading: 'idle', + error: DEFAULT_ERROR, + }, }, }); expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); - expect(screen.queryByText(bioEntitiesContentFixture[0].bioEntity.name)).toBeVisible(); + expect(screen.queryByText(modelElementFixture.name)).toBeVisible(); }); }); diff --git a/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.tsx b/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.tsx index ea016f66f90120c15f6f9cd8524be5df020e48b8..28121e3b9a02128ed609dcb3ceb83d48421df90c 100644 --- a/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.tsx +++ b/src/components/Map/Drawer/ReactionDrawer/ConnectedBioEntitiesList/ConnectedBioEntitiesList.component.tsx @@ -1,15 +1,15 @@ -import { - bioEntityDataListSelector, - bioEntityLoadingSelector, -} from '@/redux/bioEntity/bioEntity.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { LoadingIndicator } from '@/shared/LoadingIndicator'; import React from 'react'; +import { + searchModelElementsListSelector, + searchModelElementsLoadingSelector, +} from '@/redux/modelElements/modelElements.selector'; import { BioEntitiesPinsListItem } from '../../SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem'; export const ConnectedBioEntitiesList = (): React.ReactNode => { - const bioEntityLoading = useAppSelector(bioEntityLoadingSelector); - const bioEntityData = useAppSelector(bioEntityDataListSelector); + const bioEntityLoading = useAppSelector(searchModelElementsLoadingSelector); + const searchModelElements = useAppSelector(searchModelElementsListSelector); const isPending = bioEntityLoading === 'pending'; if (isPending) { @@ -19,12 +19,12 @@ export const ConnectedBioEntitiesList = (): React.ReactNode => { return ( <div> <h3 className="mb-1 font-semibold">Reaction elements:</h3> - {bioEntityData && - bioEntityData.map(item => ( + {searchModelElements && + searchModelElements.map(item => ( <BioEntitiesPinsListItem - name={item.bioEntity.name} - pin={item.bioEntity} - key={item.bioEntity.name} + name={item.modelElement.name} + pin={item.modelElement} + key={item.modelElement.name} /> ))} </div> diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx index 283dd29f7d8a3bb9e7764a4981081507f8c9e13e..20b474798065288fce906a5846bfc609bfae78fb 100644 --- a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx +++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx @@ -28,7 +28,6 @@ const reference = { ...referenceFixture, link: 'https://uni.lu' }; describe('ReactionDrawer - component', () => { beforeEach(() => { - jest.resetAllMocks(); jest.clearAllMocks(); }); diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx index 633c55a8b063830f4ae0fea575051d67ec0f8594..4d65a3b31174c2932d2adfe38db1824a9c4c64a2 100644 --- a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx +++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx @@ -1,12 +1,14 @@ import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { currentDrawerReactionCommentsSelector } from '@/redux/bioEntity/bioEntity.selectors'; import { CommentItem } from '@/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component'; import { ZERO } from '@/constants/common'; import ReactionTypeEnum from '@/utils/reaction/ReactionTypeEnum'; import React from 'react'; import { AnnotationItemList } from '@/components/Map/Drawer/BioEntityDrawer/AnnotationItem/AnnotationItemList.component'; -import { currentDrawerNewReactionSelector } from '@/redux/newReactions/newReactions.selectors'; +import { + currentDrawerNewReactionSelector, + currentDrawerReactionCommentsSelector, +} from '@/redux/newReactions/newReactions.selectors'; import { ConnectedBioEntitiesList } from './ConnectedBioEntitiesList'; export const ReactionDrawer = (): React.ReactNode => { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.tsx index 1a45d10890ed519bf5bc9a3e65744643a1888431..bfe1f3657e1c5417aec0a82681366e3d98ff1bef 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.tsx @@ -1,21 +1,30 @@ import { BioEntityContent } from '@/types/models'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem'; interface BioEntitiesPinsListProps { - bioEnititesPins: BioEntityContent[]; + bioEnititesPins: Array<BioEntityContent | SearchModelElementDataState>; } export const BioEntitiesPinsList = ({ bioEnititesPins }: BioEntitiesPinsListProps): JSX.Element => { return ( <ul className="h-[calc(100%-224px)] max-h-[calc(100%-224px)] overflow-auto px-6 py-2"> {bioEnititesPins && - bioEnititesPins.map(result => ( - <BioEntitiesPinsListItem - key={result.bioEntity.name} - name={result.bioEntity.name} - pin={result.bioEntity} - /> - ))} + bioEnititesPins.map(result => + 'bioEntity' in result ? ( + <BioEntitiesPinsListItem + key={result.bioEntity.name} + name={result.bioEntity.name} + pin={result.bioEntity} + /> + ) : ( + <BioEntitiesPinsListItem + key={result.modelElement.name} + name={result.modelElement.name} + pin={result.modelElement} + /> + ), + )} </ul> ); }; 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 f5f24509780ac842173be756e754697d0be0c17d..e56049b56c5ed3e51995b0cdbf200baebb05d858 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 @@ -16,7 +16,7 @@ import { getTypeBySBOTerm } from '@/utils/bioEntity/getTypeBySBOTerm'; import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; import { HISTAMINE_MAP_ID } from '@/constants/mocks'; import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component'; -import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; +import { PinListModelElement } from './BioEntitiesPinsListItem.types'; const BIO_ENTITY = { ...bioEntitiesContentFixture[0].bioEntity, @@ -35,7 +35,7 @@ const INITIAL_STORE_WITH_ENTITY_NUMBER: InitialStoreState = { const renderComponent = ( name: string, - pin: PinListBioEntity, + pin: PinListModelElement, initialStoreState: InitialStoreState = {}, ): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); @@ -54,7 +54,7 @@ const renderComponent = ( const renderComponentWithActionListener = ( name: string, - pin: PinListBioEntity, + pin: PinListModelElement, initialStoreState: InitialStoreState = {}, ): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState); 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 d5764b93b440275ee76a200b794dfe6791e6b0bc..e48cef381462d3f6355c90e9436983d0728c0e0e 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 @@ -20,12 +20,12 @@ import { getTypeBySBOTerm } from '@/utils/bioEntity/getTypeBySBOTerm'; import { ZERO } from '@/constants/common'; import React from 'react'; import { AnnotationItemList } from '@/components/Map/Drawer/BioEntityDrawer/AnnotationItem/AnnotationItemList.component'; -import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; +import { PinListModelElement } from './BioEntitiesPinsListItem.types'; import { isPinWithCoordinates } from './BioEntitiesPinsListItem.utils'; interface BioEntitiesPinsListItemProps { name: string; - pin: PinListBioEntity; + pin: PinListModelElement; } export const BioEntitiesPinsListItem = ({ @@ -38,7 +38,7 @@ export const BioEntitiesPinsListItem = ({ numberByEntityNumberIdSelector(state, pin.elementId || ''), ); const pinIconCanvas = getCanvasIcon({ - color: PINS_COLORS.bioEntity, + color: PINS_COLORS.modelElement, value: pinIconValue, }); 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 a916a1dc7f9da06081d656579c979b307e6f010f..a2114d00c76d0a9a46dd0a55ee84525beb817ec3 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 @@ -1,15 +1,15 @@ -import { BioEntity } from '@/types/models'; +import { ModelElement } from '@/types/models'; -export type PinListBioEntity = Pick<BioEntity, 'synonyms' | 'references'> & { - symbol?: BioEntity['symbol']; - fullName?: BioEntity['fullName']; - x?: BioEntity['x']; - y?: BioEntity['y']; - elementId?: BioEntity['elementId']; - sboTerm?: BioEntity['sboTerm']; +export type PinListModelElement = Pick<ModelElement, 'synonyms' | 'references'> & { + symbol?: ModelElement['symbol']; + fullName?: ModelElement['fullName']; + x?: ModelElement['x']; + y?: ModelElement['y']; + elementId?: ModelElement['elementId']; + sboTerm?: ModelElement['sboTerm']; }; -export type PinListBioEntityWithCoords = PinListBioEntity & { - x: BioEntity['x']; - y: BioEntity['y']; +export type PinListModelElementWithCoords = PinListModelElement & { + x: ModelElement['x']; + y: ModelElement['y']; }; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts index af28a4c6b83e8b6b1d2ec7dec5f55e2f785b5f02..c1b483d205b55f586bac333bd275300b121ee227 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts @@ -1,5 +1,10 @@ -import { PinListBioEntity, PinListBioEntityWithCoords } from './BioEntitiesPinsListItem.types'; +import { + PinListModelElement, + PinListModelElementWithCoords, +} from './BioEntitiesPinsListItem.types'; -export const isPinWithCoordinates = (pin: PinListBioEntity): pin is PinListBioEntityWithCoords => { +export const isPinWithCoordinates = ( + pin: PinListModelElement, +): pin is PinListModelElementWithCoords => { return Boolean('x' in pin && 'y' in pin); }; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx index 2f20b71dce6ab874779cbe8b735ec8fb90665143..ddd82ee268c17ee9ec8d790d4cdeef9636242f30 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx @@ -1,5 +1,4 @@ -import { FIRST_ARRAY_ELEMENT, ZERO } from '@/constants/common'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { MODELS_MOCK } from '@/models/mocks/modelsMock'; import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { AppDispatch, RootState, StoreType } from '@/redux/store'; @@ -12,6 +11,8 @@ import { import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; import { HISTAMINE_MAP_ID, MAIN_MAP_ID, PRKN_SUBSTRATES_MAP_ID } from '@/constants/mocks'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { BioEntitiesAccordion } from './BioEntitiesAccordion.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -53,17 +54,26 @@ const renderComponentWithActionListener = ( describe('BioEntitiesAccordion - component', () => { it('should display loading indicator when bioEntity search is pending', () => { renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'pending', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, + modelElements: { + data: { + 0: { + data: [modelElementFixture], + loading: 'succeeded', + error: { message: '', name: '' }, }, - ], - loading: 'pending', - error: { name: '', message: '' }, + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'pending', + error: DEFAULT_ERROR, + data: [{ modelElement: modelElementFixture, perfect: true }], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, models: { data: [], @@ -77,17 +87,39 @@ describe('BioEntitiesAccordion - component', () => { it('should render list of maps with number of entities after succeeded bio entity search', () => { renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { name: '', message: '' }, + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { ...modelElementFixture, model: HISTAMINE_MAP_ID }, + perfect: true, + }, + { + modelElement: { ...modelElementFixture, model: MAIN_MAP_ID }, + perfect: true, + }, + { + modelElement: { ...modelElementFixture, model: PRKN_SUBSTRATES_MAP_ID }, + perfect: true, + }, + ], + }, + ], + loading: 'succeeded', + error: DEFAULT_ERROR, + }, }, models: { data: MODELS_MOCK, @@ -96,40 +128,16 @@ describe('BioEntitiesAccordion - component', () => { }, }); - const countHistamine = bioEntitiesContentFixture.filter( - content => content.bioEntity.model === HISTAMINE_MAP_ID, - ).length; - const countCore = bioEntitiesContentFixture.filter( - content => content.bioEntity.model === MAIN_MAP_ID, - ).length; - const countPrkn = bioEntitiesContentFixture.filter( - content => content.bioEntity.model === PRKN_SUBSTRATES_MAP_ID, - ).length; - - const countAll = bioEntitiesContentFixture.length; - - expect(screen.getByText(`Content (${countAll})`)).toBeInTheDocument(); - expect(screen.getByText(`Core PD map (${countCore})`)).toBeInTheDocument(); - if (countHistamine > ZERO) { - expect(screen.getByText(`Histamine signaling (${countHistamine})`)).toBeInTheDocument(); - } - expect(screen.getByText(`PRKN substrates (${countPrkn})`)).toBeInTheDocument(); + expect(screen.getByText(`Content (3)`)).toBeInTheDocument(); + expect(screen.getByText(`Core PD map (1)`)).toBeInTheDocument(); + expect(screen.getByText(`Histamine signaling (1)`)).toBeInTheDocument(); + expect(screen.getByText(`PRKN substrates (1)`)).toBeInTheDocument(); }); it('should fire toggleIsContentTabOpened on accordion item button click', () => { const { store } = renderComponentWithActionListener({ ...INITIAL_STORE_STATE_MOCK, bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, - }, - ], - loading: 'succeeded', - error: { name: '', message: '' }, isContentTabOpened: false, }, models: { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx index 8754b663a1c714f0f8e3e4740ae012bc23c0ab3c..cc4d65aa1ca663e6425477e0d47f82cfeff4153c 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.tsx @@ -1,9 +1,4 @@ -import { - bioEntitiesPerModelSelector, - bioEntityIsContentTabOpenedSelector, - loadingBioEntityStatusSelector, - numberOfBioEntitiesSelector, -} from '@/redux/bioEntity/bioEntity.selectors'; +import { bioEntityIsContentTabOpenedSelector } from '@/redux/bioEntity/bioEntity.selectors'; import { toggleIsContentTabOpened } from '@/redux/bioEntity/bioEntity.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; @@ -13,13 +8,18 @@ import { AccordionItemHeading, AccordionItemPanel, } from '@/shared/Accordion'; +import { + numberOfSearchModelElementsSelector, + searchModelElementsLoadingStatusSelector, + searchModelElementsPerModelSelector, +} from '@/redux/modelElements/modelElements.selector'; import { BioEntitiesSubmapItem } from './BioEntitiesSubmapItem'; export const BioEntitiesAccordion = (): JSX.Element => { const dispatch = useAppDispatch(); - const bioEntitiesNumber = useAppSelector(numberOfBioEntitiesSelector); - const bioEntitiesState = useAppSelector(loadingBioEntityStatusSelector); - const bioEntitiesPerModel = useAppSelector(bioEntitiesPerModelSelector); + const searchModelElementsNumber = useAppSelector(numberOfSearchModelElementsSelector); + const searchModelElementsLoadingStatus = useAppSelector(searchModelElementsLoadingStatusSelector); + const searchModelElementsPerModel = useAppSelector(searchModelElementsPerModelSelector); const isContentTabOpened = useAppSelector(bioEntityIsContentTabOpenedSelector); const toggleTabOpened = (): void => { @@ -30,18 +30,18 @@ export const BioEntitiesAccordion = (): JSX.Element => { <AccordionItem dangerouslySetExpanded={isContentTabOpened}> <AccordionItemHeading> <AccordionItemButton onClick={toggleTabOpened}> - Content {bioEntitiesState === 'pending' && ' (Loading...)'} - {bioEntitiesState === 'succeeded' && ` (${bioEntitiesNumber})`} + Content {searchModelElementsLoadingStatus === 'pending' && ' (Loading...)'} + {searchModelElementsLoadingStatus === 'succeeded' && ` (${searchModelElementsNumber})`} </AccordionItemButton> </AccordionItemHeading> <AccordionItemPanel> - {bioEntitiesPerModel.map(model => ( + {searchModelElementsPerModel.map(model => ( <BioEntitiesSubmapItem key={model.modelName} mapName={model.modelName} mapId={model.modelId} - numberOfEntities={model.numberOfEntities} - bioEntities={model.bioEntities} + numberOfModelElements={model.numberOfModelElements} + modelElements={model.modelElements} /> ))} </AccordionItemPanel> diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx index a93282dbc8057e22d7db0fcdf38bb59f57290fa6..6b0d1c35bb9bcf9ca38f3243782797330af9e1a2 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx @@ -1,7 +1,6 @@ /* eslint-disable no-magic-numbers */ import { act, render, screen } from '@testing-library/react'; import { StoreType } from '@/redux/store'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { InitialStoreState, getReduxWrapperWithStore, @@ -14,6 +13,8 @@ import { openedMapsThreeSubmapsFixture, } from '@/redux/map/map.fixtures'; import { MAIN_MAP_ID } from '@/constants/mocks'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { BioEntitiesSubmapItem } from './BioEntitiesSubmapItem.component'; const SECOND_STEP = 2; @@ -27,8 +28,8 @@ const renderComponent = (initialStoreState: InitialStoreState = {}): { store: St <BioEntitiesSubmapItem mapName={MODELS_MOCK_SHORT[0].name} mapId={MODELS_MOCK_SHORT[0].id} - numberOfEntities={21} - bioEntities={bioEntitiesContentFixture} + numberOfModelElements={21} + modelElements={[{ modelElement: modelElementFixture, perfect: true }]} /> </Wrapper>, ), @@ -47,17 +48,26 @@ describe('BioEntitiesSubmapItem - component', () => { }); it('should navigate user to bio enitites results list after clicking button', async () => { const { store } = renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { name: '', message: '' }, + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'idle', + error: DEFAULT_ERROR, + data: [{ modelElement: modelElementFixture, perfect: true }], + }, + ], + loading: 'idle', + error: DEFAULT_ERROR, + }, }, drawer: drawerSearchStepOneFixture, }); @@ -73,25 +83,13 @@ describe('BioEntitiesSubmapItem - component', () => { }, } = store.getState(); - expect(stepType).toBe('bioEntity'); + expect(stepType).toBe('modelElement'); expect(selectedValue).toBe(undefined); expect(currentStep).toBe(SECOND_STEP); - expect(listOfBioEnitites).toBe(bioEntitiesContentFixture); + expect(listOfBioEnitites).toStrictEqual([{ modelElement: modelElementFixture, perfect: true }]); }); it("should open submap and set it to active if it's not already opened", async () => { const { store } = renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, - }, - ], - loading: 'succeeded', - error: { name: '', message: '' }, - }, drawer: drawerSearchStepOneFixture, models: { data: MODELS_MOCK_SHORT, loading: 'succeeded', error: { name: '', message: '' } }, map: { @@ -100,6 +98,27 @@ describe('BioEntitiesSubmapItem - component', () => { error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, }, + modelElements: { + data: { + 0: { + data: [modelElementFixture], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'idle', + error: DEFAULT_ERROR, + data: [{ modelElement: modelElementFixture, perfect: true }], + }, + ], + loading: 'idle', + error: DEFAULT_ERROR, + }, + }, }); const { @@ -134,18 +153,6 @@ describe('BioEntitiesSubmapItem - component', () => { }); it("should set map active if it's already opened", async () => { const { store } = renderComponent({ - bioEntity: { - data: [ - { - searchQueryElement: '', - loading: 'succeeded', - error: { name: '', message: '' }, - data: bioEntitiesContentFixture, - }, - ], - loading: 'succeeded', - error: { name: '', message: '' }, - }, drawer: drawerSearchStepOneFixture, models: { data: MODELS_MOCK_SHORT, loading: 'succeeded', error: { name: '', message: '' } }, map: { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.tsx index 47f2e4b205195d6f35e457fb5920f79e327c516a..086ae4ce450dc4183570f32a3006dac7e348b116 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.tsx @@ -7,20 +7,20 @@ import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selec import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { Icon } from '@/shared/Icon'; -import { BioEntityContent } from '@/types/models'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; export interface BioEntitiesSubmapItemProps { mapName: string; mapId: number; - numberOfEntities: string | number; - bioEntities: BioEntityContent[]; + numberOfModelElements: string | number; + modelElements: SearchModelElementDataState[]; } export const BioEntitiesSubmapItem = ({ mapName, mapId, - numberOfEntities, - bioEntities, + numberOfModelElements, + modelElements, }: BioEntitiesSubmapItemProps): JSX.Element => { const dispatch = useAppDispatch(); const openedMaps = useAppSelector(mapOpenedMapsSelector); @@ -43,7 +43,7 @@ export const BioEntitiesSubmapItem = ({ const onSubmapClick = (): void => { openSubmap(); - dispatch(displayBioEntitiesList(bioEntities)); + dispatch(displayBioEntitiesList(modelElements)); const locationButton = document.querySelector<HTMLButtonElement>(`#${LOCATION_BTN_ID}`); if (locationButton) { @@ -59,7 +59,7 @@ export const BioEntitiesSubmapItem = ({ data-testid="bio-entites-submap-button" > <p className="text-sm font-normal"> - {mapName} ({numberOfEntities}) + {mapName} ({numberOfModelElements}) </p> <Icon name="arrow" className="h-6 w-6 fill-font-500" data-testid="arrow-icon" /> </button> diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.test.tsx index ec42327545d17bdddbf3fee361ee6597b717ea54..1835a35aa3b6f0f72b858aab5c99e6a641c20d48 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.test.tsx @@ -61,8 +61,8 @@ describe('PinsList - component ', () => { expect(screen.getByTestId('accordions-details')).toBeInTheDocument(); }); - it('should not display list of bio enities when bioEntity is searched', () => { - renderComponent([], 'bioEntity'); + it('should not display list of bio enities when modelElement is searched', () => { + renderComponent([], 'modelElement'); expect(screen.queryByTestId('pins-list')).toBeNull(); }); 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 d612aae39c45cc2e5bd3bd258ab2e9a7775b2d4f..931d6f33a0a968c2421191ec5b9fdac1a81671dc 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx @@ -37,7 +37,7 @@ export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => { </div> ); } - case 'bioEntity': + case 'modelElement': return <div />; case 'comment': return <div />; 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 a72f8629e2eb718e43e56b5471183f837f785aba..a72a61edcbfa4f7aac9a33e865bb8ac1a69071cb 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 { BioEntity, Chemical, Drug, PinDetailsItem } from '@/types/models'; +import { Chemical, Drug, ModelElement, PinDetailsItem } from '@/types/models'; import { PinType } from '@/types/pin'; export type PinItem = { @@ -17,5 +17,5 @@ export type AvailableSubmaps = { export type TargetElement = { target: PinDetailsItem; - element: BioEntity; + element: ModelElement; }; 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 ce80ff1c3bee164941694df5993bc573e4bea953..d6a031e994ac91a7c94ccfd381cb805e217462c9 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 @@ -3,15 +3,15 @@ import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFi import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; import { drugsFixture } from '@/models/fixtures/drugFixtures'; import { AppDispatch, RootState } from '@/redux/store'; -import { BioEntity, PinDetailsItem } from '@/types/models'; +import { ModelElement, 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 { 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 { MAIN_MAP_ID } from '@/constants/mocks'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; import { PinTypeWithNone } from '../PinsList.types'; import { PinsListItem } from './PinsListItem.component'; @@ -26,10 +26,7 @@ const CHEMICALS_PIN = { }; const PIN_NUMBER = 10; -const BIO_ENTITY = { - ...bioEntitiesContentFixture[0].bioEntity, - model: 5053, -}; +const MODEL_ELEMENT = modelElementFixture; const INITIAL_STORE_STATE: InitialStoreState = { models: MODELS_DATA_MOCK_WITH_MAIN_MAP, @@ -48,7 +45,7 @@ const renderComponent = ( name: string, pin: PinDetailsItem, type: PinTypeWithNone, - element: BioEntity, + element: ModelElement, initialStoreState: InitialStoreState = {}, ): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState); @@ -72,14 +69,14 @@ describe('PinsListItem - component ', () => { chemicalsFixture[0].targets[0].targetParticipants[1].link = 'https://example.com/plugin.js'; it('should display full name of pin', () => { - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', MODEL_ELEMENT, 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', BIO_ENTITY, INITIAL_STORE_STATE); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', MODEL_ELEMENT, INITIAL_STORE_STATE); const firstPinElementType = drugsFixture[0].targets[0].targetParticipants[0].type; const firstPinElementResource = drugsFixture[0].targets[0].targetParticipants[0].resource; @@ -95,7 +92,7 @@ describe('PinsListItem - component ', () => { } }); it('should display list of references for pin', () => { - renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); + renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', MODEL_ELEMENT, INITIAL_STORE_STATE); const firstPinReferenceType = drugsFixture[0].targets[0].references[0].type; const firstPinReferenceResource = drugsFixture[0].targets[0].references[0].resource; @@ -112,7 +109,7 @@ describe('PinsListItem - component ', () => { CHEMICALS_PIN.name, CHEMICALS_PIN.pin, 'chemicals', - BIO_ENTITY, + MODEL_ELEMENT, INITIAL_STORE_STATE, ); @@ -129,7 +126,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', BIO_ENTITY); + renderComponent(CHEMICALS_PIN.name, CHEMICALS_PIN.pin, 'drugs', MODEL_ELEMENT); const bioEntityName = bioEntitiesContentFixture[2].bioEntity.fullName ? bioEntitiesContentFixture[2].bioEntity.fullName @@ -147,7 +144,7 @@ describe('PinsListItem - component ', () => { CHEMICALS_PIN.name, chemicalWithoutSubmaps, 'chemicals', - BIO_ENTITY, + MODEL_ELEMENT, INITIAL_STORE_STATE, ); @@ -159,7 +156,7 @@ describe('PinsListItem - component ', () => { DRUGS_PIN.pin, 'drugs', { - ...BIO_ENTITY, + ...MODEL_ELEMENT, x: 1000, y: 500, }, @@ -168,7 +165,7 @@ describe('PinsListItem - component ', () => { map: { data: { ...initialMapDataFixture, - modelId: BIO_ENTITY.model, + modelId: MODEL_ELEMENT.model, }, loading: 'succeeded', error: { message: '', name: '' }, @@ -191,7 +188,7 @@ describe('PinsListItem - component ', () => { DRUGS_PIN.pin, 'drugs', { - ...BIO_ENTITY, + ...MODEL_ELEMENT, x: 1000, y: 500, model: 52, @@ -201,7 +198,7 @@ describe('PinsListItem - component ', () => { map: { data: { ...initialMapDataFixture, - modelId: BIO_ENTITY.model, + modelId: MODEL_ELEMENT.model, }, loading: 'succeeded', error: { message: '', name: '' }, 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 510763ad3763e75131c61b0bde95796dd9edd1d3..880561dabd92d6e104c71871d0a7d7752ad0373b 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 @@ -9,7 +9,7 @@ import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selec import { openMapAndSetActive, setActiveMap, setMapPosition } from '@/redux/map/map.slice'; import { modelsDataSelector, modelsNameMapSelector } from '@/redux/models/models.selectors'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { BioEntity, PinDetailsItem } from '@/types/models'; +import { ModelElement, PinDetailsItem } from '@/types/models'; import { AvailableSubmaps, PinTypeWithNone } from '../PinsList.types'; import { getListOfAvailableSubmaps } from './PinsListItem.component.utils'; @@ -17,7 +17,7 @@ interface PinsListItemProps { name: string; type: PinTypeWithNone; pin: PinDetailsItem; - element: BioEntity; + element: ModelElement; number: number; } diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.utils.ts b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.utils.ts index 26f457761b94201d5e7c45aef0e0270376aef058..7a0bffd5d253e02239e44d9b9ec2f0eb175cd40a 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.utils.ts +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.utils.ts @@ -5,7 +5,7 @@ const MAIN_MAP_ID = 52; export const getPinColor = (type: PinTypeWithNone): string => { const pinColors: Record<PinTypeWithNone, string> = { - bioEntity: 'fill-primary-500', + modelElement: 'fill-primary-500', drugs: 'fill-orange', chemicals: 'fill-purple', comment: 'fill-blue', diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx index 3d5b627bc96d13a0b96a00150c8627afae1664f7..a55ff7807be96e614dd378aa698d9f00214b687c 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx @@ -38,7 +38,7 @@ describe('SearchDrawerWrapper - component', () => { drawerName: 'search', searchDrawerState: { currentStep: 2, - stepType: 'bioEntity', + stepType: 'modelElement', selectedValue: undefined, listOfBioEnitites: [], selectedSearchElement: '', diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx index 573435d06b78f5c47a9028078886e65112eadd67..66912c6d22d277753ed0cd1b9371f202cc3f4e6f 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.tsx @@ -1,4 +1,4 @@ -import { BIO_ENTITY, DRUGS_CHEMICALS } from '@/constants'; +import { MODEL_ELEMENT, DRUGS_CHEMICALS } from '@/constants'; import { STEP } from '@/constants/searchDrawer'; import { currentStepDrawerStateSelector, @@ -15,7 +15,7 @@ export const SearchDrawerWrapper = (): JSX.Element => { const currentStep = useSelector(currentStepDrawerStateSelector); const stepType = useSelector(stepTypeDrawerSelector); - const isBioEntityType = stepType === BIO_ENTITY; + const isBioEntityType = stepType === MODEL_ELEMENT; const isChemicalsOrDrugsType = DRUGS_CHEMICALS.includes(stepType); return ( diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index df0e5eac330e382b22ebb4ec72a74eead31b5206..67bae92f4b7e44fb785ff29ba9cbab7d0651b45e 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -4,17 +4,11 @@ import { Legend } from '@/components/Map/Legend'; import { MapViewer } from '@/components/Map/MapViewer'; import { MapLoader } from '@/components/Map/MapLoader/MapLoader.component'; import { MapVectorBackgroundSelector } from '@/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component'; -import { MapActiveLayerSelector } from '@/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component'; -import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { MapAdditionalLogos } from '@/components/Map/MapAdditionalLogos'; -import { MapDrawActions } from '@/components/Map/MapDrawActions/MapDrawActions.component'; -import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; import { MapAdditionalActions } from './MapAdditionalActions'; import { PluginsDrawer } from './PluginsDrawer'; export const Map = (): JSX.Element => { - const activeLayer = useAppSelector(layersActiveLayerSelector); - return ( <div className="relative z-0 h-screen w-full overflow-hidden bg-black" @@ -22,8 +16,6 @@ export const Map = (): JSX.Element => { > <MapViewer /> <MapVectorBackgroundSelector /> - <MapActiveLayerSelector /> - {activeLayer && <MapDrawActions />} <Drawer /> <PluginsDrawer /> <Legend /> diff --git a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx deleted file mode 100644 index e9bec5dbcfea7f1633a00c650c999aa83a034775..0000000000000000000000000000000000000000 --- a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { twMerge } from 'tailwind-merge'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { Select } from '@/shared/Select'; -import { - layersActiveLayerSelector, - layersForCurrentModelSelector, - layersVisibilityForCurrentModelSelector, -} from '@/redux/layers/layers.selectors'; -import { useEffect, useMemo } from 'react'; -import { setActiveLayer } from '@/redux/layers/layers.slice'; -import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; - -export const MapActiveLayerSelector = (): JSX.Element => { - const dispatch = useAppDispatch(); - const layers = useAppSelector(layersForCurrentModelSelector); - const layersVisibility = useAppSelector(layersVisibilityForCurrentModelSelector); - const currentModelId = useAppSelector(currentModelIdSelector); - const activeLayer = useAppSelector(layersActiveLayerSelector); - - const handleChange = (activeLayerId: string | number): void => { - dispatch(setActiveLayer({ modelId: currentModelId, layerId: +activeLayerId })); - }; - - const options: Array<{ id: number; name: string }> = useMemo(() => { - return layers - .filter(layer => layersVisibility[layer.details.id]) - .map(layer => { - return { - id: layer.details.id, - name: layer.details.name, - }; - }); - }, [layers, layersVisibility]); - - useEffect(() => { - const selectedOption = options.find(option => option.id === activeLayer) || null; - if (selectedOption || !currentModelId) { - return; - } - if (options.length === 0) { - dispatch(setActiveLayer({ modelId: currentModelId, layerId: null })); - } else { - dispatch(setActiveLayer({ modelId: currentModelId, layerId: options[0].id })); - } - }, [activeLayer, currentModelId, dispatch, options]); - - useEffect(() => { - if (!options.length) { - dispatch(setActiveLayer({ modelId: currentModelId, layerId: null })); - dispatch(mapEditToolsSetActiveAction(null)); - } - }, [currentModelId, dispatch, options]); - - return ( - <div className={twMerge('absolute right-6 top-[calc(64px+40px+84px)] flex')}> - {Boolean(options.length) && ( - <Select options={options} selectedId={activeLayer} onChange={handleChange} width={140} /> - )} - </div> - ); -}; diff --git a/src/components/Map/MapAdditionalActions/utils/useVisibleBioEntitiesPolygonCoordinates.test.ts b/src/components/Map/MapAdditionalActions/utils/useVisibleBioEntitiesPolygonCoordinates.test.ts index 7cfb93047a9a73ad2edca9f6b92c8dde539c1120..0e377e4f3bd7f02cbf644dde62fb3d4298c27205 100644 --- a/src/components/Map/MapAdditionalActions/utils/useVisibleBioEntitiesPolygonCoordinates.test.ts +++ b/src/components/Map/MapAdditionalActions/utils/useVisibleBioEntitiesPolygonCoordinates.test.ts @@ -1,6 +1,5 @@ import { drugsFixture } from '@/models/fixtures/drugFixtures'; /* eslint-disable no-magic-numbers */ -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; import { modelsFixture } from '@/models/fixtures/modelsFixture'; import { BIOENTITY_INITIAL_STATE_MOCK } from '@/redux/bioEntity/bioEntity.mock'; @@ -10,6 +9,9 @@ import { RootState } from '@/redux/store'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { renderHook } from '@testing-library/react'; import { HISTAMINE_MAP_ID } from '@/constants/mocks'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../../../../redux/chemicals/chemicals.mock'; import { DRUGS_INITIAL_STATE_MOCK } from '../../../../redux/drugs/drugs.mock'; import { DEFAULT_POSITION, MAIN_MAP, MAP_INITIAL_STATE } from '../../../../redux/map/map.constants'; @@ -65,28 +67,39 @@ const getInitalState = ( { modelId: HISTAMINE_MAP_ID, modelName: MAIN_MAP, lastPosition: DEFAULT_POSITION }, ], }, - bioEntity: { - ...BIOENTITY_INITIAL_STATE_MOCK, - data: [ - { - searchQueryElement: 'search', - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - model: HISTAMINE_MAP_ID, - x: 16, - y: 16, - z: 1, - }, - }, - ].slice(0, elementsLimit), + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', error: { message: '', name: '' }, }, - ], - }, + }, + search: { + data: [ + { + searchQueryElement: 'search', + loading: 'pending', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + model: HISTAMINE_MAP_ID, + x: 16, + y: 16, + z: 1, + }, + perfect: true, + }, + ], + }, + ].slice(0, elementsLimit), + loading: 'pending', + error: DEFAULT_ERROR, + }, + } as ModelElementsState, + bioEntity: BIOENTITY_INITIAL_STATE_MOCK, chemicals: { ...CHEMICALS_INITIAL_STATE_MOCK, data: [ diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx deleted file mode 100644 index 164f191c65674221f9c901cbff3d3d44b6e2780d..0000000000000000000000000000000000000000 --- a/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { MapDrawActions } from '@/components/Map/MapDrawActions/MapDrawActions.component'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; -import { - getReduxWrapperWithStore, - InitialStoreState, -} from '@/utils/testing/getReduxWrapperWithStore'; -import { StoreType } from '@/redux/store'; -import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { - LAYER_STATE_DEFAULT_DATA, - LAYERS_STATE_INITIAL_LAYER_MOCK, -} from '@/redux/layers/layers.mock'; -import { MAIN_MAP_ID } from '@/constants/mocks'; -import { layerFixture } from '@/models/fixtures/layerFixture'; - -jest.mock('../../../redux/hooks/useAppDispatch', () => ({ - useAppDispatch: jest.fn(), -})); - -const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { - const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); - - return ( - render( - <Wrapper> - <MapDrawActions /> - </Wrapper>, - ), - { - store, - } - ); -}; - -describe('MapDrawActions', () => { - const mockDispatch = jest.fn(() => {}); - - beforeEach(() => { - (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); - }); - - it('renders the MapDrawActionsButton and toggles action on click', () => { - const layerId = 0; - renderComponent({ - mapEditTools: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK, - layers: { - [layerId]: { - ...LAYERS_STATE_INITIAL_LAYER_MOCK, - data: { - ...LAYER_STATE_DEFAULT_DATA, - layersVisibility: { [MAIN_MAP_ID]: true }, - layers: [ - { - details: { ...layerFixture, id: MAIN_MAP_ID }, - texts: {}, - rects: [], - ovals: [], - lines: [], - images: {}, - }, - ], - }, - }, - }, - }); - const button = screen.getByRole('button', { name: /draw image/i }); - expect(button).toBeInTheDocument(); - fireEvent.click(button); - - expect(mockDispatch).toHaveBeenCalledWith( - mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE), - ); - }); -}); diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx deleted file mode 100644 index b8cdf9b606b55e923a586a71317d7b552f59cc40..0000000000000000000000000000000000000000 --- a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component'; -import { MapDrawEditActionsComponent } from '@/components/Map/MapDrawActions/MapDrawEditActions.component'; -import { useMemo } from 'react'; -import { - layersForCurrentModelSelector, - layersVisibilityForCurrentModelSelector, -} from '@/redux/layers/layers.selectors'; - -export const MapDrawActions = (): React.JSX.Element | null => { - const activeAction = useAppSelector(mapEditToolsActiveActionSelector); - const dispatch = useAppDispatch(); - const layers = useAppSelector(layersForCurrentModelSelector); - const layersVisibility = useAppSelector(layersVisibilityForCurrentModelSelector); - - const toggleMapEditAction = (action: keyof typeof MAP_EDIT_ACTIONS): void => { - dispatch(mapEditToolsSetActiveAction(action)); - }; - - const visibleLayersLength: number = useMemo(() => { - return layers.filter(layer => layersVisibility[layer.details.id]).length; - }, [layers, layersVisibility]); - - if (visibleLayersLength === 0) { - return null; - } - - return ( - <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col items-end gap-4"> - <MapDrawActionsButton - isActive={activeAction === MAP_EDIT_ACTIONS.DRAW_IMAGE} - toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)} - icon="image" - title="Draw image" - /> - <MapDrawActionsButton - isActive={activeAction === MAP_EDIT_ACTIONS.ADD_TEXT} - toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.ADD_TEXT)} - icon="text" - title="Add text" - /> - <MapDrawEditActionsComponent - isActive={activeAction === MAP_EDIT_ACTIONS.TRANSFORM_IMAGE} - toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.TRANSFORM_IMAGE)} - /> - </div> - ); -}; diff --git a/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx b/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx deleted file mode 100644 index eacefa311d4aa5be906188da02218b1341f7b011..0000000000000000000000000000000000000000 --- a/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { Icon } from '@/shared/Icon'; -import type { IconTypes } from '@/types/iconTypes'; - -type MapDrawActionsButtonProps = { - isActive: boolean; - toggleMapEditAction: () => void; - icon: IconTypes; - title?: string; -}; - -export const MapDrawActionsButton = ({ - isActive, - toggleMapEditAction, - icon, - title = '', -}: MapDrawActionsButtonProps): React.JSX.Element => { - return ( - <button - type="button" - className={`flex h-12 w-12 items-center justify-center rounded-full ${ - isActive ? 'bg-primary-100' : 'bg-white drop-shadow-primary' - }`} - onClick={() => toggleMapEditAction()} - title={title} - > - <Icon - className={`h-[28px] w-[28px] ${isActive ? 'text-primary-500' : 'text-black'}`} - name={icon} - /> - </button> - ); -}; diff --git a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx deleted file mode 100644 index 01ebfff1765a9bf0de482ff60118819bcea71676..0000000000000000000000000000000000000000 --- a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component'; -import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice'; -import { removeLayerImage, updateLayerImageObject } from '@/redux/layers/layers.thunks'; -import { mapModelIdSelector } from '@/redux/map/map.selectors'; -import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; -import { layerDeleteImage, layerUpdateImage } from '@/redux/layers/layers.slice'; -import { useMapInstance } from '@/utils/context/mapInstanceContext'; -import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; -import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; -import { useState } from 'react'; -import { showToast } from '@/utils/showToast'; -import { SerializedError } from '@reduxjs/toolkit'; -import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; - -type MapDrawEditActionsComponentProps = { - toggleMapEditAction: () => void; - isActive: boolean; -}; - -export const MapDrawEditActionsComponent = ({ - toggleMapEditAction, - isActive, -}: MapDrawEditActionsComponentProps): React.JSX.Element => { - const currentModelId = useAppSelector(mapModelIdSelector); - const activeLayer = useAppSelector(layersActiveLayerSelector); - const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); - const dispatch = useAppDispatch(); - const { mapInstance } = useMapInstance(); - - const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false); - - const editMapObject = (): void => { - dispatch(openLayerImageObjectEditFactoryModal()); - }; - - const removeImage = (): void => { - setIsRemoveModalOpen(true); - }; - - const updateZIndex = async (value: number): Promise<void> => { - if (!activeLayer || !layerImageObject) { - return; - } - const layerImage = await dispatch( - updateLayerImageObject({ - modelId: currentModelId, - layerId: activeLayer, - ...layerImageObject, - z: layerImageObject.z + value, - }), - ).unwrap(); - if (layerImage) { - dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage })); - dispatch(mapEditToolsSetLayerObject(layerImage)); - updateGlyph(mapInstance, activeLayer, layerImage); - } - }; - - const rejectRemove = (): void => { - setIsRemoveModalOpen(false); - }; - - const confirmRemove = async (): Promise<void> => { - if (!layerImageObject || !activeLayer) { - return; - } - try { - await dispatch( - removeLayerImage({ - modelId: currentModelId, - layerId: activeLayer, - imageId: layerImageObject.id, - }), - ).unwrap(); - dispatch( - layerDeleteImage({ - modelId: currentModelId, - layerId: activeLayer, - imageId: layerImageObject.id, - }), - ); - removeElementFromLayer({ mapInstance, layerId: activeLayer, featureId: layerImageObject.id }); - showToast({ - type: 'success', - message: 'The layer image has been successfully removed', - }); - setIsRemoveModalOpen(false); - } catch (error) { - const typedError = error as SerializedError; - showToast({ - type: 'error', - message: typedError.message || 'An error occurred while removing the layer image', - }); - } - }; - - return ( - <div className="flex flex-row-reverse gap-4"> - <QuestionModal - isOpen={isRemoveModalOpen} - onClose={rejectRemove} - onConfirm={confirmRemove} - question="Are you sure you want to remove the image?" - /> - <MapDrawActionsButton - isActive={isActive} - toggleMapEditAction={toggleMapEditAction} - icon="pencil" - title="Edit image" - /> - {layerImageObject && ( - <> - <MapDrawActionsButton - isActive={false} - toggleMapEditAction={() => editMapObject()} - icon="edit-image" - title="Edit image" - /> - <MapDrawActionsButton - isActive={false} - toggleMapEditAction={removeImage} - icon="trash" - title="Remove image" - /> - <MapDrawActionsButton - isActive={false} - toggleMapEditAction={() => updateZIndex(1)} - icon="arrow-double-up" - title="Remove image" - /> - <MapDrawActionsButton - isActive={false} - toggleMapEditAction={() => updateZIndex(-1)} - icon="arrow-double-down" - title="Remove image" - /> - </> - )} - </div> - ); -}; diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index f92c8762704f05531702e4fb3de0031c2eb45559..162d345c015bf0d698b35bb57413bdefc0ed61ea 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -2,13 +2,14 @@ import { Collection, Feature } from 'ol'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { - layersActiveLayerSelector, + layersActiveLayersSelector, + layersDrawLayerSelector, layersForCurrentModelSelector, layersLoadingSelector, layersVisibilityForCurrentModelSelector, @@ -22,7 +23,10 @@ import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.sel import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { LayerState } from '@/redux/layers/layers.types'; -import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { + mapEditToolsActiveActionSelector, + mapEditToolsLayerImageObjectSelector, +} from '@/redux/mapEditTools/mapEditTools.selectors'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import getDrawBoundingBoxInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawBoundingBoxInteraction'; import { @@ -30,8 +34,14 @@ import { openLayerTextFactoryModal, } from '@/redux/modal/modal.slice'; import { Extent } from 'ol/extent'; -import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { + mapEditToolsSetActiveAction, + mapEditToolsSetLayerObject, +} from '@/redux/mapEditTools/mapEditTools.slice'; import getTransformImageInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction'; +import { useWebSocketEntityUpdatesContext } from '@/utils/websocket-entity-updates/webSocketEntityUpdatesProvider'; +import processMessage from '@/components/Map/MapViewer/utils/websocket/processMessage'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, @@ -48,7 +58,10 @@ export const useOlMapAdditionalLayers = ( const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); const layersLoading = useAppSelector(layersLoadingSelector); const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); - const activeLayer = useAppSelector(layersActiveLayerSelector); + const activeLayers = useAppSelector(layersActiveLayersSelector); + const drawLayer = useAppSelector(layersDrawLayerSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); const [layersState, setLayersState] = useState<Array<LayerState>>([]); const [layersLoadingState, setLayersLoadingState] = useState(false); @@ -57,6 +70,16 @@ export const useOlMapAdditionalLayers = ( const arrowTypes = useSelector(arrowTypesSelector); const pointToProjection = usePointToProjection(); + const { lastJsonMessage } = useWebSocketEntityUpdatesContext(); + + useEffect(() => { + if (!lastJsonMessage || !('entityType' in lastJsonMessage)) { + return; + } + + processMessage({ jsonMessage: lastJsonMessage, mapInstance }); + }, [lastJsonMessage, mapInstance]); + const restrictionExtent: Extent = useMemo(() => { const restrictionMinPoint = pointToProjection({ x: 0, y: 0 }); const restrictionMaxPoint = pointToProjection({ x: mapSize.width, y: mapSize.height }); @@ -131,23 +154,27 @@ export const useOlMapAdditionalLayers = ( }, [layersForCurrentModel, layersLoading, layersLoadingState]); const transformInteraction = useMemo(() => { - if (!dispatch || !currentModelId || !activeLayer) { + if (!dispatch || !currentModelId || !activeLayers.length) { return null; } - let imagesFeatures: Collection<Feature<Geometry>> = new Collection(); - const vectorLayer = vectorLayers.find(layer => layer.get('id') === activeLayer); - if (vectorLayer) { - imagesFeatures = new Collection(vectorLayer.get('imagesFeatures')); - } + const imagesFeatures: Array<Feature<Geometry>> = []; + const activeVectorLayers = vectorLayers.filter(layer => activeLayers.includes(layer.get('id'))); + activeVectorLayers.forEach(vectorLayer => { + imagesFeatures.push(...vectorLayer.get('imagesFeatures')); + }); + const imagesFeaturesCollection = new Collection(imagesFeatures); return getTransformImageInteraction( dispatch, mapSize, currentModelId, - activeLayer, - imagesFeatures, + imagesFeaturesCollection, restrictionExtent, ); - }, [dispatch, mapSize, currentModelId, restrictionExtent, activeLayer, vectorLayers]); + }, [dispatch, mapSize, currentModelId, restrictionExtent, activeLayers, vectorLayers]); + const transformRef = useRef(transformInteraction); + useEffect(() => { + transformRef.current = transformInteraction; + }, [transformInteraction]); useEffect(() => { vectorLayers.forEach(layer => { @@ -159,32 +186,77 @@ export const useOlMapAdditionalLayers = ( }, [layersVisibilityForCurrentModel, vectorLayers]); useEffect(() => { - const activeVectorLayer = vectorLayers.find(layer => { + const selectedFeature = transformInteraction?.getFeatures().item(0); + if (!selectedFeature) { + return; + } + if (!layersVisibilityForCurrentModel[selectedFeature.get('layer')]) { + transformInteraction?.setSelection(new Collection<Feature>()); + } + }, [layersVisibilityForCurrentModel, transformInteraction]); + + // Selecting feature using the layers panel + useEffect(() => { + if (!transformRef.current) { + return; + } + const transformFeatures = transformRef.current.getFeatures(); + if ( + mapEditToolsLayerImageObject && + (!transformFeatures.getLength() || + transformFeatures.item(0).getId() !== mapEditToolsLayerImageObject.id) + ) { + const layer = vectorLayers.find(vectorLayer => { + const layerId = vectorLayer.get('id'); + return layerId === mapEditToolsLayerImageObject.layer; + }); + if (!layer) { + return; + } + const source = layer.getSource(); + if (!source) { + return; + } + const feature = source.getFeatureById(mapEditToolsLayerImageObject.id); + if (!feature) { + return; + } + transformRef.current.setSelection(new Collection<Feature>([feature])); + } + }, [mapEditToolsLayerImageObject, vectorLayers]); + + useEffect(() => { + const activeVectorLayers = vectorLayers.filter(layer => { const layerId = layer.get('id'); - return layerId === activeLayer; + return activeLayers.includes(layerId); }); - if (!activeVectorLayer) { + if (!activeVectorLayers.length) { return () => {}; } const removeFeatureHandler = (): void => { transformInteraction?.setSelection(new Collection<Feature>()); }; - const source = activeVectorLayer.getSource(); - source?.on('removefeature', removeFeatureHandler); + activeVectorLayers.forEach(activeVectorLayer => { + const source = activeVectorLayer.getSource(); + source?.on('removefeature', removeFeatureHandler); + }); return () => { - if (source) { - source.un('removefeature', removeFeatureHandler); - } + activeVectorLayers.forEach(activeVectorLayer => { + const source = activeVectorLayer.getSource(); + source?.un('removefeature', removeFeatureHandler); + }); }; - }, [activeLayer, layersVisibilityForCurrentModel, transformInteraction, vectorLayers]); + }, [activeLayers, layersVisibilityForCurrentModel, transformInteraction, vectorLayers]); useEffect(() => { - if (!transformInteraction) { - return () => {}; - } - if (!activeLayer || activeAction !== MAP_EDIT_ACTIONS.TRANSFORM_IMAGE) { + if ( + !transformInteraction || + !activeLayers.length || + !hasPrivilegeToWriteProject || + activeAction + ) { return () => {}; } mapInstance?.addInteraction(transformInteraction); @@ -192,29 +264,60 @@ export const useOlMapAdditionalLayers = ( dispatch(mapEditToolsSetLayerObject(null)); mapInstance?.removeInteraction(transformInteraction); }; - }, [activeAction, activeLayer, dispatch, mapInstance, transformInteraction]); + }, [ + activeAction, + activeLayers, + dispatch, + hasPrivilegeToWriteProject, + mapInstance, + transformInteraction, + ]); useEffect(() => { - if (!drawImageInteraction) { + if (!drawImageInteraction || !hasPrivilegeToWriteProject) { return; } mapInstance?.removeInteraction(drawImageInteraction); - if (!activeLayer || activeAction !== MAP_EDIT_ACTIONS.DRAW_IMAGE) { + if (!drawLayer || activeAction !== MAP_EDIT_ACTIONS.DRAW_IMAGE) { return; } mapInstance?.addInteraction(drawImageInteraction); - }, [activeAction, activeLayer, currentModelId, drawImageInteraction, mapInstance]); + }, [ + activeAction, + drawLayer, + currentModelId, + drawImageInteraction, + mapInstance, + hasPrivilegeToWriteProject, + ]); + + useEffect(() => { + const turnOffDrawInteraction = (): void => { + dispatch(mapEditToolsSetActiveAction(null)); + }; + mapInstance?.getViewport()?.addEventListener('contextmenu', turnOffDrawInteraction); + return () => { + mapInstance?.getViewport()?.removeEventListener('contextmenu', turnOffDrawInteraction); + }; + }, [dispatch, mapInstance]); useEffect(() => { - if (!addTextInteraction) { + if (!addTextInteraction || !hasPrivilegeToWriteProject) { return; } mapInstance?.removeInteraction(addTextInteraction); - if (!activeLayer || activeAction !== MAP_EDIT_ACTIONS.ADD_TEXT) { + if (!drawLayer || activeAction !== MAP_EDIT_ACTIONS.ADD_TEXT) { return; } mapInstance?.addInteraction(addTextInteraction); - }, [activeAction, activeLayer, currentModelId, addTextInteraction, mapInstance]); + }, [ + activeAction, + drawLayer, + currentModelId, + addTextInteraction, + mapInstance, + hasPrivilegeToWriteProject, + ]); return vectorLayers; }; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useBioEntitiesWithSubmapLinks.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useBioEntitiesWithSubmapLinks.test.ts index 9dc07fc46c5ef2f63828858a77aa687996c2fbc1..9e57146c7ab87d71bac1080267b844141ddafbe0 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useBioEntitiesWithSubmapLinks.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useBioEntitiesWithSubmapLinks.test.ts @@ -10,9 +10,10 @@ import { OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, } from '@/redux/overlayBioEntity/overlayBioEntity.mock'; import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; -import { MAIN_MAP_ID } from '@/constants/mocks'; -import { BIO_ENTITY_LINKING_TO_SUBMAP } from '@/redux/bioEntity/bioEntity.mock'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { + MODEL_ELEMENT_LINKING_TO_SUBMAP, + MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, +} from '@/redux/modelElements/modelElements.mock'; import { useBioEntitiesWithSubmapsLinks } from './useBioEntitiesWithSubmapLinks'; const RESULT_SUBMAP_LINKS_DIFFERENT_VALUES = [ @@ -184,12 +185,15 @@ describe('useBioEntitiesWithSubmapsLinks', () => { }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, modelElements: { - [MAIN_MAP_ID]: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 52: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, map: mapStateWithCurrentlySelectedMainMapFixture, }); @@ -214,12 +218,15 @@ describe('useBioEntitiesWithSubmapsLinks', () => { }, }, modelElements: { - 52: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 52: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, map: mapStateWithCurrentlySelectedMainMapFixture, @@ -262,12 +269,15 @@ describe('useBioEntitiesWithSubmapsLinks', () => { }, }, modelElements: { - 52: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 52: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, map: mapStateWithCurrentlySelectedMainMapFixture, @@ -298,12 +308,15 @@ describe('useBioEntitiesWithSubmapsLinks', () => { }, }, modelElements: { - 52: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 52: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, map: mapStateWithCurrentlySelectedMainMapFixture, @@ -346,12 +359,15 @@ describe('useBioEntitiesWithSubmapsLinks', () => { }, }, modelElements: { - 52: { - data: [BIO_ENTITY_LINKING_TO_SUBMAP], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 52: { + data: [MODEL_ELEMENT_LINKING_TO_SUBMAP], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, - } as ModelElementsState, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, + }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, map: mapStateWithCurrentlySelectedMainMapFixture, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts index 4103719aa1ac8767c06c9cce13cefbd4d65c035f..0514e367e2ad74f083346df932760ebceb0d269d 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts @@ -1,11 +1,11 @@ import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; -import { BioEntityWithPinType } from '@/types/bioEntity'; +import { ModelElementWithPinType } from '@/types/modelElement'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { getBioEntitySingleFeature } from './getBioEntitySingleFeature'; +import { getModelElementSingleFeature } from './getModelElementSingleFeature'; export const getBioEntitiesFeatures = ( - bioEntites: BioEntityWithPinType[], + bioEntites: ModelElementWithPinType[], { pointToProjection, entityNumber, @@ -17,7 +17,7 @@ export const getBioEntitiesFeatures = ( }, ): Feature[] => { return bioEntites.map(bioEntity => - getBioEntitySingleFeature(bioEntity, { + getModelElementSingleFeature(bioEntity, { pointToProjection, type: bioEntity.type, // pin's index number 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 ea28c2543c3ca8e368b66490cf2b872f4e656d64..dc7c44c51ff5c2010b62433d9aa53f06da8fed47 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts @@ -2,7 +2,7 @@ import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { initialMapStateFixture } from '@/redux/map/map.fixtures'; import { PinType } from '@/types/pin'; -import { UsePointToProjectionResult, usePointToProjection } from '@/utils/map/usePointToProjection'; +import { usePointToProjection, UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { GetReduxWrapperUsingSliceReducer, getReduxWrapperWithStore, @@ -26,10 +26,9 @@ describe('getBioEntitiesFeatures - subUtil', () => { const { Wrapper } = getReduxWrapperWithStore({ map: initialMapStateFixture, }); - const bioEntititesContent = bioEntitiesContentFixture; - const bioEntities = bioEntititesContent.map(({ bioEntity }) => ({ + const bioEntities = bioEntitiesContentFixture.map(({ bioEntity }) => ({ ...bioEntity, - type: 'bioEntity' as PinType, + type: 'modelElement' as PinType, })); const pointToProjection = getPointToProjection(Wrapper); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts index c4114ff55e165d946b93c9733355bc4a69bc0bb9..2313616d12732d821f2ad31c5304772c3a119f19 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts @@ -13,7 +13,7 @@ export const getMarkerSingleFeature = ( pointToProjection: UsePointToProjectionResult; }, ): Feature => { - const feature = getPinFeature(marker, pointToProjection, 'bioEntity'); + const feature = getPinFeature(marker, pointToProjection, 'modelElement'); const style = getPinStyle({ color: addAlphaToHexString(marker.color, marker.opacity), value: marker.number, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.test.ts similarity index 83% rename from src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.test.ts rename to src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.test.ts index 2455acf1750809a890cb70699aff608d84a21b80..23b9cdd9a6f5838fbe9b569b3b84188d74056e42 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.test.ts @@ -1,6 +1,5 @@ /* eslint-disable no-magic-numbers */ import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { initialMapStateFixture } from '@/redux/map/map.fixtures'; import { PinType } from '@/types/pin'; import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; @@ -12,7 +11,8 @@ import { import { renderHook } from '@testing-library/react'; import { Feature } from 'ol'; import Style from 'ol/style/Style'; -import { getBioEntitySingleFeature } from './getBioEntitySingleFeature'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { getModelElementSingleFeature } from './getModelElementSingleFeature'; import * as getPinStyle from './getPinStyle'; jest.mock('./getPinStyle', () => ({ @@ -30,18 +30,17 @@ const getPointToProjection = ( return usePointToProjectionHook.current; }; -describe('getBioEntitySingleFeature - subUtil', () => { +describe('getModelElementSingleFeature - subUtil', () => { const { Wrapper } = getReduxWrapperWithStore({ map: initialMapStateFixture, }); - const { bioEntity } = bioEntityContentFixture; const pointToProjection = getPointToProjection(Wrapper); const value = 1448; - const pinTypes: PinType[] = ['bioEntity', 'drugs', 'chemicals']; + const pinTypes: PinType[] = ['modelElement', 'drugs', 'chemicals']; it.each(pinTypes)('should return instance of Feature with Style type=%s', type => { - const result = getBioEntitySingleFeature(bioEntity, { + const result = getModelElementSingleFeature(modelElementFixture, { pointToProjection, type, value, @@ -57,7 +56,7 @@ describe('getBioEntitySingleFeature - subUtil', () => { it.each(pinTypes)('should run getPinStyle with valid args for type=%s', type => { const getPinStyleSpy = jest.spyOn(getPinStyle, 'getPinStyle'); - getBioEntitySingleFeature(bioEntity, { + getModelElementSingleFeature(modelElementFixture, { pointToProjection, type, value, @@ -76,7 +75,7 @@ describe('getBioEntitySingleFeature - subUtil', () => { type => { const getPinStyleSpy = jest.spyOn(getPinStyle, 'getPinStyle'); - getBioEntitySingleFeature(bioEntity, { + getModelElementSingleFeature(modelElementFixture, { pointToProjection, type, value, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.ts similarity index 83% rename from src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts rename to src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.ts index 4fd4447318e8f5e4ee9435f07c0b9fdf367b6f1c..18a6f33748e56529803991c30a989f13a5f47af2 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getModelElementSingleFeature.ts @@ -1,5 +1,5 @@ import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas'; -import { BioEntity } from '@/types/models'; +import { ModelElement } from '@/types/models'; import { PinType } from '@/types/pin'; import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; @@ -9,8 +9,8 @@ import { getPinStyle } from './getPinStyle'; const INACTIVE_ELEMENT_OPACITY = 0.5; -export const getBioEntitySingleFeature = ( - bioEntity: BioEntity, +export const getModelElementSingleFeature = ( + modelElement: ModelElement, { pointToProjection, type, @@ -31,7 +31,7 @@ export const getBioEntitySingleFeature = ( ? TEXT_COLOR : addAlphaToHexString(TEXT_COLOR, INACTIVE_ELEMENT_OPACITY); - const feature = getPinFeature(bioEntity, pointToProjection, type); + const feature = getPinFeature(modelElement, pointToProjection, type); const style = getPinStyle({ color, value, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts index 61726c4926a67f6e0e48a21d8367f82509123e9b..104839b6e0197bf60f74c475df4186b13f8e2764 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts @@ -1,6 +1,6 @@ import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas'; import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; -import { BioEntityWithPinType } from '@/types/bioEntity'; +import { ModelElementWithPinType } from '@/types/modelElement'; import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; import { mix } from 'polished'; import { GetCanvasIconArgs } from '../getCanvasIcon'; @@ -15,7 +15,7 @@ interface Options { } export const getMultipinCanvasArgs = ( - { type, ...element }: BioEntityWithPinType, + { type, ...element }: ModelElementWithPinType, { entityNumber, activeIds, isDarkColor }: Options, ): GetCanvasIconArgs => { const value = entityNumber?.[element.elementId]; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts index 10906ccf7ab8f950ad4ab1d773210424b1fec595..d4ffaacb98f019d20891b2b340a029a9ca4c2a21 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts @@ -2,7 +2,7 @@ import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; import { initialMapStateFixture } from '@/redux/map/map.fixtures'; -import { MultiPinBioEntity } from '@/types/bioEntity'; +import { MultiPinModelElement } from '@/types/modelElement'; import { PinType } from '@/types/pin'; import { UsePointToProjectionResult, usePointToProjection } from '@/utils/map/usePointToProjection'; import { @@ -20,10 +20,10 @@ jest.mock('./getMultipinStyle', () => ({ ...jest.requireActual('./getMultipinStyle'), })); -const ONE_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ +const ONE_MULTI_BIO_ENTITIES: MultiPinModelElement = [ { ...bioEntityContentFixture.bioEntity, - type: 'bioEntity' as PinType, + type: 'modelElement' as PinType, x: 100, y: 100, }, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts index d7280c47ee5ffcd745ab7f982e3347c788e47968..cc9e5a78bd5ac475af1ef5e2d357f74d583d9d64 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts @@ -1,6 +1,6 @@ import { ONE, ZERO } from '@/constants/common'; import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; -import { BioEntityWithPinType, MultiPinBioEntity } from '@/types/bioEntity'; +import { ModelElementWithPinType, MultiPinModelElement } from '@/types/modelElement'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; import { getMultipinCanvasArgs } from './getMultipinCanvasArgs'; @@ -8,7 +8,7 @@ import { getMultipinStyle } from './getMultipinStyle'; import { getPinFeature } from './getPinFeature'; export const getMultipinSingleFeature = ( - multipin: MultiPinBioEntity, + multipin: MultiPinModelElement, { pointToProjection, entityNumber, @@ -30,7 +30,7 @@ export const getMultipinSingleFeature = ( isDarkColor: true, }); - const canvasPinsArgs = sortedElements.map((element: BioEntityWithPinType) => + const canvasPinsArgs = sortedElements.map((element: ModelElementWithPinType) => getMultipinCanvasArgs(element, { activeIds, entityNumber: {}, // additional elements id's should be not visible diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts deleted file mode 100644 index 3c5778deb7d30cbad7c1f14e0d3f1f2c5b6185eb..0000000000000000000000000000000000000000 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { MultiPinBioEntity } from '@/types/bioEntity'; -import { PinType } from '@/types/pin'; -import { getMultipinsBioEntities } from './getMultipinsBioEntities'; - -const ZERO_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ - { - ...bioEntityContentFixture.bioEntity, - type: 'bioEntity' as PinType, - }, - { - ...bioEntityContentFixture.bioEntity, - x: 1000, - type: 'bioEntity' as PinType, - }, -]; - -const ONE_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ - { - ...bioEntityContentFixture.bioEntity, - type: 'bioEntity' as PinType, - x: 100, - y: 100, - }, - { - ...bioEntityContentFixture.bioEntity, - type: 'drugs' as PinType, - x: 100, - y: 100, - }, -]; - -const FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE: MultiPinBioEntity = [ - { - ...bioEntityContentFixture.bioEntity, - type: 'bioEntity' as PinType, - x: 100, - y: 100, - }, - { - ...bioEntityContentFixture.bioEntity, - type: 'drugs' as PinType, - x: 100, - y: 100, - }, - { - ...bioEntityContentFixture.bioEntity, - type: 'drugs' as PinType, - x: 100, - y: 100, - }, - { - ...bioEntityContentFixture.bioEntity, - type: 'drugs' as PinType, - x: 100, - y: 100, - }, -]; - -describe('getMultipinsBioEntities - util', () => { - it('should return empty array if theres no multi pins', () => { - expect(getMultipinsBioEntities({ bioEntities: ZERO_MULTI_BIO_ENTITIES })).toStrictEqual([]); - }); - - it('should return valid multi pins', () => { - expect(getMultipinsBioEntities({ bioEntities: ONE_MULTI_BIO_ENTITIES })).toStrictEqual([ - ONE_MULTI_BIO_ENTITIES, - ]); - }); - - it('should return valid multi pins if theres few types of pins', () => { - expect( - getMultipinsBioEntities({ bioEntities: FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE }), - ).toStrictEqual([ONE_MULTI_BIO_ENTITIES]); - }); -}); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts index ffe07c3a1c3a2189522a5246553a5e6e14af9d1c..385e0c956313efeebb5c4c5b2b52d6958f293bd4 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts @@ -1,4 +1,5 @@ -import { MultiPinBioEntity } from '@/types/bioEntity'; +import { MultiPinModelElement } from '@/types/modelElement'; -export const getMultipinBioEntititesIds = (multipins: MultiPinBioEntity[]): (string | number)[] => - multipins.flat().map(({ id }) => id); +export const getMultipinBioEntititesIds = ( + multipins: MultiPinModelElement[], +): (string | number)[] => multipins.flat().map(({ id }) => id); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts index 4ebcb067d4b7def348074cb366a97d4a656a1c72..01b26958d519cb1159546c6d135da1393cbe9815 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts @@ -1,11 +1,11 @@ import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; -import { MultiPinBioEntity } from '@/types/bioEntity'; +import { MultiPinModelElement } from '@/types/modelElement'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; import { getMultipinSingleFeature } from './getMultipinSingleFeature'; export const getMultipinFeatures = ( - multipins: MultiPinBioEntity[], + multipins: MultiPinModelElement[], { pointToProjection, entityNumber, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5287405fddf50aa38b2f68fe49064567e6b507b --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.test.ts @@ -0,0 +1,76 @@ +import { MultiPinModelElement } from '@/types/modelElement'; +import { PinType } from '@/types/pin'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { getMultipinsModelElements } from './getMultipinsModelElements'; + +const ZERO_MULTI_BIO_ENTITIES: MultiPinModelElement = [ + { + ...modelElementFixture, + type: 'modelElement' as PinType, + }, + { + ...modelElementFixture, + x: 1000, + type: 'modelElement' as PinType, + }, +]; + +const ONE_MULTI_BIO_ENTITIES: MultiPinModelElement = [ + { + ...modelElementFixture, + type: 'modelElement' as PinType, + x: 100, + y: 100, + }, + { + ...modelElementFixture, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, +]; + +const FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE: MultiPinModelElement = [ + { + ...modelElementFixture, + type: 'modelElement' as PinType, + x: 100, + y: 100, + }, + { + ...modelElementFixture, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, + { + ...modelElementFixture, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, + { + ...modelElementFixture, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, +]; + +describe('getMultipinsBioEntities - util', () => { + it('should return empty array if theres no multi pins', () => { + expect(getMultipinsModelElements({ modelElements: ZERO_MULTI_BIO_ENTITIES })).toStrictEqual([]); + }); + + it('should return valid multi pins', () => { + expect(getMultipinsModelElements({ modelElements: ONE_MULTI_BIO_ENTITIES })).toStrictEqual([ + ONE_MULTI_BIO_ENTITIES, + ]); + }); + + it('should return valid multi pins if theres few types of pins', () => { + expect( + getMultipinsModelElements({ modelElements: FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE }), + ).toStrictEqual([ONE_MULTI_BIO_ENTITIES]); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.ts similarity index 53% rename from src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts rename to src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.ts index 2ee7139cac0b9e48ae611fe3b0b66520fa79e2be..651bf6058a9c4f63d596d88635f5c0de84aa8448 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsModelElements.ts @@ -1,16 +1,16 @@ import { ONE } from '@/constants/common'; -import { BioEntityWithPinType, MultiPinBioEntity } from '@/types/bioEntity'; -import { BioEntity } from '@/types/models'; +import { ModelElementWithPinType, MultiPinModelElement } from '@/types/modelElement'; +import { ModelElement } from '@/types/models'; import { PinType } from '@/types/pin'; interface Args { - bioEntities: MultiPinBioEntity; + modelElements: MultiPinModelElement; } const SEPARATOR = '-'; const POSITION_PRESCISION_SEPERATOR = '.'; -const getUniqueKey = (element: Pick<BioEntity, 'x' | 'y'>): string => { +const getUniqueKey = (element: Pick<ModelElement, 'x' | 'y'>): string => { const [x] = `${element.x}`.split(POSITION_PRESCISION_SEPERATOR); const [y] = `${element.y}`.split(POSITION_PRESCISION_SEPERATOR); @@ -18,9 +18,9 @@ const getUniqueKey = (element: Pick<BioEntity, 'x' | 'y'>): string => { }; const groupByPosition = ( - accumulator: Record<string, MultiPinBioEntity>, - element: BioEntityWithPinType, -): Record<string, MultiPinBioEntity> => { + accumulator: Record<string, MultiPinModelElement>, + element: ModelElementWithPinType, +): Record<string, MultiPinModelElement> => { const key = getUniqueKey(element); return { @@ -29,25 +29,25 @@ const groupByPosition = ( }; }; -const toUniqueTypeMultipin = (multipin: MultiPinBioEntity): MultiPinBioEntity => { +const toUniqueTypeMultipin = (multipin: MultiPinModelElement): MultiPinModelElement => { const allTypes: PinType[] = multipin.map(pin => pin.type); const uniqueTypes = [...new Set(allTypes)]; return uniqueTypes .map(type => multipin.find(pin => pin.type === type)) - .filter((value): value is BioEntityWithPinType => value !== undefined); + .filter((value): value is ModelElementWithPinType => value !== undefined); }; -export const getMultipinsBioEntities = ({ bioEntities }: Args): MultiPinBioEntity[] => { - const multipiledBioEntities = bioEntities.filter( +export const getMultipinsModelElements = ({ modelElements }: Args): MultiPinModelElement[] => { + const multipiledModelElements = modelElements.filter( baseElement => - bioEntities.filter(element => getUniqueKey(baseElement) === getUniqueKey(element)).length > + modelElements.filter(element => getUniqueKey(baseElement) === getUniqueKey(element)).length > ONE, ); - const duplicatedMultipinsGroupedByPosition = multipiledBioEntities.reduce( + const duplicatedMultipinsGroupedByPosition = multipiledModelElements.reduce( groupByPosition, - {} as Record<string, MultiPinBioEntity>, + {} as Record<string, MultiPinModelElement>, ); const allGroupedMultipins = Object.values(duplicatedMultipinsGroupedByPosition); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts index c6ab67812279340c3fdb67a4f92f1b7c2ab7c7b3..fd52572485f4ae7df3049d0cee0a65692e6510d7 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts @@ -17,7 +17,7 @@ describe('getPinFeature - subUtil', () => { wrapper: Wrapper, }); const pointToProjection = usePointToProjectionHook.current; - const result = getPinFeature(bioEntity, pointToProjection, 'bioEntity'); + const result = getPinFeature(bioEntity, pointToProjection, 'modelElement'); it('should return instance of Feature', () => { expect(result).toBeInstanceOf(Feature); @@ -38,7 +38,7 @@ describe('getPinFeature - subUtil', () => { }); describe('when is Marker Pin', () => { - const pinMarkerResult = getPinFeature(PIN_MARKER, pointToProjection, 'bioEntity'); + const pinMarkerResult = getPinFeature(PIN_MARKER, pointToProjection, 'modelElement'); it('should return point parsed with point to projection', () => { const [x, y] = pinMarkerResult.getGeometry()?.getExtent() || []; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts index 5113acc7cb5976a82167d39750b3cd91409f1d9e..ae2e8d05f2856e2923a2752736c135be82a6de5b 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts @@ -1,7 +1,7 @@ import { ZERO } from '@/constants/common'; import { HALF } from '@/constants/dividers'; import { FEATURE_TYPE } from '@/constants/features'; -import { BioEntity, MarkerWithPosition } from '@/types/models'; +import { ModelElement, MarkerWithPosition } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import isUUID from 'is-uuid'; import { Feature } from 'ol'; @@ -15,7 +15,7 @@ export const getPinFeature = ( width, height, id, - }: Pick<BioEntity, 'id' | 'width' | 'height' | 'x' | 'y'> | MarkerWithPosition, + }: Pick<ModelElement, 'id' | 'width' | 'height' | 'x' | 'y'> | MarkerWithPosition, pointToProjection: UsePointToProjectionResult, pinType: PinType, ): Feature => { diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index c2e6d9082c948893a49999ac38869032597c129c..f7b6001c8936ad65c06f65dff888fb822c781831 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -5,7 +5,7 @@ import { } from '@/redux/bioEntity/bioEntity.selectors'; import { entityNumberDataSelector } from '@/redux/entityNumber/entityNumber.selectors'; import { markersPinsOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; -import { BioEntity } from '@/types/models'; +import { ModelElement } from '@/types/models'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import Feature from 'ol/Feature'; import { Geometry } from 'ol/geom'; @@ -16,26 +16,26 @@ import { useSelector } from 'react-redux'; import { LAYER_TYPE } from '@/components/Map/MapViewer/MapViewer.constants'; import { getBioEntitiesFeatures } from './getBioEntitiesFeatures'; import { getMarkersFeatures } from './getMarkersFeatures'; -import { getMultipinsBioEntities } from './getMultipinsBioEntities'; +import { getMultipinsModelElements } from './getMultipinsModelElements'; import { getMultipinBioEntititesIds } from './getMultipinsBioEntitiesIds'; import { getMultipinFeatures } from './getMultipinsFeatures'; export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { const pointToProjection = usePointToProjection(); const activeIds = useSelector(allVisibleBioEntitiesIdsSelector); - const bioEntities = useSelector(allBioEntitiesWithTypeOfCurrentMapSelector); + const modelElements = useSelector(allBioEntitiesWithTypeOfCurrentMapSelector); const markersEntities = useSelector(markersPinsOfCurrentMapDataSelector); const entityNumber = useSelector(entityNumberDataSelector); const multiPinsBioEntities = useMemo( () => - getMultipinsBioEntities({ - bioEntities, + getMultipinsModelElements({ + modelElements, }), - [bioEntities], + [modelElements], ); const multipinsIds = getMultipinBioEntititesIds(multiPinsBioEntities); const isMultiPin = useCallback( - (b: BioEntity): boolean => multipinsIds.includes(b.id), + (b: ModelElement): boolean => multipinsIds.includes(b.id), [multipinsIds], ); @@ -43,7 +43,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> () => [ getBioEntitiesFeatures( - bioEntities.filter(b => !isMultiPin(b)), + modelElements.filter(b => !isMultiPin(b)), { pointToProjection, entityNumber, @@ -58,7 +58,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> getMarkersFeatures(markersEntities, { pointToProjection }), ].flat(), [ - bioEntities, + modelElements, pointToProjection, entityNumber, activeIds, diff --git a/src/components/Map/MapViewer/utils/config/processLayer/processModelElements.ts b/src/components/Map/MapViewer/utils/config/processLayer/processModelElements.ts index b4c0b8767ff7b4c6fc788e7af954dc3cdc359fe2..e558ff3767c1e40022ad8024d74a2b4cc94bfaf7 100644 --- a/src/components/Map/MapViewer/utils/config/processLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/utils/config/processLayer/processModelElements.ts @@ -56,6 +56,7 @@ export default function processModelElements( sboTerm: element.sboTerm, complexId: element.complex, compartmentId: element.compartment, + pathwayId: element.pathway, x: element.x, y: element.y, nameX: element.nameX, @@ -98,6 +99,7 @@ export default function processModelElements( id: element.id, complexId: element.complex, compartmentId: element.compartment, + pathwayId: element.pathway, sboTerm: element.sboTerm, shapes: elementShapes, x: element.x, diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts index 4b52f2cdfaa0afb63eee908a751ac43ce7b89d65..542934c95ec4fb47304cb23561211ba01e14ecd2 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts @@ -29,6 +29,10 @@ Object.defineProperty(useRefValue, 'current', { jest.spyOn(React, 'useRef').mockReturnValue(useRefValue); +jest.mock('./additionalLayers/useOlMapAdditionalLayers', () => ({ + useOlMapAdditionalLayers: jest.fn(() => []), +})); + describe('useOlMapLayers - util', () => { const getRenderedHookResults = (): BaseLayer[] => { const { Wrapper } = getReduxWrapperWithStore({ diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts index 0a786468aa112aa2f982c4d1146ca55f744ffa96..8abb9f605422b85861ac760a6ecc8ce762dca494 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts @@ -31,6 +31,10 @@ Object.defineProperty(useRefValue, 'current', { jest.spyOn(React, 'useRef').mockReturnValue(useRefValue); +jest.mock('./additionalLayers/useOlMapAdditionalLayers', () => ({ + useOlMapAdditionalLayers: jest.fn(() => []), +})); + describe('useOlMapView - util', () => { it('should modify view of the map instance on INITIAL position config change', async () => { const { Wrapper, store } = getReduxWrapperWithStore({ diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleOpenImmediateLink.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleOpenImmediateLink.ts index 360df8c0cb43c8e90ea7ed3c390fbf7a12601c02..503b0f6953b60d713fc7473269e458de6eac081a 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleOpenImmediateLink.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleOpenImmediateLink.ts @@ -1,7 +1,7 @@ -import { BioEntity } from '@/types/models'; import { showToast } from '@/utils/showToast'; +import { ModelElement } from '@/types/models'; -export const handleOpenImmediateLink = (bioEntity: BioEntity): void => { +export const handleOpenImmediateLink = (bioEntity: ModelElement): void => { const link = bioEntity.immediateLink; if (link !== null) { const tab = window.open(link, '_blank'); diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/clickHandleReaction.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/clickHandleReaction.ts index 2cd86bfae5d72887a88c33e33e69252a4b05d0f2..9cca913507e20fc0a54dd35adff6ce189be325f4 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/clickHandleReaction.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/clickHandleReaction.ts @@ -2,21 +2,20 @@ import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { BioEntity, ModelElement, NewReaction } from '@/types/models'; +import { ModelElement, NewReaction } from '@/types/models'; import { FEATURE_TYPE } from '@/constants/features'; -import { setMultipleBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; import { setReactions } from '@/redux/reactions/reactions.slice'; -import { mapReactionToBioEntity } from '@/utils/bioEntity/mapReactionToBioEntity'; +import { mapReactionToModelElement } from '@/utils/bioEntity/mapReactionToModelElement'; import getModelElementsIdsFromReaction from '@/components/Map/MapViewer/utils/listeners/mouseClick/getModelElementsIdsFromReaction'; -import { mapModelElementToBioEntity } from '@/utils/bioEntity/mapModelElementToBioEntity'; +import { setMultipleModelElementSearch } from '@/redux/modelElements/modelElements.slice'; /* prettier-ignore */ export const clickHandleReaction = (dispatch: AppDispatch, hasFitBounds = false) => ( modelElements: Array<ModelElement>, reactions: Array<NewReaction>, reactionId: number, modelId: number): void => { - const reactionBioEntities: Array<BioEntity> = []; + const reactionModelElements: Array<ModelElement> = []; const reaction = reactions.find(newReaction => newReaction.id === reactionId); if(!reaction) { return; @@ -29,20 +28,20 @@ export const clickHandleReaction = if(!modelElement) { return; } - reactionBioEntities.push(mapModelElementToBioEntity(modelElement)); + reactionModelElements.push(modelElement); }); dispatch(openReactionDrawerById(reactionId)); dispatch(selectTab('')); - const bioEntityReaction = mapReactionToBioEntity(reaction); - dispatch(setMultipleBioEntityContents(reactionBioEntities)); - dispatch(addNumbersToEntityNumberData(reactionBioEntities.map(reactionBioEntity => reactionBioEntity.elementId))); + const reactionModelElement = mapReactionToModelElement(reaction); + dispatch(setMultipleModelElementSearch(reactionModelElements)); + dispatch(addNumbersToEntityNumberData(reactionModelElements.map(modelElement => modelElement.elementId))); dispatch(setReactions([reaction])); - const result = reactionBioEntities.map((bioEntity) => {return { bioEntity, perfect: true };}); - result.push({ bioEntity: bioEntityReaction, perfect: true }); + const result = reactionModelElements.map((modelElement) => {return { modelElement, perfect: true };}); + result.push({ modelElement: reactionModelElement, perfect: true }); PluginsEventBus.dispatchEvent('onSearch', { type: 'reaction', searchValues: [{ id: reactionId, modelId, type: FEATURE_TYPE.REACTION }], diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts index 17b253ef568f7cb05c66bcc5f2bd0f56fa3b5652..a27080bee6400739dfde7cab90fc53842f17a24b 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts @@ -37,9 +37,9 @@ describe('leftClickHandleAlias', () => { type: 'bioEntity', searchValues: [{ id: 1, modelId, type: FEATURE_TYPE.ALIAS }], results: [ - mockBioEntities.map(bioEntity => ({ + mockBioEntities.map(modelElement => ({ perfect: true, - bioEntity, + bioEntity: modelElement, })), ], }); diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts index 50d3f5b62a88953a4690469bcab8f26d245e863c..2ce079a2ccd381cda0f71b7a760325879f679d75 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts @@ -5,8 +5,8 @@ import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; import { FEATURE_TYPE } from '@/constants/features'; import { ModelElement } from '@/types/models'; -import { setMultipleBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; +import { setModelElementSearch } from '@/redux/modelElements/modelElements.slice'; import { handleOpenImmediateLink } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleOpenImmediateLink'; /* prettier-ignore */ @@ -26,7 +26,7 @@ export const leftClickHandleAlias = dispatch(selectTab(`${id}`)); dispatch(openBioEntityDrawerById(id)); - dispatch(setMultipleBioEntityContents([modelElement])); + dispatch(setModelElementSearch({ modelElement, perfect: true })); dispatch(addNumbersToEntityNumberData([modelElement.elementId])); const searchValue = { id, modelId, type: FEATURE_TYPE.ALIAS }; diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts index 6cf59664d96a7a3def32e75f5980884d824c5ede..a47c88a2ee5b4af248049faaa3034cd26dc039e8 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts @@ -2,7 +2,6 @@ import { updateLastClick } from '@/redux/map/map.slice'; import { closeDrawer } from '@/redux/drawer/drawer.slice'; import { resetReactionsData } from '@/redux/reactions/reactions.slice'; -import { clearBioEntities } from '@/redux/bioEntity/bioEntity.slice'; import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/handleFeaturesClick'; import Map from 'ol/Map'; import { onMapLeftClick } from '@/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick'; @@ -12,6 +11,7 @@ import { Feature } from 'ol'; import { FEATURE_TYPE } from '@/constants/features'; import VectorLayer from 'ol/layer/Vector'; import { LAYER_TYPE } from '@/components/Map/MapViewer/MapViewer.constants'; +import { clearSearchModelElements } from '@/redux/modelElements/modelElements.slice'; import * as leftClickHandleAlias from './leftClickHandleAlias'; import * as clickHandleReaction from '../clickHandleReaction'; @@ -69,7 +69,7 @@ describe('onMapLeftClick', () => { expect(dispatch).toHaveBeenCalledWith(updateLastClick(expect.any(Object))); expect(dispatch).toHaveBeenCalledWith(closeDrawer()); expect(dispatch).toHaveBeenCalledWith(resetReactionsData()); - expect(dispatch).toHaveBeenCalledWith(clearBioEntities()); + expect(dispatch).toHaveBeenCalledWith(clearSearchModelElements()); }); it('calls leftClickHandleAlias if feature type is ALIAS', async () => { diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts index c62b97bfd7e647b5f2b6b2ea020f5bf6f82f02b4..3502d52eb7566de2209df25e516bf6a9d9a4d470 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts @@ -7,7 +7,6 @@ import { updateLastClick } from '@/redux/map/map.slice'; import { toLonLat } from 'ol/proj'; import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { closeDrawer } from '@/redux/drawer/drawer.slice'; -import { clearBioEntities } from '@/redux/bioEntity/bioEntity.slice'; import { leftClickHandleAlias } from '@/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias'; import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mouseClick/mouseLeftClick/handleFeaturesClick'; import { resetReactionsData } from '@/redux/reactions/reactions.slice'; @@ -15,6 +14,7 @@ import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mous import { FEATURE_TYPE } from '@/constants/features'; import { clickHandleReaction } from '@/components/Map/MapViewer/utils/listeners/mouseClick/clickHandleReaction'; import getFeatureAtCoordinate from '@/components/Map/MapViewer/utils/listeners/mouseClick/getFeatureAtCoordinate'; +import { clearSearchModelElements } from '@/redux/modelElements/modelElements.slice'; /* prettier-ignore */ export const onMapLeftClick = @@ -52,7 +52,7 @@ export const onMapLeftClick = } dispatch(resetReactionsData()); - dispatch(clearBioEntities()); + dispatch(clearSearchModelElements()); return; } diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts index ae53b95c6010adaab6744b7284e8be76ae7e2932..9aa9dea6ba85480fff6862e104705e10f7871cb7 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts @@ -1,9 +1,9 @@ /* eslint-disable no-magic-numbers */ import { rightClickHandleAlias } from '@/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias'; import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; -import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice'; +import { setModelElementSearch } from '@/redux/modelElements/modelElements.slice'; jest.mock('../../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); @@ -19,7 +19,7 @@ describe('rightClickHandleAlias', () => { await rightClickHandleAlias(dispatch)(modelElementFixture.id, modelElementFixture); expect(dispatch).toHaveBeenCalledTimes(3); - expect(dispatch).toHaveBeenCalledWith(setBioEntityContents(expect.any(Object))); + expect(dispatch).toHaveBeenCalledWith(setModelElementSearch(expect.any(Object))); expect(dispatch).toHaveBeenCalledWith( addNumbersToEntityNumberData([modelElementFixture.elementId]), ); diff --git a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts index 4968eea674bcc5190f28f3eb1ae4c16ff7209645..05acb8af5b6f992bb65085c75d9315eaa5516826 100644 --- a/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts +++ b/src/components/Map/MapViewer/utils/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts @@ -1,16 +1,14 @@ import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice'; import { AppDispatch } from '@/redux/store'; import { ModelElement } from '@/types/models'; -import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; -import { mapModelElementToBioEntity } from '@/utils/bioEntity/mapModelElementToBioEntity'; +import { setModelElementSearch } from '@/redux/modelElements/modelElements.slice'; /* prettier-ignore */ export const rightClickHandleAlias = (dispatch: AppDispatch) => async (id: number, modelElement: ModelElement): Promise<void> => { - const bioEntity = mapModelElementToBioEntity(modelElement); - dispatch(setBioEntityContents({ bioEntity, perfect: true })); - dispatch(addNumbersToEntityNumberData([bioEntity.elementId])); + dispatch(setModelElementSearch({ modelElement, perfect: true })); + dispatch(addNumbersToEntityNumberData([modelElement.elementId])); dispatch(setCurrentSelectedBioEntityId(id)); }; diff --git a/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.test.ts b/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.test.ts index 9f65d1d9ff48ba94d13497f10a2c88902db1ea64..5f69920e411c9ee86e3793aa7a6e448c846a0f76 100644 --- a/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.test.ts @@ -1,9 +1,10 @@ import { ZERO } from '@/constants/common'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { initialStateFixture } from '@/redux/drawer/drawerFixture'; import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { renderHook } from '@testing-library/react'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { DEFAULT_ERROR } from '@/constants/errors'; import { useHandlePinIconClick } from './useHandlePinIconClick'; describe('useHandlePinIconClick - util', () => { @@ -12,27 +13,36 @@ describe('useHandlePinIconClick - util', () => { const pinId = 123; const { Wrapper, store } = getReduxStoreWithActionsListener({ ...INITIAL_STORE_STATE_MOCK, - bioEntity: { - data: [ - { - searchQueryElement: '', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: null, - id: pinId, - model: ZERO, - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: null, + id: pinId, + model: ZERO, + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, }); @@ -64,27 +74,36 @@ describe('useHandlePinIconClick - util', () => { selectedSearchElement: 'search-tab', }, }, - bioEntity: { - data: [ - { - searchQueryElement: 'search-tab', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: null, - id: pinId, - model: ZERO, - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: 'search-tab', + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: null, + id: pinId, + model: ZERO, + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, }); @@ -104,7 +123,7 @@ describe('useHandlePinIconClick - util', () => { }); describe('when pin is marker', () => { - const pinId = 'af57c6ce-8b83-47e4-a7eb-9511634c7c5f'; + const pinId = 1; const { Wrapper, store } = getReduxStoreWithActionsListener({ ...INITIAL_STORE_STATE_MOCK, drawer: { @@ -116,27 +135,36 @@ describe('useHandlePinIconClick - util', () => { selectedSearchElement: 'search-tab', }, }, - bioEntity: { - data: [ - { - searchQueryElement: 'search-tab', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: null, - id: pinId, - model: ZERO, - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: 'search-tab', + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: null, + id: pinId, + model: ZERO, + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, }); @@ -168,27 +196,36 @@ describe('useHandlePinIconClick - util', () => { selectedSearchElement: 'search-tab-2', }, }, - bioEntity: { - data: [ - { - searchQueryElement: 'search-tab', + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - fullName: null, - id: pinId, - model: ZERO, - }, - }, - ], + error: { message: '', name: '' }, }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, + }, + search: { + data: [ + { + searchQueryElement: 'search-tab', + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { + ...modelElementFixture, + fullName: null, + id: pinId, + model: ZERO, + }, + perfect: true, + }, + ], + }, + ], + loading: 'pending', + error: DEFAULT_ERROR, + }, }, }); diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index f2952ea6c15a3295ab906ba2bd6b2a8a7a1ef65a..d231ca5b1c74e872a5a7f67cf976b1b2d367cfea 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -1,11 +1,11 @@ import { OPTIONS } from '@/constants/map'; -import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors'; +import { openedDrawerSelector, resultDrawerOpen } from '@/redux/drawer/drawer.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { MapInstance } from '@/types/map'; import { unByKey } from 'ol/Observable'; -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useDebouncedCallback } from 'use-debounce'; import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; @@ -19,6 +19,8 @@ import { useHandlePinIconClick } from '@/components/Map/MapViewer/utils/listener import { onMapPositionChange } from '@/components/Map/MapViewer/utils/listeners/onMapPositionChange'; import { onPointerMove } from '@/components/Map/MapViewer/utils/listeners/onPointerMove'; import { View } from 'ol'; +import { isMapEditToolsActiveSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; interface UseOlMapListenersInput { view: View; @@ -31,6 +33,12 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) const isResultDrawerOpen = useSelector(resultDrawerOpen); const modelElementsForCurrentModel = useSelector(modelElementsForCurrentModelSelector); const newReactionsForCurrentModel = useSelector(newReactionsForCurrentModelSelector); + const isMapEditToolsActive = useSelector(isMapEditToolsActiveSelector); + const openedDrawer = useAppSelector(openedDrawerSelector); + const isLayersDrawerOpen = useMemo(() => { + return openedDrawer === 'layers'; + }, [openedDrawer]); + const dispatch = useAppDispatch(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -90,7 +98,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) }, [mapInstance]); useEffect(() => { - if (!mapInstance) { + if (!mapInstance || isMapEditToolsActive || isLayersDrawerOpen) { return; } @@ -100,10 +108,10 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) // eslint-disable-next-line consistent-return return () => unByKey(key); - }, [mapInstance, handleMapLeftClick]); + }, [mapInstance, handleMapLeftClick, isMapEditToolsActive, isLayersDrawerOpen]); useEffect(() => { - if (!mapInstance) { + if (!mapInstance || isMapEditToolsActive || isLayersDrawerOpen) { return; } @@ -123,5 +131,5 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) // eslint-disable-next-line consistent-return return () => mapInstance.getViewport().removeEventListener('contextmenu', rightClickEvent); - }, [mapInstance, handleRightClick]); + }, [mapInstance, handleRightClick, isMapEditToolsActive, isLayersDrawerOpen]); }; diff --git a/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts index 59af80b8ae93f7de39c2c1559c7c5a13eec1297d..309eba9fdccd3426280262f31b2b9857a5eb2d73 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts @@ -30,6 +30,7 @@ export interface BaseMapElementProps { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; x: number; y: number; width: number; @@ -65,6 +66,8 @@ export default abstract class BaseMultiPolygon { compartmentId: number | null; + pathwayId: number | null; + x: number; y: number; @@ -134,6 +137,7 @@ export default abstract class BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, x, y, width, @@ -162,6 +166,7 @@ export default abstract class BaseMultiPolygon { this.id = id; this.complexId = complexId; this.compartmentId = compartmentId; + this.pathwayId = pathwayId; this.x = x; this.y = y; this.width = width; @@ -251,6 +256,7 @@ export default abstract class BaseMultiPolygon { id: this.id, complexId: this.complexId, compartmentId: this.compartmentId, + pathwayId: this.pathwayId, type: this.type, }); this.feature.setId(this.id); @@ -338,7 +344,8 @@ export default abstract class BaseMultiPolygon { feature, resolution, sboTerm: this.sboTerm, - compartmentId: this.compartmentId, + compartmentId: feature.get('compartmentId'), + pathwayId: this.pathwayId, complexId: this.complexId, }); const { cover } = semanticViewData; diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/utils/shapes/elements/Compartment.ts index 76770a4bec63953d3f36ed140145f2e777b28398..1e936900289fd78b560d9fb75e52c599ed3b7934 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/Compartment.ts @@ -21,6 +21,7 @@ export interface CompartmentProps { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; sboTerm: string; x: number; y: number; @@ -66,6 +67,7 @@ export default abstract class Compartment extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, @@ -98,6 +100,7 @@ export default abstract class Compartment extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.test.ts index 1d5f41d6dedb945ea9299475fb4c18a4bf647f76..130e5cad80ac682d3337fc335cbce551e933ecfa 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.test.ts @@ -43,6 +43,7 @@ describe('CompartmentCircle', () => { id: 1, complexId: null, compartmentId: null, + pathwayId: null, sboTerm: 'SBO:0000253', x: 0, y: 0, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.ts index 0de0037bb5e440153f67297d22f8c82fc110233d..8684f10eff4220d10bc09faee4da14fff3c43b8b 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentCircle.ts @@ -18,6 +18,7 @@ export type CompartmentCircleProps = { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; sboTerm: string; x: number; y: number; @@ -51,6 +52,7 @@ export default class CompartmentCircle extends Compartment { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, @@ -82,6 +84,7 @@ export default class CompartmentCircle extends Compartment { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.test.ts index 0b0da3b41ac5474a087c52112217ec9fc6deacc6..dc369e18a378d89ae18079e7e46c87252fe2a25c 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.test.ts @@ -43,6 +43,7 @@ describe('CompartmentPathway', () => { id: 1, complexId: null, compartmentId: null, + pathwayId: null, sboTerm: 'SBO:0000253', x: 0, y: 0, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts index bf6b269e8dc4b8ff4c75d4dd79bd51a3615256dd..5764212db2cf8fbadd7856af211383709ed38b0a 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts @@ -23,6 +23,7 @@ export type CompartmentPathwayProps = { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; sboTerm: string; x: number; y: number; @@ -58,6 +59,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, @@ -88,6 +90,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.test.ts index ebfdcd7a5feb18c639be73daa83b363bb69287dc..48befabd15d690ef5b1adb03d61820ca9fef88ff 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.test.ts @@ -41,6 +41,7 @@ describe('CompartmentSquare', () => { id: 1, complexId: null, compartmentId: null, + pathwayId: null, sboTerm: 'SBO:0000253', x: 0, y: 0, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.ts index 32bb5a24c5c7e540a0cfe7feac4a103a3bf7d9f2..a9939228b85b2737e2991faae4d15970840ea02e 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentSquare.ts @@ -17,6 +17,7 @@ export type CompartmentSquareProps = { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; sboTerm: string; x: number; y: number; @@ -50,6 +51,7 @@ export default class CompartmentSquare extends Compartment { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, @@ -81,6 +83,7 @@ export default class CompartmentSquare extends Compartment { id, complexId, compartmentId, + pathwayId, sboTerm, x, y, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts index 1da2b4fb996db0e2387e7ec7455febce1f067d70..0dca36a15704c5ca55e6f188fe4d7ec0adcee5e9 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts @@ -153,7 +153,6 @@ export default class Glyph { }); this.feature.set('setCoordinates', this.setCoordinates.bind(this)); - this.feature.set('getGlyphData', this.getGlyphData.bind(this)); this.feature.set('refreshPolygon', this.refreshPolygon.bind(this)); this.feature.set('update', this.update.bind(this)); this.feature.setId(this.elementId); @@ -254,18 +253,6 @@ export default class Glyph { img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`; } - private getGlyphData(): LayerImage { - return { - id: this.elementId, - x: this.x, - y: this.y, - width: this.width, - height: this.height, - glyph: this.glyphId, - z: this.zIndex, - }; - } - protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { const scale = this.minResolution / resolution; const getAnchorAndCoords = feature.get('getAnchorAndCoords'); diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff893bdf860f74ce2a2d0c69e9aa6fce96400419 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts @@ -0,0 +1,66 @@ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { MapInstance } from '@/types/map'; +import { MapSize } from '@/redux/map/map.types'; +import { LayerImage as LayerImageModel } from '@/types/models'; +import Glyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph'; + +export type LayerImageProps = { + elementId: number; + glyphId: number | null; + x: number; + y: number; + width: number; + height: number; + layer: number; + zIndex: number; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; + mapSize: MapSize; +}; + +export default class LayerImage extends Glyph { + layer: number; + + constructor({ + elementId, + glyphId, + x, + y, + width, + height, + layer, + zIndex, + pointToProjection, + mapInstance, + mapSize, + }: LayerImageProps) { + super({ + elementId, + glyphId, + x, + y, + width, + height, + zIndex, + pointToProjection, + mapInstance, + mapSize, + }); + this.layer = layer; + this.feature.set('getGlyphData', this.getGlyphData.bind(this)); + this.feature.set('layer', layer); + } + + private getGlyphData(): LayerImageModel { + return { + id: this.elementId, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + glyph: this.glyphId, + z: this.zIndex, + layer: this.layer, + }; + } +} diff --git a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.test.ts index 2137147cf731eed3b05fd99db72228ef33d4ca69..b16cd965bc7078a2d8cb331217c1855c60bfbf11 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.test.ts @@ -42,6 +42,7 @@ describe('MapElement', () => { id: 1, complexId: null, compartmentId: null, + pathwayId: null, sboTerm: 'SBO:2313123', shapes: shapesFixture, x: 0, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts index 8f5f66d34a833f83b87e295864ebf2f6b9031ea8..92d65a91d8250fa7f3838ae93f4dee434b471729 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts @@ -34,6 +34,7 @@ export type MapElementProps = { id: number; complexId?: number | null; compartmentId: number | null; + pathwayId: number | null; sboTerm: string; shapes: Array<Shape>; x: number; @@ -99,6 +100,7 @@ export default class MapElement extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, sboTerm, shapes, x, @@ -140,6 +142,7 @@ export default class MapElement extends BaseMultiPolygon { id, complexId, compartmentId, + pathwayId, x, y, width, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.test.ts b/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.test.ts index c2d9e620468b27f4a5bc7ffea4fc9a8b051af1a4..aa46b7773a585024d4e337d12364ca44792ae346 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.test.ts @@ -58,6 +58,7 @@ describe('handleSemanticView', () => { resolution: 1, sboTerm: 'SBO:123456', compartmentId: 2, + pathwayId: null, }); expect(result).toEqual({ @@ -83,6 +84,7 @@ describe('handleSemanticView', () => { resolution: 1, sboTerm: 'SBO:123456', compartmentId: null, + pathwayId: null, complexId: 1, }); @@ -105,6 +107,29 @@ describe('handleSemanticView', () => { resolution: 1, sboTerm: 'SBO:123456', compartmentId: 2, + pathwayId: null, + }); + + expect(result).toEqual({ + cover: true, + hide: true, + largestExtent: [0, 0, 10, 5], + }); + }); + + it('should return hide = true when pathwayId points to a filled feature', () => { + const compartmentFeature = new Feature({ filled: true }); + jest + .spyOn(vectorSource, 'getFeatureById') + .mockImplementation(id => (id === 2 ? compartmentFeature : null)); + + const result = handleSemanticView({ + vectorSource, + feature, + resolution: 1, + sboTerm: 'SBO:123456', + compartmentId: null, + pathwayId: 2, }); expect(result).toEqual({ diff --git a/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.ts b/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.ts index cd5aa1f44093b3237e6d2743225bf06ac86009fc..20c6efbb47be2c7798f42206e95434dd057c5e1c 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/handleSemanticView.ts @@ -1,7 +1,7 @@ /* eslint-disable no-magic-numbers */ import Feature from 'ol/Feature'; import VectorSource from 'ol/source/Vector'; -import { Extent } from 'ol/extent'; +import { containsExtent, Extent, getHeight, getWidth } from 'ol/extent'; import { COMPLEX_SBO_TERMS, MAP_ELEMENT_TYPES, @@ -9,6 +9,7 @@ import { import findLargestExtent from '@/components/Map/MapViewer/utils/shapes/coords/findLargestExtent'; import getDividedExtents from '@/components/Map/MapViewer/utils/shapes/coords/getDividedExtents'; import isFeatureInCompartment from '@/components/Map/MapViewer/utils/shapes/elements/isFeatureInCompartment'; +import isFeatureInPathway from '@/components/Map/MapViewer/utils/shapes/elements/isFeatureInPathway'; export default function handleSemanticView({ vectorSource, @@ -16,6 +17,7 @@ export default function handleSemanticView({ resolution, sboTerm, compartmentId, + pathwayId, complexId, }: { vectorSource: VectorSource; @@ -23,6 +25,7 @@ export default function handleSemanticView({ resolution: number; sboTerm: string; compartmentId: number | null; + pathwayId: number | null; complexId?: number | null; }): { cover: boolean; hide: boolean; largestExtent: Extent | null } { const featureId = feature.getId(); @@ -35,12 +38,15 @@ export default function handleSemanticView({ let cover = false; let hide = false; let largestExtent: Extent | null = null; + let minimalCompartmentExtent = Infinity; + let minimalCompartmentId = null; if ( getMapExtent instanceof Function && (type === MAP_ELEMENT_TYPES.COMPARTMENT || COMPLEX_SBO_TERMS.includes(sboTerm)) ) { const mapExtent = getMapExtent(resolution); const featureExtent = feature.getGeometry()?.getExtent(); + if (featureExtent && mapExtent) { const mapArea = Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); const compartmentArea = @@ -51,13 +57,32 @@ export default function handleSemanticView({ cover = true; let remainingExtents = [featureExtent]; vectorSource.forEachFeatureIntersectingExtent(featureExtent, intersectingFeature => { + const intersectingFeatureType = intersectingFeature.get('type'); + const intersectingFeatureExtent = intersectingFeature.getGeometry()?.getExtent(); + if ( - intersectingFeature.get('type') === MAP_ELEMENT_TYPES.COMPARTMENT && + featureId !== intersectingFeature.getId() && + !compartmentId && + intersectingFeatureType === MAP_ELEMENT_TYPES.COMPARTMENT && + intersectingFeatureExtent && + containsExtent(intersectingFeatureExtent, featureExtent) + ) { + const width = getWidth(intersectingFeatureExtent); + const height = getHeight(intersectingFeatureExtent); + const area = width * height; + if (area < minimalCompartmentExtent) { + minimalCompartmentId = intersectingFeature.getId(); + minimalCompartmentExtent = area; + } + } + + if ( + intersectingFeatureType === MAP_ELEMENT_TYPES.COMPARTMENT && intersectingFeature.get('zIndex') > feature.get('zIndex') && intersectingFeature.get('filled') && - !isFeatureInCompartment(+featureId, vectorSource, intersectingFeature) + !isFeatureInCompartment(+featureId, vectorSource, intersectingFeature) && + !isFeatureInPathway(+featureId, vectorSource, intersectingFeature) ) { - const intersectingFeatureExtent = intersectingFeature.getGeometry()?.getExtent(); if (intersectingFeatureExtent) { remainingExtents = getDividedExtents(remainingExtents, intersectingFeatureExtent); } @@ -69,6 +94,9 @@ export default function handleSemanticView({ } } + if (!compartmentId && minimalCompartmentId) { + feature.set('compartmentId', minimalCompartmentId); + } if (complexId) { const complex = vectorSource.getFeatureById(complexId); if (complex && complex.get('filled')) { @@ -81,6 +109,12 @@ export default function handleSemanticView({ hide = true; } } + if (pathwayId) { + const pathway = vectorSource.getFeatureById(pathwayId); + if (pathway && pathway.get('filled')) { + hide = true; + } + } return { cover, hide, largestExtent }; } diff --git a/src/components/Map/MapViewer/utils/shapes/elements/isFeatureInPathway.ts b/src/components/Map/MapViewer/utils/shapes/elements/isFeatureInPathway.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba726ebae9b966cc348075203f4121fc3e909d27 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/elements/isFeatureInPathway.ts @@ -0,0 +1,22 @@ +import VectorSource from 'ol/source/Vector'; +import Feature from 'ol/Feature'; + +export default function isFeatureInPathway( + parentPathwayId: number, + vectorSource: VectorSource, + feature: Feature, +): boolean { + const pathwayId: undefined | null | number = feature.get('pathwayId'); + + if (!pathwayId) { + return false; + } + if (pathwayId === parentPathwayId || pathwayId === feature.get('id')) { + return true; + } + const compartmentFeature = vectorSource.getFeatureById(pathwayId); + if (!compartmentFeature) { + return false; + } + return isFeatureInPathway(parentPathwayId, vectorSource, compartmentFeature); +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts index 2dc0af86e66c00d41ae154da9b17a03f461d9f9e..7b8bb9715d0301419dcbaeee9ff3e9acadca1b7d 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts @@ -45,6 +45,7 @@ describe('Layer', () => { z: 3, width: 100, height: 100, + layer: 1, fontSize: 12, notes: 'XYZ', verticalAlign: 'MIDDLE', @@ -122,6 +123,7 @@ describe('Layer', () => { x: 1, y: 1, width: 1, + layer: 1, height: 1, z: 1, }, diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index b907081123bd0ca23c4066d8d68c6656c5dc857e..2149e63b551b9d2ec7a5e42561a2d805b5f8165f 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -1,5 +1,11 @@ /* eslint-disable no-magic-numbers */ -import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { + LayerImage as LayerImageModel, + LayerLine, + LayerOval, + LayerRect, + LayerText, +} from '@/types/models'; import { MapInstance } from '@/types/map'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; @@ -20,19 +26,19 @@ import { import { Stroke } from 'ol/style'; import { MapSize } from '@/redux/map/map.types'; import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; -import Glyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph'; import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/getArrowFeature'; import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getEllipseCoords from '@/components/Map/MapViewer/utils/shapes/coords/getEllipseCoords'; import Text from '@/components/Map/MapViewer/utils/shapes/text/Text'; +import LayerImage from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage'; export interface LayerProps { texts: { [key: string]: LayerText }; rects: Array<LayerRect>; ovals: Array<LayerOval>; lines: Array<LayerLine>; - images: { [key: string]: LayerImage }; + images: { [key: string]: LayerImageModel }; visible: boolean; layerId: number; lineTypes: LineTypeDict; @@ -53,7 +59,7 @@ export default class Layer { lines: Array<LayerLine>; - images: { [key: string]: LayerImage }; + images: { [key: string]: LayerImageModel }; lineTypes: LineTypeDict; @@ -133,7 +139,9 @@ export default class Layer { zIndex: text.z, width: text.width, height: text.height, + layer: text.layer, fontColor: text.color, + borderColor: text.borderColor, fontSize: text.fontSize, text: text.notes, verticalAlign: text.verticalAlign as VerticalAlign, @@ -157,7 +165,9 @@ export default class Layer { zIndex: text.z, width: text.width, height: text.height, + layer: text.layer, fontColor: text.color, + borderColor: text.borderColor, fontSize: text.fontSize, text: text.notes, verticalAlign: text.verticalAlign as VerticalAlign, @@ -330,7 +340,7 @@ export default class Layer { }); } - private drawImage(image: LayerImage): void { + private drawImage(image: LayerImageModel): void { const glyphFeature = this.getGlyphFeature(image); const imagesFeatures = this.vectorLayer.get('imagesFeatures'); if (imagesFeatures && Array.isArray(imagesFeatures)) { @@ -339,14 +349,15 @@ export default class Layer { this.vectorSource.addFeature(glyphFeature); } - private getGlyphFeature(image: LayerImage): Feature<Polygon> { - const glyph = new Glyph({ + private getGlyphFeature(image: LayerImageModel): Feature<Polygon> { + const glyph = new LayerImage({ elementId: image.id, glyphId: image.glyph, x: image.x, y: image.y, width: image.width, height: image.height, + layer: image.layer, zIndex: image.z, pointToProjection: this.pointToProjection, mapInstance: this.mapInstance, diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts index 1285218e4daffc24b538b5ea3964fe6f979a92f9..c2f77a2985e3e4a4e7bcf3b10c4add2ba9085751 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts @@ -19,7 +19,6 @@ jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({ describe('getTransformImageInteraction', () => { let store = {} as ToolkitStoreWithSingleSlice<ModalState>; let modelIdMock: number; - let layerIdMock: number; let featuresCollectionMock: Collection<Feature<Geometry>>; const mockDispatch = jest.fn(() => {}); @@ -36,7 +35,6 @@ describe('getTransformImageInteraction', () => { store = createStoreInstanceUsingSliceReducer('modal', modalReducer); store.dispatch = mockDispatch; modelIdMock = 1; - layerIdMock = 1; featuresCollectionMock = new Collection<Feature<Geometry>>(); }); @@ -45,7 +43,6 @@ describe('getTransformImageInteraction', () => { store.dispatch, mapSize, modelIdMock, - layerIdMock, featuresCollectionMock, [1000, 1000, 1000, 1000], ); diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts index 596205e6265c44436940e0204f460eee722fb6e6..3a1435fd1a8db4b9dc6b2d9d669f9ddc799e0412 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts @@ -11,12 +11,12 @@ import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/co import { MapSize } from '@/redux/map/map.types'; import { Extent } from 'ol/extent'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { openDrawer } from '@/redux/drawer/drawer.slice'; export default function getTransformImageInteraction( dispatch: AppDispatch, mapSize: MapSize, modelId: number, - activeLayer: number, featuresCollection: Collection<Feature<Geometry>>, restrictionExtent: Extent, ): Transform { @@ -100,6 +100,7 @@ export default function getTransformImageInteraction( if (getGlyphData && getGlyphData instanceof Function) { const glyphData = getGlyphData(); dispatch(mapEditToolsSetLayerObject(glyphData)); + dispatch(openDrawer('layers')); } }); @@ -115,10 +116,15 @@ export default function getTransformImageInteraction( try { const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), mapSize); const layerImage = await dispatch( - updateLayerImageObject({ modelId, layerId: activeLayer, ...glyphData, ...boundingBox }), + updateLayerImageObject({ + modelId, + layerId: glyphData.layer, + ...glyphData, + ...boundingBox, + }), ).unwrap(); if (layerImage) { - dispatch(layerUpdateImage({ modelId, layerId: activeLayer, layerImage })); + dispatch(layerUpdateImage({ modelId, layerId: layerImage.layer, layerImage })); dispatch(mapEditToolsSetLayerObject(layerImage)); } if (geometry instanceof Polygon && setCoordinates instanceof Function) { diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts b/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts index d40cf4593f9bcbd82b8a62d5059d2363d4a3cabb..2f2e145da280fd69de1abcadbef8d3e97c5ce839 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts @@ -31,12 +31,14 @@ describe('Text', () => { width: 100, height: 100, zIndex: 1, + layer: 1, text: 'Test', fontSize: 12, fontColor: BLACK_COLOR, + borderColor: BLACK_COLOR, verticalAlign: 'MIDDLE', horizontalAlign: 'CENTER', - pointToProjection: jest.fn(), + pointToProjection: jest.fn(({ x, y }) => [x, y]), mapInstance, }; diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.ts b/src/components/Map/MapViewer/utils/shapes/text/Text.ts index c853b3691f6f2bcc13d998729b4c3c8e02ec208f..c1bc5799c7d386b1abde85d309bdfde5254ebc62 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.ts +++ b/src/components/Map/MapViewer/utils/shapes/text/Text.ts @@ -11,16 +11,23 @@ import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapVi import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; +import Polygon from 'ol/geom/Polygon'; +import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; +import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; +import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; +import { Stroke } from 'ol/style'; export interface TextProps { x: number; y: number; width: number; height: number; + layer: number; zIndex: number; text: string; fontSize: number; fontColor: Color; + borderColor: Color; verticalAlign: VerticalAlign; horizontalAlign: HorizontalAlign; pointToProjection: UsePointToProjectionResult; @@ -34,6 +41,10 @@ export default class Text { style: Style; + polygonStyle: Style; + + strokeStyle: Stroke; + point: Point; feature: Feature<Point>; @@ -43,10 +54,12 @@ export default class Text { y, width, height, + layer, zIndex, text, fontSize, fontColor, + borderColor, verticalAlign, horizontalAlign, pointToProjection, @@ -65,6 +78,27 @@ export default class Text { horizontalAlign, pointToProjection, }); + const borderPolygon = new Polygon([ + [ + pointToProjection({ x, y }), + pointToProjection({ x: x + width, y }), + pointToProjection({ x: x + width, y: y + height }), + pointToProjection({ x, y: y + height }), + pointToProjection({ x, y }), + ], + ]); + this.strokeStyle = getStroke({ + color: rgbToHex(borderColor), + width: 1, + }); + this.polygonStyle = getStyle({ + geometry: borderPolygon, + borderColor, + fillColor: { rgb: 0, alpha: 0 }, + lineWidth: 1, + zIndex, + }); + const textStyle = getTextStyle({ text, fontSize, @@ -78,7 +112,7 @@ export default class Text { this.feature = new Feature({ geometry: this.point, - getTextScale: (resolution: number): number => { + getScale: (resolution: number): number => { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); if (maxZoom) { const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); @@ -88,21 +122,24 @@ export default class Text { } return 1; }, + layer, }); this.feature.setStyle(this.getStyle.bind(this)); } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { - const getTextScale = feature.get('getTextScale'); - let textScale = 1; - if (getTextScale instanceof Function) { - textScale = getTextScale(resolution); + const getScale = feature.get('getScale'); + let scale = 1; + if (getScale instanceof Function) { + scale = getScale(resolution); } - if (textScale < TEXT_CUTOFF_SCALE) { + if (scale < TEXT_CUTOFF_SCALE) { return undefined; } - this.style.getText()?.setScale(textScale); - return this.style; + return [ + getScaledElementStyle(this.polygonStyle, this.strokeStyle, scale), + getScaledElementStyle(this.style, undefined, scale), + ]; } } diff --git a/src/components/Map/MapViewer/utils/useOlMap.test.ts b/src/components/Map/MapViewer/utils/useOlMap.test.tsx similarity index 92% rename from src/components/Map/MapViewer/utils/useOlMap.test.ts rename to src/components/Map/MapViewer/utils/useOlMap.test.tsx index 5b5fa97bfb941ce2007dea7da7566aba0086f9c1..8c102689f218f7009d287d5c9c49159beeef5f1e 100644 --- a/src/components/Map/MapViewer/utils/useOlMap.test.ts +++ b/src/components/Map/MapViewer/utils/useOlMap.test.tsx @@ -26,6 +26,10 @@ Object.defineProperty(useRefValue, 'current', { jest.spyOn(React, 'useRef').mockReturnValue(useRefValue); +jest.mock('./config/additionalLayers/useOlMapAdditionalLayers', () => ({ + useOlMapAdditionalLayers: jest.fn(() => []), +})); + describe('useOlMap - util', () => { const { Wrapper } = getReduxWrapperWithStore({ map: initialMapStateFixture, diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..84018d80755cc437d318b11683ae4ac5815aa7ed --- /dev/null +++ b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts @@ -0,0 +1,53 @@ +import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.types'; +import { store } from '@/redux/store'; +import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; +import { getLayerImage } from '@/redux/layers/layers.thunks'; +import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import { MapInstance } from '@/types/map'; +import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { layerDeleteImage } from '@/redux/layers/layers.slice'; +import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; + +export default async function processLayerImage({ + data, + mapInstance, +}: { + data: WebSocketEntityUpdateInterface; + mapInstance: MapInstance; +}): Promise<void> { + const { dispatch } = store; + if ( + data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED || + data.type === ENTITY_OPERATION_TYPES.ENTITY_UPDATED + ) { + const resultImage = await dispatch( + getLayerImage({ + modelId: data.mapId, + layerId: data.layerId, + imageId: data.entityId, + }), + ).unwrap(); + if (!resultImage) { + return; + } + if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + drawElementOnLayer({ + mapInstance, + activeLayer: data.layerId, + object: resultImage, + drawFunctionKey: 'drawImage', + }); + } else { + updateGlyph(mapInstance, data.layerId, resultImage); + } + } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { + dispatch( + layerDeleteImage({ + modelId: data.mapId, + layerId: data.layerId, + imageId: data.entityId, + }), + ); + removeElementFromLayer({ mapInstance, layerId: data.layerId, featureId: data.entityId }); + } +} diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerText.ts b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts new file mode 100644 index 0000000000000000000000000000000000000000..f31697c4ea5ebbb4e8337d7df876bbb04fbf8bdc --- /dev/null +++ b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts @@ -0,0 +1,34 @@ +import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.types'; +import { store } from '@/redux/store'; +import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; +import { getLayerText } from '@/redux/layers/layers.thunks'; +import { MapInstance } from '@/types/map'; +import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; + +export default async function processLayerText({ + data, + mapInstance, +}: { + data: WebSocketEntityUpdateInterface; + mapInstance: MapInstance; +}): Promise<void> { + const { dispatch } = store; + if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + const resultText = await dispatch( + getLayerText({ + modelId: data.mapId, + layerId: data.layerId, + textId: data.entityId, + }), + ).unwrap(); + if (!resultText) { + return; + } + drawElementOnLayer({ + mapInstance, + activeLayer: data.layerId, + object: resultText, + drawFunctionKey: 'drawText', + }); + } +} diff --git a/src/components/Map/MapViewer/utils/websocket/processMessage.ts b/src/components/Map/MapViewer/utils/websocket/processMessage.ts new file mode 100644 index 0000000000000000000000000000000000000000..37fc0a9171c110fa59d9d236a174e1135bbfd0a6 --- /dev/null +++ b/src/components/Map/MapViewer/utils/websocket/processMessage.ts @@ -0,0 +1,24 @@ +import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.types'; +import processLayerImage from '@/components/Map/MapViewer/utils/websocket/processLayerImage'; +import { MapInstance } from '@/types/map'; +import { ENTITY_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; +import processLayerText from '@/components/Map/MapViewer/utils/websocket/processLayerText'; + +export default async function processMessage({ + jsonMessage, + mapInstance, +}: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jsonMessage: WebSocketEntityUpdateInterface; + mapInstance: MapInstance; +}): Promise<void> { + try { + if (jsonMessage.entityType === ENTITY_TYPES.LAYER_IMAGE) { + await processLayerImage({ data: jsonMessage, mapInstance }); + } else if (jsonMessage.entityType === ENTITY_TYPES.LAYER_TEXT) { + await processLayerText({ data: jsonMessage, mapInstance }); + } + } catch { + throw new Error(`Process websocket message error`); + } +} diff --git a/src/components/Map/MapViewer/utils/websocket/websocket.constants.ts b/src/components/Map/MapViewer/utils/websocket/websocket.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f2fb74238c0bf56d78302276341fa39e1f4eb4e --- /dev/null +++ b/src/components/Map/MapViewer/utils/websocket/websocket.constants.ts @@ -0,0 +1,16 @@ +export const OPERATION_TYPES = { + ENTITY_CREATED: 'ENTITY_CREATED', + ENTITY_UPDATED: 'ENTITY_UPDATED', + ENTITY_DELETED: 'ENTITY_DELETED', + MESSAGE_PROCESSED_SUCCESSFULLY: 'MESSAGE_PROCESSED_SUCCESSFULLY', +} as const; + +export const ENTITY_TYPES = { + GLYPH: 'GLYPH', + LAYER: 'LAYER', + LAYER_IMAGE: 'LAYER_IMAGE', + LAYER_LINE: 'LAYER_LINE', + LAYER_OVAL: 'LAYER_OVAL', + LAYER_RECTANGLE: 'LAYER_RECTANGLE', + LAYER_TEXT: 'LAYER_TEXT', +} as const; diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts index e5ab204a5e6465bf226c874f336676ecc86637f8..ff6f42580a5287b9538b59299d4cd3ca061b6837 100644 --- a/src/constants/canvas.ts +++ b/src/constants/canvas.ts @@ -15,7 +15,7 @@ export const PIN_SIZE = { export const PINS_COLORS: Record<PinType, string> = { drugs: '#F48C41', chemicals: '#008325', - bioEntity: '#106AD7', + modelElement: '#106AD7', comment: '#106AD7', }; diff --git a/src/constants/index.ts b/src/constants/index.ts index 4cfb0b9bcae9f9aee3c0dc0b6e0781c35adc8db9..5f890d3f46f03364f10bcbd8756630e2bbb8f083 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -8,7 +8,7 @@ export const BASE_NEW_API_URL = getConfigValue('BASE_NEW_API_URL'); export const DEFAULT_PROJECT_ID = getConfigValue('DEFAULT_PROJECT_ID'); export const PROJECT_ID = getProjectIdFromUrl() || DEFAULT_PROJECT_ID; export const ZOD_SEED = parseInt(process.env.ZOD_SEED || '123', 10); -export const BIO_ENTITY = 'bioEntity'; +export const MODEL_ELEMENT = 'modelElement'; export const DRUGS_CHEMICALS = ['drugs', 'chemicals']; export const MINERVA_WEBSITE_URL = 'https://minerva.pages.uni.lu/doc/'; export const ADMIN_PANEL_URL = getConfigValue('ADMIN_PANEL_URL'); diff --git a/src/constants/pin.ts b/src/constants/pin.ts deleted file mode 100644 index 0af91625d8fdae1ec8f7a2a6417e96831f4f6b66..0000000000000000000000000000000000000000 --- a/src/constants/pin.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PinType } from '@/types/pin'; - -export const DEFAULT_PIN_TYPE: PinType = 'bioEntity'; diff --git a/src/models/bioEntitySchema.ts b/src/models/bioEntitySchema.ts index 8a347db6cf8d224d8875d4ada8e3f02b5c81736b..ec78db95e39c5e0db6129ffabd28873a1653bb17 100644 --- a/src/models/bioEntitySchema.ts +++ b/src/models/bioEntitySchema.ts @@ -1,17 +1,17 @@ import { ZERO } from '@/constants/common'; import { z } from 'zod'; import { reactionProduct } from '@/models/reactionProduct'; +import { modelElementModificationSchema } from '@/models/modelElementModificationSchema'; import { colorSchema } from './colorSchema'; import { glyphSchema } from './glyphSchema'; import { lineSchema } from './lineSchema'; -import { modificationResiduesSchema } from './modificationResiduesSchema'; import { operatorSchema } from './operatorSchema'; import { referenceSchema } from './referenceSchema'; import { structuralStateSchema } from './structuralStateSchema'; import { submodelSchema } from './submodelSchema'; export const bioEntitySchema = z.object({ - id: z.union([z.number().int().positive(), z.string()]), + id: z.number().int().positive(), immediateLink: z.string().nullable().optional(), name: z.string(), elementId: z.string(), @@ -21,12 +21,12 @@ export const bioEntitySchema = z.object({ notes: z.string(), symbol: z.string().nullable(), homodimer: z.number().optional(), - nameX: z.number().nullable().optional(), - nameY: z.number().nullable().optional(), - nameWidth: z.number().nullable().optional(), - nameHeight: z.number().nullable().optional(), - nameVerticalAlign: z.string().nullable().optional(), - nameHorizontalAlign: z.string().nullable().optional(), + nameX: z.number(), + nameY: z.number(), + nameWidth: z.number(), + nameHeight: z.number(), + nameVerticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), + nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), width: z .number() .optional() @@ -35,15 +35,15 @@ export const bioEntitySchema = z.object({ .number() .optional() .transform(height => height ?? ZERO), - visibilityLevel: z.string().nullable(), - transparencyLevel: z.string().nullable().optional(), + visibilityLevel: z.string(), + transparencyLevel: z.string(), synonyms: z.array(z.string()), - formerSymbols: z.array(z.string()).nullable().optional(), - fullName: z.string().nullable().nullable().optional(), + formerSymbols: z.array(z.string()), + fullName: z.string().nullable(), compartmentName: z.string().nullable().optional(), abbreviation: z.string().nullable(), formula: z.string().nullable(), - glyph: glyphSchema.nullable().optional(), + glyph: glyphSchema.nullable(), activity: z.boolean().optional(), structuralState: z.optional(structuralStateSchema.nullable()), hypothetical: z.boolean().nullable().optional(), @@ -54,11 +54,12 @@ export const bioEntitySchema = z.object({ charge: z.number().nullable().optional(), substanceUnits: z.boolean().nullable().optional(), onlySubstanceUnits: z.boolean().optional().nullable(), - shape: z.string().nullable().optional(), - modificationResidues: z.optional(z.array(modificationResiduesSchema)), + shape: z.enum(['SQUARE_COMPARTMENT', 'OVAL_COMPARTMENT', 'PATHWAY']).optional(), + modificationResidues: z.array(modelElementModificationSchema).optional(), complex: z.number().nullable().optional(), - compartment: z.number().nullable().optional(), - submodel: submodelSchema.nullable().optional(), + compartment: z.number().nullable(), + pathway: z.number().nullable(), + submodel: submodelSchema.nullable(), x: z .number() .optional() @@ -68,10 +69,10 @@ export const bioEntitySchema = z.object({ .optional() .transform(y => y ?? ZERO), lineWidth: z.number().optional(), - fontColor: colorSchema.nullable().optional(), - fontSize: z.number().nullable().optional(), - fillColor: colorSchema.nullable().optional(), - borderColor: colorSchema.nullable().optional(), + fontColor: colorSchema, + fontSize: z.number(), + fillColor: colorSchema, + borderColor: colorSchema, smiles: z.optional(z.string()).nullable(), inChI: z.optional(z.string().nullable()), inChIKey: z.optional(z.string().nullable()), diff --git a/src/models/fixtures/bioEntityFixture.ts b/src/models/fixtures/publicationElementFixture.ts similarity index 55% rename from src/models/fixtures/bioEntityFixture.ts rename to src/models/fixtures/publicationElementFixture.ts index 652ee55c6445b76c6604577d9723d79f445f1c73..297630605a09b578886222f80a43798f55483688 100644 --- a/src/models/fixtures/bioEntityFixture.ts +++ b/src/models/fixtures/publicationElementFixture.ts @@ -1,9 +1,9 @@ import { ZOD_SEED } from '@/constants'; // eslint-disable-next-line import/no-extraneous-dependencies import { createFixture } from 'zod-fixture'; -import { bioEntitySchema } from '@/models/bioEntitySchema'; +import { publicationElementSchema } from '@/models/publicationElementSchema'; -export const bioEntityFixture = createFixture(bioEntitySchema, { +export const publicationElementFixture = createFixture(publicationElementSchema, { seed: ZOD_SEED, array: { min: 2, max: 2 }, }); diff --git a/src/models/layerImageSchema.ts b/src/models/layerImageSchema.ts index 8547b313555bdef3d17f894372d4e4ba75ce19d7..51600dc42a146c4c241072e60c71eb43130f04b3 100644 --- a/src/models/layerImageSchema.ts +++ b/src/models/layerImageSchema.ts @@ -7,5 +7,6 @@ export const layerImageSchema = z.object({ z: z.number(), width: z.number(), height: z.number(), + layer: z.number(), glyph: z.number().nullable(), }); diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts index 6858da82cc89c2f05336b3cdfb7b5b49df1a34e7..0ec90665b7532e11a8e2782cf28c9e9ef3164fe4 100644 --- a/src/models/layerTextSchema.ts +++ b/src/models/layerTextSchema.ts @@ -8,6 +8,7 @@ export const layerTextSchema = z.object({ z: z.number(), width: z.number(), height: z.number(), + layer: z.number(), fontSize: z.number(), notes: z.string(), verticalAlign: z.string(), diff --git a/src/models/mocks/publicationsResponseMock.ts b/src/models/mocks/publicationsResponseMock.ts index 01240a657bac40972c623c608a5383181a42e5ae..45ec7d9484f7a3c2580ad9060911830e115d09a8 100644 --- a/src/models/mocks/publicationsResponseMock.ts +++ b/src/models/mocks/publicationsResponseMock.ts @@ -1,12 +1,12 @@ import { FilteredPageOf, Publication } from '@/types/models'; -import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture'; +import { publicationElementFixture } from '@/models/fixtures/publicationElementFixture'; export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publication> = { content: [ { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 19519, model: 52, }, @@ -24,7 +24,7 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 16167, model: 61, }, @@ -55,12 +55,12 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 17823, model: 52, }, { - ...bioEntityFixture, + ...publicationElementFixture, id: 19461, model: 52, }, @@ -79,12 +79,12 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 18189, model: 52, }, { - ...bioEntityFixture, + ...publicationElementFixture, id: 18729, model: 52, }, @@ -102,12 +102,12 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 16077, model: 58, }, { - ...bioEntityFixture, + ...publicationElementFixture, id: 16135, model: 58, }, @@ -137,7 +137,7 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 15955, model: 55, }, @@ -166,12 +166,12 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 15937, model: 55, }, { - ...bioEntityFixture, + ...publicationElementFixture, id: 15955, model: 55, }, @@ -190,7 +190,7 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 15948, model: 55, }, @@ -209,7 +209,7 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 16286, model: 62, }, @@ -236,12 +236,12 @@ export const PUBLICATIONS_DEFAULT_SEARCH_FIRST_10_MOCK: FilteredPageOf<Publicati { elements: [ { - ...bioEntityFixture, + ...publicationElementFixture, id: 17780, model: 52, }, { - ...bioEntityFixture, + ...publicationElementFixture, id: 17937, model: 52, }, diff --git a/src/models/modelElementSchema.ts b/src/models/modelElementSchema.ts index 7f8cf7a5800c4f9fa66b9c79932aef7d9116ae0a..86b82f678a94bb3efd212ea75ec522df9d7511e1 100644 --- a/src/models/modelElementSchema.ts +++ b/src/models/modelElementSchema.ts @@ -6,12 +6,13 @@ import { modelElementModificationSchema } from '@/models/modelElementModificatio import { glyphSchema } from '@/models/glyphSchema'; export const modelElementSchema = z.object({ - id: z.number(), + id: z.number().int().positive(), model: z.number(), glyph: glyphSchema.nullable(), submodel: submodelSchema.nullable(), compartment: z.number().nullable(), - immediateLink: z.string().nullable(), + pathway: z.number().nullable(), + immediateLink: z.string().nullable().optional(), elementId: z.string(), x: z.number(), y: z.number(), diff --git a/src/models/publicationElementSchema.ts b/src/models/publicationElementSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bd5e4f04f624e2daf7c882d67b13a3060826ca3 --- /dev/null +++ b/src/models/publicationElementSchema.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { reactionProduct } from '@/models/reactionProduct'; +import { lineSchema } from './lineSchema'; +import { operatorSchema } from './operatorSchema'; +import { referenceSchema } from './referenceSchema'; + +export const publicationElementSchema = z.object({ + abbreviation: z.string().nullable(), + elementId: z.string(), + formula: z.string().nullable(), + geneProteinReaction: z.string().nullable().optional(), + id: z.union([z.number().int().positive(), z.string()]), + idReaction: z.string().optional(), + kinetics: z.null().optional(), + line: lineSchema.optional(), + lowerBound: z.boolean().nullable().optional(), + mechanicalConfidenceScore: z.boolean().nullable().optional(), + model: z.number(), + modifiers: z.array(reactionProduct).optional(), + name: z.string(), + notes: z.string(), + operators: z.array(operatorSchema).optional(), + processCoordinates: z.null().optional(), + products: z.array(reactionProduct).optional(), + reactants: z.array(reactionProduct).optional(), + references: z.array(referenceSchema), + reversible: z.boolean().optional(), + sboTerm: z.string(), + subsystem: z.string().nullable().optional(), + symbol: z.string().nullable(), + synonyms: z.array(z.string()), + upperBound: z.boolean().nullable().optional(), + visibilityLevel: z.string(), + z: z.number(), +}); diff --git a/src/models/publicationsSchema.ts b/src/models/publicationsSchema.ts index 14ac77ff60234eeed55cadd991fec4c8ce6a7018..8fff78d787bd47a7382f891bd7c5d31f26dfd72d 100644 --- a/src/models/publicationsSchema.ts +++ b/src/models/publicationsSchema.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { bioEntitySchema } from '@/models/bioEntitySchema'; +import { publicationElementSchema } from '@/models/publicationElementSchema'; import { articleSchema } from './articleSchema'; export const publicationSchema = z.object({ - elements: z.array(bioEntitySchema), + elements: z.array(publicationElementSchema), article: articleSchema, }); diff --git a/src/models/sessionValidSchema.ts b/src/models/sessionValidSchema.ts index 371cd7cd86d9fefe455aab774ae1541c318dc27c..df8a64f5776279b0f200ee232a1b8bd1bef212df 100644 --- a/src/models/sessionValidSchema.ts +++ b/src/models/sessionValidSchema.ts @@ -2,4 +2,5 @@ import { z } from 'zod'; export const sessionSchemaValid = z.object({ login: z.string(), + token: z.string(), }); diff --git a/src/models/targetSchema.ts b/src/models/targetSchema.ts index 015626e16f4e06ce2276a5e9a597701b63c7a031..a9f480632fcfac48a0072af4f54433b87bf04d88 100644 --- a/src/models/targetSchema.ts +++ b/src/models/targetSchema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { bioEntitySchema } from './bioEntitySchema'; +import { modelElementSchema } from '@/models/modelElementSchema'; import { referenceSchema } from './referenceSchema'; import { targetParticipantSchema } from './targetParticipantSchema'; @@ -9,7 +9,7 @@ export const targetSchema = z.object({ /** list of target references */ references: z.array(referenceSchema), /** list of elements on the map associated with this target */ - targetElements: z.array(bioEntitySchema), + targetElements: z.array(modelElementSchema), /** list of identifiers associated with this target */ targetParticipants: z.array(targetParticipantSchema), }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index d14f7fc5d1847e1188a31119f0fa4c5c11855550..d42b790a93973ca77e038fa46fd44052cad2836f 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -24,8 +24,6 @@ export const apiPath = { isPerfectMatch, }: PerfectSearchParams): string => `projects/${PROJECT_ID}/models/*/bioEntities/:search?query=${searchQuery}&size=1000&perfectMatch=${isPerfectMatch}`, - getReactionsWithIds: (ids: number[]): string => - `projects/${PROJECT_ID}/models/*/bioEntities/reactions/?id=${ids.join(',')}&size=1000`, getDrugsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/drugs:search?query=${searchQuery}`, getDrugsStringWithColumnsTarget: (columns: string, target: string): string => @@ -56,12 +54,16 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, addLayerImageObject: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/`, - updateLayerImageObject: (modelId: number, layerId: number, imageId: number): string => + updateLayerImageObject: (modelId: number, layerId: number, imageId: number | string): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/${imageId}`, - removeLayerImageObject: (modelId: number, layerId: number, imageId: number): string => + removeLayerImageObject: (modelId: number, layerId: number, imageId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/${imageId}`, + getLayerImageObject: (modelId: number, layerId: number, imageId: number | string): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/${imageId}`, addLayerText: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/`, + getLayerText: (modelId: number, layerId: number, imageId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${imageId}`, getLayer: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, getGlyphImage: (glyphId: number): string => @@ -118,7 +120,6 @@ export const apiPath = { registerPluign: (): string => `plugins/`, getPlugin: (pluginId: string): string => `plugins/${pluginId}/`, getAllPlugins: (): string => `/plugins/`, - getSubmapConnections: (): string => `projects/${PROJECT_ID}/submapConnections/`, logout: (): string => `doLogout`, user: (login: string): string => `users/${login}`, updateUser: (login: string): string => `users/${login}`, diff --git a/src/redux/bioEntity/bioEntity.constants.ts b/src/redux/bioEntity/bioEntity.constants.ts index 70b92fc4ec9863719d389c994002db131975e998..283283d5d66b10a0272f04973b8deeb0deb13d86 100644 --- a/src/redux/bioEntity/bioEntity.constants.ts +++ b/src/redux/bioEntity/bioEntity.constants.ts @@ -1,24 +1,7 @@ -import { FetchDataState } from '@/types/fetchDataState'; -import { BioEntity } from '@/types/models'; import { BioEntityContentsState } from './bioEntity.types'; -export const DEFAULT_BIOENTITY_PARAMS = { - perfectMatch: false, -}; - export const BIO_ENTITY_FETCHING_ERROR_PREFIX = 'Failed to fetch bio entity'; -export const MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX = 'Failed to fetch multi bio entity'; - -export const BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE: FetchDataState<BioEntity[]> = { - data: [], - loading: 'idle', - error: { name: '', message: '' }, -}; export const BIOENTITY_INITIAL_STATE: BioEntityContentsState = { - data: [], - loading: 'idle', - error: { name: '', message: '' }, - submapConnections: BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE, isContentTabOpened: false, }; diff --git a/src/redux/bioEntity/bioEntity.mock.ts b/src/redux/bioEntity/bioEntity.mock.ts index 929790fa1834f0f2e07fe5fe9659b407c9a144e6..f104198e4c63549e58fc5d2c6c7085b91cb9ea03 100644 --- a/src/redux/bioEntity/bioEntity.mock.ts +++ b/src/redux/bioEntity/bioEntity.mock.ts @@ -1,34 +1,5 @@ -import { DEFAULT_ERROR } from '@/constants/errors'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { MultiSearchData } from '@/types/fetchDataState'; -import { BioEntity, BioEntityContent } from '@/types/models'; -import { HISTAMINE_MAP_ID } from '@/constants/mocks'; import { BioEntityContentsState } from './bioEntity.types'; export const BIOENTITY_INITIAL_STATE_MOCK: BioEntityContentsState = { - data: [], - loading: 'idle', - error: DEFAULT_ERROR, - submapConnections: { - data: [], - loading: 'idle', - error: DEFAULT_ERROR, - }, + isContentTabOpened: false, }; - -export const BIO_ENTITY_LINKING_TO_SUBMAP: BioEntity = { - ...bioEntityContentFixture.bioEntity, - submodel: { - mapId: HISTAMINE_MAP_ID, - type: 'DONWSTREAM_TARGETS', - }, -}; - -export const BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK: MultiSearchData<BioEntityContent[]>[] = [ - { - data: [{ bioEntity: BIO_ENTITY_LINKING_TO_SUBMAP, perfect: false }], - searchQueryElement: '', - loading: 'succeeded', - error: DEFAULT_ERROR, - }, -]; diff --git a/src/redux/bioEntity/bioEntity.reducers.test.ts b/src/redux/bioEntity/bioEntity.reducers.test.ts deleted file mode 100644 index 8fa2002f40d92211b93a63ff3f013c91560dc03a..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/bioEntity.reducers.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { DEFAULT_ERROR } from '@/constants/errors'; -import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { apiPath } from '@/redux/apiPath'; -import { - ToolkitStoreWithSingleSlice, - createStoreInstanceUsingSliceReducer, -} from '@/utils/createStoreInstanceUsingSliceReducer'; -import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; -import { unwrapResult } from '@reduxjs/toolkit'; -import { HttpStatusCode } from 'axios'; -import bioEntityContentsReducer from './bioEntity.slice'; -import { getBioEntity } from './bioEntity.thunks'; -import { BioEntityContentsState } from './bioEntity.types'; - -const mockedAxiosClient = mockNetworkNewAPIResponse(); -const SEARCH_QUERY = 'park7'; - -jest.mock('../../utils/error-report/errorReporting'); - -const INITIAL_STATE: BioEntityContentsState = { - data: [], - loading: 'idle', - isContentTabOpened: false, - error: { name: '', message: '' }, - submapConnections: { - data: [], - loading: 'idle', - error: { name: '', message: '' }, - }, -}; - -describe('bioEntity reducer', () => { - let store = {} as ToolkitStoreWithSingleSlice<BioEntityContentsState>; - beforeEach(() => { - store = createStoreInstanceUsingSliceReducer('bioEntity', bioEntityContentsReducer); - }); - - it('should match initial state', () => { - const action = { type: 'unknown' }; - - expect(bioEntityContentsReducer(undefined, action)).toEqual(INITIAL_STATE); - }); - - it('should update store after succesfull getBioEntity query', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); - - const { type } = await store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - const { data } = store.getState().bioEntity; - const bioEnityWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, - ); - - expect(type).toBe('project/getBioEntityContents/fulfilled'); - expect(bioEnityWithSearchElement).toEqual({ - searchQueryElement: SEARCH_QUERY, - data: bioEntityResponseFixture.content, - loading: 'succeeded', - error: DEFAULT_ERROR, - }); - }); - - it('should update store after failed getBioEntity query', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.NotFound, bioEntityResponseFixture); - - const action = await store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - const { data } = store.getState().bioEntity; - - const bioEntityWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, - ); - expect(action.type).toBe('project/getBioEntityContents/rejected'); - expect(() => unwrapResult(action)).toThrow( - "Failed to fetch bio entity: The page you're looking for doesn't exist. Please verify the URL and try again.", - ); - expect(bioEntityWithSearchElement).toEqual({ - searchQueryElement: SEARCH_QUERY, - data: undefined, - loading: 'failed', - error: DEFAULT_ERROR, - }); - }); - - it('should update store on loading getBioEntity query', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); - - const bioEntityContentsPromise = store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - - const { data } = store.getState().bioEntity; - const bioEntityWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, - ); - - expect(bioEntityWithSearchElement).toEqual({ - searchQueryElement: SEARCH_QUERY, - data: undefined, - loading: 'pending', - error: DEFAULT_ERROR, - }); - - bioEntityContentsPromise.then(() => { - const { data: dataPromiseFulfilled } = store.getState().bioEntity; - - const bioEntityWithSearchElementFulfilled = dataPromiseFulfilled.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, - ); - - expect(bioEntityWithSearchElementFulfilled).toEqual({ - searchQueryElement: SEARCH_QUERY, - data: bioEntityResponseFixture.content, - loading: 'succeeded', - error: DEFAULT_ERROR, - }); - }); - }); -}); diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts index b24e17c1b8889e85fa13201210cee9d78e1ab312..f4406d501b6a50029a230996721dcf53c1f1954b 100644 --- a/src/redux/bioEntity/bioEntity.reducers.ts +++ b/src/redux/bioEntity/bioEntity.reducers.ts @@ -1,99 +1,9 @@ -import { DEFAULT_ERROR } from '@/constants/errors'; -import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; -import { BioEntity, BioEntityContent } from '@/types/models'; -import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks'; +import { PayloadAction } from '@reduxjs/toolkit'; import { BioEntityContentsState } from './bioEntity.types'; -export const getBioEntityContentsReducer = ( - builder: ActionReducerMapBuilder<BioEntityContentsState>, -): void => { - builder.addCase(getBioEntity.pending, (state, action) => { - state.data.push({ - searchQueryElement: action.meta.arg.searchQuery, - 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, - ); - if (bioEntities) { - bioEntities.data = action.payload; - bioEntities.loading = 'succeeded'; - } - }); - builder.addCase(getBioEntity.rejected, (state, action) => { - 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 - } - }); -}; - -export const getMultiBioEntityContentsReducer = ( - builder: ActionReducerMapBuilder<BioEntityContentsState>, -): void => { - builder.addCase(getMultiBioEntity.pending, state => { - state.data = []; - state.loading = 'pending'; - }); - builder.addCase(getMultiBioEntity.fulfilled, state => { - state.loading = 'succeeded'; - }); - builder.addCase(getMultiBioEntity.rejected, state => { - state.loading = 'failed'; - // TODO: error management to be discussed in the team - }); -}; - -export const clearBioEntitiesReducer = (state: BioEntityContentsState): void => { - state.data = []; - state.loading = 'idle'; -}; - export const toggleIsContentTabOpenedReducer = ( state: BioEntityContentsState, action: PayloadAction<boolean>, ): void => { state.isContentTabOpened = action.payload; }; - -export const setBioEntityContentsReducer = ( - state: BioEntityContentsState, - action: PayloadAction<BioEntityContent>, -): void => { - state.data = [ - { - data: [action.payload], - loading: 'succeeded', - error: DEFAULT_ERROR, - searchQueryElement: action.payload.bioEntity.id.toString(), - }, - ]; - state.loading = 'succeeded'; -}; - -export const setMultipleBioEntityContentsReducer = ( - state: BioEntityContentsState, - action: PayloadAction<Array<BioEntity>>, -): void => { - state.data = action.payload.map(bioEntity => { - return { - data: [ - { - bioEntity, - perfect: true, - }, - ], - searchQueryElement: bioEntity.id.toString(), - loading: 'succeeded', - error: DEFAULT_ERROR, - }; - }); - state.loading = 'succeeded'; -}; diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index d9c63a0a6ab297c775a0f251498eed4926a05e29..1abdb89dec884daff6e3938f89b6591a59ee461b 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -1,196 +1,58 @@ -import { ONE, SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common'; -import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; +import { ONE, ZERO } from '@/constants/common'; import { rootSelector } from '@/redux/root/root.selectors'; -import { BioEntityWithPinType } from '@/types/bioEntity'; +import { ModelElementWithPinType } from '@/types/modelElement'; import { ElementIdTabObj } from '@/types/elements'; -import { MultiSearchData } from '@/types/fetchDataState'; -import { BioEntity, BioEntityContent, Comment, MapModel, ModelElement } from '@/types/models'; +import { ModelElement } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; import { - currentDrawerModelElementSelector, - modelElementsWithSubmapConnectionForCurrentModelSelector, + allModelElementsSubmapConnectionsForCurrentSubmapSelector, + allSearchModelElementForCurrentModelSelector, + allSearchModelElementsIdTabForCurrentModelSelector, + searchedModelElementsForCurrentModelSelector, + searchedModelElementsSelector, } from '@/redux/modelElements/modelElements.selector'; -import { currentDrawerNewReactionSelector } from '@/redux/newReactions/newReactions.selectors'; import { - allChemicalsBioEntitesOfCurrentMapSelector, + allDrugsElementsOfCurrentMapSelector, + allDrugsIdTabSelectorOfCurrentMap, + drugsElementsForSelectedSearchElementSelector, + searchedDrugsElementsOfCurrentMapSelector, +} from '@/redux/drugs/drugs.selectors'; +import { + allChemicalsElementsOfCurrentMapSelector, allChemicalsIdTabSelectorOfCurrentMap, - chemicalsBioEntitiesForSelectedSearchElementSelector, - searchedChemicalsBioEntitesOfCurrentMapSelector, + chemicalsElementsForSelectedSearchElementSelector, + searchedChemicalsElementsOfCurrentMapSelector, } from '../chemicals/chemicals.selectors'; -import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector'; -import { currentSelectedSearchElement } from '../drawer/drawer.selectors'; -import { - allDrugsBioEntitesOfCurrentMapSelector, - allDrugsIdTabSelectorOfCurrentMap, - drugsBioEntitiesForSelectedSearchElementSelector, - searchedDrugsBioEntitesOfCurrentMapSelector, -} from '../drugs/drugs.selectors'; -import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors'; export const bioEntitySelector = createSelector(rootSelector, state => state.bioEntity); -export const bioEntityDataSelector = createSelector(bioEntitySelector, bioEntity => bioEntity.data); - export const bioEntityIsContentTabOpenedSelector = createSelector( bioEntitySelector, bioEntity => bioEntity.isContentTabOpened, ); -export const bioEntityLoadingSelector = createSelector( - bioEntitySelector, - bioEntity => bioEntity.loading, -); - -export const bioEntityDataListSelector = createSelector(bioEntityDataSelector, bioEntityData => - bioEntityData.map(b => b.data || []).flat(), -); - -export const allSubmapConnectionsBioEntityOfCurrentSubmapSelector = createSelector( - modelElementsWithSubmapConnectionForCurrentModelSelector, - currentModelIdSelector, - (submapConnectionsBioEntity, currentModel): ModelElement[] => - submapConnectionsBioEntity.filter(({ model }) => model === currentModel), -); - -export const bioEntitiesForSelectedSearchElement = createSelector( - bioEntitySelector, - currentSelectedSearchElement, - (bioEntitiesState, currentSearchElement): MultiSearchData<BioEntityContent[]> | undefined => - bioEntitiesState.data.find( - ({ searchQueryElement }) => searchQueryElement === currentSearchElement, - ), -); - -export const searchedBioEntityElementForContextMapSelector = createSelector( - bioEntitySelector, - currentSelectedBioEntityIdSelector, - (bioEntitiesState, currentBioEntityId): BioEntity | undefined => { - return bioEntitiesState.data - .find(({ searchQueryElement }) => searchQueryElement === currentBioEntityId.toString()) - ?.data?.find(({ bioEntity }) => bioEntity.id === currentBioEntityId)?.bioEntity; - }, -); - -export const searchedBioEntityElementUniProtIdSelector = createSelector( - searchedBioEntityElementForContextMapSelector, - (bioEntitiesState): string | undefined => { - return bioEntitiesState?.references.find(({ type }) => type === 'UNIPROT')?.resource; - }, -); - -export const loadingBioEntityStatusSelector = createSelector( - bioEntitiesForSelectedSearchElement, - state => state?.loading, -); - -export const searchedBioEntitesSelectorOfCurrentMap = createSelector( - bioEntitySelector, - currentSelectedSearchElement, - currentModelIdSelector, - (bioEntities, currentSearchElement, currentModelId): BioEntity[] => { - if (!bioEntities) { - return []; - } - - return (bioEntities?.data || []) - .filter(({ searchQueryElement }) => - currentSearchElement ? searchQueryElement === currentSearchElement : true, - ) - .map(({ data }) => data || []) - .flat() - .filter(({ bioEntity }) => bioEntity.model === currentModelId) - .map(({ bioEntity }) => bioEntity); - }, -); - -export const allBioEntitesSelectorOfCurrentMap = createSelector( - bioEntitySelector, - currentModelIdSelector, - (bioEntities, currentModelId): BioEntity[] => { - if (!bioEntities) { - return []; - } - - return (bioEntities?.data || []) - .map(({ data }) => data || []) - .flat() - .filter(({ bioEntity }) => bioEntity.model === currentModelId) - .map(({ bioEntity }) => bioEntity); - }, -); - -export const allBioEntitesIdTabSelectorOfCurrentMap = createSelector( - bioEntitySelector, - currentModelIdSelector, - (bioEntities, currentModelId): ElementIdTabObj => { - if (!bioEntities) { - return {}; - } - - return Object.fromEntries( - (bioEntities?.data || []) - .map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement]) - .map(([data, tab]) => - (data || []) - .flat() - .filter(({ bioEntity }) => bioEntity.model === currentModelId) - .map(d => [d.bioEntity.id, tab]), - ) - .flat(), - ); - }, -); - -export const numberOfBioEntitiesSelector = createSelector( - bioEntitiesForSelectedSearchElement, - state => (state?.data ? state.data.length : SIZE_OF_EMPTY_ARRAY), -); - -export const bioEntitiesPerModelSelector = createSelector( - bioEntitiesForSelectedSearchElement, - modelsDataSelector, - (bioEntities, models) => { - const bioEntitiesPerModelPerSearchElement = (models || []).map(model => { - const bioEntitiesInGivenModel = (bioEntities?.data || []).filter( - entity => model.id === entity.bioEntity.model, - ); - - return { - modelName: model.name, - modelId: model.id, - numberOfEntities: bioEntitiesInGivenModel.length, - bioEntities: bioEntitiesInGivenModel, - }; - }); - - return bioEntitiesPerModelPerSearchElement.filter( - model => model.numberOfEntities !== SIZE_OF_EMPTY_ARRAY, - ); - }, -); - export const allVisibleBioEntitiesSelector = createSelector( - searchedBioEntitesSelectorOfCurrentMap, - searchedChemicalsBioEntitesOfCurrentMapSelector, - searchedDrugsBioEntitesOfCurrentMapSelector, - (content, chemicals, drugs): BioEntity[] => { + searchedModelElementsForCurrentModelSelector, + searchedChemicalsElementsOfCurrentMapSelector, + searchedDrugsElementsOfCurrentMapSelector, + (content, chemicals, drugs): Array<ModelElement> => { return [content, chemicals, drugs].flat(); }, ); -export const allElementsForSearchElementSelector = createSelector( - bioEntitiesForSelectedSearchElement, - chemicalsBioEntitiesForSelectedSearchElementSelector, - drugsBioEntitiesForSelectedSearchElementSelector, - (content, chemicals, drugs): BioEntity[] => { - const contentBioEntities = (content?.data || []).map(({ bioEntity }) => bioEntity); +export const allBioEntitiesForSearchElementSelector = createSelector( + searchedModelElementsSelector, + chemicalsElementsForSelectedSearchElementSelector, + drugsElementsForSelectedSearchElementSelector, + (content, chemicals, drugs): Array<ModelElement> => { + const contentBioEntities = (content?.data || []).map(({ modelElement }) => modelElement); return [contentBioEntities, chemicals || [], drugs || []].flat(); }, ); export const allElementsForSearchElementNumberByModelId = createSelector( - allElementsForSearchElementSelector, + allBioEntitiesForSearchElementSelector, (elements): Record<number, number> => { return elements.reduce( (acc, { model }) => ({ @@ -203,7 +65,7 @@ export const allElementsForSearchElementNumberByModelId = createSelector( ); export const allBioEntitiesElementsIdsSelector = createSelector( - allBioEntitesIdTabSelectorOfCurrentMap, + allSearchModelElementsIdTabForCurrentModelSelector, allChemicalsIdTabSelectorOfCurrentMap, allDrugsIdTabSelectorOfCurrentMap, (content, chemicals, drugs): ElementIdTabObj => { @@ -215,16 +77,9 @@ export const allBioEntitiesElementsIdsSelector = createSelector( }, ); -export const currentDrawerBioEntityRelatedSubmapSelector = createSelector( - currentDrawerModelElementSelector, - modelsDataSelector, - (bioEntity, models): MapModel | undefined => - models.find(({ id }) => id === bioEntity?.submodel?.mapId), -); - export const allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector = createSelector( - allSubmapConnectionsBioEntityOfCurrentSubmapSelector, + allModelElementsSubmapConnectionsForCurrentSubmapSelector, allElementsForSearchElementNumberByModelId, (submapConnectionsBioEntity, modelElementsNumber): ModelElement[] => { return submapConnectionsBioEntity.filter( @@ -234,16 +89,16 @@ export const allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSele ); export const allBioEntitiesWithTypeOfCurrentMapSelector = createSelector( - allBioEntitesSelectorOfCurrentMap, - allChemicalsBioEntitesOfCurrentMapSelector, - allDrugsBioEntitesOfCurrentMapSelector, + allSearchModelElementForCurrentModelSelector, + allChemicalsElementsOfCurrentMapSelector, + allDrugsElementsOfCurrentMapSelector, allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector, - (content, chemicals, drugs, submapConnections): BioEntityWithPinType[] => { + (content, chemicals, drugs, submapConnections): ModelElementWithPinType[] => { return [ - content.map(v => ({ ...v, type: 'bioEntity' as const })), + content.map(v => ({ ...v, type: 'modelElement' as const })), chemicals.map(v => ({ ...v, type: 'chemicals' as const })), drugs.map(v => ({ ...v, type: 'drugs' as const })), - submapConnections.map(v => ({ ...v, type: 'bioEntity' as const })), + submapConnections.map(v => ({ ...v, type: 'modelElement' as const })), ].flat(); }, ); @@ -255,35 +110,3 @@ export const allVisibleBioEntitiesIdsSelector = createSelector( return [...elements, ...submapConnections].map(e => e.id); }, ); - -export const currentDrawerElementCommentsSelector = createSelector( - currentDrawerModelElementSelector, - allCommentsSelectorOfCurrentMap, - (element, comments): Comment[] => { - if (element) { - return comments.filter( - comment => - comment.type === 'ALIAS' && - comment.modelId === element.model && - Number(comment.elementId) === element.id, - ); - } - return []; - }, -); - -export const currentDrawerReactionCommentsSelector = createSelector( - currentDrawerNewReactionSelector, - allCommentsSelectorOfCurrentMap, - (reaction, comments): Comment[] => { - if (reaction) { - return comments.filter( - comment => - comment.type === 'REACTION' && - comment.modelId === reaction.model && - Number(comment.elementId) === reaction.id, - ); - } - return []; - }, -); diff --git a/src/redux/bioEntity/bioEntity.slice.ts b/src/redux/bioEntity/bioEntity.slice.ts index 8e40b023c9c5100821ef1e1980a08143a100904a..45a1b7e721bb130c8c556c6e693140061a820f41 100644 --- a/src/redux/bioEntity/bioEntity.slice.ts +++ b/src/redux/bioEntity/bioEntity.slice.ts @@ -1,34 +1,15 @@ import { createSlice } from '@reduxjs/toolkit'; import { BIOENTITY_INITIAL_STATE } from './bioEntity.constants'; -import { - clearBioEntitiesReducer, - getBioEntityContentsReducer, - getMultiBioEntityContentsReducer, - setBioEntityContentsReducer, - setMultipleBioEntityContentsReducer, - toggleIsContentTabOpenedReducer, -} from './bioEntity.reducers'; +import { toggleIsContentTabOpenedReducer } from './bioEntity.reducers'; export const bioEntityContentsSlice = createSlice({ name: 'bioEntityContents', initialState: BIOENTITY_INITIAL_STATE, reducers: { - clearBioEntities: clearBioEntitiesReducer, toggleIsContentTabOpened: toggleIsContentTabOpenedReducer, - setBioEntityContents: setBioEntityContentsReducer, - setMultipleBioEntityContents: setMultipleBioEntityContentsReducer, - }, - extraReducers: builder => { - getBioEntityContentsReducer(builder); - getMultiBioEntityContentsReducer(builder); }, }); -export const { - clearBioEntities, - toggleIsContentTabOpened, - setBioEntityContents, - setMultipleBioEntityContents, -} = bioEntityContentsSlice.actions; +export const { toggleIsContentTabOpened } = bioEntityContentsSlice.actions; export default bioEntityContentsSlice.reducer; diff --git a/src/redux/bioEntity/bioEntity.thunks.test.ts b/src/redux/bioEntity/bioEntity.thunks.test.ts deleted file mode 100644 index fb433fb0fb0b18033b883c6fa8213432f5f3260b..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/bioEntity.thunks.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { apiPath } from '@/redux/apiPath'; -import { - ToolkitStoreWithSingleSlice, - createStoreInstanceUsingSliceReducer, -} from '@/utils/createStoreInstanceUsingSliceReducer'; -import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; -import { HttpStatusCode } from 'axios'; -import { unwrapResult } from '@reduxjs/toolkit'; -import contentsReducer from './bioEntity.slice'; -import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks'; -import { BioEntityContentsState } from './bioEntity.types'; - -jest.mock('../../utils/error-report/errorReporting'); - -const mockedAxiosClient = mockNetworkNewAPIResponse(); -const SEARCH_QUERY = 'park7'; - -describe('bioEntityContents thunks', () => { - let store = {} as ToolkitStoreWithSingleSlice<BioEntityContentsState>; - beforeEach(() => { - store = createStoreInstanceUsingSliceReducer('bioEntityContents', contentsReducer); - }); - describe('getBioEntityContents', () => { - it('should return data when data response from API is valid', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); - - const { payload } = await store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - expect(payload).toEqual(bioEntityResponseFixture.content); - }); - it('should return undefined when data response from API is not valid ', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - - const { payload } = await store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - expect(payload).toEqual(undefined); - }); - it('should handle error message when getBioEntityContents failed', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.NotFound, null); - - const action = await store.dispatch( - getBioEntity({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ); - expect(() => unwrapResult(action)).toThrow( - "Failed to fetch bio entity: The page you're looking for doesn't exist. Please verify the URL and try again.", - ); - }); - }); - describe('getMultiBioEntity', () => { - it('should return transformed bioEntityContent array', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); - - const data = await store - .dispatch( - getMultiBioEntity({ - searchQueries: [SEARCH_QUERY], - isPerfectMatch: false, - }), - ) - .unwrap(); - - expect(data).toEqual(bioEntityResponseFixture.content); - }); - it('should combine all returned bioEntityContent arrays and return array with all provided bioEntityContent elements', async () => { - mockedAxiosClient - .onGet( - apiPath.getBioEntityContentsStringWithQuery({ - searchQuery: SEARCH_QUERY, - isPerfectMatch: false, - }), - ) - .reply(HttpStatusCode.Ok, bioEntityResponseFixture); - - const data = await store - .dispatch( - getMultiBioEntity({ - searchQueries: [SEARCH_QUERY, SEARCH_QUERY], - isPerfectMatch: false, - }), - ) - .unwrap(); - - expect(data).toEqual([ - ...bioEntityResponseFixture.content, - ...bioEntityResponseFixture.content, - ]); - }); - }); -}); diff --git a/src/redux/bioEntity/bioEntity.thunks.ts b/src/redux/bioEntity/bioEntity.thunks.ts deleted file mode 100644 index 18fa812c93182be6346b9bff5f77a9168308a870..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/bioEntity.thunks.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { getBioEntity } from './thunks/getBioEntity'; -import { getMultiBioEntity } from './thunks/getMultiBioEntity'; - -export { getBioEntity, getMultiBioEntity }; diff --git a/src/redux/bioEntity/bioEntity.types.ts b/src/redux/bioEntity/bioEntity.types.ts index 9540daab08abcbe0efede2f8113e75ed81d2709c..07f11577cebfe10a899859f5e9c35850a6ba6534 100644 --- a/src/redux/bioEntity/bioEntity.types.ts +++ b/src/redux/bioEntity/bioEntity.types.ts @@ -1,19 +1,3 @@ -import { FetchDataState, MultiFetchDataState } from '@/types/fetchDataState'; -import { BioEntity, BioEntityContent } from '@/types/models'; -import { PayloadAction } from '@reduxjs/toolkit'; - -export type BioEntityContentsState = MultiFetchDataState<BioEntityContent[]> & { - submapConnections?: FetchDataState<BioEntity[]>; +export type BioEntityContentsState = { isContentTabOpened?: boolean; }; - -export type BioEntityContentSearchQuery = { - query: string | string[]; - params: { - perfectMatch: boolean; - }; -}; - -export type SetBioEntityContentActionPayload = BioEntityContent[]; - -export type SetBioEntityContentAction = PayloadAction<SetBioEntityContentActionPayload>; diff --git a/src/redux/bioEntity/thunks/getBioEntity.ts b/src/redux/bioEntity/thunks/getBioEntity.ts deleted file mode 100644 index bf54870a8db007d3785bb07606516fa293044a6f..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/thunks/getBioEntity.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { ThunkConfig } from '@/types/store'; -import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { getError } from '@/utils/error-report/getError'; -import { addNumbersToEntityNumberData } from '../../entityNumber/entityNumber.slice'; -import { BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants'; - -type GetBioEntityProps = PerfectSearchParams; - -export const getBioEntity = createAsyncThunk< - BioEntityContent[] | undefined, - GetBioEntityProps, - ThunkConfig ->( - 'project/getBioEntityContents', - async ({ searchQuery, isPerfectMatch, addNumbersToEntityNumber = true }, { dispatch }) => { - try { - const response = await axiosInstanceNewAPI.get<BioEntityResponse>( - apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch }), - ); - - const isDataValidBioEnity = validateDataUsingZodSchema( - response.data, - bioEntityResponseSchema, - ); - - 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) { - 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 deleted file mode 100644 index cddf3ff17b9f335ab6f654365febfe99293e46ff..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/thunks/getMultiBioEntity.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ZERO } from '@/constants/common'; -import type { AppDispatch, store } from '@/redux/store'; -import { BioEntityContent } from '@/types/models'; -import { 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 { fetchReactionsAndGetBioEntitiesIds } from './utils/fetchReactionsAndGetBioEntitiesIds'; - -type GetMultiBioEntityProps = PerfectMultiSearchParams; -type GetMultiBioEntityActions = PayloadAction<BioEntityContent[] | undefined | string>[]; // if error thrown, string containing error message is returned - -export const getMultiBioEntity = createAsyncThunk< - BioEntityContent[], - GetMultiBioEntityProps, - ThunkConfig ->( - 'project/getMultiBioEntity', - // eslint-disable-next-line consistent-return - async ({ searchQueries, isPerfectMatch }, { dispatch, getState }) => { - try { - const asyncGetBioEntityFunctions = searchQueries.map(searchQuery => - dispatch(getBioEntity({ searchQuery, isPerfectMatch, addNumbersToEntityNumber: false })), - ); - - const bioEntityContentsActions = (await Promise.all( - asyncGetBioEntityFunctions, - )) as GetMultiBioEntityActions; - - const bioEntityContents = bioEntityContentsActions - .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)); - - const bioEntitiesIds = await fetchReactionsAndGetBioEntitiesIds({ - bioEntityContents, - dispatch: dispatch as AppDispatch, - getState: getState as typeof store.getState, - }); - const bioEntitiesStringIds = bioEntitiesIds.map(id => String(id)); - if (bioEntitiesIds.length > ZERO) { - await dispatch( - getMultiBioEntity({ searchQueries: bioEntitiesStringIds, isPerfectMatch: true }), - ); - } - - return bioEntityContents; - } catch (error) { - return Promise.reject(getError({ error, prefix: MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX })); - } - }, -); diff --git a/src/redux/bioEntity/thunks/utils/fetchReactionsAndGetBioEntitiesIds.ts b/src/redux/bioEntity/thunks/utils/fetchReactionsAndGetBioEntitiesIds.ts deleted file mode 100644 index a29317ed7c590388c192556d7763505226acf27d..0000000000000000000000000000000000000000 --- a/src/redux/bioEntity/thunks/utils/fetchReactionsAndGetBioEntitiesIds.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; -import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; -import { openMapAndOrSetActiveIfSelected } from '@/redux/map/map.slice'; -import { modelsNameMapSelector } from '@/redux/models/models.selectors'; -import { getReactionsByIds } from '@/redux/reactions/reactions.thunks'; -import type { AppDispatch, store } from '@/redux/store'; -import type { BioEntityContent, NewReaction } from '@/types/models'; -import getModelElementsIdsFromReaction from '@/components/Map/MapViewer/utils/listeners/mouseClick/getModelElementsIdsFromReaction'; - -interface Args { - bioEntityContents: BioEntityContent[]; - dispatch: AppDispatch; - getState: typeof store.getState; -} - -type ReactionId = { - id: number; - modelId: number; -}; - -const getReactionsIdsFromBioEntities = (bioEntites: BioEntityContent[]): ReactionId[] => { - return bioEntites - .filter(c => c?.bioEntity?.idReaction) - .filter(c => typeof c?.bioEntity?.id === 'number') - .map(c => { - let id: number; - if (typeof c.bioEntity.id === 'string') { - id = parseInt(c.bioEntity.id, 10); - } else { - id = c.bioEntity.id; - } - return { id, modelId: c.bioEntity.model }; - }); -}; - -const fetchReactions = async ( - reactionsIds: ReactionId[], - dispatch: AppDispatch, -): Promise<NewReaction[]> => { - const result = await dispatch( - getReactionsByIds({ - ids: reactionsIds, - shouldConcat: true, - }), - ); - - // if it has error (toast show should be handled by getReactionsByIds) - if (typeof result.payload === 'string') { - return []; - } - - const reactions = result.payload?.data; - if (!reactions) { - return []; - } - - return reactions; -}; - -const handleReactionShowInfoAndOpenMap = async ( - { dispatch, getState }: Args, - firstReaction: NewReaction, -): Promise<void> => { - const modelsNames = modelsNameMapSelector(getState()); - - dispatch(openReactionDrawerById(firstReaction.id)); - dispatch(selectTab('')); - dispatch( - openMapAndOrSetActiveIfSelected({ - modelId: firstReaction.model, - modelName: modelsNames[firstReaction.model], - }), - ); -}; - -export const fetchReactionsAndGetBioEntitiesIds = async (args: Args): Promise<number[]> => { - const { dispatch, bioEntityContents } = args; - - const bioEntityReactionsIds = getReactionsIdsFromBioEntities(bioEntityContents || []); - if (bioEntityReactionsIds.length === SIZE_OF_EMPTY_ARRAY) { - return []; - } - - const reactions = await fetchReactions(bioEntityReactionsIds, dispatch); - if (reactions.length === SIZE_OF_EMPTY_ARRAY) { - return []; - } - - const bioEntitiesIds = reactions - .map(reaction => getModelElementsIdsFromReaction(reaction)) - .flat(); - const firstReaction = reactions[FIRST_ARRAY_ELEMENT]; - if (firstReaction) { - handleReactionShowInfoAndOpenMap(args, firstReaction); - } - - return bioEntitiesIds; -}; diff --git a/src/redux/chemicals/chemicals.selectors.ts b/src/redux/chemicals/chemicals.selectors.ts index 7f11e09a16adb38b957f2b516be5afcdda901fde..b9443769ab482e316ad6ed0adeda29058b1bcd79 100644 --- a/src/redux/chemicals/chemicals.selectors.ts +++ b/src/redux/chemicals/chemicals.selectors.ts @@ -2,7 +2,7 @@ import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { rootSelector } from '@/redux/root/root.selectors'; import { ElementId, ElementIdTabObj, Tab } from '@/types/elements'; import { MultiSearchData } from '@/types/fetchDataState'; -import { BioEntity, Chemical } from '@/types/models'; +import { Chemical, ModelElement } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; import { currentSelectedSearchElement } from '../drawer/drawer.selectors'; import { currentModelIdSelector } from '../models/models.selectors'; @@ -24,10 +24,10 @@ export const chemicalsForSelectedSearchElementSelector = createSelector( ), ); -export const chemicalsBioEntitiesForSelectedSearchElementSelector = createSelector( +export const chemicalsElementsForSelectedSearchElementSelector = createSelector( chemicalsSelector, currentSelectedSearchElement, - (chemicalsState, currentSearchElement): BioEntity[] => { + (chemicalsState, currentSearchElement): ModelElement[] => { return (chemicalsState?.data || []) .filter(({ searchQueryElement }) => currentSearchElement ? searchQueryElement === currentSearchElement : true, @@ -40,25 +40,25 @@ export const chemicalsBioEntitiesForSelectedSearchElementSelector = createSelect }, ); -export const searchedChemicalsBioEntitesOfCurrentMapSelector = createSelector( - chemicalsBioEntitiesForSelectedSearchElementSelector, +export const searchedChemicalsElementsOfCurrentMapSelector = createSelector( + chemicalsElementsForSelectedSearchElementSelector, currentModelIdSelector, - (chemicalsBioEntities, currentModelId): BioEntity[] => { - return (chemicalsBioEntities || []).filter(bioEntity => bioEntity.model === currentModelId); + (chemicalsElements, currentModelId): ModelElement[] => { + return (chemicalsElements || []).filter(element => element.model === currentModelId); }, ); -export const allChemicalsBioEntitesOfCurrentMapSelector = createSelector( +export const allChemicalsElementsOfCurrentMapSelector = createSelector( chemicalsSelector, currentModelIdSelector, - (chemicalsState, currentModelId): BioEntity[] => { + (chemicalsState, currentModelId): ModelElement[] => { return (chemicalsState?.data || []) .map(({ data }) => data || []) .flat() .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) .flat() .flat() - .filter(bioEntity => bioEntity.model === currentModelId); + .filter(element => element.model === currentModelId); }, ); @@ -79,8 +79,8 @@ export const allChemicalsIdTabSelectorOfCurrentMap = createSelector( .map(({ targetElements }) => targetElements) .flat() .flat() - .filter(bioEntity => bioEntity.model === currentModelId) - .map(bioEntity => [bioEntity.id, tab]), + .filter(element => element.model === currentModelId) + .map(element => [element.id, tab]), ), ) .flat() @@ -89,18 +89,6 @@ export const allChemicalsIdTabSelectorOfCurrentMap = createSelector( }, ); -export const allChemicalsBioEntitesOfAllMapsSelector = createSelector( - chemicalsSelector, - (chemicalsState): BioEntity[] => { - return (chemicalsState?.data || []) - .map(({ data }) => data || []) - .flat() - .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) - .flat() - .flat(); - }, -); - export const loadingChemicalsStatusSelector = createSelector( chemicalsForSelectedSearchElementSelector, state => state?.loading, diff --git a/src/redux/comment/comment.types.ts b/src/redux/comment/comment.types.ts index 1e727562c842f4e3835c43f3ff18df3110a305fe..c0e77c4e218fcb4746cef614eeacdc246d612ddc 100644 --- a/src/redux/comment/comment.types.ts +++ b/src/redux/comment/comment.types.ts @@ -1,11 +1,11 @@ import { FetchDataState } from '@/types/fetchDataState'; -import { BioEntity, Comment, NewReaction } from '@/types/models'; +import { ModelElement, Comment, NewReaction } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; import { Point } from '@/types/map'; export interface CommentsState extends FetchDataState<Comment[], []> { isOpen: boolean; - commentElement: BioEntity | null; + commentElement: ModelElement | null; commentReaction: NewReaction | null; } diff --git a/src/redux/contextMenu/contextMenu.selector.ts b/src/redux/contextMenu/contextMenu.selector.ts index 9c2db16a7bfd0824d0bb86093d8c756437a05de1..dc1222f8d5b026ad2fbafab431042ae7bd6a8e5c 100644 --- a/src/redux/contextMenu/contextMenu.selector.ts +++ b/src/redux/contextMenu/contextMenu.selector.ts @@ -3,8 +3,6 @@ import { rootSelector } from '../root/root.selectors'; export const contextMenuSelector = createSelector(rootSelector, state => state.contextMenu); -export const isContextMenuOpenSelector = createSelector(contextMenuSelector, state => state.isOpen); - export const currentSelectedBioEntityIdSelector = createSelector( contextMenuSelector, state => state.currentSelectedBioEntityId, diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 156866b2cfc3c75af9c2bea6a0980bce988dce42..e63d4610dc4aab756c608fa520ffeaf8383249de 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -86,7 +86,7 @@ export const displayBioEntitiesListReducer = ( state.drawerName = 'search'; state.searchDrawerState.currentStep = STEP.SECOND; state.searchDrawerState.listOfBioEnitites = action.payload; - state.searchDrawerState.stepType = 'bioEntity'; + state.searchDrawerState.stepType = 'modelElement'; }; export const displayGroupedSearchResultsReducer = (state: DrawerState): void => { diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts index a5f109ea50ed068bb49535e51bb3c9ec8a5eabed..26299df5e58059466425075913dca08a25b6387d 100644 --- a/src/redux/drawer/drawer.selectors.ts +++ b/src/redux/drawer/drawer.selectors.ts @@ -100,7 +100,7 @@ export const resultListSelector = createSelector( data: chemical, })); } - case 'bioEntity': + case 'modelElement': return undefined; case 'none': return undefined; diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index 4cf1440f73bc5b3fdb006c0dac47e1507503f298..4a80eea10a783e56b21c20561227e2c1b94f1ea1 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -2,12 +2,13 @@ import type { DrawerName } from '@/types/drawerName'; import { KeyedFetchDataState } from '@/types/fetchDataState'; import { BioEntityContent, Chemical, Drug } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; export type SearchDrawerState = { currentStep: number; - stepType: 'bioEntity' | 'drugs' | 'chemicals' | 'none'; + stepType: 'modelElement' | 'drugs' | 'chemicals' | 'none'; selectedValue: BioEntityContent | Drug | Chemical | undefined; - listOfBioEnitites: BioEntityContent[]; + listOfBioEnitites: Array<SearchModelElementDataState>; selectedSearchElement: string; }; @@ -49,8 +50,5 @@ export type OpenReactionDrawerByIdAction = PayloadAction<OpenReactionDrawerByIdP export type OpenBioEntityDrawerByIdPayload = number | string; export type OpenBioEntityDrawerByIdAction = PayloadAction<OpenBioEntityDrawerByIdPayload>; -export type SetSelectedSearchElementPayload = string; -export type SetSelectedSearchElementAction = PayloadAction<SetSelectedSearchElementPayload>; - export type OpenCommentDrawerByIdPayload = number; export type OpenCommentDrawerByIdAction = PayloadAction<OpenCommentDrawerByIdPayload>; diff --git a/src/redux/drugs/drugs.reducers.test.ts b/src/redux/drugs/drugs.reducers.test.ts index 69d29267ef92cdddead5846ce3655c0785b3c2d7..55af806f91a1b139bdd6fd7bf64916f68e98b756 100644 --- a/src/redux/drugs/drugs.reducers.test.ts +++ b/src/redux/drugs/drugs.reducers.test.ts @@ -40,7 +40,7 @@ describe('drugs reducer', () => { const { type } = await store.dispatch(getDrugs(SEARCH_QUERY)); const { data } = store.getState().drugs; const drugsWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, + searchModelElement => searchModelElement.searchQueryElement === SEARCH_QUERY, ); expect(type).toBe('project/getDrugs/fulfilled'); @@ -60,7 +60,7 @@ describe('drugs reducer', () => { const action = await store.dispatch(getDrugs(SEARCH_QUERY)); const { data } = store.getState().drugs; const drugsWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, + searchModelElement => searchModelElement.searchQueryElement === SEARCH_QUERY, ); expect(() => unwrapResult(action)).toThrow( @@ -84,7 +84,7 @@ describe('drugs reducer', () => { const { data } = store.getState().drugs; const drugsWithSearchElement = data.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, + searchModelElement => searchModelElement.searchQueryElement === SEARCH_QUERY, ); expect(drugsWithSearchElement).toEqual({ @@ -98,7 +98,7 @@ describe('drugs reducer', () => { const { data: dataPromiseFulfilled } = store.getState().drugs; const drugsWithSearchElementFulfilled = dataPromiseFulfilled.find( - bioEntity => bioEntity.searchQueryElement === SEARCH_QUERY, + searchModelElement => searchModelElement.searchQueryElement === SEARCH_QUERY, ); expect(drugsWithSearchElementFulfilled).toEqual({ searchQueryElement: SEARCH_QUERY, diff --git a/src/redux/drugs/drugs.selectors.ts b/src/redux/drugs/drugs.selectors.ts index ecaf082afd32d1c1a5e0724bc28250d71e9f39eb..a2a616c40db92fb1d6e43a6407be1c109fce8f1c 100644 --- a/src/redux/drugs/drugs.selectors.ts +++ b/src/redux/drugs/drugs.selectors.ts @@ -2,7 +2,7 @@ import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { rootSelector } from '@/redux/root/root.selectors'; import { ElementId, ElementIdTabObj, Tab } from '@/types/elements'; import { MultiSearchData } from '@/types/fetchDataState'; -import { BioEntity, Drug } from '@/types/models'; +import { Drug, ModelElement } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; import { currentSelectedSearchElement } from '../drawer/drawer.selectors'; import { currentModelIdSelector } from '../models/models.selectors'; @@ -38,10 +38,10 @@ export const numberOfDrugsSelector = createSelector( }, ); -export const drugsBioEntitiesForSelectedSearchElementSelector = createSelector( +export const drugsElementsForSelectedSearchElementSelector = createSelector( drugsSelector, currentSelectedSearchElement, - (drugsState, currentSearchElement): BioEntity[] => { + (drugsState, currentSearchElement): ModelElement[] => { return (drugsState?.data || []) .filter(({ searchQueryElement }) => currentSearchElement ? searchQueryElement === currentSearchElement : true, @@ -54,25 +54,25 @@ export const drugsBioEntitiesForSelectedSearchElementSelector = createSelector( }, ); -export const searchedDrugsBioEntitesOfCurrentMapSelector = createSelector( - drugsBioEntitiesForSelectedSearchElementSelector, +export const searchedDrugsElementsOfCurrentMapSelector = createSelector( + drugsElementsForSelectedSearchElementSelector, currentModelIdSelector, - (drugsBioEntities, currentModelId): BioEntity[] => { - return (drugsBioEntities || []).filter(bioEntity => bioEntity.model === currentModelId); + (drugsElements, currentModelId): ModelElement[] => { + return (drugsElements || []).filter(element => element.model === currentModelId); }, ); -export const allDrugsBioEntitesOfCurrentMapSelector = createSelector( +export const allDrugsElementsOfCurrentMapSelector = createSelector( drugsSelector, currentModelIdSelector, - (drugsState, currentModelId): BioEntity[] => { + (drugsState, currentModelId): ModelElement[] => { return (drugsState?.data || []) .map(({ data }) => data || []) .flat() .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) .flat() .flat() - .filter(bioEntity => bioEntity.model === currentModelId); + .filter(element => element.model === currentModelId); }, ); @@ -93,8 +93,8 @@ export const allDrugsIdTabSelectorOfCurrentMap = createSelector( .map(({ targetElements }) => targetElements) .flat() .flat() - .filter(bioEntity => bioEntity.model === currentModelId) - .map(bioEntity => [bioEntity.id, tab]), + .filter(element => element.model === currentModelId) + .map(element => [element.id, tab]), ), ) .flat() @@ -102,15 +102,3 @@ export const allDrugsIdTabSelectorOfCurrentMap = createSelector( ); }, ); - -export const allDrugsBioEntitesOfAllMapsSelector = createSelector( - drugsSelector, - (drugsState): BioEntity[] => { - return (drugsState?.data || []) - .map(({ data }) => data || []) - .flat() - .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) - .flat() - .flat(); - }, -); diff --git a/src/redux/glyphs/glyphs.selectors.ts b/src/redux/glyphs/glyphs.selectors.ts index 6f99b412a61aa37a7d10383424794f63e1b9638e..b5692b2511866f15824e6f1cb2b3ac815bbf31c5 100644 --- a/src/redux/glyphs/glyphs.selectors.ts +++ b/src/redux/glyphs/glyphs.selectors.ts @@ -4,3 +4,13 @@ import { rootSelector } from '@/redux/root/root.selectors'; export const glyphsSelector = createSelector(rootSelector, state => state.glyphs); export const glyphsDataSelector = createSelector(glyphsSelector, state => state.data); + +export const glyphFileNameByIdSelector = createSelector( + [glyphsDataSelector, (_state, glyphId: number | null): number | null => glyphId], + (state, glyphId) => { + if (glyphId) { + return state.find(glyph => glyph.id === glyphId)?.filename; + } + return 'No image file'; + }, +); diff --git a/src/redux/layers/layers.mock.ts b/src/redux/layers/layers.mock.ts index 729624ff10b3019218ee7d49e025d394cb3e4295..e949602bf949559bdfd71ce62e6c233e4e887778 100644 --- a/src/redux/layers/layers.mock.ts +++ b/src/redux/layers/layers.mock.ts @@ -7,7 +7,8 @@ export const LAYERS_STATE_INITIAL_MOCK: LayersState = {}; export const LAYER_STATE_DEFAULT_DATA = { layers: [], layersVisibility: {}, - activeLayer: null, + activeLayers: [], + drawLayer: null, }; export const LAYERS_STATE_INITIAL_LAYER_MOCK: FetchDataState<LayersVisibilitiesState> = { diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index d115cbd570e9c420caa96349562765b060a8245b..7bcb10bafd7f54305f08846cfbec804fb43436a2 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -25,7 +25,9 @@ import layersReducer, { layerAddText, layerDeleteImage, layerUpdateImage, - setActiveLayer, + setDrawLayer, + setLayerToActive, + setLayerToInactive, setLayerVisibility, } from './layers.slice'; import { LayersState } from './layers.types'; @@ -33,7 +35,7 @@ import { LayersState } from './layers.types'; const mockedAxiosClient = mockNetworkNewAPIResponse(); const INITIAL_STATE: LayersState = LAYERS_STATE_INITIAL_MOCK; - +const activeLayers = layersFixture.content.filter(layer => !layer.locked).map(layer => layer.id); const layersState: LayersState = { 1: { data: { @@ -50,7 +52,8 @@ const layersState: LayersState = { images: {}, }, ], - activeLayer: layerFixture.id, + activeLayers, + drawLayer: null, }, loading: 'idle', error: DEFAULT_ERROR, @@ -93,7 +96,8 @@ describe('layers reducer', () => { expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual({ - activeLayer: null, + activeLayers, + drawLayer: null, layers: [ { details: layersFixture.content[0], @@ -123,7 +127,8 @@ describe('layers reducer', () => { expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual({ - activeLayer: null, + activeLayers: [], + drawLayer: null, layers: [], layersVisibility: {}, }); @@ -157,7 +162,8 @@ describe('layers reducer', () => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers[1]; expect(dataPromiseFulfilled).toEqual({ - activeLayer: null, + activeLayers, + drawLayer: null, layers: [ { details: layersFixture.content[0], @@ -186,12 +192,28 @@ describe('layers reducer', () => { expect(layersStore.getState().layers[1].data?.layersVisibility[layerFixture.id]).toBe(false); }); - it('should handle setActiveLayerReducer', () => { + it('should handle setLayerToActiveReducer', () => { + const { store: layersStore } = getReduxWrapperWithStore({ + layers: layersState, + }); + layersStore.dispatch(setLayerToActive({ modelId: 1, layerId: layerFixture.id })); + expect(layersStore.getState().layers[1].data?.activeLayers).toContain(layerFixture.id); + }); + + it('should handle setLayerToActiveReducer', () => { + const { store: layersStore } = getReduxWrapperWithStore({ + layers: layersState, + }); + layersStore.dispatch(setLayerToInactive({ modelId: 1, layerId: layerFixture.id })); + expect(layersStore.getState().layers[1].data?.activeLayers).not.toContain(layerFixture.id); + }); + + it('should handle setDrawLayerReducer', () => { const { store: layersStore } = getReduxWrapperWithStore({ layers: layersState, }); - layersStore.dispatch(setActiveLayer({ modelId: 1, layerId: layerFixture.id })); - expect(layersStore.getState().layers[1].data?.activeLayer).toBe(layerFixture.id); + layersStore.dispatch(setDrawLayer({ modelId: 1, layerId: layerFixture.id })); + expect(layersStore.getState().layers[1].data?.drawLayer).toBe(layerFixture.id); }); it('should handle layerAddImageReducer', () => { diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index 17c01b90279b482f34258ce36e550abac79d5bb4..0213d7145366de6126d4855bab3665d5de0c3c66 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -1,6 +1,6 @@ /* eslint-disable no-magic-numbers */ import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; -import { getLayersForModel } from '@/redux/layers/layers.thunks'; +import { getLayerImage, getLayersForModel, getLayerText } from '@/redux/layers/layers.thunks'; import { LayersState } from '@/redux/layers/layers.types'; import { LAYER_STATE_DEFAULT_DATA, @@ -38,6 +38,38 @@ export const getLayersForModelReducer = (builder: ActionReducerMapBuilder<Layers }); }; +export const getLayerImageReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { + builder.addCase(getLayerImage.fulfilled, (state, action) => { + const { modelId, layerId } = action.meta.arg; + const { data } = state[modelId]; + const layerImage = action.payload; + if (!data || !layerImage) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + layer.images[layerImage.id] = layerImage; + }); +}; + +export const getLayerTextReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { + builder.addCase(getLayerText.fulfilled, (state, action) => { + const { modelId, layerId } = action.meta.arg; + const { data } = state[modelId]; + const layerText = action.payload; + if (!data || !layerText) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + layer.texts[layerText.id] = layerText; + }); +}; + export const setLayerVisibilityReducer = ( state: LayersState, action: PayloadAction<{ modelId: number; visible: boolean; layerId: number }>, @@ -52,7 +84,43 @@ export const setLayerVisibilityReducer = ( } }; -export const setActiveLayerReducer = ( +export const setLayerToInactiveReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number }>, +): void => { + const { modelId, layerId } = action.payload; + if (!state[modelId]) { + return; + } + const { data } = state[modelId]; + if (!data) { + return; + } + const index = data.activeLayers.findIndex(activeLayerId => activeLayerId === layerId); + if (index !== -1) { + data.activeLayers.splice(index, 1); + } +}; + +export const setLayerToActiveReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number }>, +): void => { + const { modelId, layerId } = action.payload; + if (!state[modelId]) { + return; + } + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.activeLayers.find(activeLayerId => activeLayerId === layerId); + if (!layer) { + data.activeLayers.push(layerId); + } +}; + +export const setDrawLayerReducer = ( state: LayersState, action: PayloadAction<{ modelId: number; layerId: number | null }>, ): void => { @@ -64,7 +132,7 @@ export const setActiveLayerReducer = ( if (!data) { return; } - data.activeLayer = layerId; + data.drawLayer = layerId; }; export const layerAddImageReducer = ( diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 8f0c8664560b8c129fd1d1250ad517fc4717aa86..8f016c8aa15c09cc7a866f887217ad2df2f3b3fd 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -11,9 +11,19 @@ export const layersStateForCurrentModelSelector = createSelector( (state, currentModelId) => state[currentModelId], ); -export const layersActiveLayerSelector = createSelector( +export const layersActiveLayersSelector = createSelector( layersStateForCurrentModelSelector, - state => state?.data?.activeLayer || null, + state => state?.data?.activeLayers || [], +); + +export const layersDrawLayerSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.data?.drawLayer, +); + +export const layerByIdSelector = createSelector( + [layersStateForCurrentModelSelector, (_state, layerId: number): number => layerId], + (state, layerId) => state?.data?.layers.find(layer => layer.details.id === layerId), ); export const layersLoadingSelector = createSelector( @@ -49,3 +59,22 @@ export const highestZIndexSelector = createSelector(layersForCurrentModelSelecto return Math.max(maxZ, layerMaxZ); }, 0); }); + +export const lowestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { + if (!layers || layers.length === 0) return 0; + + const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; + + return layers.reduce((minZ, layer) => { + const textsMinZ = getMinZFromItems(Object.values(layer.texts)); + const rectsMinZ = getMinZFromItems(layer.rects); + const ovalsMinZ = getMinZFromItems(layer.ovals); + const linesMinZ = getMinZFromItems(layer.lines); + const imagesMinZ = getMinZFromItems(Object.values(layer.images)); + + const layerMinZ = Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); + + return Math.min(minZ, layerMinZ); + }, 0); +}); diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index 82f15ba3d10350c510aac0ab57f25e51d5456b26..dcd20ceba8302ea711ac5f583abffbf65e051dac 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -1,13 +1,17 @@ import { createSlice } from '@reduxjs/toolkit'; import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; import { + getLayerImageReducer, + getLayerTextReducer, getLayersForModelReducer, layerAddImageReducer, layerAddTextReducer, layerDeleteImageReducer, layerUpdateImageReducer, - setActiveLayerReducer, setLayerVisibilityReducer, + setDrawLayerReducer, + setLayerToInactiveReducer, + setLayerToActiveReducer, } from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ @@ -15,24 +19,30 @@ export const layersSlice = createSlice({ initialState: LAYERS_STATE_INITIAL_MOCK, reducers: { setLayerVisibility: setLayerVisibilityReducer, - setActiveLayer: setActiveLayerReducer, + setLayerToInactive: setLayerToInactiveReducer, + setLayerToActive: setLayerToActiveReducer, layerAddImage: layerAddImageReducer, layerUpdateImage: layerUpdateImageReducer, layerDeleteImage: layerDeleteImageReducer, layerAddText: layerAddTextReducer, + setDrawLayer: setDrawLayerReducer, }, extraReducers: builder => { getLayersForModelReducer(builder); + getLayerImageReducer(builder); + getLayerTextReducer(builder); }, }); export const { setLayerVisibility, - setActiveLayer, + setLayerToInactive, + setLayerToActive, layerAddImage, layerUpdateImage, layerDeleteImage, layerAddText, + setDrawLayer, } = layersSlice.actions; export default layersSlice.reducer; diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index 160bdcdfb7fbaa105fbc03acbc595ca4d783f97f..77e10bdb3dba0dee10813778ab1ad3316cf8822f 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -58,8 +58,12 @@ describe('layers thunks', () => { .reply(HttpStatusCode.Ok, layerImagesFixture); const { payload } = await store.dispatch(getLayersForModel(1)); + const activeLayers = layersFixture.content + .filter(layer => !layer.locked) + .map(layer => layer.id); expect(payload).toEqual({ - activeLayer: null, + activeLayers, + drawLayer: null, layers: [ { details: layersFixture.content[0], diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 217e407f22e8b8b6d92cae5a31bfcce9b841ea56..847d84d22a8a47c26f12b1d3be6beea86e0c3ab3 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -84,15 +84,14 @@ export const getLayersForModel = createAsyncThunk< acc[layer.details.id] = layer.details.visible; return acc; }, {}); - let activeLayer = null; - const activeLayers = layers.filter(layer => layer.details.visible); - if (activeLayers.length) { - activeLayer = activeLayers[0].details.id; - } + const activeLayers = layers + .filter(layer => !layer.details.locked) + .map(layer => layer.details.id); return { layers, layersVisibility, - activeLayer, + activeLayers, + drawLayer: null, }; } catch (error) { return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); @@ -150,6 +149,27 @@ export const removeLayer = createAsyncThunk< } }); +export const getLayerImage = createAsyncThunk< + LayerImage | null, + { + modelId: number; + layerId: number; + imageId: number; + }, + ThunkConfig +>('vectorMap/getLayerImage', async ({ modelId, layerId, imageId }) => { + try { + const { data } = await axiosInstanceNewAPI.get<LayerImage>( + apiPath.getLayerImageObject(modelId, layerId, imageId), + ); + const isDataValid = validateDataUsingZodSchema(data, layerImageSchema); + + return isDataValid ? data : null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + export const addLayerImageObject = createAsyncThunk< LayerImage | null, { @@ -266,3 +286,24 @@ export const addLayerText = createAsyncThunk< return Promise.reject(getError({ error })); } }); + +export const getLayerText = createAsyncThunk< + LayerText | null, + { + modelId: number; + layerId: number; + textId: number; + }, + ThunkConfig +>('vectorMap/getLayerText', async ({ modelId, layerId, textId }) => { + try { + const { data } = await axiosInstanceNewAPI.get<LayerText>( + apiPath.getLayerText(modelId, layerId, textId), + ); + const isDataValid = validateDataUsingZodSchema(data, layerTextSchema); + + return isDataValid ? data : null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index de12354090eae1189860a26f2b7e6d9139b3d56e..60e42717dfce7d988376e591468ba449484e14e7 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -32,7 +32,8 @@ export type LayerVisibilityState = { export type LayersVisibilitiesState = { layersVisibility: LayerVisibilityState; layers: LayerState[]; - activeLayer: number | null; + activeLayers: Array<number>; + drawLayer: number | null; }; export type LayersState = KeyedFetchDataState<LayersVisibilitiesState>; diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts index ce150602d346927bee17c9f8be85421ff5dbadc1..da3f5f7f7d57126be4f93d49166881a093f58a47 100644 --- a/src/redux/mapEditTools/mapEditTools.reducers.ts +++ b/src/redux/mapEditTools/mapEditTools.reducers.ts @@ -8,11 +8,7 @@ export const mapEditToolsSetActiveActionReducer = ( state: MapEditToolsState, action: PayloadAction<keyof typeof MAP_EDIT_ACTIONS | null>, ): void => { - if (state.activeAction !== action.payload) { - state.activeAction = action.payload; - } else { - state.activeAction = null; - } + state.activeAction = action.payload; }; export const mapEditToolsSetLayerObjectReducer = ( diff --git a/src/redux/modelElements/modelElements.constants.ts b/src/redux/modelElements/modelElements.constants.ts index 42427c11f62830690150e10913bfb1ea0c0ec2bc..0d017748a38a4f949893a95b5418d4b458b1bcdd 100644 --- a/src/redux/modelElements/modelElements.constants.ts +++ b/src/redux/modelElements/modelElements.constants.ts @@ -1,3 +1,7 @@ export const MODEL_ELEMENTS_FETCHING_ERROR_PREFIX = 'Failed to fetch model elements'; export const MODEL_ELEMENTS_DEFAULT_COMPARTMENT_NAME = 'default'; + +export const MULTI_MODEL_ELEMENTS_SEARCH_ERROR_PREFIX = 'Failed to search multi model elements'; + +export const MODEL_ELEMENT_SEARCH_ERROR_PREFIX = 'Failed to search model element'; diff --git a/src/redux/modelElements/modelElements.mock.ts b/src/redux/modelElements/modelElements.mock.ts index 583f03f397d76cd5f9e857f1f46cb64d8e2d6614..70dc3406202feddeb2d3a5327eb802d2292d5020 100644 --- a/src/redux/modelElements/modelElements.mock.ts +++ b/src/redux/modelElements/modelElements.mock.ts @@ -1,12 +1,47 @@ import { DEFAULT_ERROR } from '@/constants/errors'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; -import { FetchDataState } from '@/types/fetchDataState'; +import { + ModelElementsState, + SearchModelElementDataState, +} from '@/redux/modelElements/modelElements.types'; +import { FetchDataState, MultiFetchDataState, MultiSearchData } from '@/types/fetchDataState'; import { ModelElement } from '@/types/models'; +import { HISTAMINE_MAP_ID } from '@/constants/mocks'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; -export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = {}; +export const MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK: MultiFetchDataState< + SearchModelElementDataState[] +> = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; + +export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = { + data: {}, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, +}; export const MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK: FetchDataState<Array<ModelElement>> = { data: [], loading: 'idle', error: DEFAULT_ERROR, }; + +export const MODEL_ELEMENT_LINKING_TO_SUBMAP: ModelElement = { + ...modelElementFixture, + submodel: { + mapId: HISTAMINE_MAP_ID, + type: 'DONWSTREAM_TARGETS', + }, +}; + +export const MODEL_ELEMENTS_SEARCH_LINKING_TO_SUBMAP_DATA_MOCK: MultiSearchData< + SearchModelElementDataState[] +>[] = [ + { + data: [{ modelElement: MODEL_ELEMENT_LINKING_TO_SUBMAP, perfect: false }], + searchQueryElement: '', + loading: 'succeeded', + error: DEFAULT_ERROR, + }, +]; diff --git a/src/redux/modelElements/modelElements.reducers.test.ts b/src/redux/modelElements/modelElements.reducers.test.ts index 8c499a6dc5384cfa611805bf79ff1c831b24a860..9a3bc2a9e0052636b990c6682ed82723353c6345 100644 --- a/src/redux/modelElements/modelElements.reducers.test.ts +++ b/src/redux/modelElements/modelElements.reducers.test.ts @@ -9,10 +9,17 @@ import { HttpStatusCode } from 'axios'; import { unwrapResult } from '@reduxjs/toolkit'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; +import { + getModelElementsForModel, + searchModelElement, +} from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; +import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; const mockedAxiosClient = mockNetworkNewAPIResponse(); +const SEARCH_QUERY = 'park7'; describe('model elements reducer', () => { let store = {} as ToolkitStoreWithSingleSlice<ModelElementsState>; @@ -23,7 +30,7 @@ describe('model elements reducer', () => { it('should match initial state', () => { const action = { type: 'unknown' }; - expect(modelElementsReducer(undefined, action)).toEqual({}); + expect(modelElementsReducer(undefined, action)).toEqual(MODEL_ELEMENTS_INITIAL_STATE_MOCK); }); it('should update store after successful getModelElementsForModel query', async () => { @@ -32,7 +39,7 @@ describe('model elements reducer', () => { .reply(HttpStatusCode.Ok, modelElementsFixture); const { type } = await store.dispatch(getModelElementsForModel(0)); - const { data, loading, error } = store.getState().modelElements[0]; + const { data, loading, error } = store.getState().modelElements.data[0]; expect(type).toBe('modelElements/getModelElementsForModel/fulfilled'); expect(loading).toEqual('succeeded'); @@ -44,7 +51,7 @@ describe('model elements reducer', () => { mockedAxiosClient.onGet(apiPath.getModelElements(0)).reply(HttpStatusCode.NotFound, []); const action = await store.dispatch(getModelElementsForModel(0)); - const { data, loading, error } = store.getState().modelElements[0]; + const { data, loading, error } = store.getState().modelElements.data[0]; expect(action.type).toBe('modelElements/getModelElementsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( @@ -62,16 +69,129 @@ describe('model elements reducer', () => { const modelElementsPromise = store.dispatch(getModelElementsForModel(0)); - const { data, loading } = store.getState().modelElements[0]; + const { data, loading } = store.getState().modelElements.data[0]; expect(data).toEqual([]); expect(loading).toEqual('pending'); modelElementsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = - store.getState().modelElements[0]; + store.getState().modelElements.data[0]; expect(dataPromiseFulfilled).toEqual(modelElementsFixture.content); expect(promiseFulfilled).toEqual('succeeded'); }); }); + + it('should update store after succesfull searchModelElement query', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + const { type } = await store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + const { search } = store.getState().modelElements; + const modelElementWithSearchQuery = search.data.find( + modelElement => modelElement.searchQueryElement === SEARCH_QUERY, + ); + + expect(type).toBe('modelElements/searchModelElement/fulfilled'); + expect(modelElementWithSearchQuery).toEqual({ + searchQueryElement: SEARCH_QUERY, + data: bioEntityResponseFixture.content.map(data => ({ + modelElement: data.bioEntity, + perfect: data.perfect, + })), + loading: 'succeeded', + error: DEFAULT_ERROR, + }); + }); + + it('should update store after failed getBioEntity query', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.NotFound, bioEntityResponseFixture); + + const action = await store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + const { search } = store.getState().modelElements; + + const modelElementWithSearchQuery = search.data.find( + modelElement => modelElement.searchQueryElement === SEARCH_QUERY, + ); + expect(action.type).toBe('modelElements/searchModelElement/rejected'); + expect(() => unwrapResult(action)).toThrow( + "Failed to search model element: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); + expect(modelElementWithSearchQuery).toEqual({ + searchQueryElement: SEARCH_QUERY, + data: undefined, + loading: 'failed', + error: DEFAULT_ERROR, + }); + }); + + it('should update store on loading getBioEntity query', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + const searchModelElementPromise = store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + + const { search } = store.getState().modelElements; + const modelElementWithSearchQuery = search.data.find( + modelElement => modelElement.searchQueryElement === SEARCH_QUERY, + ); + + expect(modelElementWithSearchQuery).toEqual({ + searchQueryElement: SEARCH_QUERY, + data: undefined, + loading: 'pending', + error: DEFAULT_ERROR, + }); + + searchModelElementPromise.then(() => { + const { search: searchPromiseFulfilled } = store.getState().modelElements; + const modelElementWithSearchQueryFulfilled = searchPromiseFulfilled.data.find( + modelElement => modelElement.searchQueryElement === SEARCH_QUERY, + ); + + expect(modelElementWithSearchQueryFulfilled).toEqual({ + searchQueryElement: SEARCH_QUERY, + data: bioEntityResponseFixture.content.map(data => ({ + modelElement: data.bioEntity, + perfect: data.perfect, + })), + loading: 'succeeded', + error: DEFAULT_ERROR, + }); + }); + }); }); diff --git a/src/redux/modelElements/modelElements.reducers.ts b/src/redux/modelElements/modelElements.reducers.ts index 777295cf15053679c1d33127f58f2fbc8babc2b2..933f2e698ed77e6c3b96353e8ca799111d48b22f 100644 --- a/src/redux/modelElements/modelElements.reducers.ts +++ b/src/redux/modelElements/modelElements.reducers.ts @@ -1,36 +1,135 @@ -import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { + getModelElementsForModel, + searchModelElement, + searchMultiModelElements, +} from '@/redux/modelElements/modelElements.thunks'; +import { + ModelElementsState, + SearchModelElementDataState, +} from '@/redux/modelElements/modelElements.types'; import { MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK } from '@/redux/modelElements/modelElements.mock'; import { DEFAULT_ERROR } from '@/constants/errors'; +import { ModelElement } from '@/types/models'; export const getModelElementsReducer = ( builder: ActionReducerMapBuilder<ModelElementsState>, ): void => { builder.addCase(getModelElementsForModel.pending, (state, action) => { const modelId = action.meta.arg; - if (state[modelId]) { - state[modelId].loading = 'pending'; + if (state.data[modelId]) { + state.data[modelId].loading = 'pending'; } else { - state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'pending' }; + state.data[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'pending' }; } }); builder.addCase(getModelElementsForModel.fulfilled, (state, action) => { const modelId = action.meta.arg; const data = action.payload || []; - if (state[modelId]) { - state[modelId].data = data; - state[modelId].loading = 'succeeded'; + if (state.data[modelId]) { + state.data[modelId].data = data; + state.data[modelId].loading = 'succeeded'; } else { - state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; + state.data[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; } }); builder.addCase(getModelElementsForModel.rejected, (state, action) => { const modelId = action.meta.arg; - if (state[modelId]) { - state[modelId].loading = 'failed'; + if (state.data[modelId]) { + state.data[modelId].loading = 'failed'; } else { - state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'failed' }; + state.data[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'failed' }; } }); }; + +export const searchModelElementReducer = ( + builder: ActionReducerMapBuilder<ModelElementsState>, +): void => { + builder.addCase(searchModelElement.pending, (state, action) => { + state.search.data.push({ + searchQueryElement: action.meta.arg.searchQuery, + data: undefined, + loading: 'pending', + error: DEFAULT_ERROR, + }); + }); + builder.addCase(searchModelElement.fulfilled, (state, action) => { + const bioEntities = state.search.data.find( + bioEntity => bioEntity.searchQueryElement === action.meta.arg.searchQuery, + ); + if (bioEntities) { + bioEntities.data = action.payload?.map(data => ({ + modelElement: data.bioEntity, + perfect: data.perfect, + })); + bioEntities.loading = 'succeeded'; + } + }); + builder.addCase(searchModelElement.rejected, (state, action) => { + const bioEntities = state.search.data.find( + bioEntity => bioEntity.searchQueryElement === action.meta.arg.searchQuery, + ); + if (bioEntities) { + bioEntities.loading = 'failed'; + } + }); +}; + +export const searchMultiModelElementsReducer = ( + builder: ActionReducerMapBuilder<ModelElementsState>, +): void => { + builder.addCase(searchMultiModelElements.pending, state => { + state.search.data = []; + state.search.loading = 'pending'; + }); + builder.addCase(searchMultiModelElements.fulfilled, state => { + state.search.loading = 'succeeded'; + }); + builder.addCase(searchMultiModelElements.rejected, state => { + state.search.loading = 'failed'; + }); +}; + +export const setModelElementSearchReducer = ( + state: ModelElementsState, + action: PayloadAction<SearchModelElementDataState>, +): void => { + state.search = { + data: [ + { + data: [action.payload], + loading: 'succeeded', + error: DEFAULT_ERROR, + searchQueryElement: action.payload.modelElement.id.toString(), + }, + ], + loading: 'succeeded', + error: DEFAULT_ERROR, + }; +}; + +export const setMultipleModelElementSearchReducer = ( + state: ModelElementsState, + action: PayloadAction<ModelElement[]>, +): void => { + state.search.data = action.payload.map(modelElement => { + return { + data: [ + { + modelElement, + perfect: true, + }, + ], + loading: 'succeeded', + error: DEFAULT_ERROR, + searchQueryElement: modelElement.id.toString(), + }; + }); + state.search.loading = 'succeeded'; +}; + +export const clearSearchModelElementsReducer = (state: ModelElementsState): void => { + state.search.data = []; + state.search.loading = 'idle'; +}; diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index 55c08d7a0b13596f074c08a319c8d13bd0dfbb73..317aa053f9301b1f18dd39bc4968ff7c246aea2c 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -1,22 +1,39 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; -import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { currentSearchedBioEntityId } from '@/redux/drawer/drawer.selectors'; -import { ModelElement } from '@/types/models'; +import { currentModelIdSelector, modelsDataSelector } from '@/redux/models/models.selectors'; +import { + currentSearchedBioEntityId, + currentSelectedSearchElement, +} from '@/redux/drawer/drawer.selectors'; +import { Comment, MapModel, ModelElement } from '@/types/models'; import { MODEL_ELEMENTS_DEFAULT_COMPARTMENT_NAME } from '@/redux/modelElements/modelElements.constants'; import { COMPARTMENT_SBO_TERM } from '@/components/Map/MapViewer/MapViewer.constants'; +import { MultiSearchData } from '@/types/fetchDataState'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { currentSelectedBioEntityIdSelector } from '@/redux/contextMenu/contextMenu.selector'; +import { ElementIdTabObj } from '@/types/elements'; +import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; + +export const modelElementsStateSelector = createSelector( + rootSelector, + state => state.modelElements, +); -export const modelElementsSelector = createSelector(rootSelector, state => state.modelElements); +export const modelElementsDataSelector = createSelector( + modelElementsStateSelector, + state => state.data, +); export const modelElementsStateForCurrentModelSelector = createSelector( - modelElementsSelector, + modelElementsDataSelector, currentModelIdSelector, - (state, currentModelId) => state[currentModelId], + (data, currentModelId) => data[currentModelId], ); export const modelElementsByModelIdSelector = createSelector( - [modelElementsSelector, (_state, modelId: number): number => modelId], - (state, modelId) => state[modelId]?.data || [], + [modelElementsDataSelector, (_state, modelId: number): number => modelId], + (data, modelId) => data[modelId]?.data || [], ); export const modelElementsCurrentModelLoadingSelector = createSelector( @@ -24,8 +41,9 @@ export const modelElementsCurrentModelLoadingSelector = createSelector( state => state?.loading, ); -export const modelElementsAnyModelLoadingSelector = createSelector(modelElementsSelector, state => - Object.values(state).some(modelElementState => modelElementState.loading === 'pending'), +export const modelElementsAnyModelLoadingSelector = createSelector( + modelElementsDataSelector, + state => Object.values(state).some(modelElementState => modelElementState.loading === 'pending'), ); export const modelElementsForCurrentModelSelector = createSelector( @@ -64,7 +82,7 @@ export const currentDrawerModelElementSelector = createSelector( ); export const compartmentPathwaysSelector = createSelector( - modelElementsSelector, + modelElementsDataSelector, (state): ModelElement[] => { const pathways: ModelElement[] = []; Object.values(state).forEach(modelState => { @@ -77,3 +95,158 @@ export const compartmentPathwaysSelector = createSelector( return pathways; }, ); + +export const allModelElementsSubmapConnectionsForCurrentSubmapSelector = createSelector( + modelElementsWithSubmapConnectionForCurrentModelSelector, + currentModelIdSelector, + (submapConnectionsModelElement, currentModel): ModelElement[] => + submapConnectionsModelElement.filter(({ model }) => model === currentModel), +); + +// ------SEARCH (OLD BIO ENTITIES)------ +export const modelElementsSearchSelector = createSelector( + modelElementsStateSelector, + state => state.search, +); + +export const modelElementsSearchDataSelector = createSelector( + modelElementsSearchSelector, + state => state.data, +); + +export const searchModelElementsListSelector = createSelector( + modelElementsSearchDataSelector, + modelElementsSearchData => modelElementsSearchData.map(b => b.data || []).flat(), +); + +export const allSearchModelElementForCurrentModelSelector = createSelector( + modelElementsSearchDataSelector, + currentModelIdSelector, + (modelElementsSearchData, currentModelId): ModelElement[] => { + return modelElementsSearchData + .map(({ data }) => data || []) + .flat() + .filter(({ modelElement }) => modelElement.model === currentModelId) + .map(({ modelElement }) => modelElement); + }, +); + +export const searchedModelElementsSelector = createSelector( + modelElementsSearchDataSelector, + currentSelectedSearchElement, + ( + modelElementsSearchData, + currentSearchElement, + ): MultiSearchData<SearchModelElementDataState[]> | undefined => + modelElementsSearchData.find( + ({ searchQueryElement }) => searchQueryElement === currentSearchElement, + ), +); + +export const searchedModelElementsForCurrentModelSelector = createSelector( + modelElementsSearchDataSelector, + currentModelIdSelector, + currentSelectedSearchElement, + (modelElementsSearchData, currentModelId, currentSearchElement): ModelElement[] => { + return modelElementsSearchData + .filter(({ searchQueryElement }) => + currentSearchElement ? searchQueryElement === currentSearchElement : true, + ) + .map(({ data }) => data || []) + .flat() + .filter(({ modelElement }) => modelElement.model === currentModelId) + .map(({ modelElement }) => modelElement); + }, +); + +export const searchModelElementsLoadingSelector = createSelector( + modelElementsSearchSelector, + search => search.loading, +); + +export const searchModelElementsLoadingStatusSelector = createSelector( + searchedModelElementsSelector, + state => state?.loading, +); + +export const numberOfSearchModelElementsSelector = createSelector( + searchedModelElementsSelector, + state => (state?.data ? state.data.length : SIZE_OF_EMPTY_ARRAY), +); + +export const searchedModelElementForContextMapSelector = createSelector( + modelElementsSearchDataSelector, + currentSelectedBioEntityIdSelector, + (modelElementsSearchData, currentBioEntityId): ModelElement | undefined => { + return modelElementsSearchData + .find(({ searchQueryElement }) => searchQueryElement === currentBioEntityId.toString()) + ?.data?.find(({ modelElement }) => modelElement.id === currentBioEntityId)?.modelElement; + }, +); + +export const searchedModelElementUniProtIdSelector = createSelector( + searchedModelElementForContextMapSelector, + (modelElement): string | undefined => { + return modelElement?.references.find(({ type }) => type === 'UNIPROT')?.resource; + }, +); + +export const allSearchModelElementsIdTabForCurrentModelSelector = createSelector( + modelElementsSearchDataSelector, + currentModelIdSelector, + (modelElementsSearchData, currentModelId): ElementIdTabObj => { + const entries = modelElementsSearchData.flatMap(({ data, searchQueryElement }) => + (data || []) + .flat() + .filter(({ modelElement }) => modelElement.model === currentModelId) + .map(({ modelElement }) => [modelElement.id, searchQueryElement]), + ); + return Object.fromEntries(entries); + }, +); + +export const searchModelElementsPerModelSelector = createSelector( + searchedModelElementsSelector, + modelsDataSelector, + (modelElements, models) => { + const modelElementsPerModelPerSearchElement = (models || []).map(model => { + const modelElementsInGivenModel = (modelElements?.data || []).filter( + entity => model.id === entity.modelElement.model, + ); + + return { + modelName: model.name, + modelId: model.id, + numberOfModelElements: modelElementsInGivenModel.length, + modelElements: modelElementsInGivenModel, + }; + }); + + return modelElementsPerModelPerSearchElement.filter( + model => model.numberOfModelElements !== SIZE_OF_EMPTY_ARRAY, + ); + }, +); + +export const currentDrawerModelElementRelatedSubmapSelector = createSelector( + currentDrawerModelElementSelector, + modelsDataSelector, + (bioEntity, models): MapModel | undefined => + models.find(({ id }) => id === bioEntity?.submodel?.mapId), +); + +export const currentDrawerElementCommentsSelector = createSelector( + currentDrawerModelElementSelector, + allCommentsSelectorOfCurrentMap, + (element, comments): Comment[] => { + if (element) { + return comments.filter( + comment => + comment.type === 'ALIAS' && + comment.modelId === element.model && + Number(comment.elementId) === element.id, + ); + } + return []; + }, +); diff --git a/src/redux/modelElements/modelElements.slice.ts b/src/redux/modelElements/modelElements.slice.ts index cbf16d002fe05a7ef90c2334d844106da209d4a5..d2130dec7ad749d9f908baa7ea04d0a23dee0334 100644 --- a/src/redux/modelElements/modelElements.slice.ts +++ b/src/redux/modelElements/modelElements.slice.ts @@ -1,14 +1,30 @@ import { createSlice } from '@reduxjs/toolkit'; -import { getModelElementsReducer } from '@/redux/modelElements/modelElements.reducers'; +import { + clearSearchModelElementsReducer, + getModelElementsReducer, + setModelElementSearchReducer, + setMultipleModelElementSearchReducer, + searchModelElementReducer, + searchMultiModelElementsReducer, +} from '@/redux/modelElements/modelElements.reducers'; import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; export const modelElements = createSlice({ name: 'modelElements', initialState: MODEL_ELEMENTS_INITIAL_STATE_MOCK, - reducers: {}, + reducers: { + setModelElementSearch: setModelElementSearchReducer, + setMultipleModelElementSearch: setMultipleModelElementSearchReducer, + clearSearchModelElements: clearSearchModelElementsReducer, + }, extraReducers: builder => { + searchModelElementReducer(builder); + searchMultiModelElementsReducer(builder); getModelElementsReducer(builder); }, }); +export const { setModelElementSearch, setMultipleModelElementSearch, clearSearchModelElements } = + modelElements.actions; + export default modelElements.reducer; diff --git a/src/redux/modelElements/modelElements.thunks.test.ts b/src/redux/modelElements/modelElements.thunks.test.ts index cb0c5c52b057ca7f95c6e09e1e3c7685778bfd1f..7fdf47a95bf60a555059241ff2765af5145660c6 100644 --- a/src/redux/modelElements/modelElements.thunks.test.ts +++ b/src/redux/modelElements/modelElements.thunks.test.ts @@ -8,9 +8,16 @@ import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; +import { + getModelElementsForModel, + searchModelElement, + searchMultiModelElements, +} from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; +import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { unwrapResult } from '@reduxjs/toolkit'; +const SEARCH_QUERY = 'park7'; const mockedAxiosClient = mockNetworkNewAPIResponse(); describe('model elements thunks', () => { @@ -38,4 +45,112 @@ describe('model elements thunks', () => { expect(payload).toEqual(undefined); }); }); + + describe('searchMultiModelElements', () => { + it('should return transformed bioEntityContent array', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + const data = await store + .dispatch( + searchMultiModelElements({ + searchQueries: [SEARCH_QUERY], + isPerfectMatch: false, + }), + ) + .unwrap(); + + expect(data).toEqual(bioEntityResponseFixture.content); + }); + + it('should combine all returned bioEntityContent arrays and return array with all provided bioEntityContent elements', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + const data = await store + .dispatch( + searchMultiModelElements({ + searchQueries: [SEARCH_QUERY, SEARCH_QUERY], + isPerfectMatch: false, + }), + ) + .unwrap(); + + expect(data).toEqual([ + ...bioEntityResponseFixture.content, + ...bioEntityResponseFixture.content, + ]); + }); + }); + + describe('searchModelElement', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, bioEntityResponseFixture); + + const { payload } = await store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + expect(payload).toEqual(bioEntityResponseFixture.content); + }); + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + expect(payload).toEqual(undefined); + }); + it('should handle error message when getBioEntityContents failed', async () => { + mockedAxiosClient + .onGet( + apiPath.getBioEntityContentsStringWithQuery({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ) + .reply(HttpStatusCode.NotFound, null); + + const action = await store.dispatch( + searchModelElement({ + searchQuery: SEARCH_QUERY, + isPerfectMatch: false, + }), + ); + expect(() => unwrapResult(action)).toThrow( + "Failed to search model element: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); + }); + }); }); diff --git a/src/redux/modelElements/modelElements.thunks.ts b/src/redux/modelElements/modelElements.thunks.ts index 68f2419e35f2033c0f271c96805d96f1037f451e..90eb0c9c8bd9f67a8823803ad07b2cc076fe9445 100644 --- a/src/redux/modelElements/modelElements.thunks.ts +++ b/src/redux/modelElements/modelElements.thunks.ts @@ -4,10 +4,21 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { ModelElement, ModelElements } from '@/types/models'; -import { MODEL_ELEMENTS_FETCHING_ERROR_PREFIX } from '@/redux/modelElements/modelElements.constants'; +import { BioEntityContent, BioEntityResponse, ModelElement, ModelElements } from '@/types/models'; +import { + MODEL_ELEMENT_SEARCH_ERROR_PREFIX, + MODEL_ELEMENTS_FETCHING_ERROR_PREFIX, + MULTI_MODEL_ELEMENTS_SEARCH_ERROR_PREFIX, +} from '@/redux/modelElements/modelElements.constants'; import { modelElementSchema } from '@/models/modelElementSchema'; import { pageableSchema } from '@/models/pageableSchema'; +import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; +import { + SearchModelElementProps, + SearchMultiModelElementsActions, + SearchMultiModelElementsProps, +} from '@/redux/modelElements/modelElements.types'; +import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema'; export const getModelElementsForModel = createAsyncThunk< Array<ModelElement> | undefined, @@ -24,3 +35,63 @@ export const getModelElementsForModel = createAsyncThunk< return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); } }); + +export const searchModelElement = createAsyncThunk< + BioEntityContent[] | undefined, + SearchModelElementProps, + ThunkConfig +>( + 'modelElements/searchModelElement', + async ({ searchQuery, isPerfectMatch, addNumbersToEntityNumber = true }, { dispatch }) => { + try { + const { data } = await axiosInstanceNewAPI.get<BioEntityResponse>( + apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch }), + ); + + const isDataValid = validateDataUsingZodSchema(data, bioEntityResponseSchema); + const { content } = data; + if (addNumbersToEntityNumber && content) { + const modelElementsIds = content.map(b => b.bioEntity.elementId); + dispatch(addNumbersToEntityNumberData(modelElementsIds)); + } + + return isDataValid ? content : undefined; + } catch (error) { + return Promise.reject(getError({ error, prefix: MODEL_ELEMENT_SEARCH_ERROR_PREFIX })); + } + }, +); + +export const searchMultiModelElements = createAsyncThunk< + BioEntityContent[], + SearchMultiModelElementsProps, + ThunkConfig +>( + 'modelElements/searchMultiModelElements', + async ({ searchQueries, isPerfectMatch }, { dispatch }) => { + try { + const asyncGetBioEntityFunctions = searchQueries.map(searchQuery => + dispatch( + searchModelElement({ searchQuery, isPerfectMatch, addNumbersToEntityNumber: false }), + ), + ); + + const multiModelElementsActions = (await Promise.all( + asyncGetBioEntityFunctions, + )) as SearchMultiModelElementsActions; + + const multiModelElements = multiModelElementsActions + .map(multiModelElementsAction => multiModelElementsAction?.payload || []) + .flat() + .filter((payload): payload is BioEntityContent => typeof payload !== 'string') + .filter(payload => 'bioEntity' in payload || {}); + + const modelElementsIds = multiModelElements.map(b => b.bioEntity.elementId); + dispatch(addNumbersToEntityNumberData(modelElementsIds)); + + return multiModelElements; + } catch (error) { + return Promise.reject(getError({ error, prefix: MULTI_MODEL_ELEMENTS_SEARCH_ERROR_PREFIX })); + } + }, +); diff --git a/src/redux/modelElements/modelElements.types.ts b/src/redux/modelElements/modelElements.types.ts index b2d4ee6861e60602de84d7b45a021a36d11b93d5..c240d572e914d1661bfa1d568f3d585339a333d7 100644 --- a/src/redux/modelElements/modelElements.types.ts +++ b/src/redux/modelElements/modelElements.types.ts @@ -1,4 +1,22 @@ -import { KeyedFetchDataState } from '@/types/fetchDataState'; -import { ModelElement } from '@/types/models'; +import { KeyedFetchDataState, MultiFetchDataState } from '@/types/fetchDataState'; +import { BioEntityContent, ModelElement } from '@/types/models'; +import { PerfectMultiSearchParams, PerfectSearchParams } from '@/types/search'; +import { PayloadAction } from '@reduxjs/toolkit'; -export type ModelElementsState = KeyedFetchDataState<Array<ModelElement>>; +export type SearchModelElementDataState = { + perfect: boolean; + modelElement: ModelElement; +}; + +export type ModelElementsState = { + data: KeyedFetchDataState<ModelElement[]>; + search: MultiFetchDataState<SearchModelElementDataState[]>; +}; + +export type SearchMultiModelElementsProps = PerfectMultiSearchParams; + +export type SearchMultiModelElementsActions = PayloadAction< + BioEntityContent[] | undefined | string +>[]; + +export type SearchModelElementProps = PerfectSearchParams; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index b8ba305fae603e3cf75f6a6d4f4f530736ae83e6..a0ef6652d2f01a0f17a215fb70676c76daced78d 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -1,7 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { currentDrawerReactionIdSelector } from '@/redux/drawer/drawer.selectors'; -import { NewReaction } from '@/types/models'; +import { Comment, NewReaction } from '@/types/models'; +import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); @@ -34,3 +35,19 @@ export const currentDrawerNewReactionSelector = createSelector( return newReactions.find(newReaction => newReaction.id === currentDrawerReactionId); }, ); + +export const currentDrawerReactionCommentsSelector = createSelector( + currentDrawerNewReactionSelector, + allCommentsSelectorOfCurrentMap, + (reaction, comments): Comment[] => { + if (reaction) { + return comments.filter( + comment => + comment.type === 'REACTION' && + comment.modelId === reaction.model && + Number(comment.elementId) === reaction.id, + ); + } + return []; + }, +); diff --git a/src/redux/reactions/isReactionBioentity.ts b/src/redux/reactions/isReactionBioentity.ts deleted file mode 100644 index e4e673a2411855bd9b9355ba5b09017299575d88..0000000000000000000000000000000000000000 --- a/src/redux/reactions/isReactionBioentity.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BioEntity } from '@/types/models'; - -export const isReactionBioEntity = (bioEntity: BioEntity): boolean => { - return bioEntity.idReaction !== undefined && bioEntity.idReaction !== null; -}; diff --git a/src/redux/reactions/isReactionElement.ts b/src/redux/reactions/isReactionElement.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9a3d781899e7f6750a6f00aa5d1efc673d915fb --- /dev/null +++ b/src/redux/reactions/isReactionElement.ts @@ -0,0 +1,5 @@ +import { PublicationElement } from '@/types/models'; + +export const isReactionElement = (element: PublicationElement): boolean => { + return element.idReaction !== undefined && element.idReaction !== null; +}; diff --git a/src/redux/search/search.thunks.ts b/src/redux/search/search.thunks.ts index b95730d28ef1660c6dca265e31f746822d7f4948..ddbfc4af081cf31554e63567e6f5dd0e6d6fe77a 100644 --- a/src/redux/search/search.thunks.ts +++ b/src/redux/search/search.thunks.ts @@ -1,10 +1,10 @@ -import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks'; import { getMultiChemicals } from '@/redux/chemicals/chemicals.thunks'; import { getMultiDrugs } from '@/redux/drugs/drugs.thunks'; import { PerfectMultiSearchParams } from '@/types/search'; import { ThunkConfig } from '@/types/store'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { getError } from '@/utils/error-report/getError'; +import { searchMultiModelElements } from '@/redux/modelElements/modelElements.thunks'; import { resetReactionsData } from '../reactions/reactions.slice'; import type { RootState } from '../store'; import { DATA_SEARCHING_ERROR_PREFIX } from './search.constants'; @@ -30,13 +30,13 @@ export const getSearchData = createAsyncThunk< } if (containsDisease) { await Promise.all([ - dispatch(getMultiBioEntity({ searchQueries, isPerfectMatch })), + dispatch(searchMultiModelElements({ searchQueries, isPerfectMatch })), dispatch(getMultiDrugs(searchQueries)), dispatch(getMultiChemicals(searchQueries)), ]); } else { await Promise.all([ - dispatch(getMultiBioEntity({ searchQueries, isPerfectMatch })), + dispatch(searchMultiModelElements({ searchQueries, isPerfectMatch })), dispatch(getMultiDrugs(searchQueries)), ]); } diff --git a/src/redux/search/search.thunks.utils.ts b/src/redux/search/search.thunks.utils.ts index 0744617c819c6b4a18db01dc8062a80085e0ae01..43e7504a4eccf1abb096dc7d07ed1e2a5cae2e8a 100644 --- a/src/redux/search/search.thunks.utils.ts +++ b/src/redux/search/search.thunks.utils.ts @@ -2,8 +2,17 @@ import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import type { RootState } from '../store'; export const dispatchPluginsEvents = (searchQueries: string[], state: RootState): void => { - const bioEntities = state.bioEntity.data; - const bioEntitiesResults = bioEntities.map(bioEntity => (bioEntity.data ? bioEntity.data : [])); + const searchModelElements = state.modelElements.search.data; + const modelElementsResults = searchModelElements + .map(searchElement => (searchElement.data ? searchElement.data : [])) + .map(data => { + return data.map(result => { + return { + perfect: result.perfect, + bioEntity: result.modelElement, + }; + }); + }); const drugs = state.drugs.data; const drugsResults = drugs.map(drug => (drug.data ? drug.data : [])); @@ -14,7 +23,7 @@ export const dispatchPluginsEvents = (searchQueries: string[], state: RootState) PluginsEventBus.dispatchEvent('onSearch', { type: 'bioEntity', searchValues: searchQueries, - results: bioEntitiesResults, + results: modelElementsResults, }); PluginsEventBus.dispatchEvent('onSearch', { type: 'drugs', diff --git a/src/redux/user/user.mock.ts b/src/redux/user/user.mock.ts index ac2813cfdadb09548212b8b0f0dd5e80bef14511..b469f8d5b7f83bafc7d831387ee24e35af8ecd48 100644 --- a/src/redux/user/user.mock.ts +++ b/src/redux/user/user.mock.ts @@ -7,4 +7,5 @@ export const USER_INITIAL_STATE_MOCK: UserState = { login: null, role: null, userData: null, + token: null, }; diff --git a/src/redux/user/user.reducers.test.ts b/src/redux/user/user.reducers.test.ts index 3033a51affb22201af934a6d97121115644077b9..50968161c6469676859b3b0fa0e0c0c23a91387e 100644 --- a/src/redux/user/user.reducers.test.ts +++ b/src/redux/user/user.reducers.test.ts @@ -26,6 +26,7 @@ const INITIAL_STATE: UserState = { login: null, role: null, userData: null, + token: null, }; describe('user reducer', () => { diff --git a/src/redux/user/user.reducers.ts b/src/redux/user/user.reducers.ts index 2cb2002a5ad810bd67698c60aaa5618ec3ab41b3..26ca31cd4e1e95ab2b231f01c304c0d2da4a2b87 100644 --- a/src/redux/user/user.reducers.ts +++ b/src/redux/user/user.reducers.ts @@ -13,6 +13,7 @@ export const loginReducer = (builder: ActionReducerMapBuilder<UserState>): void state.role = action.payload?.role || null; state.login = action.payload?.login || null; state.userData = action.payload?.userData || null; + state.token = action.payload?.token || null; }) .addCase(login.rejected, state => { state.authenticated = false; @@ -31,6 +32,7 @@ export const getSessionValidReducer = (builder: ActionReducerMapBuilder<UserStat state.login = action.payload?.login || null; state.role = action.payload?.role || null; state.userData = action.payload?.userData || null; + state.token = action.payload?.token || null; }) .addCase(getSessionValid.rejected, state => { state.authenticated = false; @@ -50,6 +52,7 @@ export const logoutReducer = (builder: ActionReducerMapBuilder<UserState>): void state.role = null; state.login = null; state.userData = null; + state.token = null; }) .addCase(logout.rejected, state => { state.loading = 'failed'; diff --git a/src/redux/user/user.selectors.ts b/src/redux/user/user.selectors.ts index ed8794400548afee44a275e81d19e97d7bebdbf0..c760c3541cbf5da5527a1b36a2c6011c4be6921e 100644 --- a/src/redux/user/user.selectors.ts +++ b/src/redux/user/user.selectors.ts @@ -1,5 +1,8 @@ import { rootSelector } from '@/redux/root/root.selectors'; import { createSelector } from '@reduxjs/toolkit'; +import { projectIdSelector } from '@/redux/project/project.selectors'; +import { hasPrivilegeToObject } from '@/redux/user/user.utils'; +import { UserPrivilege } from '@/types/models'; export const userSelector = createSelector(rootSelector, state => state.user); @@ -7,3 +10,18 @@ export const authenticatedUserSelector = createSelector(userSelector, state => s export const loadingUserSelector = createSelector(userSelector, state => state.loading); export const loginUserSelector = createSelector(userSelector, state => state.login); export const userRoleSelector = createSelector(userSelector, state => state.role); +export const userPrivilegesSelector = createSelector( + userSelector, + state => state.userData?.privileges || [], +); +export const hasPrivilegeToWriteProjectSelector = createSelector( + userPrivilegesSelector, + projectIdSelector, + (userPrivileges: UserPrivilege[], projectId: string | undefined): boolean => { + if (!projectId) { + return false; + } + return hasPrivilegeToObject(userPrivileges, 'WRITE_PROJECT', projectId); + }, +); +export const userTokenSelector = createSelector(userSelector, state => state.token); diff --git a/src/redux/user/user.slice.ts b/src/redux/user/user.slice.ts index bf9f7f388be72f4f379e258fe78705dd60dcbcf3..505f4e46bdd6ae31bcc08971e78eccd1b8a207a3 100644 --- a/src/redux/user/user.slice.ts +++ b/src/redux/user/user.slice.ts @@ -9,6 +9,7 @@ export const initialState: UserState = { login: null, role: null, userData: null, + token: null, }; export const userSlice = createSlice({ diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts index 89e404dadd301142b1cbbe4668fb59db73c434d9..ec48eb97bafff4d38757ab8cd5e532fed9a27277 100644 --- a/src/redux/user/user.thunks.ts +++ b/src/redux/user/user.thunks.ts @@ -39,12 +39,12 @@ export const login = createAsyncThunk( async (credentials: { login: string; password: string }, { dispatch }) => { try { const searchParams = new URLSearchParams(credentials); - const response = await axiosInstance.post<Login>(apiPath.postLogin(), searchParams, { + const { data } = await axiosInstance.post<Login>(apiPath.postLogin(), searchParams, { withCredentials: true, }); - const isDataValid = validateDataUsingZodSchema(response.data, loginSchema); - const loginName = response.data.login; + const isDataValid = validateDataUsingZodSchema(data, loginSchema); + const loginName = data.login; if (isDataValid) { setLoginForOldMinerva(loginName); @@ -62,6 +62,7 @@ export const login = createAsyncThunk( login: loginName, role, userData, + token: data.token, }; } @@ -89,7 +90,7 @@ export const getSessionValid = createAsyncThunk('user/getSessionValid', async () const isDataValid = validateDataUsingZodSchema(response.data, sessionSchemaValid); const { - data: { login: loginName }, + data: { login: loginName, token }, } = response; const userData = await getUserData(loginName); @@ -102,6 +103,7 @@ export const getSessionValid = createAsyncThunk('user/getSessionValid', async () login: loginName, userData, role, + token, }; } diff --git a/src/redux/user/user.types.ts b/src/redux/user/user.types.ts index 08210c0a3b06dbb700b583fcbf76073ecd838784..d4e8b494cb4d51c5d3ebb42757f0321d5fabe2a1 100644 --- a/src/redux/user/user.types.ts +++ b/src/redux/user/user.types.ts @@ -8,4 +8,5 @@ export type UserState = { login: null | string; role: string | null; userData: User | null; + token: string | null; }; diff --git a/src/redux/user/user.utils.ts b/src/redux/user/user.utils.ts index ecb9c6239006f351b806b4fbf32d7bcf5cacd652..b346b37c547c5bdc33786164c81ad98229d3332b 100644 --- a/src/redux/user/user.utils.ts +++ b/src/redux/user/user.utils.ts @@ -3,3 +3,15 @@ import { UserPrivilege } from '@/types/models'; export const hasPrivilege = (privileges: UserPrivilege[], privilegeType: string): boolean => { return privileges.some(privilege => privilege.privilegeType === privilegeType); }; + +export const hasPrivilegeToObject = ( + privileges: UserPrivilege[], + privilegeType: string, + objectId: string, +): boolean => { + return Boolean( + privileges.find( + privilege => privilege.privilegeType === privilegeType && privilege.objectId === objectId, + ), + ); +}; diff --git a/src/services/pluginsManager/bioEntities/clearAllElements.ts b/src/services/pluginsManager/bioEntities/clearAllElements.ts index ce8a4ee72e3f6d781d7dca40cffadffa729c51fc..f8dfe461c7ae11abafe48dd838f6b76d0da8444b 100644 --- a/src/services/pluginsManager/bioEntities/clearAllElements.ts +++ b/src/services/pluginsManager/bioEntities/clearAllElements.ts @@ -1,8 +1,8 @@ -import { clearBioEntities } from '@/redux/bioEntity/bioEntity.slice'; import { clearChemicalsData } from '@/redux/chemicals/chemicals.slice'; import { clearDrugsData } from '@/redux/drugs/drugs.slice'; import { setMarkersData } from '@/redux/markers/markers.slice'; import { store } from '@/redux/store'; +import { clearSearchModelElements } from '@/redux/modelElements/modelElements.slice'; type ElementName = 'drugs' | 'chemicals' | 'content' | 'marker'; @@ -10,7 +10,7 @@ export const clearAllElements = (elements: ElementName[]): void => { const { dispatch } = store; if (elements.includes('content')) { - dispatch(clearBioEntities()); + dispatch(clearSearchModelElements()); } if (elements.includes('chemicals')) { diff --git a/src/services/pluginsManager/bioEntities/getAllContent.ts b/src/services/pluginsManager/bioEntities/getAllContent.ts index af8f6d119499c61aea0ecaaa3c2c3bdd38d28c3d..ba999eca1896bd42972a917fca6c8f9dab5bca22 100644 --- a/src/services/pluginsManager/bioEntities/getAllContent.ts +++ b/src/services/pluginsManager/bioEntities/getAllContent.ts @@ -1,10 +1,10 @@ -import { bioEntityDataListSelector } from '@/redux/bioEntity/bioEntity.selectors'; import { store } from '@/redux/store'; -import { BioEntityContent } from '@/types/models'; +import { searchModelElementsListSelector } from '@/redux/modelElements/modelElements.selector'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; -export const getAllBioEntities = (): BioEntityContent[] => { +export const getAllBioEntities = (): SearchModelElementDataState[] => { const { getState } = store; - const bioEntities = bioEntityDataListSelector(getState()); + const bioEntities = searchModelElementsListSelector(getState()); return bioEntities || []; }; diff --git a/src/services/pluginsManager/bioEntities/getShownElements.ts b/src/services/pluginsManager/bioEntities/getShownElements.ts index 89cf67bdd2ad961dc7bd660bc5e5046cacd3ece4..be9de04ad6dbbe4000eec3a28185648e66a1b1ed 100644 --- a/src/services/pluginsManager/bioEntities/getShownElements.ts +++ b/src/services/pluginsManager/bioEntities/getShownElements.ts @@ -1,16 +1,16 @@ -import { searchedBioEntitesSelectorOfCurrentMap } from '@/redux/bioEntity/bioEntity.selectors'; -import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '@/redux/chemicals/chemicals.selectors'; -import { searchedDrugsBioEntitesOfCurrentMapSelector } from '@/redux/drugs/drugs.selectors'; +import { searchedChemicalsElementsOfCurrentMapSelector } from '@/redux/chemicals/chemicals.selectors'; +import { searchedDrugsElementsOfCurrentMapSelector } from '@/redux/drugs/drugs.selectors'; import { markersPinsOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; import { store } from '@/redux/store'; +import { searchedModelElementsForCurrentModelSelector } from '@/redux/modelElements/modelElements.selector'; import { GetShownElementsPluginMethodResult } from './getShownElements.types'; export const getShownElements = (): GetShownElementsPluginMethodResult => { const { getState } = store; - const content = searchedBioEntitesSelectorOfCurrentMap(getState()); - const chemicals = searchedChemicalsBioEntitesOfCurrentMapSelector(getState()); - const drugs = searchedDrugsBioEntitesOfCurrentMapSelector(getState()); + const content = searchedModelElementsForCurrentModelSelector(getState()); + const chemicals = searchedChemicalsElementsOfCurrentMapSelector(getState()); + const drugs = searchedDrugsElementsOfCurrentMapSelector(getState()); const markers = markersPinsOfCurrentMapDataSelector(getState()); return { diff --git a/src/services/pluginsManager/bioEntities/getShownElements.types.ts b/src/services/pluginsManager/bioEntities/getShownElements.types.ts index 210a0e0194f54a7f0f1ecb6e49f17f01502c00a2..d0e71af064cdf80f1dc952aa024b31fe569ce37b 100644 --- a/src/services/pluginsManager/bioEntities/getShownElements.types.ts +++ b/src/services/pluginsManager/bioEntities/getShownElements.types.ts @@ -1,8 +1,8 @@ -import { BioEntity, Marker } from '@/types/models'; +import { ModelElement, Marker } from '@/types/models'; export interface GetShownElementsPluginMethodResult { - content: BioEntity[]; - drugs: BioEntity[]; - chemicals: BioEntity[]; + content: ModelElement[]; + drugs: ModelElement[]; + chemicals: ModelElement[]; markers: Marker[]; } diff --git a/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts index 35661b14ce73d5d232d649d773c72fdfe7acc45a..fd3f701882759c3c1550aaf56b04237836e8cc23 100644 --- a/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts @@ -1,11 +1,14 @@ /* eslint-disable no-magic-numbers */ import { MAP_INITIAL_STATE } from '@/redux/map/map.constants'; import { RootState, store } from '@/redux/store'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; import { HISTAMINE_MAP_ID } from '@/constants/mocks'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; +import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { getVisibleBioEntitiesPolygonCoordinates } from './getVisibleBioEntitiesPolygonCoordinates'; jest.mock('../../../../redux/store'); @@ -35,10 +38,7 @@ describe('getVisibleBioEntitiesPolygonCoordinates', () => { }, }, }, - bioEntity: { - loading: 'succeeded', - error: { message: '', name: '' }, - }, + modelElements: MODEL_ELEMENTS_INITIAL_STATE_MOCK, drugs: { loading: 'succeeded', error: { message: '', name: '' }, @@ -65,7 +65,7 @@ describe('getVisibleBioEntitiesPolygonCoordinates', () => { ...MAP_INITIAL_STATE, data: { ...MAP_INITIAL_STATE.data, - modelId: 52, + modelId: 0, size: { width: 256, height: 256, @@ -75,39 +75,36 @@ describe('getVisibleBioEntitiesPolygonCoordinates', () => { }, }, }, - bioEntity: { - data: [ - { - searchQueryElement: bioEntityContentFixture.bioEntity.name, + modelElements: { + data: { + 0: { + data: [modelElementFixture], loading: 'succeeded', - error: { name: '', message: '' }, - data: [ - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - model: 52, - x: 97, - y: 53, - z: 1, + error: { message: '', name: '' }, + }, + }, + search: { + data: [ + { + searchQueryElement: modelElementFixture.id.toString(), + loading: 'succeeded', + error: DEFAULT_ERROR, + data: [ + { + modelElement: { ...modelElementFixture, model: 0, x: 97, y: 53, z: 1 }, + perfect: true, }, - }, - { - ...bioEntityContentFixture, - bioEntity: { - ...bioEntityContentFixture.bioEntity, - model: 52, - x: 12, - y: 25, - z: 1, + { + modelElement: { ...modelElementFixture, model: 0, x: 12, y: 25, z: 1 }, + perfect: true, }, - }, - ], - }, - ], - loading: 'succeeded', - error: { message: '', name: '' }, - }, + ], + }, + ], + loading: 'succeeded', + error: DEFAULT_ERROR, + }, + } as ModelElementsState, drugs: { data: [ { @@ -141,7 +138,7 @@ describe('getVisibleBioEntitiesPolygonCoordinates', () => { }, searchDrawerState: { ...DRAWER_INITIAL_STATE.searchDrawerState, - selectedSearchElement: bioEntityContentFixture.bioEntity.name, + selectedSearchElement: modelElementFixture.id.toString(), }, }, }) as RootState, diff --git a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts index 4a1bcd16b2bea4b700b212551ba164d0857d5c25..a813ecf9206f5451481b743b9bc468438a621881 100644 --- a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts +++ b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts @@ -13,7 +13,7 @@ import { modelElementsByModelIdSelector } from '@/redux/modelElements/modelEleme import { newReactionsByModelIdSelector } from '@/redux/newReactions/newReactions.selectors'; import { closeDrawer } from '@/redux/drawer/drawer.slice'; import { resetReactionsData } from '@/redux/reactions/reactions.slice'; -import { clearBioEntities } from '@/redux/bioEntity/bioEntity.slice'; +import { clearSearchModelElements } from '@/redux/modelElements/modelElements.slice'; import { Coordinates } from './triggerSearch.types'; export const searchByCoordinates = async ( @@ -45,7 +45,7 @@ export const searchByCoordinates = async ( } dispatch(resetReactionsData()); - dispatch(clearBioEntities()); + dispatch(clearSearchModelElements()); return; } diff --git a/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts index 45c0e9dfdd8892ba3d44e2eb2391cdc1ff16fecd..b8e0c9fb99e64bdcfc65ae538a90f35acef812d7 100644 --- a/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts @@ -30,10 +30,6 @@ const MOCK_SEARCH_BY_QUERY_STORE = { }, }, }, - bioEntity: { - loading: 'succeeded', - error: { message: '', name: '' }, - }, drugs: { loading: 'succeeded', error: { message: '', name: '' }, diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts index 0b3e97df409d58f3f7145bd49959d7939993e4f1..9b591d8d9cb0602ed12811b25a8a7dfcf8e992c7 100644 --- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts @@ -18,6 +18,7 @@ import { MapManager } from '@/services/pluginsManager/map/mapManager'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; import { LAYER_TYPE } from '@/components/Map/MapViewer/MapViewer.constants'; +import { MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; import { ERROR_INVALID_MODEL_ID_TYPE } from '../../errorMessages'; import { triggerSearch } from './triggerSearch'; @@ -37,11 +38,14 @@ const MOCK_STATE = { openedMaps: openedMapsThreeSubmapsFixture, }, modelElements: { - 0: { - data: [], - loading: 'succeeded', - error: { message: '', name: '' }, + data: { + 0: { + data: [], + loading: 'succeeded', + error: { message: '', name: '' }, + }, }, + search: MODEL_ELEMENTS_SEARCH_INITIAL_STATE_MOCK, } as ModelElementsState, newReactions: { 0: { diff --git a/src/services/pluginsManager/pluginContextMenu/pluginsContextMenu.types.ts b/src/services/pluginsManager/pluginContextMenu/pluginsContextMenu.types.ts index 0ea6b81167c6eb565b6097aac0fef7c49008edc0..0c8231adbc33e439e5631440f6e21d868cf5fc20 100644 --- a/src/services/pluginsManager/pluginContextMenu/pluginsContextMenu.types.ts +++ b/src/services/pluginsManager/pluginContextMenu/pluginsContextMenu.types.ts @@ -1,4 +1,4 @@ -import { BioEntity, NewReaction } from '@/types/models'; +import { ModelElement, NewReaction } from '@/types/models'; export type ClickCoordinates = { modelId: number; @@ -13,7 +13,10 @@ export type PluginContextMenuItemType = { name: string; style: string; enabled: boolean; - callback: (coordinates: ClickCoordinates, element: BioEntity | NewReaction | undefined) => void; + callback: ( + coordinates: ClickCoordinates, + element: ModelElement | NewReaction | undefined, + ) => void; }; export type PluginsContextMenuType = { @@ -23,7 +26,10 @@ export type PluginsContextMenuType = { name: string, style: string, enabled: boolean, - callback: (coordinates: ClickCoordinates, element: BioEntity | NewReaction | undefined) => void, + callback: ( + coordinates: ClickCoordinates, + element: ModelElement | NewReaction | undefined, + ) => void, ) => string; removeMenu: (hash: string, id: string) => void; updateMenu: (hash: string, id: string, name: string, style: string, enabled: boolean) => void; diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts index d7bc2d51fc2b29e70af2834191b30ffd55475b82..1abdd1574c7ab45ad50f5671b772a79a1545807d 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts @@ -1,4 +1,5 @@ -import { BioEntityContent, Chemical, Drug, ElementSearchResult, MapOverlay } from '@/types/models'; +import { Chemical, Drug, ElementSearchResult, MapOverlay, ModelElement } from '@/types/models'; +import { SearchModelElementDataState } from '@/redux/modelElements/modelElements.types'; import { dispatchEvent } from './pluginsEventBus'; export type BackgroundEvents = 'onBackgroundOverlayChange'; @@ -49,13 +50,18 @@ export type ClickedSurfaceOverlay = { export type SearchDataReaction = { type: 'reaction'; searchValues: string[] | ElementSearchResult[]; - results: BioEntityContent[][]; + results: SearchModelElementDataState[][]; }; -export type SearchDataBioEntity = { +type SearchDataModelElementResults = { + perfect: boolean; + bioEntity: ModelElement; +}; + +export type SearchDataModelElement = { type: 'bioEntity'; searchValues: string[] | ElementSearchResult[]; - results: BioEntityContent[][]; + results: SearchDataModelElementResults[][]; }; export type SearchDataDrugs = { @@ -75,7 +81,7 @@ export type PluginUnloaded = { }; export type SearchData = - | SearchDataBioEntity + | SearchDataModelElement | SearchDataDrugs | SearchDataChemicals | SearchDataReaction; diff --git a/src/shared/ColorPicker/ColorTilePicker.component.test.tsx b/src/shared/ColorPicker/ColorTilePicker.component.test.tsx index bab787c03bad0a524b69eb07eb3912f5494d5fc5..e9c129586d0a797236cfb81fda3e99441fc5a157 100644 --- a/src/shared/ColorPicker/ColorTilePicker.component.test.tsx +++ b/src/shared/ColorPicker/ColorTilePicker.component.test.tsx @@ -4,13 +4,13 @@ import { ColorTilePicker } from './ColorTilePicker.component'; describe('ColorTilePicker', () => { it('renders with the initial color', () => { - render(<ColorTilePicker initialColor="#00ff00" colorChange={jest.fn()} />); + render(<ColorTilePicker initialColor={{ rgb: 65280, alpha: 255 }} colorChange={jest.fn()} />); const colorTile = screen.getByRole('button'); expect(colorTile).toHaveStyle({ backgroundColor: '#00ff00' }); }); it('toggles color picker visibility on click', () => { - render(<ColorTilePicker initialColor="#000000" colorChange={jest.fn()} />); + render(<ColorTilePicker initialColor={{ rgb: 0, alpha: 255 }} colorChange={jest.fn()} />); const colorTile = screen.getByRole('button'); expect(screen.queryByTestId('color-picker')).not.toBeInTheDocument(); @@ -23,7 +23,7 @@ describe('ColorTilePicker', () => { }); it('closes color picker when clicking outside', () => { - render(<ColorTilePicker initialColor="#000000" colorChange={jest.fn()} />); + render(<ColorTilePicker initialColor={{ rgb: 0, alpha: 255 }} colorChange={jest.fn()} />); const colorTile = screen.getByRole('button'); fireEvent.click(colorTile); @@ -34,7 +34,7 @@ describe('ColorTilePicker', () => { }); it('handles keyboard interaction (Enter key)', () => { - render(<ColorTilePicker initialColor="#000000" colorChange={jest.fn()} />); + render(<ColorTilePicker initialColor={{ rgb: 0, alpha: 255 }} colorChange={jest.fn()} />); const colorTile = screen.getByRole('button'); expect(screen.queryByTestId('color-picker')).not.toBeInTheDocument(); diff --git a/src/shared/ColorPicker/ColorTilePicker.component.tsx b/src/shared/ColorPicker/ColorTilePicker.component.tsx index 044ae2c19bd3a294f5bc55b5513500bc25fc53ff..ce47fc4d0e3393a55b7d7bda172240f13d216034 100644 --- a/src/shared/ColorPicker/ColorTilePicker.component.tsx +++ b/src/shared/ColorPicker/ColorTilePicker.component.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect, useRef } from 'react'; import ColorPicker, { Color, ColorPickerProps } from '@rc-component/color-picker'; +import { Color as RgbIntAlphaColor } from '@/types/models'; import '@rc-component/color-picker/assets/index.css'; +import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; type ColorTilePickerProps = { - initialColor?: string; + initialColor?: RgbIntAlphaColor; colorChange: (color: string) => void; height?: string; testId?: string; @@ -15,14 +17,16 @@ export const ColorTilePicker: React.FC<ColorTilePickerProps> = ({ height = '40px', testId = 'color-tile-picker', }) => { - const [color, setColor] = useState<string>(initialColor || '#000000'); + const [color, setColor] = useState<Color>( + () => new Color(initialColor ? rgbToHex(initialColor) : '#000000'), + ); const [visible, setVisible] = useState<boolean>(false); const pickerRef = useRef<HTMLDivElement>(null); // Referencja do elementu pickera const handleChange: ColorPickerProps['onChange'] = (newColor: Color) => { - setColor(newColor.toHexString()); - colorChange(color); + setColor(newColor); + colorChange(newColor.toHexString()); }; const handleKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => { @@ -54,7 +58,7 @@ export const ColorTilePicker: React.FC<ColorTilePickerProps> = ({ onKeyDown={handleKeyPress} style={{ height, - backgroundColor: color, + backgroundColor: color.toHexString(), }} /> {visible && ( diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index b68ff0849d674a539eb9cd0c1314910268e6364e..2c51e771c48419399d4e7eacb08b96af246953ff 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -26,6 +26,13 @@ import { TrashIcon } from '@/shared/Icon/Icons/TrashIcon'; import { ArrowDoubleUpIcon } from '@/shared/Icon/Icons/ArrowDoubleUpIcon'; import { ArrowDoubleDownIcon } from '@/shared/Icon/Icons/ArrowDoubleDownIcon'; import { TextIcon } from '@/shared/Icon/Icons/TextIcon'; +import { EyeIcon } from '@/shared/Icon/Icons/EyeIcon'; +import { PadlockOpenIcon } from '@/shared/Icon/Icons/PadlockOpenIcon'; +import { PadlockLockedIcon } from '@/shared/Icon/Icons/PadlockLockedIcon'; +import { CrossedEyeIcon } from '@/shared/Icon/Icons/CrossedEyeIcon'; +import { CenterIcon } from '@/shared/Icon/Icons/CenterIcon'; +import { BringFrontIcon } from '@/shared/Icon/Icons/BringFrontIcon'; +import { BringBackIcon } from '@/shared/Icon/Icons/BringBackIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -75,6 +82,13 @@ const icons: Record<IconTypes, IconComponentType> = { 'arrow-double-up': ArrowDoubleUpIcon, 'arrow-double-down': ArrowDoubleDownIcon, text: TextIcon, + eye: EyeIcon, + 'crossed-eye': CrossedEyeIcon, + 'padlock-open': PadlockOpenIcon, + 'padlock-locked': PadlockLockedIcon, + center: CenterIcon, + 'bring-front': BringFrontIcon, + 'bring-back': BringBackIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/BringBackIcon.tsx b/src/shared/Icon/Icons/BringBackIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d086ac9b556df8701815ac3aac5147f44afe611f --- /dev/null +++ b/src/shared/Icon/Icons/BringBackIcon.tsx @@ -0,0 +1,28 @@ +interface BringBackIconProps { + className?: string; +} + +export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="0 0 100 100" + xmlns="http://www.w3.org/2000/svg" + fill="none" + className={className} + > + <rect x="32" y="32" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="white" /> + <rect + x="4" + y="4" + width="68" + height="68" + rx="12" + ry="12" + strokeWidth="16" + stroke="white" + fill="none" + /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + </svg> +); diff --git a/src/shared/Icon/Icons/BringFrontIcon.tsx b/src/shared/Icon/Icons/BringFrontIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e5aa50175f5605a1695f8f308987170da28c1491 --- /dev/null +++ b/src/shared/Icon/Icons/BringFrontIcon.tsx @@ -0,0 +1,28 @@ +interface BringFrontIconProps { + className?: string; +} + +export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="0 0 100 100" + xmlns="http://www.w3.org/2000/svg" + fill="none" + className={className} + > + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + <rect + x="30" + y="30" + width="68" + height="68" + rx="12" + ry="12" + strokeWidth="16" + stroke="white" + fill="none" + /> + <rect x="32" y="32" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="white" /> + </svg> +); diff --git a/src/shared/Icon/Icons/CenterIcon.tsx b/src/shared/Icon/Icons/CenterIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d5f0df632dd14166bc683229dbd617912ae8ed23 --- /dev/null +++ b/src/shared/Icon/Icons/CenterIcon.tsx @@ -0,0 +1,20 @@ +interface CenterIconProps { + className?: string; +} + +export const CenterIcon = ({ className }: CenterIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="0 0 100 100" + xmlns="http://www.w3.org/2000/svg" + fill="none" + className={className} + > + <circle cx="50" cy="50" r="45" strokeWidth="6" fill="none" /> + <line x1="50" y1="5" x2="50" y2="30" strokeWidth="6" /> + <line x1="50" y1="70" x2="50" y2="95" strokeWidth="6" /> + <line x1="5" y1="50" x2="30" y2="50" strokeWidth="6" /> + <line x1="65" y1="50" x2="95" y2="50" strokeWidth="6" /> + </svg> +); diff --git a/src/shared/Icon/Icons/CrossedEyeIcon.tsx b/src/shared/Icon/Icons/CrossedEyeIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6706f0880227af1dabeaf42b5ff91de6a82d591b --- /dev/null +++ b/src/shared/Icon/Icons/CrossedEyeIcon.tsx @@ -0,0 +1,26 @@ +import { JSX } from 'react'; + +interface CrossedEyeIconProps { + className?: string; +} + +export const CrossedEyeIcon = ({ className }: CrossedEyeIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + strokeWidth="1.5" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z" + fill="none" + strokeLinecap="round" + strokeLinejoin="round" + /> + <circle cx="12" cy="12" r="3" fill="none" strokeLinecap="round" strokeLinejoin="round" /> + <line x1="3" y1="3" x2="21" y2="21" fill="none" /> + </svg> +); diff --git a/src/shared/Icon/Icons/DotsIcon.tsx b/src/shared/Icon/Icons/DotsIcon.tsx index 8696aa3f941b30450892334efb70ed273c9d5987..437365ab1b80dad1de57becdfabb44052954c2f8 100644 --- a/src/shared/Icon/Icons/DotsIcon.tsx +++ b/src/shared/Icon/Icons/DotsIcon.tsx @@ -4,8 +4,8 @@ interface DotsIconProps { export const DotsIcon = ({ className }: DotsIconProps): JSX.Element => ( <svg - width="14" - height="14" + width="24" + height="24" viewBox="0 0 14 14" fill="none" className={className} diff --git a/src/shared/Icon/Icons/EditImageIcon.tsx b/src/shared/Icon/Icons/EditImageIcon.tsx index 7cf8046996760387ef6167a4ed59fdba27ae1fc3..1cb37615a15c0feb267a72e8008a6699cc55bffc 100644 --- a/src/shared/Icon/Icons/EditImageIcon.tsx +++ b/src/shared/Icon/Icons/EditImageIcon.tsx @@ -4,8 +4,8 @@ interface EditImageIconProps { export const EditImageIcon = ({ className }: EditImageIconProps): JSX.Element => ( <svg - width="24" - height="24" + width="20" + height="20" viewBox="0 0 24 24" fill="none" className={className} diff --git a/src/shared/Icon/Icons/EyeIcon.tsx b/src/shared/Icon/Icons/EyeIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..302f111af8b26a5cb1a5fa0f8ccf1107fbbf56ab --- /dev/null +++ b/src/shared/Icon/Icons/EyeIcon.tsx @@ -0,0 +1,25 @@ +import { JSX } from 'react'; + +interface EyeIconProps { + className?: string; +} + +export const EyeIcon = ({ className }: EyeIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + strokeWidth="1.5" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z" + fill="none" + strokeLinecap="round" + strokeLinejoin="round" + /> + <circle cx="12" cy="12" r="3" fill="none" strokeLinecap="round" strokeLinejoin="round" /> + </svg> +); diff --git a/src/shared/Icon/Icons/ImageIcon.tsx b/src/shared/Icon/Icons/ImageIcon.tsx index 1c616b7fd83cb0bcdd543afb5f3223f041f0a55f..9a737e8d00f044f6021066ac972890cc601304dc 100644 --- a/src/shared/Icon/Icons/ImageIcon.tsx +++ b/src/shared/Icon/Icons/ImageIcon.tsx @@ -4,22 +4,25 @@ interface ImageIconProps { export const ImageIcon = ({ className }: ImageIconProps): JSX.Element => ( <svg - width="24" - height="24" - viewBox="0 0 24 24" + width="20" + height="20" + viewBox="0 0 200 200" fill="none" className={className} xmlns="http://www.w3.org/2000/svg" > - <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5" /> - <circle cx="8" cy="8" r="1.5" stroke="currentColor" strokeWidth="1.5" fill="none" /> - <path - d="M4 18L9 13L12 16L16 12L20 18H4Z" - stroke="currentColor" - strokeWidth="1.5" + <rect + x="5" + y="20" + width="190" + height="155" + stroke="black" + strokeWidth="6" + fill="none" strokeLinecap="round" strokeLinejoin="round" - fill="none" /> + <circle cx="40" cy="55" r="20" fill="black" /> + <path d="M5 175 L5 155 L50 105 L80 135 L140 65 L195 135 L195 175 Z" fill="black" /> </svg> ); diff --git a/src/shared/Icon/Icons/LayersIcon.tsx b/src/shared/Icon/Icons/LayersIcon.tsx index bebcbea3e67f7c083ab14369650a45523340de1b..a3e7bcc479a8396dc0eb2a5a524c690fea3f4a38 100644 --- a/src/shared/Icon/Icons/LayersIcon.tsx +++ b/src/shared/Icon/Icons/LayersIcon.tsx @@ -6,31 +6,28 @@ export const LayersIcon = ({ className }: LayersIconProps): JSX.Element => ( <svg width="20" height="20" - viewBox="0 0 24 24" + viewBox="0 0 22 22" fill="none" className={className} xmlns="http://www.w3.org/2000/svg" > <path d="M12 4L4 8.5L12 13L20 8.5L12 4Z" - stroke="black" - strokeWidth="1" + strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /> <path d="M4 12.5L12 17L20 12.5" - stroke="black" - strokeWidth="1" + strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /> <path d="M4 16.5L12 21L20 16.5" - stroke="black" - strokeWidth="1" + strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" diff --git a/src/shared/Icon/Icons/PadlockLockedIcon.tsx b/src/shared/Icon/Icons/PadlockLockedIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4491a472dc10a99a344c3e1dc1325e51216aa0e3 --- /dev/null +++ b/src/shared/Icon/Icons/PadlockLockedIcon.tsx @@ -0,0 +1,20 @@ +import { JSX } from 'react'; + +interface PadlockLockedIconProps { + className?: string; +} + +export const PadlockLockedIcon = ({ className }: PadlockLockedIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + strokeWidth="1.5" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <rect x="5" y="10" width="14" height="10" rx="2" fill="none" /> + <path d="M8 10V6a4 4 0 0 1 8 0v4" fill="none" /> + </svg> +); diff --git a/src/shared/Icon/Icons/PadlockOpenIcon.tsx b/src/shared/Icon/Icons/PadlockOpenIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2c966931fcd748154220e0d341842e7467ad090f --- /dev/null +++ b/src/shared/Icon/Icons/PadlockOpenIcon.tsx @@ -0,0 +1,20 @@ +import { JSX } from 'react'; + +interface PadlockOpenIconProps { + className?: string; +} + +export const PadlockOpenIcon = ({ className }: PadlockOpenIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + strokeWidth="1.5" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <rect x="5" y="10" width="14" height="10" rx="2" fill="none" /> + <path d="M16 10V6a4 4 0 0 0-8 0" fill="none" /> + </svg> +); diff --git a/src/shared/Icon/Icons/TextIcon.tsx b/src/shared/Icon/Icons/TextIcon.tsx index 0cc947a6591c0dcb59a34089d5c926b577ea04d5..32a4ee0d5adeaa101c01160a47aa1da60b7bb526 100644 --- a/src/shared/Icon/Icons/TextIcon.tsx +++ b/src/shared/Icon/Icons/TextIcon.tsx @@ -6,14 +6,13 @@ interface TextIconProps { export const TextIcon = ({ className }: TextIconProps): JSX.Element => ( <svg - width="24" - height="24" + width="20" + height="20" viewBox="0 0 24 24" fill="none" className={className} xmlns="http://www.w3.org/2000/svg" > - <circle cx="12" cy="12" r="12" fill="none" /> <text x="50%" y="50%" diff --git a/src/shared/Icon/Icons/TrashIcon.tsx b/src/shared/Icon/Icons/TrashIcon.tsx index cf0b83b7965f17dbe37a1e20711dc3ef48d77a2c..f7d093a664a1168dc170fe232dfab75dcd72503b 100644 --- a/src/shared/Icon/Icons/TrashIcon.tsx +++ b/src/shared/Icon/Icons/TrashIcon.tsx @@ -4,27 +4,12 @@ interface TrashIconProps { export const TrashIcon = ({ className }: TrashIconProps): JSX.Element => ( <svg - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - className={className} xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 22 22" + width="20" + height="20" + className={className} > - <path d="M7 6H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> - <path d="M9 4H15V6H9V4Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" /> - <rect - x="6" - y="6" - width="12" - height="14" - rx="1" - stroke="currentColor" - strokeWidth="1.5" - fill="none" - /> - <path d="M9 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> - <path d="M12 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> - <path d="M15 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> + <path d="M9 3V4H4V6H5V20C5 21.1 5.9 22 7 22H17C18.1 22 19 21.1 19 20V6H20V4H15V3H9M7 6H17V20H7V6M9 8V18H11V8H9M13 8V18H15V8H13Z" /> </svg> ); diff --git a/src/shared/IconButton/IconButton.component.tsx b/src/shared/IconButton/IconButton.component.tsx index be43b0d0d27d9e89016dffe112b2eb44b768f223..569d745b012b62a24094848b3addd30c99cba68b 100644 --- a/src/shared/IconButton/IconButton.component.tsx +++ b/src/shared/IconButton/IconButton.component.tsx @@ -27,6 +27,18 @@ export const IconButton = ({ throw new Error('IconButton component must have a icon property!'); } + const isStrokeIcon = [ + 'plugin', + 'bring-back', + 'bring-front', + 'center', + 'eye', + 'crossed-eye', + 'padlock-open', + 'padlock-locked', + 'layers', + ].includes(icon); + return ( <button className={twMerge( @@ -41,9 +53,9 @@ export const IconButton = ({ > <Icon className={twMerge( - icon !== 'plugin' - ? 'fill-font-400 group-hover:fill-primary-500 group-active:fill-primary-500' - : 'stroke-font-400 group-hover:stroke-primary-500 group-active:stroke-primary-500', + isStrokeIcon + ? 'stroke-font-400 group-hover:stroke-primary-500 group-active:stroke-primary-500' + : 'fill-font-400 group-hover:fill-primary-500 group-active:fill-primary-500', isActive && getActiveFillOrStrokeColor(icon), classNameIcon, )} diff --git a/src/types/bioEntity.ts b/src/types/bioEntity.ts deleted file mode 100644 index 1d7984699daa6b6beddc0e655b10f537119f4eb0..0000000000000000000000000000000000000000 --- a/src/types/bioEntity.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BioEntity } from './models'; -import { PinType } from './pin'; - -export interface BioEntityWithPinType extends BioEntity { - type: PinType; -} - -export type MultiPinBioEntity = BioEntityWithPinType[]; diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index bcd62e8969f377a8c51ef828a8f90a513aaf0387..b87711646ef5794efb756ef91f93a3266785ecde 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -32,6 +32,13 @@ export type IconTypes = | 'pencil' | 'arrow-double-up' | 'arrow-double-down' - | 'text'; + | 'text' + | 'eye' + | 'crossed-eye' + | 'padlock-open' + | 'padlock-locked' + | 'center' + | 'bring-front' + | 'bring-back'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; diff --git a/src/types/modelElement.ts b/src/types/modelElement.ts new file mode 100644 index 0000000000000000000000000000000000000000..faa3d49b66f94ebc755fb4261bd34959eb74f3ba --- /dev/null +++ b/src/types/modelElement.ts @@ -0,0 +1,8 @@ +import { ModelElement } from './models'; +import { PinType } from './pin'; + +export interface ModelElementWithPinType extends ModelElement { + type: PinType; +} + +export type MultiPinModelElement = ModelElementWithPinType[]; diff --git a/src/types/models.ts b/src/types/models.ts index 03578facf09c3ac92bb5705a969a32f029eabb2a..fd2d8a3733b0b89049f46c9b59fe368228837250 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -1,6 +1,5 @@ import { bioEntityContentSchema } from '@/models/bioEntityContentSchema'; import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema'; -import { bioEntitySchema } from '@/models/bioEntitySchema'; import { chemicalSchema } from '@/models/chemicalSchema'; import { colorSchema } from '@/models/colorSchema'; import { configurationOptionSchema } from '@/models/configurationOptionSchema'; @@ -71,6 +70,7 @@ import { segmentSchema } from '@/models/segmentSchema'; import { layerImageSchema } from '@/models/layerImageSchema'; import { glyphSchema } from '@/models/glyphSchema'; import { overlayGroupSchema } from '@/models/overlayGroupSchema'; +import { publicationElementSchema } from '@/models/publicationElementSchema'; export type Project = z.infer<typeof projectSchema>; export type OverviewImageView = z.infer<typeof overviewImageView>; @@ -104,7 +104,6 @@ export type Modification = z.infer<typeof modelElementModificationSchema>; export type MapOverlay = z.infer<typeof mapOverlaySchema>; export type Drug = z.infer<typeof drugSchema>; export type PinDetailsItem = z.infer<typeof targetSchema>; -export type BioEntity = z.infer<typeof bioEntitySchema>; export type BioEntityContent = z.infer<typeof bioEntityContentSchema>; export type BioEntityResponse = z.infer<typeof bioEntityResponseSchema>; export type Chemical = z.infer<typeof chemicalSchema>; @@ -134,6 +133,7 @@ export type CreatedOverlayFile = z.infer<typeof createdOverlayFileSchema>; export type Color = z.infer<typeof colorSchema>; export type Statistics = z.infer<typeof statisticsSchema>; export type Publication = z.infer<typeof publicationSchema>; +export type PublicationElement = z.infer<typeof publicationElementSchema>; export type ExportNetwork = z.infer<typeof exportNetworkchema>; export type ExportElements = z.infer<typeof exportElementsSchema>; export type MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interferes with global Plugin type diff --git a/src/types/pin.ts b/src/types/pin.ts index 7310215f63f09860d9f84880f2fbf024063adff0..8c0e92172748128bdcee2c91e1541b9ea703503d 100644 --- a/src/types/pin.ts +++ b/src/types/pin.ts @@ -1 +1 @@ -export type PinType = 'chemicals' | 'drugs' | 'bioEntity' | 'comment'; +export type PinType = 'chemicals' | 'drugs' | 'modelElement' | 'comment'; diff --git a/src/utils/bioEntity/mapModelElementToBioEntity.ts b/src/utils/bioEntity/mapModelElementToBioEntity.ts deleted file mode 100644 index e8be81f2163ebdecd5034fee53d807cbf9d2fab0..0000000000000000000000000000000000000000 --- a/src/utils/bioEntity/mapModelElementToBioEntity.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BioEntity, ModelElement } from '@/types/models'; - -export function mapModelElementToBioEntity(modelElement: ModelElement): BioEntity { - return { - id: modelElement.id, - name: modelElement.name, - model: modelElement.model, - elementId: modelElement.elementId, - references: modelElement.references, - z: modelElement.z, - notes: modelElement.notes, - symbol: modelElement.symbol, - homodimer: modelElement.homodimer, - nameX: modelElement.nameX, - nameY: modelElement.nameY, - nameWidth: modelElement.nameWidth, - nameHeight: modelElement.nameHeight, - nameVerticalAlign: modelElement.nameVerticalAlign, - nameHorizontalAlign: modelElement.nameHorizontalAlign, - width: modelElement.width, - height: modelElement.height, - visibilityLevel: modelElement.visibilityLevel, - transparencyLevel: modelElement.transparencyLevel, - synonyms: modelElement.synonyms, - formerSymbols: modelElement.formerSymbols, - fullName: modelElement.fullName, - abbreviation: modelElement.abbreviation, - formula: modelElement.formula, - glyph: modelElement.glyph, - activity: modelElement.activity, - hypothetical: modelElement.hypothetical, - boundaryCondition: modelElement.boundaryCondition, - constant: modelElement.constant, - initialAmount: modelElement.initialAmount, - initialConcentration: modelElement.initialConcentration, - charge: modelElement.charge, - substanceUnits: modelElement.substanceUnits, - onlySubstanceUnits: modelElement.onlySubstanceUnits, - modificationResidues: modelElement.modificationResidues, - complex: modelElement.complex, - submodel: modelElement.submodel, - x: modelElement.x, - y: modelElement.y, - lineWidth: modelElement.lineWidth, - fontColor: modelElement.fontColor, - fontSize: modelElement.fontSize, - fillColor: modelElement.fillColor, - borderColor: modelElement.borderColor, - sboTerm: modelElement.sboTerm, - } as BioEntity; -} diff --git a/src/utils/bioEntity/mapReactionToBioEntity.ts b/src/utils/bioEntity/mapReactionToModelElement.ts similarity index 72% rename from src/utils/bioEntity/mapReactionToBioEntity.ts rename to src/utils/bioEntity/mapReactionToModelElement.ts index 0aab81b7588933094cc21149a60c3a688db15e3e..2f2f2dc572aa6ce43f71b9e1ac58f95af9903bdc 100644 --- a/src/utils/bioEntity/mapReactionToBioEntity.ts +++ b/src/utils/bioEntity/mapReactionToModelElement.ts @@ -1,6 +1,6 @@ -import { BioEntity, NewReaction } from '@/types/models'; +import { ModelElement, NewReaction } from '@/types/models'; -export function mapReactionToBioEntity(reaction: NewReaction): BioEntity { +export function mapReactionToModelElement(reaction: NewReaction): ModelElement { return { id: reaction.id, name: reaction.name, @@ -15,5 +15,5 @@ export function mapReactionToBioEntity(reaction: NewReaction): BioEntity { abbreviation: reaction.abbreviation, formula: reaction.formula, sboTerm: reaction.sboTerm, - } as BioEntity; + } as ModelElement; } diff --git a/src/utils/testing/getReduxWrapperWithStore.tsx b/src/utils/testing/getReduxWrapperWithStore.tsx index 18c3beb8cf9e415abdd6e5820e9360a981e1b70e..e2ce968ba230a5359a6bc841d29dc7f0915ef98c 100644 --- a/src/utils/testing/getReduxWrapperWithStore.tsx +++ b/src/utils/testing/getReduxWrapperWithStore.tsx @@ -1,6 +1,7 @@ import { RootState, StoreType, middlewares, reducers } from '@/redux/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; +import { WebSocketEntityUpdatesProvider } from '@/utils/websocket-entity-updates/webSocketEntityUpdatesProvider'; import { MapInstanceContext, MapInstanceProvider } from '../context/mapInstanceContext'; interface WrapperProps { @@ -21,6 +22,21 @@ export type GetReduxWrapperUsingSliceReducer = ( store: StoreType; }; +jest.mock('react-use-websocket', () => ({ + __esModule: true, + default: jest.fn(() => ({ + sendMessage: jest.fn(), + lastJsonMessage: null, + readyState: 1, + })), + ReadyState: { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + }, +})); + export const getReduxWrapperWithStore: GetReduxWrapperUsingSliceReducer = ( preloadedState: InitialStoreState = {}, options: Options = {}, @@ -33,7 +49,9 @@ export const getReduxWrapperWithStore: GetReduxWrapperUsingSliceReducer = ( const Wrapper = ({ children }: WrapperProps): JSX.Element => ( <MapInstanceProvider initialValue={options.mapInstanceContextValue}> - <Provider store={testStore}>{children}</Provider> + <Provider store={testStore}> + <WebSocketEntityUpdatesProvider>{children}</WebSocketEntityUpdatesProvider> + </Provider> </MapInstanceProvider> ); diff --git a/src/utils/websocket-entity-updates/webSocketEntityUpdates.constants.ts b/src/utils/websocket-entity-updates/webSocketEntityUpdates.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2486f9acb813a9a95b5b79daccce30b41b82731 --- /dev/null +++ b/src/utils/websocket-entity-updates/webSocketEntityUpdates.constants.ts @@ -0,0 +1,19 @@ +export const ENTITY_OPERATION_TYPES = { + ENTITY_CREATED: 'ENTITY_CREATED', + ENTITY_UPDATED: 'ENTITY_UPDATED', + ENTITY_DELETED: 'ENTITY_DELETED', +} as const; + +export const ACKNOWLEDGE_OPERATION_TYPES = { + MESSAGE_PROCESSED_SUCCESSFULLY: 'MESSAGE_PROCESSED_SUCCESSFULLY', +} as const; + +export const ENTITY_TYPES = { + GLYPH: 'GLYPH', + LAYER: 'LAYER', + LAYER_IMAGE: 'LAYER_IMAGE', + LAYER_LINE: 'LAYER_LINE', + LAYER_OVAL: 'LAYER_OVAL', + LAYER_RECTANGLE: 'LAYER_RECTANGLE', + LAYER_TEXT: 'LAYER_TEXT', +} as const; diff --git a/src/utils/websocket-entity-updates/webSocketEntityUpdates.types.ts b/src/utils/websocket-entity-updates/webSocketEntityUpdates.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e985d56a558456eb477898a25ea600f6d8012b6 --- /dev/null +++ b/src/utils/websocket-entity-updates/webSocketEntityUpdates.types.ts @@ -0,0 +1,27 @@ +import { + ENTITY_TYPES, + ENTITY_OPERATION_TYPES, + ACKNOWLEDGE_OPERATION_TYPES, +} from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; + +export type EntityType = (typeof ENTITY_TYPES)[keyof typeof ENTITY_TYPES]; +export type EntityOperationType = + (typeof ENTITY_OPERATION_TYPES)[keyof typeof ENTITY_OPERATION_TYPES]; +export type AcknowledgeOperationType = + (typeof ACKNOWLEDGE_OPERATION_TYPES)[keyof typeof ACKNOWLEDGE_OPERATION_TYPES]; + +export interface WebSocketEntityUpdateInterface { + entityId: number; + entityType: EntityType; + entityVersion: number; + mapId: number; + projectId: string; + layerId: number; + type: EntityOperationType; +} + +export interface WebSocketAcknowledgeInterface { + commandId: string | null; + message: string; + type: AcknowledgeOperationType; +} diff --git a/src/utils/websocket-entity-updates/webSocketEntityUpdatesProvider.tsx b/src/utils/websocket-entity-updates/webSocketEntityUpdatesProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..88eaefed5befa0ba5cc3c9c41cd5008b50c264cf --- /dev/null +++ b/src/utils/websocket-entity-updates/webSocketEntityUpdatesProvider.tsx @@ -0,0 +1,96 @@ +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import useWebSocket, { ReadyState } from 'react-use-websocket'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { userTokenSelector } from '@/redux/user/user.selectors'; +import { projectIdSelector } from '@/redux/project/project.selectors'; +import { BASE_API_URL } from '@/constants'; +import { ACKNOWLEDGE_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; +import { + WebSocketAcknowledgeInterface, + WebSocketEntityUpdateInterface, +} from '@/utils/websocket-entity-updates/webSocketEntityUpdates.types'; + +export interface WebSocketEntityUpdatesContextInterface { + sendMessage: (msg: string) => void; + lastJsonMessage: WebSocketEntityUpdateInterface | WebSocketAcknowledgeInterface | null; + readyState: ReadyState; +} + +const WebSocketEntityUpdatesContext = createContext< + WebSocketEntityUpdatesContextInterface | undefined +>(undefined); +const SOCKET_URL = `${BASE_API_URL.replace('https', 'wss')}/websocket/entity-updates`; +const LOGIN_COMMAND_ID = 'login'; + +export const WebSocketEntityUpdatesProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const userToken = useAppSelector(userTokenSelector); + const projectId = useAppSelector(projectIdSelector); + const [isLogged, setIsLogged] = useState(false); + const { lastJsonMessage, sendMessage, readyState } = useWebSocket< + WebSocketEntityUpdateInterface | WebSocketAcknowledgeInterface + >(SOCKET_URL, { + shouldReconnect: () => true, + reconnectInterval: 5000, + share: true, + onMessage: (messageEvent: MessageEvent) => { + try { + const data = JSON.parse(messageEvent.data); + if ( + data.commandId === LOGIN_COMMAND_ID && + data.type === ACKNOWLEDGE_OPERATION_TYPES.MESSAGE_PROCESSED_SUCCESSFULLY && + data.message === 'ok' + ) { + setIsLogged(true); + } + } catch { + throw new Error('Websocket message parsing error'); + } + }, + }); + + useEffect(() => { + if (readyState !== ReadyState.OPEN) { + return; + } + if (userToken && !isLogged) { + sendMessage( + JSON.stringify({ command: 'login', token: userToken, commandId: LOGIN_COMMAND_ID }), + ); + } else if (!userToken) { + setIsLogged(false); + } + }, [isLogged, readyState, sendMessage, userToken]); + + useEffect(() => { + if (readyState !== ReadyState.OPEN) { + return; + } + + if (projectId && isLogged) { + sendMessage(JSON.stringify({ command: 'register-for-project-updates', projectId })); + } + }, [projectId, readyState, sendMessage, isLogged]); + + const contextValue = useMemo( + () => ({ sendMessage, lastJsonMessage, readyState }), + [sendMessage, lastJsonMessage, readyState], + ); + + return ( + <WebSocketEntityUpdatesContext.Provider value={contextValue}> + {children} + </WebSocketEntityUpdatesContext.Provider> + ); +}; + +export const useWebSocketEntityUpdatesContext = (): WebSocketEntityUpdatesContextInterface => { + const context = useContext(WebSocketEntityUpdatesContext); + if (!context) { + throw new Error( + 'useWebSocketEntityUpdatesContext must be used inside a WebSocketEntityUpdatesProvider', + ); + } + return context; +};