Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Showing
with 271 additions and 19 deletions
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[]>;
......@@ -47,13 +47,14 @@ describe('plugins reducer', () => {
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
withoutPanel: false,
}),
);
expect(type).toBe('plugins/registerPlugin/fulfilled');
const { data, pluginsId } = store.getState().plugins.activePlugins;
expect(data[pluginFixture.hash]).toEqual(pluginFixture);
expect(data[pluginFixture.hash]).toEqual({ ...pluginFixture, withoutPanel: false });
expect(pluginsId).toContain(pluginFixture.hash);
});
......@@ -68,6 +69,7 @@ describe('plugins reducer', () => {
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
withoutPanel: false,
}),
);
......@@ -93,6 +95,7 @@ describe('plugins reducer', () => {
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
withoutPanel: false,
}),
);
......@@ -113,6 +116,7 @@ describe('plugins reducer', () => {
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName,
withoutPanel: false,
}),
);
......@@ -122,6 +126,7 @@ describe('plugins reducer', () => {
[pluginFixture.hash]: {
...pluginFixture,
name: extendedPluginName,
withoutPanel: false,
},
});
});
......@@ -136,6 +141,7 @@ describe('plugins reducer', () => {
pluginUrl: pluginFixture.urls[0],
pluginVersion: pluginFixture.version,
extendedPluginName: pluginFixture.name,
withoutPanel: false,
}),
);
......
......@@ -22,10 +22,12 @@ export const registerPluginReducer = (builder: ActionReducerMapBuilder<PluginsSt
});
builder.addCase(registerPlugin.fulfilled, (state, action) => {
if (action.payload) {
const { hash } = action.meta.arg;
const { hash, withoutPanel } = action.meta.arg;
state.activePlugins.data[hash] = action.payload;
state.drawer.currentPluginHash = hash;
if (!withoutPanel) {
state.drawer.currentPluginHash = hash;
}
}
});
builder.addCase(registerPlugin.rejected, state => {
......
......@@ -53,6 +53,10 @@ export const allActivePluginsSelector = createSelector(
},
);
export const allActivePluginsWithPanelSelector = createSelector(allActivePluginsSelector, data => {
return data.filter(plugin => !plugin.withoutPanel);
});
export const privateActivePluginsSelector = createSelector(
allActivePluginsSelector,
activePlugins => {
......
......@@ -23,6 +23,7 @@ type RegisterPlugin = {
pluginVersion: string;
isPublic: boolean;
extendedPluginName: string;
withoutPanel: boolean | undefined;
};
export const registerPlugin = createAsyncThunk<
......@@ -31,12 +32,20 @@ export const registerPlugin = createAsyncThunk<
ThunkConfig
>(
'plugins/registerPlugin',
async ({ hash, isPublic, pluginName, pluginUrl, pluginVersion, extendedPluginName }) => {
async ({
hash,
isPublic,
pluginName,
pluginUrl,
pluginVersion,
extendedPluginName,
withoutPanel,
}) => {
try {
const hashWihtoutPrefix = getPluginHashWithoutPrefix(hash);
const hashWithoutPrefix = getPluginHashWithoutPrefix(hash);
const payload = {
hash: hashWihtoutPrefix,
hash: hashWithoutPrefix,
url: pluginUrl,
name: pluginName,
version: pluginVersion,
......@@ -58,6 +67,7 @@ export const registerPlugin = createAsyncThunk<
...response.data,
hash,
name: extendedPluginName,
withoutPanel,
};
}
......
......@@ -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,
......
......@@ -138,7 +138,7 @@ export const updateUser = createAsyncThunk<undefined, User, ThunkConfig>(
},
);
validateDataUsingZodSchema(newUser, userSchema);
validateDataUsingZodSchema(newUser.data, userSchema);
showToast({ type: 'success', message: 'ToS agreement registered' });
} catch (error) {
......
......@@ -161,7 +161,7 @@ export const PluginsManager: PluginsManagerType = {
PluginsManager.pluginsOccurrences[hash] = ZERO;
}
},
registerPlugin({ pluginName, pluginVersion, pluginUrl }) {
registerPlugin({ pluginName, pluginVersion, pluginUrl, withoutPanel }) {
const hash = PluginsManager.hashedPlugins[pluginUrl];
const extendedHash = PluginsManager.getExtendedPluginHash(hash);
......@@ -182,12 +182,16 @@ export const PluginsManager: PluginsManagerType = {
pluginName,
pluginUrl,
pluginVersion,
withoutPanel,
}),
);
const element = PluginsManager.createAndGetPluginContent({
hash: extendedHash,
});
const element = PluginsManager.createAndGetPluginContent(
{
hash: extendedHash,
},
!!withoutPanel,
);
return {
element,
......@@ -202,12 +206,14 @@ export const PluginsManager: PluginsManagerType = {
},
};
},
createAndGetPluginContent({ hash }) {
createAndGetPluginContent({ hash }, detached) {
const element = document.createElement('div');
element.setAttribute(PLUGINS_CONTENT_ELEMENT_ATTR_NAME, hash);
const wrapper = document.querySelector(`#${PLUGINS_CONTENT_ELEMENT_ID}`);
wrapper?.append(element);
if (!detached) {
const wrapper = document.querySelector(`#${PLUGINS_CONTENT_ELEMENT_ID}`);
wrapper?.append(element);
}
return element;
},
......
......@@ -6,6 +6,7 @@ export type RegisterPlugin = {
pluginName: string;
pluginVersion: string;
pluginUrl: string;
withoutPanel?: boolean | undefined;
};
export type MinervaConfiguration = ReturnType<typeof configurationMapper>;
......@@ -16,10 +17,10 @@ export type PluginsManagerType = {
};
setHashedPlugin({ pluginUrl, pluginScript }: { pluginUrl: string; pluginScript: string }): string;
init(): Unsubscribe;
registerPlugin({ pluginName, pluginVersion, pluginUrl }: RegisterPlugin): {
registerPlugin({ pluginName, pluginVersion, pluginUrl, withoutPanel }: RegisterPlugin): {
element: HTMLDivElement;
};
createAndGetPluginContent(plugin: Pick<MinervaPlugin, 'hash'>): HTMLDivElement;
createAndGetPluginContent(plugin: Pick<MinervaPlugin, 'hash'>, detached: boolean): HTMLDivElement;
removePluginContent(plugin: Pick<MinervaPlugin, 'hash'>): void;
activePlugins: {
[pluginId: string]: string[];
......
......@@ -80,6 +80,9 @@ 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';
import { operatorSchema } from '@/models/operatorSchema';
export type Project = z.infer<typeof projectSchema>;
export type OverviewImageView = z.infer<typeof overviewImageView>;
......@@ -116,6 +119,11 @@ 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 Operator = z.infer<typeof operatorSchema>;
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>;
......@@ -143,7 +151,7 @@ export type PublicationsResponse = z.infer<typeof publicationsResponseSchema>;
export type Publication = z.infer<typeof publicationSchema>;
export type ExportNetwork = z.infer<typeof exportNetworkchema>;
export type ExportElements = z.infer<typeof exportElementsSchema>;
export type MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interfers with global Plugin type
export type MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interferes with global Plugin type
export type GeneVariant = z.infer<typeof geneVariant>;
export type TargetSearchNameResult = z.infer<typeof targetSearchNameResult>;
export type TargetElement = z.infer<typeof targetElementSchema>;
......
......@@ -25,7 +25,10 @@ export const getFirstVisibleParent = async ({
apiPath.getElementById(parentId, bioEntity.model),
);
const parent = parentResponse.data;
if (parseInt(parent.visibilityLevel, 10) > Math.ceil(considerZoomLevel)) {
if (
parent.visibilityLevel !== null &&
parseInt(parent.visibilityLevel, 10) > Math.ceil(considerZoomLevel)
) {
return getFirstVisibleParent({
bioEntity: parent,
considerZoomLevel,
......@@ -70,8 +73,9 @@ export const getElementsByPoint = async ({
);
const element = elementResponse.data;
if (
element.visibilityLevel != null &&
parseInt(element.visibilityLevel, 10) - (ONE - FRACTIONAL_ZOOM_AT_WHICH_IMAGE_LAYER_CHANGE) >
(considerZoomLevel || Number.MAX_SAFE_INTEGER)
(considerZoomLevel || Number.MAX_SAFE_INTEGER)
) {
const visibleParent = await getFirstVisibleParent({
bioEntity: element,
......