Skip to content
Snippets Groups Projects
Commit fec39902 authored by Piotr Gawron's avatar Piotr Gawron
Browse files

Merge remote-tracking branch 'origin/development' into...

Merge remote-tracking branch 'origin/development' into 254-min-321-form-for-reporting-errors-in-minerva
parents addd8360 79be1a4f
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!199Resolve "[MIN-321] form for reporting errors in minerva"
Pipeline #91821 passed
This commit is part of merge request !199. Comments created here will be created in the context of that merge request.
Showing
with 360 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 },
});
......@@ -96,4 +96,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/*/`,
};
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,
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment