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

feat(vector-map): implement reactions between two elements

parent 2f80506e
No related branches found
No related tags found
1 merge request!283feat(vector-map): implement reactions between two elements
Showing with 219 additions and 0 deletions
......@@ -64,6 +64,8 @@ export const apiPath = {
`projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`,
getGlyphImage: (glyphId: number): string =>
`projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`,
getNewReactions: (modelId: number): string =>
`projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=2000`,
getChemicalsStringWithQuery: (searchQuery: string): string =>
`projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`,
getAllOverlaysByProjectIdQuery: (
......
import { DEFAULT_ERROR } from '@/constants/errors';
import { NewReactionsState } from '@/redux/newReactions/newReactions.types';
export const NEW_REACTIONS_INITIAL_STATE: NewReactionsState = {
data: [],
loading: 'idle',
error: DEFAULT_ERROR,
};
export const NEW_REACTIONS_FETCHING_ERROR_PREFIX = 'Failed to fetch new reactions';
import { DEFAULT_ERROR } from '@/constants/errors';
import { NewReactionsState } from '@/redux/newReactions/newReactions.types';
export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = {
data: [],
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 newReactionsReducer from '@/redux/newReactions/newReactions.slice';
import { NewReactionsState } from '@/redux/newReactions/newReactions.types';
import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock';
import { getNewReactions } from '@/redux/newReactions/newReactions.thunks';
import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture';
const mockedAxiosClient = mockNetworkNewAPIResponse();
const INITIAL_STATE: NewReactionsState = NEW_REACTIONS_INITIAL_STATE_MOCK;
describe('newReactions reducer', () => {
let store = {} as ToolkitStoreWithSingleSlice<NewReactionsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('newReactions', newReactionsReducer);
});
it('should match initial state', () => {
const action = { type: 'unknown' };
expect(newReactionsReducer(undefined, action)).toEqual(INITIAL_STATE);
});
it('should update store after successful getNewReactions query', async () => {
mockedAxiosClient
.onGet(apiPath.getNewReactions(1))
.reply(HttpStatusCode.Ok, newReactionsFixture);
const { type } = await store.dispatch(getNewReactions(1));
const { data, loading, error } = store.getState().newReactions;
expect(type).toBe('newReactions/getNewReactions/fulfilled');
expect(loading).toEqual('succeeded');
expect(error).toEqual({ message: '', name: '' });
expect(data).toEqual(newReactionsFixture.content);
});
it('should update store after failed getNewReactions query', async () => {
mockedAxiosClient.onGet(apiPath.getNewReactions(1)).reply(HttpStatusCode.NotFound, []);
const action = await store.dispatch(getNewReactions(1));
const { data, loading, error } = store.getState().newReactions;
expect(action.type).toBe('newReactions/getNewReactions/rejected');
expect(() => unwrapResult(action)).toThrow(
"Failed to fetch new reactions: 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([]);
});
it('should update store on loading getNewReactions query', async () => {
mockedAxiosClient
.onGet(apiPath.getNewReactions(1))
.reply(HttpStatusCode.Ok, newReactionsFixture);
const newReactionsPromise = store.dispatch(getNewReactions(1));
const { data, loading } = store.getState().newReactions;
expect(data).toEqual([]);
expect(loading).toEqual('pending');
newReactionsPromise.then(() => {
const { data: dataPromiseFulfilled, loading: promiseFulfilled } =
store.getState().newReactions;
expect(dataPromiseFulfilled).toEqual(newReactionsFixture.content);
expect(promiseFulfilled).toEqual('succeeded');
});
});
});
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getNewReactions } from '@/redux/newReactions/newReactions.thunks';
import { NewReactionsState } from '@/redux/newReactions/newReactions.types';
export const getNewReactionsReducer = (
builder: ActionReducerMapBuilder<NewReactionsState>,
): void => {
builder.addCase(getNewReactions.pending, state => {
state.loading = 'pending';
});
builder.addCase(getNewReactions.fulfilled, (state, action) => {
state.data = action.payload || [];
state.loading = 'succeeded';
});
builder.addCase(getNewReactions.rejected, state => {
state.loading = 'failed';
});
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions);
export const newReactionsDataSelector = createSelector(
newReactionsSelector,
reactions => reactions.data || [],
);
import { createSlice } from '@reduxjs/toolkit';
import { NEW_REACTIONS_INITIAL_STATE } from '@/redux/newReactions/newReactions.constants';
import { getNewReactionsReducer } from '@/redux/newReactions/newReactions.reducers';
export const newReactionsSlice = createSlice({
name: 'reactions',
initialState: NEW_REACTIONS_INITIAL_STATE,
reducers: {},
extraReducers: builder => {
getNewReactionsReducer(builder);
},
});
export default newReactionsSlice.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 newReactionsReducer from '@/redux/newReactions/newReactions.slice';
import { NewReactionsState } from '@/redux/newReactions/newReactions.types';
import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture';
import { getNewReactions } from '@/redux/newReactions/newReactions.thunks';
const mockedAxiosClient = mockNetworkNewAPIResponse();
describe('newReactions thunks', () => {
let store = {} as ToolkitStoreWithSingleSlice<NewReactionsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('newReactions', newReactionsReducer);
});
describe('getReactions', () => {
it('should return data when data response from API is valid', async () => {
mockedAxiosClient
.onGet(apiPath.getNewReactions(1))
.reply(HttpStatusCode.Ok, newReactionsFixture);
const { payload } = await store.dispatch(getNewReactions(1));
expect(payload).toEqual(newReactionsFixture.content);
});
it('should return undefined when data response from API is not valid ', async () => {
mockedAxiosClient
.onGet(apiPath.getNewReactions(1))
.reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' });
const { payload } = await store.dispatch(getNewReactions(1));
expect(payload).toEqual(undefined);
});
});
});
import { apiPath } from '@/redux/apiPath';
import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
import { NewReaction, NewReactions } from '@/types/models';
import { ThunkConfig } from '@/types/store';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getError } from '@/utils/error-report/getError';
import { newReactionSchema } from '@/models/newReactionSchema';
import { pageableSchema } from '@/models/pageableSchema';
import { NEW_REACTIONS_FETCHING_ERROR_PREFIX } from '@/redux/newReactions/newReactions.constants';
export const getNewReactions = createAsyncThunk<
Array<NewReaction> | undefined,
number,
ThunkConfig
>('newReactions/getNewReactions', async (modelId: number) => {
try {
const { data } = await axiosInstanceNewAPI.get<NewReactions>(apiPath.getNewReactions(modelId));
const isDataValid = validateDataUsingZodSchema(data, pageableSchema(newReactionSchema));
return isDataValid ? data.content : undefined;
} catch (error) {
return Promise.reject(getError({ error, prefix: NEW_REACTIONS_FETCHING_ERROR_PREFIX }));
}
});
import { FetchDataState } from '@/types/fetchDataState';
import { NewReaction } from '@/types/models';
export type NewReactionsState = FetchDataState<NewReaction[]>;
......@@ -6,6 +6,7 @@ import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.co
import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock';
import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock';
import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.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,6 +54,7 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
oauth: OAUTH_INITIAL_STATE_MOCK,
overlays: OVERLAYS_INITIAL_STATE_MOCK,
reactions: REACTIONS_STATE_INITIAL_MOCK,
newReactions: NEW_REACTIONS_INITIAL_STATE_MOCK,
configuration: CONFIGURATION_INITIAL_STATE,
constant: CONSTANT_INITIAL_STATE,
overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
......
......@@ -19,6 +19,7 @@ import overlaysReducer from '@/redux/overlays/overlays.slice';
import projectReducer from '@/redux/project/project.slice';
import projectsReducer from '@/redux/projects/projects.slice';
import reactionsReducer from '@/redux/reactions/reactions.slice';
import newReactionsReducer from '@/redux/newReactions/newReactions.slice';
import searchReducer from '@/redux/search/search.slice';
import userReducer from '@/redux/user/user.slice';
import {
......@@ -66,6 +67,7 @@ export const reducers = {
modelElements: modelElementsReducer,
layers: layersReducer,
reactions: reactionsReducer,
newReactions: newReactionsReducer,
contextMenu: contextMenuReducer,
cookieBanner: cookieBannerReducer,
user: userReducer,
......
......@@ -80,6 +80,8 @@ import { arrowTypeSchema } from '@/models/arrowTypeSchema';
import { arrowSchema } from '@/models/arrowSchema';
import { shapeRelAbsSchema } from '@/models/shapeRelAbsSchema';
import { shapeRelAbsBezierPointSchema } from '@/models/shapeRelAbsBezierPointSchema';
import { newReactionSchema } from '@/models/newReactionSchema';
import { reactionProduct } from '@/models/reactionProduct';
export type Project = z.infer<typeof projectSchema>;
export type OverviewImageView = z.infer<typeof overviewImageView>;
......@@ -116,6 +118,10 @@ export type BioEntityContent = z.infer<typeof bioEntityContentSchema>;
export type BioEntityResponse = z.infer<typeof bioEntityResponseSchema>;
export type Chemical = z.infer<typeof chemicalSchema>;
export type Reaction = z.infer<typeof reactionSchema>;
export type NewReaction = z.infer<typeof newReactionSchema>;
const newReactionsSchema = pageableSchema(newReactionSchema);
export type NewReactions = z.infer<typeof newReactionsSchema>;
export type ReactionProduct = z.infer<typeof reactionProduct>;
export type Reference = z.infer<typeof referenceSchema>;
export type ReactionLine = z.infer<typeof reactionLineSchema>;
export type ElementSearchResult = z.infer<typeof elementSearchResult>;
......
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