Skip to content
Snippets Groups Projects
Commit 459f350f authored by Miłosz Grocholewski's avatar Miłosz Grocholewski
Browse files

Add drawing shapes on a vector map

parent aa276cbb
No related branches found
No related tags found
1 merge request!249Add drawing shapes on a vector map
Showing
with 306 additions and 3 deletions
import { z } from 'zod';
import { modelElementSchema } from '@/models/modelElementSchema';
export const modelElementsSchema = z.object({
content: z.array(modelElementSchema),
totalPages: z.number(),
totalElements: z.number(),
numberOfElements: z.number(),
size: z.number(),
number: z.number(),
});
import { z } from 'zod';
import { shapeRelAbsPointSchema } from '@/models/shapeRelAbsPointSchema';
import { shapeRelAbsRadiusSchema } from '@/models/shapeRelAbsRadiusSchema';
export const shapeEllipseSchema = z.object({
type: z.literal('ELLIPSE'),
center: shapeRelAbsPointSchema,
radius: shapeRelAbsRadiusSchema,
});
import { z } from 'zod';
import { shapeRelAbsBezierPointSchema } from '@/models/shapeRelAbsBezierPointSchema';
import { shapeRelAbsPointSchema } from '@/models/shapeRelAbsPointSchema';
export const shapePolygonSchema = z.object({
type: z.literal('POLYGON'),
points: z.array(z.union([shapeRelAbsPointSchema, shapeRelAbsBezierPointSchema])),
});
import { z } from 'zod';
export const shapeRelAbsBezierPointSchema = z.object({
type: z.literal('REL_ABS_BEZIER_POINT'),
absoluteX1: z.number(),
absoluteY1: z.number(),
relativeX1: z.number(),
relativeY1: z.number(),
relativeHeightForX1: z.number().nullable(),
relativeWidthForY1: z.number().nullable(),
absoluteX2: z.number(),
absoluteY2: z.number(),
relativeX2: z.number(),
relativeY2: z.number(),
relativeHeightForX2: z.number().nullable(),
relativeWidthForY2: z.number().nullable(),
absoluteX3: z.number(),
absoluteY3: z.number(),
relativeX3: z.number(),
relativeY3: z.number(),
relativeHeightForX3: z.number().nullable(),
relativeWidthForY3: z.number().nullable(),
});
import { z } from 'zod';
export const shapeRelAbsPointSchema = z.object({
type: z.literal('REL_ABS_POINT'),
absoluteX: z.number(),
absoluteY: z.number(),
relativeX: z.number(),
relativeY: z.number(),
relativeHeightForX: z.number().nullable(),
relativeWidthForY: z.number().nullable(),
});
import { z } from 'zod';
export const shapeRelAbsRadiusSchema = z.object({
type: z.literal('REL_ABS_RADIUS'),
absoluteX: z.number(),
absoluteY: z.number(),
relativeX: z.number(),
relativeY: z.number(),
});
import { z } from 'zod';
import { shapeEllipseSchema } from '@/models/shapeEllipseSchema';
import { shapePolygonSchema } from '@/models/shapePolygonSchema';
export const shapeSchema = z.union([shapeEllipseSchema, shapePolygonSchema]);
......@@ -48,6 +48,9 @@ export const apiPath = {
getChemicalsStringWithColumnsTarget: (columns: string, target: string): string =>
`projects/${PROJECT_ID}/chemicals:search?columns=${columns}&target=${target}`,
getModelsString: (): string => `projects/${PROJECT_ID}/models/`,
getModelElements: (modelId: number): string =>
`projects/${PROJECT_ID}/maps/${modelId}/bioEntities/elements/?size=10000`,
getShapes: (): string => `projects/${PROJECT_ID}/shapes/`,
getChemicalsStringWithQuery: (searchQuery: string): string =>
`projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`,
getAllOverlaysByProjectIdQuery: (
......
export const MODEL_ELEMENTS_FETCHING_ERROR_PREFIX = 'Failed to fetch model elements';
import { DEFAULT_ERROR } from '@/constants/errors';
import { ModelElementsState } from '@/redux/modelElements/modelElements.types';
export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = {
data: null,
loading: 'idle',
error: DEFAULT_ERROR,
};
/* eslint-disable no-magic-numbers */
import { apiPath } from '@/redux/apiPath';
import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { HttpStatusCode } from 'axios';
import { unwrapResult } from '@reduxjs/toolkit';
import { ModelElementsState } from '@/redux/modelElements/modelElements.types';
import modelElementsReducer from '@/redux/modelElements/modelElements.slice';
import { getModelElements } from '@/redux/modelElements/modelElements.thunks';
import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture';
const mockedAxiosClient = mockNetworkNewAPIResponse();
const INITIAL_STATE: ModelElementsState = {
data: null,
loading: 'idle',
error: { name: '', message: '' },
};
describe('model elements reducer', () => {
let store = {} as ToolkitStoreWithSingleSlice<ModelElementsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('modelElements', modelElementsReducer);
});
it('should match initial state', () => {
const action = { type: 'unknown' };
expect(modelElementsReducer(undefined, action)).toEqual(INITIAL_STATE);
});
it('should update store after successful getModelElements query', async () => {
mockedAxiosClient
.onGet(apiPath.getModelElements(0))
.reply(HttpStatusCode.Ok, modelElementsFixture);
const { type } = await store.dispatch(getModelElements(0));
const { data, loading, error } = store.getState().modelElements;
expect(type).toBe('vectorMap/getModelElements/fulfilled');
expect(loading).toEqual('succeeded');
expect(error).toEqual({ message: '', name: '' });
expect(data).toEqual(modelElementsFixture);
});
it('should update store after failed getModelElements query', async () => {
mockedAxiosClient.onGet(apiPath.getModelElements(0)).reply(HttpStatusCode.NotFound, []);
const action = await store.dispatch(getModelElements(0));
const { data, loading, error } = store.getState().modelElements;
expect(action.type).toBe('vectorMap/getModelElements/rejected');
expect(() => unwrapResult(action)).toThrow(
"Failed to fetch model elements: The page you're looking for doesn't exist. Please verify the URL and try again.",
);
expect(loading).toEqual('failed');
expect(error).toEqual({ message: '', name: '' });
expect(data).toEqual(null);
});
it('should update store on loading getModelElements query', async () => {
mockedAxiosClient
.onGet(apiPath.getModelElements(0))
.reply(HttpStatusCode.Ok, modelElementsFixture);
const modelElementsPromise = store.dispatch(getModelElements(0));
const { data, loading } = store.getState().modelElements;
expect(data).toEqual(null);
expect(loading).toEqual('pending');
modelElementsPromise.then(() => {
const { data: dataPromiseFulfilled, loading: promiseFulfilled } =
store.getState().modelElements;
expect(dataPromiseFulfilled).toEqual(modelElementsFixture);
expect(promiseFulfilled).toEqual('succeeded');
});
});
});
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getModelElements } from '@/redux/modelElements/modelElements.thunks';
import { ModelElementsState } from '@/redux/modelElements/modelElements.types';
export const getModelElementsReducer = (
builder: ActionReducerMapBuilder<ModelElementsState>,
): void => {
builder.addCase(getModelElements.pending, state => {
state.loading = 'pending';
});
builder.addCase(getModelElements.fulfilled, (state, action) => {
state.data = action.payload || null;
state.loading = 'succeeded';
});
builder.addCase(getModelElements.rejected, state => {
state.loading = 'failed';
});
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '@/redux/root/root.selectors';
export const modelElementsSelector = createSelector(
rootSelector,
state => state.modelElements.data,
);
import { createSlice } from '@reduxjs/toolkit';
import { getModelElementsReducer } from '@/redux/modelElements/modelElements.reducers';
import { ModelElementsState } from '@/redux/modelElements/modelElements.types';
const initialState: ModelElementsState = {
data: null,
loading: 'idle',
error: { name: '', message: '' },
};
export const modelElements = createSlice({
name: 'modelElements',
initialState,
reducers: {},
extraReducers: builder => {
getModelElementsReducer(builder);
},
});
export default modelElements.reducer;
/* eslint-disable no-magic-numbers */
import { apiPath } from '@/redux/apiPath';
import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { HttpStatusCode } from 'axios';
import { ModelElementsState } from '@/redux/modelElements/modelElements.types';
import modelElementsReducer from '@/redux/modelElements/modelElements.slice';
import { getModelElements } from '@/redux/modelElements/modelElements.thunks';
import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture';
const mockedAxiosClient = mockNetworkNewAPIResponse();
describe('model elements thunks', () => {
let store = {} as ToolkitStoreWithSingleSlice<ModelElementsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('modelElements', modelElementsReducer);
});
describe('getModelElements', () => {
it('should return data when data response from API is valid', async () => {
mockedAxiosClient
.onGet(apiPath.getModelElements(0))
.reply(HttpStatusCode.Ok, modelElementsFixture);
const { payload } = await store.dispatch(getModelElements(0));
expect(payload).toEqual(modelElementsFixture);
});
it('should return undefined when data response from API is not valid ', async () => {
mockedAxiosClient
.onGet(apiPath.getModelElements(0))
.reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' });
const { payload } = await store.dispatch(getModelElements(0));
expect(payload).toEqual(undefined);
});
});
});
import { apiPath } from '@/redux/apiPath';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { ThunkConfig } from '@/types/store';
import { getError } from '@/utils/error-report/getError';
import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
import { modelElementsSchema } from '@/models/modelElementsSchema';
import { ModelElements } from '@/types/models';
import { MODEL_ELEMENTS_FETCHING_ERROR_PREFIX } from '@/redux/modelElements/modelElements.constants';
export const getModelElements = createAsyncThunk<ModelElements | undefined, number, ThunkConfig>(
'vectorMap/getModelElements',
async (modelId: number) => {
try {
const response = await axiosInstanceNewAPI.get<ModelElements>(
apiPath.getModelElements(modelId),
);
const isDataValid = validateDataUsingZodSchema(response.data, modelElementsSchema);
return isDataValid ? response.data : undefined;
} catch (error) {
return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX }));
}
},
);
import { FetchDataState } from '@/types/fetchDataState';
import { ModelElements } from '@/types/models';
export type ModelElementsState = FetchDataState<ModelElements, null>;
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
/* eslint-disable no-magic-numbers */
import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit';
import { getModels } from './models.thunks';
import { ModelsState } from './models.types';
......@@ -15,3 +16,14 @@ export const getModelsReducer = (builder: ActionReducerMapBuilder<ModelsState>):
// TODO to discuss manage state of failure
});
};
export const setModelVectorRenderingReducer = (
state: ModelsState,
action: PayloadAction<{ vectorRendering: boolean; mapId: number }>,
): void => {
const { payload } = action;
const modelIndex = state.data.findIndex(model => model.idObject === payload.mapId);
if (modelIndex !== -1) {
state.data[modelIndex].vectorRendering = payload.vectorRendering;
}
};
import { ModelsState } from '@/redux/models/models.types';
import { createSlice } from '@reduxjs/toolkit';
import { getModelsReducer } from './models.reducers';
import { getModelsReducer, setModelVectorRenderingReducer } from './models.reducers';
const initialState: ModelsState = {
data: [],
......@@ -11,10 +11,14 @@ const initialState: ModelsState = {
export const modelsSlice = createSlice({
name: 'models',
initialState,
reducers: {},
reducers: {
setModelVectorRendering: setModelVectorRenderingReducer,
},
extraReducers: builder => {
getModelsReducer(builder);
},
});
export const { setModelVectorRendering } = modelsSlice.actions;
export default modelsSlice.reducer;
......@@ -15,6 +15,7 @@ import {
import { openSelectProjectModal } from '@/redux/modal/modal.slice';
import { getProjects } from '@/redux/projects/projects.thunks';
import { getSubmapConnectionsBioEntity } from '@/redux/bioEntity/thunks/getSubmapConnectionsBioEntity';
import { getShapes } from '@/redux/shapes/shapes.thunks';
import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
import {
......@@ -58,6 +59,7 @@ export const fetchInitialAppData = createAsyncThunk<
dispatch(getAllBackgroundsByProjectId(PROJECT_ID)),
dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)),
dispatch(getModels()),
dispatch(getShapes()),
]);
if (queryData.pluginsId) {
......
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