Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Commits on Source (22)
Showing
with 293 additions and 25 deletions
......@@ -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>
);
};
......@@ -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>
);
......
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>
);
};
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>
);
};
export { CommentDrawer } from './CommentDrawer.component';
......@@ -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>
);
};
......@@ -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>
);
......
......@@ -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:
......
......@@ -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',
};
......
......@@ -32,6 +32,9 @@ const INITIAL_STATE: InitialStoreState = {
overlayDrawerState: {
currentStep: 0,
},
commentDrawerState: {
commentId: undefined,
},
},
drugs: {
data: [
......
......@@ -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,
},
},
});
......
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);
});
});
});
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 }),
);
};
/* 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;
};
......@@ -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,
......
......@@ -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,
......
......@@ -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,
......
......@@ -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() || [];
......
......@@ -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;
......
/* 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];
};