diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx index c1d4d1206263c5392b481c06f990f482b9f62b16..c03fcf7f1f04dd141a61c6ed56a261676a0b362d 100644 --- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx @@ -10,6 +10,9 @@ import { Button } from '@/shared/Button'; import { Icon } from '@/shared/Icon'; import { MouseEvent } from 'react'; import { twMerge } from 'tailwind-merge'; +import { getComments } from '@/redux/comment/thunks/getComments'; +import { commentSelector } from '@/redux/comment/comment.selectors'; +import { hideComments, showComments } from '@/redux/comment/comment.slice'; export const MapNavigation = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -20,6 +23,8 @@ export const MapNavigation = (): JSX.Element => { const isActive = (modelId: number): boolean => currentModelId === modelId; const isNotMainMap = (modelName: string): boolean => modelName !== MAIN_MAP; + const commentsOpen = useAppSelector(commentSelector).isOpen; + const onCloseSubmap = (event: MouseEvent<HTMLDivElement>, map: OppenedMap): void => { event.stopPropagation(); if (isActive(map.modelId)) { @@ -45,27 +50,47 @@ export const MapNavigation = (): JSX.Element => { } }; + const toggleComments = async (): Promise<void> => { + if (!commentsOpen) { + await dispatch(getComments()); + dispatch(showComments()); + } else { + dispatch(hideComments()); + } + }; + return ( - <div className="flex h-10 w-full flex-row flex-nowrap justify-start overflow-y-auto bg-white-pearl text-xs shadow-primary"> - {openedMaps.map(map => ( + <div className="flex h-10 w-full flex-row flex-nowrap justify-between overflow-y-auto bg-white-pearl text-xs shadow-primary"> + <div className="flex flex-row items-center justify-start"> + {openedMaps.map(map => ( + <Button + key={map.modelId} + className={twMerge( + 'relative h-10 whitespace-nowrap', + isActive(map.modelId) ? 'bg-[#EBF4FF]' : 'font-normal', + )} + variantStyles={isActive(map.modelId) ? 'secondary' : 'ghost'} + onClick={(): void => onSubmapTabClick(map)} + > + {map.modelName} + {isNotMainMap(map.modelName) && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + <div onClick={(event): void => onCloseSubmap(event, map)} data-testid="close-icon"> + <Icon name="close" className="ml-3 h-5 w-5 fill-font-400" /> + </div> + )} + </Button> + ))} + </div> + <div className="flex items-center"> <Button - key={map.modelId} - className={twMerge( - 'h-10 whitespace-nowrap', - isActive(map.modelId) ? 'bg-[#EBF4FF]' : 'font-normal', - )} - variantStyles={isActive(map.modelId) ? 'secondary' : 'ghost'} - onClick={(): void => onSubmapTabClick(map)} + className="mx-4 flex-none" + variantStyles="secondary" + onClick={() => toggleComments()} > - {map.modelName} - {isNotMainMap(map.modelName) && ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions - <div onClick={(event): void => onCloseSubmap(event, map)} data-testid="close-icon"> - <Icon name="close" className="ml-3 h-5 w-5 fill-font-400" /> - </div> - )} + {commentsOpen ? 'Hide Comments' : 'Show Comments'} </Button> - ))} + </div> </div> ); }; diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx index 3e9efc13be0c4229d62b0a7a33056d1b9b413bc5..06e815d3c8bb534d50a118613f64a912e222aac6 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -2,6 +2,7 @@ import { ZERO } from '@/constants/common'; import { currentDrawerBioEntityRelatedSubmapSelector, currentDrawerBioEntitySelector, + currentDrawerElementCommentsSelector, } from '@/redux/bioEntity/bioEntity.selectors'; import { getChemicalsForBioEntityDrawerTarget, @@ -11,6 +12,7 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { ElementSearchResultType } from '@/types/models'; +import { CommentItem } from '@/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component'; import { CollapsibleSection } from '../ExportDrawer/CollapsibleSection'; import { AnnotationItem } from './AnnotationItem'; import { AssociatedSubmap } from './AssociatedSubmap'; @@ -23,6 +25,7 @@ const TARGET_PREFIX: ElementSearchResultType = `ALIAS`; export const BioEntityDrawer = (): React.ReactNode => { const dispatch = useAppDispatch(); const bioEntityData = useAppSelector(currentDrawerBioEntitySelector); + const commentsData = useAppSelector(currentDrawerElementCommentsSelector); const relatedSubmap = useAppSelector(currentDrawerBioEntityRelatedSubmapSelector); const currentTargetId = bioEntityData?.id ? `${TARGET_PREFIX}:${bioEntityData.id}` : ''; @@ -38,6 +41,7 @@ export const BioEntityDrawer = (): React.ReactNode => { } const isReferenceAvailable = bioEntityData.references.length > ZERO; + const isCommentAvailable = commentsData.length > ZERO; return ( <div className="h-calc-drawer" data-testid="bioentity-drawer"> @@ -86,6 +90,9 @@ export const BioEntityDrawer = (): React.ReactNode => { isShowGroupedOverlays={Boolean(relatedSubmap)} isShowOverlayBioEntityName={Boolean(relatedSubmap)} /> + {isCommentAvailable && <div className="font-bold"> Comments</div>} + {isCommentAvailable && + commentsData.map(comment => <CommentItem key={comment.id} comment={comment} />)} </div> </div> ); diff --git a/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..282ee447d17e2583ad08309f2f9407635debdbc9 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx @@ -0,0 +1,20 @@ +import { Comment } from '@/types/models'; +import React from 'react'; + +interface CommentItemProps { + comment: Comment; +} + +export const CommentItem = (commentItemProps: CommentItemProps): JSX.Element => { + const { comment } = commentItemProps; + let { owner } = comment; + if (!owner) { + owner = 'Anonymous'; + } + return ( + <div className="border border-slate-400"> + <div className="p-4 font-bold"> {owner} </div> + <div className="p-4"> {comment.content} </div> + </div> + ); +}; diff --git a/src/components/Map/Drawer/CommentDrawer/CommentDrawer.component.tsx b/src/components/Map/Drawer/CommentDrawer/CommentDrawer.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5079669e629d9f5672788a377400108e53f25520 --- /dev/null +++ b/src/components/Map/Drawer/CommentDrawer/CommentDrawer.component.tsx @@ -0,0 +1,31 @@ +import { DrawerHeading } from '@/shared/DrawerHeading'; +import { useSelector } from 'react-redux'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { CommentItem } from '@/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component'; +import { ZERO } from '@/constants/common'; +import { currentDrawerCommentId } from '@/redux/drawer/drawer.selectors'; +import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; + +export const CommentDrawer = (): React.ReactNode => { + const commentId = useSelector(currentDrawerCommentId); + + const commentsData = useAppSelector(allCommentsSelectorOfCurrentMap); + + const comments = commentsData.filter(commentEntry => commentEntry.id === commentId); + + if (comments.length === ZERO) { + return null; + } + + return ( + <div className="h-full max-h-full" data-testid="reaction-drawer"> + <DrawerHeading title={<span className="font-normal">Area: </span>} /> + <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col gap-6 overflow-y-auto p-6"> + <div className="font-bold"> Comments</div> + {comments.map(comment => ( + <CommentItem key={comment.id} comment={comment} /> + ))} + </div> + </div> + ); +}; diff --git a/src/components/Map/Drawer/CommentDrawer/index.ts b/src/components/Map/Drawer/CommentDrawer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c55db1b1fc7616d7d986678344a3bcb560f8c823 --- /dev/null +++ b/src/components/Map/Drawer/CommentDrawer/index.ts @@ -0,0 +1 @@ +export { CommentDrawer } from './CommentDrawer.component'; diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index de1aa94ea44f6f9e2203ea3a386e373cbce620d6..71f10406f94ba978b951b7649e921f4c6013fa53 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -2,6 +2,7 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants'; import { drawerSelector } from '@/redux/drawer/drawer.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { twMerge } from 'tailwind-merge'; +import { CommentDrawer } from '@/components/Map/Drawer/CommentDrawer'; import { AvailablePluginsDrawer } from './AvailablePluginsDrawer'; import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; import { ExportDrawer } from './ExportDrawer'; @@ -30,6 +31,7 @@ export const Drawer = (): JSX.Element => { {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />} {isOpen && drawerName === 'export' && <ExportDrawer />} {isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />} + {isOpen && drawerName === 'comment' && <CommentDrawer />} </div> ); }; diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx index 2ba8e96d0a91fd7c88bf022494114eedb3ea63ea..ea1396ea0af8a1956983723000c72c5a2f4e91c2 100644 --- a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx +++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx @@ -4,6 +4,10 @@ import { } from '@/redux/reactions/reactions.selector'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { useSelector } from 'react-redux'; +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 { ReferenceGroup } from './ReferenceGroup'; import { ConnectedBioEntitiesList } from './ConnectedBioEntitiesList'; @@ -11,10 +15,14 @@ export const ReactionDrawer = (): React.ReactNode => { const reaction = useSelector(currentDrawerReactionSelector); const referencesGrouped = useSelector(currentDrawerReactionGroupedReferencesSelector); + const commentsData = useAppSelector(currentDrawerReactionCommentsSelector); + if (!reaction) { return null; } + const isCommentAvailable = commentsData.length > ZERO; + return ( <div className="h-full max-h-full" data-testid="reaction-drawer"> <DrawerHeading @@ -34,6 +42,9 @@ export const ReactionDrawer = (): React.ReactNode => { <ReferenceGroup key={group.source} group={group} /> ))} <ConnectedBioEntitiesList /> + {isCommentAvailable && <div className="font-bold"> Comments</div>} + {isCommentAvailable && + commentsData.map(comment => <CommentItem key={comment.id} comment={comment} />)} </div> </div> ); 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 9f0b9e949d480487a32fbc560bb64fcc722e05cd..d612aae39c45cc2e5bd3bd258ab2e9a7775b2d4f 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsList.component.tsx @@ -39,6 +39,8 @@ export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => { } case 'bioEntity': return <div />; + case 'comment': + return <div />; case 'none': return <div />; default: 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 f0775c25e6de370a0e0ace771cf6729bff5f4658..bbc87ce8971d920fe9086d6eb0cc88ee5ce7f3e8 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 @@ -8,6 +8,7 @@ export const getPinColor = (type: PinTypeWithNone): string => { bioEntity: 'fill-primary-500', drugs: 'fill-orange', chemicals: 'fill-purple', + comment: 'fill-blue', none: 'none', }; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx index 0be1f6ddb98cabbae809ae7bca96b922e69660e1..3a46ecdc9645f9e2446fc76f52da22b8d5f2a0d3 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx @@ -32,6 +32,9 @@ const INITIAL_STATE: InitialStoreState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }, drugs: { data: [ diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx index 3ad3dbabe05b7683c49f4916d616c884b0e4aa47..3d5b627bc96d13a0b96a00150c8627afae1664f7 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx @@ -51,6 +51,9 @@ describe('SearchDrawerWrapper - component', () => { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }, }); @@ -77,6 +80,9 @@ describe('SearchDrawerWrapper - component', () => { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }, }); diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.test.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a76072960afb50b534f5d194b2cbf0c5e901450 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.test.ts @@ -0,0 +1,45 @@ +import { initialMapStateFixture } from '@/redux/map/map.fixtures'; +import { PinType } from '@/types/pin'; +import { UsePointToProjectionResult, usePointToProjection } from '@/utils/map/usePointToProjection'; +import { + GetReduxWrapperUsingSliceReducer, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { Feature } from 'ol'; +import Style from 'ol/style/Style'; +import { commentsFixture } from '@/models/fixtures/commentsFixture'; +import { getCommentsFeatures } from '@/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures'; + +const getPointToProjection = ( + wrapper: ReturnType<GetReduxWrapperUsingSliceReducer>['Wrapper'], +): UsePointToProjectionResult => { + const { result: usePointToProjectionHook } = renderHook(() => usePointToProjection(), { + wrapper, + }); + + return usePointToProjectionHook.current; +}; + +describe('getCommentsFeatures', () => { + const { Wrapper } = getReduxWrapperWithStore({ + map: initialMapStateFixture, + }); + const comments = commentsFixture.map(comment => ({ + ...comment, + pinType: 'comment' as PinType, + })); + + const pointToProjection = getPointToProjection(Wrapper); + + it('should return array of instances of Feature with Style', () => { + const result = getCommentsFeatures(comments, { + pointToProjection, + }); + + result.forEach(resultElement => { + expect(resultElement).toBeInstanceOf(Feature); + expect(resultElement.getStyle()).toBeInstanceOf(Style); + }); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1df19f82da886edd828486203ec240c466df840 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures.ts @@ -0,0 +1,57 @@ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { CommentWithPinType } from '@/types/comment'; +import { getPinFeature } from '@/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature'; +import { PinType } from '@/types/pin'; +import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas'; +import { getPinStyle } from '@/components/Map/MapViewer/utils/config/pinsLayer/getPinStyle'; + +export const getCommentFeature = ( + comment: CommentWithPinType, + { + pointToProjection, + type, + value, + }: { + pointToProjection: UsePointToProjectionResult; + type: PinType; + value: number; + }, +): Feature => { + const color = PINS_COLORS[type]; + + const textColor = TEXT_COLOR; + + const feature = getPinFeature( + { + x: comment.coord.x, + height: 0, + id: comment.id, + width: 0, + y: comment.coord.y, + }, + pointToProjection, + type, + ); + const style = getPinStyle({ + color, + value, + textColor, + }); + + feature.setStyle(style); + return feature; +}; + +export const getCommentsFeatures = ( + comments: CommentWithPinType[], + { + pointToProjection, + }: { + pointToProjection: UsePointToProjectionResult; + }, +): Feature[] => { + return comments.map((comment, index) => + getCommentFeature(comment, { pointToProjection, type: comment.pinType, value: index }), + ); +}; diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eb9e2cbd4706836bcbb812e235b7a937d15d32c --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-magic-numbers */ +import { usePointToProjection } from '@/utils/map/usePointToProjection'; +import Feature from 'ol/Feature'; +import { Geometry } from 'ol/geom'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import { useSelector } from 'react-redux'; +import { + allCommentsSelectorOfCurrentMap, + commentSelector, +} from '@/redux/comment/comment.selectors'; +import { getCommentsFeatures } from '@/components/Map/MapViewer/utils/config/commentsLayer/getCommentsFeatures'; +import { useMemo } from 'react'; + +export const useOlMapCommentsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { + const pointToProjection = usePointToProjection(); + const comments = useSelector(allCommentsSelectorOfCurrentMap); + const isVisible = useSelector(commentSelector).isOpen; + + const elementsFeatures = useMemo( + () => + [ + getCommentsFeatures(isVisible ? comments : [], { + pointToProjection, + }), + ].flat(), + [comments, pointToProjection, isVisible], + ); + + const vectorSource = useMemo(() => { + return new VectorSource({ + features: [...elementsFeatures], + }); + }, [elementsFeatures]); + + const pinsLayer = useMemo( + () => + new VectorLayer({ + source: vectorSource, + }), + [vectorSource], + ); + + return pinsLayer; +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts index 0bdc00fb4c56685316a0751e57143c1e3874de05..4fd4447318e8f5e4ee9435f07c0b9fdf367b6f1c 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts @@ -31,7 +31,7 @@ export const getBioEntitySingleFeature = ( ? TEXT_COLOR : addAlphaToHexString(TEXT_COLOR, INACTIVE_ELEMENT_OPACITY); - const feature = getPinFeature(bioEntity, pointToProjection); + const feature = getPinFeature(bioEntity, pointToProjection, type); const style = getPinStyle({ color, value, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts index f024c157616bbcb03f482fc476448c01f51de71e..c4114ff55e165d946b93c9733355bc4a69bc0bb9 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); + const feature = getPinFeature(marker, pointToProjection, 'bioEntity'); const style = getPinStyle({ color: addAlphaToHexString(marker.color, marker.opacity), value: marker.number, diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts index b4a48ede09cab0a641a6349d8a50ca48d332346a..d7280c47ee5ffcd745ab7f982e3347c788e47968 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts @@ -22,7 +22,7 @@ export const getMultipinSingleFeature = ( const [mainElement, ...sortedElements] = multipin.sort( (a, b) => (activeIds.includes(b.id) ? ONE : ZERO) - (activeIds.includes(a.id) ? ONE : ZERO), ); - const feature = getPinFeature(mainElement, pointToProjection); + const feature = getPinFeature(mainElement, pointToProjection, mainElement.type); const canvasPinsArgMainElement = getMultipinCanvasArgs(mainElement, { activeIds, 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 80af09f63b9e14b963a21b64de18e09c396451c8..c6ab67812279340c3fdb67a4f92f1b7c2ab7c7b3 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); + const result = getPinFeature(bioEntity, pointToProjection, 'bioEntity'); 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); + const pinMarkerResult = getPinFeature(PIN_MARKER, pointToProjection, 'bioEntity'); 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 8b5bf85bf3928e174a47033808766e91815ff14b..5113acc7cb5976a82167d39750b3cd91409f1d9e 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts @@ -6,6 +6,7 @@ import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import isUUID from 'is-uuid'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; +import { PinType } from '@/types/pin'; export const getPinFeature = ( { @@ -16,6 +17,7 @@ export const getPinFeature = ( id, }: Pick<BioEntity, 'id' | 'width' | 'height' | 'x' | 'y'> | MarkerWithPosition, pointToProjection: UsePointToProjectionResult, + pinType: PinType, ): Feature => { const isMarker = isUUID.anyNonNil(`${id}`); @@ -24,10 +26,18 @@ export const getPinFeature = ( y: y + (height || ZERO) / HALF, }; + let type = null; + + if (pinType === 'comment') { + type = FEATURE_TYPE.PIN_ICON_COMMENT; + } else { + type = isMarker ? FEATURE_TYPE.PIN_ICON_MARKER : FEATURE_TYPE.PIN_ICON_BIOENTITY; + } + const feature = new Feature({ geometry: new Point(pointToProjection(point)), id, - type: isMarker ? FEATURE_TYPE.PIN_ICON_MARKER : FEATURE_TYPE.PIN_ICON_BIOENTITY, + type, }); return feature; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts index a42e967759c12b43c1ce2da7158b4a2641a2620d..5739c30201df49638f2e1656189c0ab561886450 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts @@ -1,6 +1,7 @@ /* eslint-disable no-magic-numbers */ import { MapInstance } from '@/types/map'; import { useEffect } from 'react'; +import { useOlMapCommentsLayer } from '@/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer'; import { MapConfig } from '../../MapViewer.types'; import { useOlMapOverlaysLayer } from './overlaysLayer/useOlMapOverlaysLayer'; import { useOlMapPinsLayer } from './pinsLayer/useOlMapPinsLayer'; @@ -16,14 +17,15 @@ export const useOlMapLayers = ({ mapInstance }: UseOlMapLayersInput): MapConfig[ const pinsLayer = useOlMapPinsLayer(); const reactionsLayer = useOlMapReactionsLayer(); const overlaysLayer = useOlMapOverlaysLayer(); + const commentsLayer = useOlMapCommentsLayer(); useEffect(() => { if (!mapInstance) { return; } - mapInstance.setLayers([tileLayer, reactionsLayer, overlaysLayer, pinsLayer]); - }, [reactionsLayer, tileLayer, pinsLayer, mapInstance, overlaysLayer]); + mapInstance.setLayers([tileLayer, reactionsLayer, overlaysLayer, pinsLayer, commentsLayer]); + }, [reactionsLayer, tileLayer, pinsLayer, mapInstance, overlaysLayer, commentsLayer]); return [tileLayer, pinsLayer, reactionsLayer, overlaysLayer]; }; diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts index 6cbc68a3cbe12b4bdb153ec5b332303623c4360a..90263615be00e6ddd603f337c812392faf3f3127 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts @@ -22,12 +22,12 @@ describe('handleFeaturesClick - util', () => { it('should dispatch event onPinIconClick', () => { const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); - handleFeaturesClick(features, dispatch); + handleFeaturesClick(features, dispatch, []); expect(dispatchEventSpy).toHaveBeenCalledWith('onPinIconClick', { id: featureId }); }); it('should return shouldBlockCoordSearch=true', () => { - expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + expect(handleFeaturesClick(features, dispatch, [])).toStrictEqual({ shouldBlockCoordSearch: true, }); }); @@ -46,21 +46,21 @@ describe('handleFeaturesClick - util', () => { it('should dispatch event onPinIconClick', () => { const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); - handleFeaturesClick(features, dispatch); + handleFeaturesClick(features, dispatch, []); expect(dispatchEventSpy).toHaveBeenCalledWith('onPinIconClick', { id: featureId }); }); it('should dispatch actions regarding opening entity drawer', () => { const { store: localStore } = getReduxStoreWithActionsListener(); const { dispatch: localDispatch } = localStore; - handleFeaturesClick(features, localDispatch); + handleFeaturesClick(features, localDispatch, []); expect(store.getActions()).toStrictEqual([ { payload: 1234, type: 'drawer/openBioEntityDrawerById' }, ]); }); it('should return shouldBlockCoordSearch=true', () => { - expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + expect(handleFeaturesClick(features, dispatch, [])).toStrictEqual({ shouldBlockCoordSearch: true, }); }); @@ -79,12 +79,12 @@ describe('handleFeaturesClick - util', () => { it('should dispatch event onSurfaceClick', () => { const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); - handleFeaturesClick(features, dispatch); + handleFeaturesClick(features, dispatch, []); expect(dispatchEventSpy).toHaveBeenCalledWith('onSurfaceClick', { id: featureId }); }); it('should return shouldBlockCoordSearch=false', () => { - expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + expect(handleFeaturesClick(features, dispatch, [])).toStrictEqual({ shouldBlockCoordSearch: false, }); }); @@ -103,12 +103,12 @@ describe('handleFeaturesClick - util', () => { it('should dispatch event onSurfaceClick', () => { const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); - handleFeaturesClick(features, dispatch); + handleFeaturesClick(features, dispatch, []); expect(dispatchEventSpy).toHaveBeenCalledWith('onSurfaceClick', { id: featureId }); }); it('should return shouldBlockCoordSearch=false', () => { - expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + expect(handleFeaturesClick(features, dispatch, [])).toStrictEqual({ shouldBlockCoordSearch: false, }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts index ea1b25f9508cbdefd215e1d86b4a966caccd3df7..f1b980a9c9670573bfb071a903289eefb026bbc7 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts @@ -1,9 +1,15 @@ -import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common'; import { FEATURE_TYPE, PIN_ICON_ANY, SURFACE_ANY } from '@/constants/features'; -import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice'; +import { + openBioEntityDrawerById, + openCommentDrawerById, + openReactionDrawerById, +} from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { FeatureLike } from 'ol/Feature'; +import { Comment } from '@/types/models'; +import { getCommentElement, getCommentReaction } from '@/redux/comment/thunks/getComments'; interface HandleFeaturesClickResult { shouldBlockCoordSearch: boolean; @@ -12,6 +18,7 @@ interface HandleFeaturesClickResult { export const handleFeaturesClick = ( features: FeatureLike[], dispatch: AppDispatch, + comments: Comment[], ): HandleFeaturesClickResult => { const pinFeatures = features.filter(feature => PIN_ICON_ANY.includes(feature.get('type'))); const surfaceFeatures = features.filter(feature => SURFACE_ANY.includes(feature.get('type'))); @@ -23,6 +30,24 @@ export const handleFeaturesClick = ( if (pin.get('type') === FEATURE_TYPE.PIN_ICON_BIOENTITY) { dispatch(openBioEntityDrawerById(pinId)); + } else if (pin.get('type') === FEATURE_TYPE.PIN_ICON_COMMENT) { + const filteredComments = comments.filter(comment => comment.id === pinId); + if (filteredComments.length > ZERO) { + const { elementId, modelId, type } = filteredComments[ZERO]; + if (type === 'ALIAS') { + dispatch(getCommentElement({ elementId: Number(elementId), modelId })); + dispatch(openBioEntityDrawerById(Number(elementId))); + } else if (type === 'REACTION') { + dispatch(getCommentReaction({ elementId: Number(elementId), modelId })); + dispatch(openReactionDrawerById(Number(elementId))); + } else if (type === 'POINT') { + dispatch(openCommentDrawerById(Number(pinId))); + } else { + throw new Error(`Unknown comment type${type}`); + } + } else { + throw new Error(`Cannot find comment with id ${pinId}`); + } } }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts index ae02e8c16624fc9ef7f18b96f8f1e1a6646ad724..138ab32fd28777c55abbf9a0b09c972e34b883e4 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts @@ -67,6 +67,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); const coordinate = [90, 90]; const event = getEvent(coordinate); @@ -100,6 +101,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); const coordinate = [90, 90]; const point = { x: 180.0008084837557, y: 179.99919151624428 }; @@ -140,6 +142,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); const coordinate = [180, 180]; const point = { x: 360.0032339350228, y: 359.9967660649771 }; @@ -198,6 +201,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).not.toBeCalled()); @@ -237,6 +241,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); @@ -278,6 +283,7 @@ describe('onMapSingleClick - util', () => { MAX_ZOOM_MOCK_MOCK, ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, + [], ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts index 31dabd9af62d249b1344f4f06e6fdfbc4b7b3e23..cb09d0a5bccf2493ae96b6361ca3e5921ee8e9ef 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts @@ -3,6 +3,7 @@ import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; import { Map, MapBrowserEvent } from 'ol'; import { FeatureLike } from 'ol/Feature'; +import { Comment } from '@/types/models'; import { getSearchResults } from './getSearchResults'; import { handleDataReset } from './handleDataReset'; import { handleFeaturesClick } from './handleFeaturesClick'; @@ -10,11 +11,12 @@ import { handleSearchResultAction } from './handleSearchResultAction'; /* prettier-ignore */ export const onMapSingleClick = - (mapSize: MapSize, modelId: number, dispatch: AppDispatch, searchDistance: string | undefined, maxZoom: number, zoom: number, isResultDrawerOpen: boolean) => + (mapSize: MapSize, modelId: number, dispatch: AppDispatch, searchDistance: string | undefined, maxZoom: number, zoom: number, isResultDrawerOpen: boolean, + comments: Comment[]) => async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { const featuresAtPixel: FeatureLike[] = []; mapInstance.forEachFeatureAtPixel(pixel, (feature) => featuresAtPixel.push(feature)); - const { shouldBlockCoordSearch } = handleFeaturesClick(featuresAtPixel, dispatch); + const { shouldBlockCoordSearch } = handleFeaturesClick(featuresAtPixel, dispatch, comments); if (shouldBlockCoordSearch) { return; diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index f5aed286c360caadb3d9ef175a8074ccdde7bc0b..33013f2182bcfc3a424a5568ec7be512966e4d4d 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -16,6 +16,7 @@ import { Pixel } from 'ol/pixel'; import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useDebouncedCallback } from 'use-debounce'; +import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; import { onMapRightClick } from './mapRightClick/onMapRightClick'; import { onMapSingleClick } from './mapSingleClick/onMapSingleClick'; import { onMapPositionChange } from './onMapPositionChange'; @@ -38,6 +39,8 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) const pixel = useRef<Pixel>([]); const dispatch = useAppDispatch(); + const comments = useSelector(allCommentsSelectorOfCurrentMap); + useHandlePinIconClick(); const handleRightClick = useDebouncedCallback( @@ -63,6 +66,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) maxZoom, lastZoom || DEFAULT_ZOOM, isResultDrawerOpen, + comments, ), OPTIONS.clickPersistTime, { leading: false }, diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts index 29931be199a20e245d7c54b6185d186b9e089839..e5ab204a5e6465bf226c874f336676ecc86637f8 100644 --- a/src/constants/canvas.ts +++ b/src/constants/canvas.ts @@ -16,6 +16,7 @@ export const PINS_COLORS: Record<PinType, string> = { drugs: '#F48C41', chemicals: '#008325', bioEntity: '#106AD7', + comment: '#106AD7', }; export const PINS_COLOR_WITH_NONE: Record<PinTypeWithNone, string> = { diff --git a/src/constants/features.ts b/src/constants/features.ts index 4995bbcf07ff0173cb4c2760bf56874b1b9b9b1c..6edbb35236777a9f7313bfd230929bc3c0bb048e 100644 --- a/src/constants/features.ts +++ b/src/constants/features.ts @@ -3,7 +3,12 @@ export const FEATURE_TYPE = { PIN_ICON_MARKER: 'PIN_ICON_MARKER', SURFACE_OVERLAY: 'SURFACE_OVERLAY', SURFACE_MARKER: 'SURFACE_MARKER', + PIN_ICON_COMMENT: 'PIN_ICON_COMMENT', } as const; -export const PIN_ICON_ANY = [FEATURE_TYPE.PIN_ICON_BIOENTITY, FEATURE_TYPE.PIN_ICON_MARKER]; +export const PIN_ICON_ANY = [ + FEATURE_TYPE.PIN_ICON_BIOENTITY, + FEATURE_TYPE.PIN_ICON_MARKER, + FEATURE_TYPE.PIN_ICON_COMMENT, +]; export const SURFACE_ANY = [FEATURE_TYPE.SURFACE_OVERLAY, FEATURE_TYPE.SURFACE_MARKER]; diff --git a/src/models/commentSchema.ts b/src/models/commentSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c14af12666c4e3c1d8d72d0407a3ca2a5f39e61a --- /dev/null +++ b/src/models/commentSchema.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +const coordinatesSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const commentSchema = z.object({ + title: z.string(), + icon: z.string(), + type: z.enum(['POINT', 'ALIAS', 'REACTION']), + content: z.string(), + removed: z.boolean(), + coord: coordinatesSchema, + modelId: z.number(), + elementId: z.string(), + id: z.number(), + pinned: z.boolean(), + owner: z.string().nullable().optional(), +}); diff --git a/src/models/fixtures/commentsFixture.ts b/src/models/fixtures/commentsFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b06271447f126d390a443f5f9537f6dcbed26ea --- /dev/null +++ b/src/models/fixtures/commentsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { commentSchema } from '@/models/commentSchema'; + +export const commentsFixture = createFixture(z.array(commentSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 10 }, +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 591ae939da2794784f0e9bc78551a5a340d1478f..ffb7a308907de9364ea48fa05affdc5695de7005 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -20,6 +20,10 @@ const getPublicationsURLSearchParams = ( }; export const apiPath = { + getElementById: (elementId: number, modelId: number): string => + `projects/${PROJECT_ID}/models/${modelId}/bioEntities/elements/${elementId}`, + getReactionById: (reactionId: number, modelId: number): string => + `projects/${PROJECT_ID}/models/${modelId}/bioEntities/reactions/?id=${reactionId}`, getBioEntityContentsStringWithQuery: ({ searchQuery, isPerfectMatch, @@ -97,4 +101,6 @@ export const apiPath = { user: (login: string): string => `users/${login}`, getStacktrace: (code: string): string => `stacktrace/${code}`, submitError: (): string => `minervanet/submitError`, + userPrivileges: (login: string): string => `users/${login}?columns=privileges`, + getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`, }; diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index ea4d544415b41e42bc602d9ca29d427b1883ea26..778d123b47fbace3b0a6f863bdf791599f26d096 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -1,10 +1,15 @@ -import { ONE, SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common'; import { rootSelector } from '@/redux/root/root.selectors'; +import { ONE, SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common'; import { BioEntityWithPinType } from '@/types/bioEntity'; import { ElementIdTabObj } from '@/types/elements'; import { MultiSearchData } from '@/types/fetchDataState'; -import { BioEntity, BioEntityContent, MapModel } from '@/types/models'; +import { BioEntity, BioEntityContent, Comment, MapModel } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; +import { + allCommentsSelectorOfCurrentMap, + commentElementSelector, +} from '@/redux/comment/comment.selectors'; +import { currentDrawerReactionSelector } from '@/redux/reactions/reactions.selector'; import { allChemicalsBioEntitesOfAllMapsSelector, allChemicalsBioEntitesOfCurrentMapSelector, @@ -256,9 +261,14 @@ export const allBioEntitiesElementsIdsSelector = createSelector( export const currentDrawerBioEntitySelector = createSelector( allBioEntitiesSelector, + commentElementSelector, currentSearchedBioEntityId, - (bioEntities, currentBioEntityId): BioEntity | undefined => - bioEntities.find(({ id }) => id === currentBioEntityId), + (bioEntities, commentElement, currentBioEntityId): BioEntity | undefined => { + if (commentElement && commentElement.id === currentBioEntityId) { + return commentElement; + } + return bioEntities.find(({ id }) => id === currentBioEntityId); + }, ); export const currentDrawerBioEntityRelatedSubmapSelector = createSelector( @@ -301,3 +311,35 @@ export const allVisibleBioEntitiesIdsSelector = createSelector( return [...elements, ...submapConnections].map(e => e.id); }, ); + +export const currentDrawerElementCommentsSelector = createSelector( + currentDrawerBioEntitySelector, + 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( + currentDrawerReactionSelector, + allCommentsSelectorOfCurrentMap, + (reaction, comments): Comment[] => { + if (reaction) { + return comments.filter( + comment => + comment.type === 'REACTION' && + comment.modelId === reaction.modelId && + Number(comment.elementId) === reaction.id, + ); + } + return []; + }, +); diff --git a/src/redux/comment/comment.constants.ts b/src/redux/comment/comment.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..cff58977fcde11178b81d374cfa3698b441c76ad --- /dev/null +++ b/src/redux/comment/comment.constants.ts @@ -0,0 +1,17 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { CommentsState } from '@/redux/comment/comment.types'; + +export const COMMENT_SUBMAP_CONNECTIONS_INITIAL_STATE: FetchDataState<Comment[]> = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +export const COMMENT_INITIAL_STATE: CommentsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, + isOpen: false, + commentElement: null, + commentReaction: null, +}; diff --git a/src/redux/comment/comment.mock.ts b/src/redux/comment/comment.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..be42596a90ce5568c6ed4faca1d230073cd685df --- /dev/null +++ b/src/redux/comment/comment.mock.ts @@ -0,0 +1,11 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { CommentsState } from '@/redux/comment/comment.types'; + +export const COMMENT_INITIAL_STATE_MOCK: CommentsState = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, + isOpen: false, + commentElement: null, + commentReaction: null, +}; diff --git a/src/redux/comment/comment.reducers.ts b/src/redux/comment/comment.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..b113cdfd76784c4724adbeb1a67fcf3a250d36de --- /dev/null +++ b/src/redux/comment/comment.reducers.ts @@ -0,0 +1,66 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { CommentsState } from '@/redux/comment/comment.types'; +import { + getCommentElement, + getCommentReaction, + getComments, +} from '@/redux/comment/thunks/getComments'; + +export const getCommentsReducer = (builder: ActionReducerMapBuilder<CommentsState>): void => { + builder.addCase(getComments.pending, state => { + state.loading = 'pending'; + }); + + builder.addCase(getComments.fulfilled, (state, action) => { + state.loading = 'succeeded'; + state.data = action.payload; + }); + + builder.addCase(getComments.rejected, state => { + state.loading = 'failed'; + }); +}; + +export const getCommentElementReducer = (builder: ActionReducerMapBuilder<CommentsState>): void => { + builder.addCase(getCommentElement.pending, state => { + state.loading = 'pending'; + state.commentElement = null; + }); + + builder.addCase(getCommentElement.fulfilled, (state, action) => { + state.loading = 'succeeded'; + state.commentElement = action.payload; + }); + + builder.addCase(getCommentElement.rejected, state => { + state.loading = 'failed'; + state.commentElement = null; + }); +}; + +export const getCommentReactionReducer = ( + builder: ActionReducerMapBuilder<CommentsState>, +): void => { + builder.addCase(getCommentReaction.pending, state => { + state.loading = 'pending'; + state.commentReaction = null; + }); + + builder.addCase(getCommentReaction.fulfilled, (state, action) => { + state.loading = 'succeeded'; + state.commentReaction = action.payload; + }); + + builder.addCase(getCommentReaction.rejected, state => { + state.loading = 'failed'; + state.commentReaction = null; + }); +}; + +export const showCommentsReducer = (state: CommentsState): void => { + state.isOpen = true; +}; + +export const hideCommentsReducer = (state: CommentsState): void => { + state.isOpen = false; +}; diff --git a/src/redux/comment/comment.selectors.ts b/src/redux/comment/comment.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..7af687a21dc94636d189bb663aba5750e9b7d20a --- /dev/null +++ b/src/redux/comment/comment.selectors.ts @@ -0,0 +1,35 @@ +import { rootSelector } from '@/redux/root/root.selectors'; +import { createSelector } from '@reduxjs/toolkit'; +import { CommentWithPinType } from '@/types/comment'; +import { currentModelIdSelector } from '../models/models.selectors'; + +export const commentSelector = createSelector(rootSelector, state => state.comment); + +export const commentElementSelector = createSelector( + commentSelector, + commentState => commentState.commentElement, +); + +export const commentReactionSelector = createSelector( + commentSelector, + commentState => commentState.commentReaction, +); + +export const allCommentsSelectorOfCurrentMap = createSelector( + commentSelector, + currentModelIdSelector, + (commentState, currentModelId): CommentWithPinType[] => { + if (!commentState) { + return []; + } + + return (commentState.data || []) + .filter(comment => comment.modelId === currentModelId) + .map(comment => { + return { + ...comment, + pinType: 'comment', + }; + }); + }, +); diff --git a/src/redux/comment/comment.slice.ts b/src/redux/comment/comment.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae4efb8179ecf725686af6f8f93e95187d013323 --- /dev/null +++ b/src/redux/comment/comment.slice.ts @@ -0,0 +1,27 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { COMMENT_INITIAL_STATE } from '@/redux/comment/comment.constants'; +import { + getCommentElementReducer, + getCommentReactionReducer, + getCommentsReducer, + hideCommentsReducer, + showCommentsReducer, +} from '@/redux/comment/comment.reducers'; + +export const commentsSlice = createSlice({ + name: 'comments', + initialState: COMMENT_INITIAL_STATE, + reducers: { + showComments: showCommentsReducer, + hideComments: hideCommentsReducer, + }, + extraReducers: builder => { + getCommentsReducer(builder); + getCommentElementReducer(builder); + getCommentReactionReducer(builder); + }, +}); + +export const { showComments, hideComments } = commentsSlice.actions; + +export default commentsSlice.reducer; diff --git a/src/redux/comment/comment.thunks.ts b/src/redux/comment/comment.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8ee25a46581d5fb4701d81fc0c98c0a4df74e41 --- /dev/null +++ b/src/redux/comment/comment.thunks.ts @@ -0,0 +1,3 @@ +import { getComments } from './thunks/getComments'; + +export { getComments }; diff --git a/src/redux/comment/comment.types.ts b/src/redux/comment/comment.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..581da6e4959f40728d402d1dcf51750c731cb15b --- /dev/null +++ b/src/redux/comment/comment.types.ts @@ -0,0 +1,17 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { BioEntity, Comment, Reaction } from '@/types/models'; +import { PayloadAction } from '@reduxjs/toolkit'; + +export interface CommentsState extends FetchDataState<Comment[], []> { + isOpen: boolean; + commentElement: BioEntity | null; + commentReaction: Reaction | null; +} + +export type OpenCommentByIdPayload = number | string; +export type OpenCommentByIdAction = PayloadAction<OpenCommentByIdPayload>; + +export type GetElementProps = { + elementId: number; + modelId: number; +}; diff --git a/src/redux/comment/thunks/getComments.ts b/src/redux/comment/thunks/getComments.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f526700e26f80c3120427bf573ec881ce81b3e2 --- /dev/null +++ b/src/redux/comment/thunks/getComments.ts @@ -0,0 +1,61 @@ +import { commentSchema } from '@/models/commentSchema'; +import { apiPath } from '@/redux/apiPath'; +import { axiosInstance, axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; +import { ThunkConfig } from '@/types/store'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { BioEntity, Comment, Reaction } from '@/types/models'; +import { z } from 'zod'; +import { bioEntitySchema } from '@/models/bioEntitySchema'; +import { GetElementProps } from '@/redux/comment/comment.types'; +import { reactionSchema } from '@/models/reaction'; +import { ZERO } from '@/constants/common'; + +export const getComments = createAsyncThunk<Comment[], void, ThunkConfig>( + 'project/getComments', + async () => { + try { + const response = await axiosInstance.get<Comment[]>(apiPath.getComments()); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(commentSchema)); + + return isDataValid ? response.data : []; + } catch (error) { + return Promise.reject(error); + } + }, +); + +export const getCommentElement = createAsyncThunk<BioEntity | null, GetElementProps, ThunkConfig>( + 'project/getCommentElement', + async ({ elementId, modelId }) => { + try { + const response = await axiosInstanceNewAPI.get<BioEntity>( + apiPath.getElementById(elementId, modelId), + ); + + const isDataValid = validateDataUsingZodSchema(response.data, bioEntitySchema); + + return isDataValid ? response.data : null; + } catch (error) { + return Promise.reject(error); + } + }, +); + +export const getCommentReaction = createAsyncThunk<Reaction | null, GetElementProps, ThunkConfig>( + 'project/getCommentReaction', + async ({ elementId, modelId }) => { + try { + const response = await axiosInstance.get<Reaction[]>( + apiPath.getReactionById(elementId, modelId), + ); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(reactionSchema)); + + return isDataValid && response.data.length > ZERO ? response.data[ZERO] : null; + } catch (error) { + return Promise.reject(error); + } + }, +); diff --git a/src/redux/drawer/drawer.constants.ts b/src/redux/drawer/drawer.constants.ts index 84246ac3f1aab141811c7659991410663c3afd9a..a55405a76b197fe36e8fe20fbcad40f8ff43fe2c 100644 --- a/src/redux/drawer/drawer.constants.ts +++ b/src/redux/drawer/drawer.constants.ts @@ -18,6 +18,9 @@ export const DRAWER_INITIAL_STATE: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const RESULT_DRAWERS = ['search', 'reaction', 'bio-entity']; diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 29333ec2c70a27cb8088e55cee67a010d4d27b0f..88f1096c6e38f71dcc0442b506fa9a832e6e9434 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -2,6 +2,7 @@ import { STEP } from '@/constants/searchDrawer'; import type { DrawerState, OpenBioEntityDrawerByIdAction, + OpenCommentDrawerByIdAction, OpenReactionDrawerByIdAction, OpenSearchDrawerWithSelectedTabReducerAction, } from '@/redux/drawer/drawer.types'; @@ -113,6 +114,15 @@ export const openBioEntityDrawerByIdReducer = ( state.bioEntityDrawerState.bioentityId = action.payload; }; +export const openCommentDrawerByIdReducer = ( + state: DrawerState, + action: OpenCommentDrawerByIdAction, +): void => { + state.isOpen = true; + state.drawerName = 'comment'; + state.commentDrawerState.commentId = action.payload; +}; + export const getBioEntityDrugsForTargetReducers = ( builder: ActionReducerMapBuilder<DrawerState>, ): void => { diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts index 96fab201b57beab6ff170e17cdecb3a3a1d8a4e0..6c3a2a016bd005b4afde1d2a6500d48f17be3c92 100644 --- a/src/redux/drawer/drawer.selectors.ts +++ b/src/redux/drawer/drawer.selectors.ts @@ -43,11 +43,21 @@ export const bioEntityDrawerStateSelector = createSelector( state => state.bioEntityDrawerState, ); +export const commentDrawerStateSelector = createSelector( + drawerSelector, + state => state.commentDrawerState, +); + export const currentSearchedBioEntityId = createSelector( bioEntityDrawerStateSelector, state => state.bioentityId, ); +export const currentDrawerCommentId = createSelector( + commentDrawerStateSelector, + state => state.commentId, +); + export const currentSearchedBioEntityDrugsSelector = createSelector( bioEntityDrawerStateSelector, currentSearchedBioEntityId, diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts index 99d928dba8b5ae28f1c860bf4bd5ad15cd9af396..ddb7c23e04cf509ffc3cf0ee470a175512ad0036 100644 --- a/src/redux/drawer/drawer.slice.ts +++ b/src/redux/drawer/drawer.slice.ts @@ -11,6 +11,7 @@ import { getBioEntityChemicalsForTargetReducers, getBioEntityDrugsForTargetReducers, openBioEntityDrawerByIdReducer, + openCommentDrawerByIdReducer, openDrawerReducer, openOverlaysDrawerReducer, openReactionDrawerByIdReducer, @@ -37,6 +38,7 @@ const drawerSlice = createSlice({ displayEntityDetails: displayEntityDetailsReducer, openReactionDrawerById: openReactionDrawerByIdReducer, openBioEntityDrawerById: openBioEntityDrawerByIdReducer, + openCommentDrawerById: openCommentDrawerByIdReducer, }, extraReducers: builder => { getBioEntityDrugsForTargetReducers(builder); @@ -59,6 +61,7 @@ export const { displayEntityDetails, openReactionDrawerById, openBioEntityDrawerById, + openCommentDrawerById, } = drawerSlice.actions; export default drawerSlice.reducer; diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index a0d5198979607da4cd56dd22cfd129a5c3517418..4cf1440f73bc5b3fdb006c0dac47e1507503f298 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -25,6 +25,10 @@ export type BioEntityDrawerState = { chemicals: KeyedFetchDataState<Chemical[], []>; }; +export type CommentDrawerState = { + commentId?: number; +}; + export type DrawerState = { isOpen: boolean; drawerName: DrawerName; @@ -32,6 +36,7 @@ export type DrawerState = { reactionDrawerState: ReactionDrawerState; bioEntityDrawerState: BioEntityDrawerState; overlayDrawerState: OverlayDrawerState; + commentDrawerState: CommentDrawerState; }; export type OpenSearchDrawerWithSelectedTabReducerPayload = string; @@ -46,3 +51,6 @@ export type OpenBioEntityDrawerByIdAction = PayloadAction<OpenBioEntityDrawerByI export type SetSelectedSearchElementPayload = string; export type SetSelectedSearchElementAction = PayloadAction<SetSelectedSearchElementPayload>; + +export type OpenCommentDrawerByIdPayload = number; +export type OpenCommentDrawerByIdAction = PayloadAction<OpenCommentDrawerByIdPayload>; diff --git a/src/redux/drawer/drawerFixture.ts b/src/redux/drawer/drawerFixture.ts index fc7138abf5033cc3c2b233d85092dd17f1788d88..a2a5adac3171713b9e27579ad7b9fd45442dac2e 100644 --- a/src/redux/drawer/drawerFixture.ts +++ b/src/redux/drawer/drawerFixture.ts @@ -18,6 +18,9 @@ export const initialStateFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const openedDrawerSubmapsFixture: DrawerState = { @@ -38,6 +41,9 @@ export const openedDrawerSubmapsFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const drawerSearchStepOneFixture: DrawerState = { @@ -58,6 +64,9 @@ export const drawerSearchStepOneFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const drawerSearchDrugsStepTwoFixture: DrawerState = { @@ -78,6 +87,9 @@ export const drawerSearchDrugsStepTwoFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const drawerSearchChemicalsStepTwoFixture: DrawerState = { @@ -98,6 +110,9 @@ export const drawerSearchChemicalsStepTwoFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; export const drawerOverlaysStepOneFixture: DrawerState = { @@ -118,6 +133,9 @@ export const drawerOverlaysStepOneFixture: DrawerState = { overlayDrawerState: { currentStep: 2, }, + commentDrawerState: { + commentId: undefined, + }, }; export const openedExportDrawerFixture: DrawerState = { @@ -138,4 +156,7 @@ export const openedExportDrawerFixture: DrawerState = { overlayDrawerState: { currentStep: 0, }, + commentDrawerState: { + commentId: undefined, + }, }; diff --git a/src/redux/reactions/reactions.selector.ts b/src/redux/reactions/reactions.selector.ts index 8dbc69904a3a2737dbbf9159989b43a3c4f2c2e0..14590fdaf229a209cbf1b052f4f76be6a5e5c85b 100644 --- a/src/redux/reactions/reactions.selector.ts +++ b/src/redux/reactions/reactions.selector.ts @@ -1,5 +1,6 @@ import { Reaction } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; +import { commentReactionSelector } from '@/redux/comment/comment.selectors'; import { currentDrawerReactionIdSelector } from '../drawer/drawer.selectors'; import { currentModelIdSelector } from '../models/models.selectors'; import { rootSelector } from '../root/root.selectors'; @@ -23,9 +24,15 @@ export const allReactionsSelectorOfCurrentMap = createSelector( export const currentDrawerReactionSelector = createSelector( reactionsDataSelector, + commentReactionSelector, currentDrawerReactionIdSelector, - (reactions, currentDrawerReactionId) => - reactions.find(({ id }) => id === currentDrawerReactionId), + (reactions, commentReaction, currentDrawerReactionId) => { + if (commentReaction && commentReaction.id === currentDrawerReactionId) { + return commentReaction; + } + + return reactions.find(({ id }) => id === currentDrawerReactionId); + }, ); export const currentDrawerReactionGroupedReferencesSelector = createSelector( diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index 285a3ba3e403d4d608a3e6381fd0a1f926836338..0f6962d29a1615c73c2fbb6de27a929e8a20d3a5 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -1,5 +1,6 @@ import { CONSTANT_INITIAL_STATE } from '@/redux/constant/constant.adapter'; import { PROJECTS_STATE_INITIAL_MOCK } from '@/redux/projects/projects.mock'; +import { COMMENT_INITIAL_STATE_MOCK } from '@/redux/comment/comment.mock'; import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; @@ -55,4 +56,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { plugins: PLUGINS_INITIAL_STATE_MOCK, markers: MARKERS_INITIAL_STATE_MOCK, entityNumber: ENTITY_NUMBER_INITIAL_STATE_MOCK, + comment: COMMENT_INITIAL_STATE_MOCK, }; diff --git a/src/redux/store.ts b/src/redux/store.ts index 8d7d04c19cbf09d40266eed72305f60fc37730de..e7649c165cbd19dc9ef1d42ba807fddec7172518 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -24,6 +24,7 @@ import { TypedStartListening, configureStore, } from '@reduxjs/toolkit'; +import commentReducer from '@/redux/comment/comment.slice'; import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice'; import entityNumberReducer from './entityNumber/entityNumber.slice'; import exportReducer from './export/export.slice'; @@ -42,6 +43,7 @@ export const reducers = { drugs: drugsReducer, chemicals: chemicalsReducer, bioEntity: bioEntityReducer, + comment: commentReducer, drawer: drawerReducer, modal: modalReducer, map: mapReducer, diff --git a/src/types/comment.ts b/src/types/comment.ts new file mode 100644 index 0000000000000000000000000000000000000000..f42abd2f28d347c4b3c352a9b731e47e6db2f6dc --- /dev/null +++ b/src/types/comment.ts @@ -0,0 +1,6 @@ +import { Comment } from './models'; +import { PinType } from './pin'; + +export interface CommentWithPinType extends Comment { + pinType: PinType; +} diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index 3715c57ddd45995b4c8194a917bf0ad8a129b5f4..d6f7961005116de9743cb35d7b2c7797de8aca3c 100644 --- a/src/types/drawerName.ts +++ b/src/types/drawerName.ts @@ -9,4 +9,5 @@ export type DrawerName = | 'reaction' | 'overlays' | 'bio-entity' + | 'comment' | 'available-plugins'; diff --git a/src/types/models.ts b/src/types/models.ts index 3cdb0f81e60bdc90f02833ce92deeb72edd7b399..044d36d8983998a08941364dedcffa28287990a6 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -61,6 +61,7 @@ import { targetSchema } from '@/models/targetSchema'; import { targetSearchNameResult } from '@/models/targetSearchNameResult'; import { userPrivilegeSchema } from '@/models/userPrivilegesSchema'; import { z } from 'zod'; +import { commentSchema } from '@/models/commentSchema'; import { userSchema } from '@/models/userSchema'; import { javaStacktraceSchema } from '@/models/javaStacktraceSchema'; @@ -122,6 +123,7 @@ export type MarkerLine = z.infer<typeof markerLineSchema>; export type MarkerWithPosition = z.infer<typeof markerWithPositionSchema>; export type Marker = z.infer<typeof markerSchema>; export type JavaStacktrace = z.infer<typeof javaStacktraceSchema>; +export type Comment = z.infer<typeof commentSchema>; export type PageOf<T> = { totalPages: number; diff --git a/src/types/pin.ts b/src/types/pin.ts index ffb2ab266d0d59f35c35896c30edf39c145930b8..7310215f63f09860d9f84880f2fbf024063adff0 100644 --- a/src/types/pin.ts +++ b/src/types/pin.ts @@ -1 +1 @@ -export type PinType = 'chemicals' | 'drugs' | 'bioEntity'; +export type PinType = 'chemicals' | 'drugs' | 'bioEntity' | 'comment'; diff --git a/src/utils/validateDataUsingZodSchema.ts b/src/utils/validateDataUsingZodSchema.ts index 704b4bacd23d801e974dd59c55d3378c439f322a..5171a271449cfe39e46b6a2449d56b7cde4a1e95 100644 --- a/src/utils/validateDataUsingZodSchema.ts +++ b/src/utils/validateDataUsingZodSchema.ts @@ -6,7 +6,7 @@ export const validateDataUsingZodSchema: IsApiResponseValid = (data, schema: Zod const validationResults = schema.safeParse(data); if (validationResults.success === false) { - // TODO - probably need to rething way of handling parsing errors, for now let's leave it to console.log + // TODO - probably need to rethink way of handling parsing errors, for now let's leave it to console.log // eslint-disable-next-line no-console console.error('Error on parsing data', validationResults.error); // eslint-disable-next-line no-console