Skip to content
Snippets Groups Projects
Commit 62cac953 authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

Merge branch 'feature/MIN-76-fetch-submaps-on-app-start' into 'development'

feat(fetch submaps): added query to fetch models(submaps) on app load

See merge request !28
parents 0544a36b 64cac688
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...,!28feat(fetch submaps): added query to fetch models(submaps) on app load
Pipeline #79518 passed
......@@ -2,6 +2,9 @@ import { Manrope } from '@next/font/google';
import { twMerge } from 'tailwind-merge';
import { Map } from '@/components/Map';
import { FunctionalArea } from '@/components/FunctionalArea';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useEffect } from 'react';
import { getModels } from '@/redux/models/models.thunks';
const manrope = Manrope({
variable: '--font-manrope',
......@@ -10,9 +13,17 @@ const manrope = Manrope({
subsets: ['latin'],
});
export const MinervaSPA = (): JSX.Element => (
<div className={twMerge('relative', manrope.variable)}>
<FunctionalArea />
<Map />
</div>
);
export const MinervaSPA = (): JSX.Element => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getModels());
}, [dispatch]);
return (
<div className={twMerge('relative', manrope.variable)}>
<FunctionalArea />
<Map />
</div>
);
};
import { z } from 'zod';
export const authorSchema = z.string();
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFixture } from 'zod-fixture';
import { z } from 'zod';
import { ZOD_SEED } from '@/constants';
import { modelSchema } from '@/models/modelSchema';
export const modelsFixture = createFixture(z.array(modelSchema), {
seed: ZOD_SEED,
array: { min: 2, max: 2 },
});
import { z } from 'zod';
import { referenceSchema } from './referenceSchema';
import { authorSchema } from './authorSchema';
export const modelSchema = z.object({
/** name of the map */
name: z.string(),
description: z.string(),
/** map id */
idObject: z.number(),
/** map width */
width: z.number(),
/** map height */
height: z.number(),
/** size of the png tile used to visualize in frontend */
tileSize: z.number(),
/** default x center used in frontend visualization */
defaultCenterX: z.union([z.number(), z.null()]),
/** default y center used in frontend visualization */
defaultCenterY: z.union([z.number(), z.null()]),
/** default zoom level used in frontend visualization */
defaultZoomLevel: z.union([z.number(), z.null()]),
/** minimum zoom level availbale for the map */
minZoom: z.number(),
/** maximum zoom level available for the map */
maxZoom: z.number(),
authors: z.array(authorSchema),
references: z.array(referenceSchema),
creationDate: z.union([z.string(), z.null()]),
modificationDates: z.array(z.string()),
});
......@@ -7,6 +7,7 @@ export const apiPath = {
`projects/${PROJECT_ID}/drugs:search?query=${searchQuery}`,
getMirnasStringWithQuery: (searchQuery: string): string =>
`projects/${PROJECT_ID}/miRnas:search?query=${searchQuery}`,
getModelsString: (): string => `projects/${PROJECT_ID}/models/`,
getChemicalsStringWithQuery: (searchQuery: string): string =>
`projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`,
};
import { HttpStatusCode } from 'axios';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { apiPath } from '@/redux/apiPath';
import { getModels } from './models.thunks';
import modelsReducer from './models.slice';
import { ModelsState } from './models.types';
const mockedAxiosClient = mockNetworkResponse();
const INITIAL_STATE: ModelsState = {
data: [],
loading: 'idle',
error: { name: '', message: '' },
};
describe('models reducer', () => {
let store = {} as ToolkitStoreWithSingleSlice<ModelsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('models', modelsReducer);
});
it('should match initial state', () => {
const action = { type: 'unknown' };
expect(modelsReducer(undefined, action)).toEqual(INITIAL_STATE);
});
it('should update store after succesfull getModels query', async () => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture);
const { type } = await store.dispatch(getModels());
const { data, loading, error } = store.getState().models;
expect(type).toBe('project/getModels/fulfilled');
expect(loading).toEqual('succeeded');
expect(error).toEqual({ message: '', name: '' });
expect(data).toEqual(modelsFixture);
});
it('should update store after failed getModels query', async () => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.NotFound, []);
const { type } = await store.dispatch(getModels());
const { data, loading, error } = store.getState().models;
expect(type).toBe('project/getModels/rejected');
expect(loading).toEqual('failed');
expect(error).toEqual({ message: '', name: '' });
expect(data).toEqual([]);
});
it('should update store on loading getModels query', async () => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture);
const modelsPromise = store.dispatch(getModels());
const { data, loading } = store.getState().models;
expect(data).toEqual([]);
expect(loading).toEqual('pending');
modelsPromise.then(() => {
const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().models;
expect(dataPromiseFulfilled).toEqual(modelsFixture);
expect(promiseFulfilled).toEqual('succeeded');
});
});
});
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { ModelsState } from './models.types';
import { getModels } from './models.thunks';
export const getModelsReducer = (builder: ActionReducerMapBuilder<ModelsState>): void => {
builder.addCase(getModels.pending, state => {
state.loading = 'pending';
});
builder.addCase(getModels.fulfilled, (state, action) => {
state.data = action.payload;
state.loading = 'succeeded';
});
builder.addCase(getModels.rejected, state => {
state.loading = 'failed';
// TODO to discuss manage state of failure
});
};
import { createSlice } from '@reduxjs/toolkit';
import { ModelsState } from '@/redux/models/models.types';
import { getModelsReducer } from './models.reducers';
const initialState: ModelsState = {
data: [],
loading: 'idle',
error: { name: '', message: '' },
};
export const modelsSlice = createSlice({
name: 'models',
initialState,
reducers: {},
extraReducers: builder => {
getModelsReducer(builder);
},
});
export default modelsSlice.reducer;
import { HttpStatusCode } from 'axios';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import {
ToolkitStoreWithSingleSlice,
createStoreInstanceUsingSliceReducer,
} from '@/utils/createStoreInstanceUsingSliceReducer';
import { ModelsState } from '@/redux/models/models.types';
import { apiPath } from '@/redux/apiPath';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import modelsReducer from './models.slice';
import { getModels } from './models.thunks';
const mockedAxiosClient = mockNetworkResponse();
describe('models thunks', () => {
let store = {} as ToolkitStoreWithSingleSlice<ModelsState>;
beforeEach(() => {
store = createStoreInstanceUsingSliceReducer('models', modelsReducer);
});
describe('getModels', () => {
it('should return data when data response from API is valid', async () => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture);
const { payload } = await store.dispatch(getModels());
expect(payload).toEqual(modelsFixture);
});
it('should return undefined when data response from API is not valid ', async () => {
mockedAxiosClient
.onGet(apiPath.getModelsString())
.reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' });
const { payload } = await store.dispatch(getModels());
expect(payload).toEqual(undefined);
});
});
});
import { z } from 'zod';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { axiosInstance } from '@/services/api/utils/axiosInstance';
import { Model } from '@/types/models';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { apiPath } from '@/redux/apiPath';
import { modelSchema } from '@/models/modelSchema';
export const getModels = createAsyncThunk(
'project/getModels',
async (): Promise<Model[] | undefined> => {
const response = await axiosInstance.get<Model[]>(apiPath.getModelsString());
const isDataValid = validateDataUsingZodSchema(response.data, z.array(modelSchema));
return isDataValid ? response.data : undefined;
},
);
import { FetchDataState } from '@/types/fetchDataState';
import { Model } from '@/types/models';
export type ModelsState = FetchDataState<Model[]>;
import bioEntityContentsReducer from '@/redux/bioEntityContents/bioEntityContents.slice';
import chemicalsReducer from '@/redux/chemicals/chemicals.slice';
import drawerReducer from '@/redux/drawer/drawer.slice';
import modelsReducer from '@/redux/models/models.slice';
import drugsReducer from '@/redux/drugs/drugs.slice';
import mirnasReducer from '@/redux/mirnas/mirnas.slice';
import projectSlice from '@/redux/project/project.slice';
......@@ -16,6 +17,7 @@ export const store = configureStore({
chemicals: chemicalsReducer,
bioEntityContents: bioEntityContentsReducer,
drawer: drawerReducer,
models: modelsReducer,
},
devTools: true,
});
......
......@@ -6,6 +6,7 @@ import { mirnaSchema } from '@/models/mirnaSchema';
import { organism } from '@/models/organism';
import { projectSchema } from '@/models/project';
import { z } from 'zod';
import { modelSchema } from '@/models/modelSchema';
export type Project = z.infer<typeof projectSchema>;
export type Organism = z.infer<typeof organism>;
......@@ -13,4 +14,5 @@ export type Disease = z.infer<typeof disease>;
export type Drug = z.infer<typeof drugSchema>;
export type Mirna = z.infer<typeof mirnaSchema>;
export type BioEntityContent = z.infer<typeof bioEntityContentSchema>;
export type Model = z.infer<typeof modelSchema>;
export type Chemical = z.infer<typeof chemicalSchema>;
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