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 (9)
Showing
with 359 additions and 19 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>
);
};
......@@ -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',
};
......
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_${comment.id}`,
width: 0,
y: comment.coord.y,
},
pointToProjection,
);
const style = getPinStyle({
color,
value,
textColor,
});
feature.setStyle(style);
return feature;
};
export const getCommentsFeatures = (
comments: CommentWithPinType[],
{
pointToProjection,
}: {
pointToProjection: UsePointToProjectionResult;
},
): Feature[] => {
return comments.map(comment =>
getCommentFeature(comment, { pointToProjection, type: comment.pinType, value: 7 }),
);
};
/* 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;
};
/* 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];
};
......@@ -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> = {
......
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().optional(),
});
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 },
});
......@@ -94,4 +94,5 @@ export const apiPath = {
getSubmapConnections: (): string => `projects/${PROJECT_ID}/submapConnections/`,
logout: (): string => `doLogout`,
userPrivileges: (login: string): string => `users/${login}?columns=privileges`,
getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`,
};
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,
};
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,
};
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { CommentsState } from '@/redux/comment/comment.types';
import { 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 showCommentsReducer = (state: CommentsState): void => {
state.isOpen = true;
};
export const hideCommentsReducer = (state: CommentsState): void => {
state.isOpen = false;
};
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 allCommentsSelectorOfCurrentMap = createSelector(
commentSelector,
currentModelIdSelector,
(commentState, currentModelId): CommentWithPinType[] => {
if (!commentState) {
return [];
}
return (commentState.data || [])
.filter(comment => comment.modelId === currentModelId)
.map(comment => {
return {
...comment,
pinType: 'comment',
};
});
},
);
import { createSlice } from '@reduxjs/toolkit';
import { COMMENT_INITIAL_STATE } from '@/redux/comment/comment.constants';
import {
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);
},
});
export const { showComments, hideComments } = commentsSlice.actions;
export default commentsSlice.reducer;
import { getComments } from './thunks/getComments';
export { getComments };
import { FetchDataState } from '@/types/fetchDataState';
import { Comment } from '@/types/models';
export interface CommentsState extends FetchDataState<Comment[], []> {
isOpen: boolean;
}
import { commentSchema } from '@/models/commentSchema';
import { apiPath } from '@/redux/apiPath';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import { ThunkConfig } from '@/types/store';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { Comment } from '@/types/models';
import { z } from 'zod';
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);
}
},
);
import { CONSTANT_INITIAL_STATE } from '@/redux/constant/constant.adapter';
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';
......@@ -53,4 +54,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,
};