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
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