Skip to content
Snippets Groups Projects
Commit e4fd45c4 authored by mateusz-winiarczyk's avatar mateusz-winiarczyk
Browse files

Merge branch 'MIN-280-displaying-users-overlays' into 'development'

fix(overlays): displaying users overlays (MIN-280)

See merge request !140
parents b39c805a 3c10830e
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...,!140fix(overlays): displaying users overlays (MIN-280)
Pipeline #86870 passed
Showing with 446 additions and 52 deletions
/* eslint-disable no-magic-numbers */
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { projectFixture } from '@/models/fixtures/projectFixture';
import {
InitialStoreState,
......@@ -16,9 +16,11 @@ import { apiPath } from '@/redux/apiPath';
import {
createdOverlayFileFixture,
createdOverlayFixture,
overlaysFixture,
uploadedOverlayFileContentFixture,
} from '@/models/fixtures/overlaysFixture';
import { OVERLAYS_INITIAL_STATE_MOCK } from '@/redux/overlays/overlays.mock';
import { DEFAULT_ERROR } from '@/constants/errors';
import { UserOverlayForm } from './UserOverlayForm.component';
const mockedAxiosClient = mockNetworkResponse();
......@@ -68,11 +70,11 @@ describe('UserOverlayForm - Component', () => {
.reply(HttpStatusCode.Ok, createdOverlayFileFixture);
mockedAxiosClient
.onPost(apiPath.uploadOverlayFileContent(123))
.onPost(apiPath.uploadOverlayFileContent(createdOverlayFileFixture.id))
.reply(HttpStatusCode.Ok, uploadedOverlayFileContentFixture);
mockedAxiosClient
.onPost(apiPath.createOverlay('pd'))
.onPost(apiPath.createOverlay(projectFixture.projectId))
.reply(HttpStatusCode.Ok, createdOverlayFixture);
renderComponent({
......@@ -189,4 +191,62 @@ describe('UserOverlayForm - Component', () => {
expect(currentStep).toBe(1);
});
it('should refetch user overlays after submit the form', async () => {
mockedAxiosClient
.onPost(apiPath.createOverlayFile())
.reply(HttpStatusCode.Ok, createdOverlayFileFixture);
mockedAxiosClient
.onPost(apiPath.uploadOverlayFileContent(createdOverlayFileFixture.id))
.reply(HttpStatusCode.Ok, uploadedOverlayFileContentFixture);
mockedAxiosClient
.onPost(apiPath.createOverlay(projectFixture.projectId))
.reply(HttpStatusCode.Ok, createdOverlayFixture);
mockedAxiosClient
.onGet(apiPath.getAllUserOverlaysByCreatorQuery({ creator: 'test', publicOverlay: false }))
.reply(HttpStatusCode.Ok, overlaysFixture);
const { store } = renderComponent({
user: {
authenticated: true,
error: DEFAULT_ERROR,
login: 'test',
loading: 'succeeded',
},
project: {
data: projectFixture,
loading: 'succeeded',
error: DEFAULT_ERROR,
},
overlays: OVERLAYS_INITIAL_STATE_MOCK,
});
const userOverlays = store.getState().overlays.userOverlays.data;
expect(userOverlays).toEqual([]);
fireEvent.change(screen.getByTestId('overlay-name'), { target: { value: 'Test Overlay' } });
fireEvent.change(screen.getByTestId('overlay-description'), {
target: { value: 'Description Overlay' },
});
fireEvent.change(screen.getByTestId('overlay-elements-list'), {
target: { value: 'Elements List Overlay' },
});
fireEvent.click(screen.getByLabelText('upload overlay'));
expect(screen.getByLabelText('upload overlay')).toBeDisabled();
await waitFor(() => {
expect(store.getState().overlays.userOverlays.loading).toBe('succeeded');
});
const refetchedUserOverlays = store.getState().overlays.userOverlays.data;
await waitFor(() => {
expect(refetchedUserOverlays).toEqual(overlaysFixture);
});
});
});
......@@ -100,12 +100,12 @@ export const useUserOverlayForm = (): ReturnType => {
filename = 'unknown.txt'; // Elements list is sent to the backend as a file, so we need to create a filename for the elements list.
}
if (!overlayContent || !projectId || !description || !name) return;
if (!overlayContent || !projectId || !name) return;
dispatch(
addOverlay({
content: overlayContent,
description,
description: description || '',
filename,
name,
projectId,
......
......@@ -4,10 +4,14 @@ import { mapStateWithCurrentlySelectedMainMapFixture } from '@/redux/map/map.fix
import { SURFACE_MARKER } from '@/redux/models/marker.mock';
import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock';
import { MOCKED_OVERLAY_BIO_ENTITY_RENDER } from '@/redux/overlayBioEntity/overlayBioEntity.mock';
import { OVERLAYS_PUBLIC_FETCHED_STATE_MOCK } from '@/redux/overlays/overlays.mock';
import {
OVERLAYS_PUBLIC_FETCHED_STATE_MOCK,
USER_OVERLAYS_MOCK,
} from '@/redux/overlays/overlays.mock';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { useOverlayFeatures } from './useOverlayFeatures';
import * as getPolygonLatitudeCoordinates from './getPolygonLatitudeCoordinates';
/**
* mocks for useOverlayFeatures
......@@ -15,7 +19,20 @@ import { useOverlayFeatures } from './useOverlayFeatures';
* point of the test is to test if all helper functions work correctly when combined together
*/
jest.mock('./getPolygonLatitudeCoordinates', () => ({
__esModule: true,
...jest.requireActual('./getPolygonLatitudeCoordinates'),
}));
const getPolygonLatitudeCoordinatesSpy = jest.spyOn(
getPolygonLatitudeCoordinates,
'getPolygonLatitudeCoordinates',
);
describe('useOverlayFeatures', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const { Wrapper } = getReduxWrapperWithStore({
configuration: {
...CONFIGURATION_INITIAL_STORE_MOCKS,
......@@ -100,4 +117,91 @@ describe('useOverlayFeatures', () => {
// @ts-ignore
expect(features[10].getStyle().getFill().getColor()).toBe('#0000001a');
});
it('should get coordinates only for active general overlays and user overlays if exist', () => {
const { Wrapper: OverlayFeaturesWrapper } = getReduxWrapperWithStore({
configuration: {
...CONFIGURATION_INITIAL_STORE_MOCKS,
},
overlayBioEntity: {
overlaysId: [11, 12, 99, 123],
data: {
// overlayId
11: {
// modelId
52: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
53: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
},
12: {
52: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
53: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
},
99: {
52: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
53: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
},
123: {
52: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
53: MOCKED_OVERLAY_BIO_ENTITY_RENDER,
},
},
},
overlays: {
...OVERLAYS_PUBLIC_FETCHED_STATE_MOCK,
userOverlays: {
data: USER_OVERLAYS_MOCK,
error: { message: '', name: '' },
loading: 'succeeded',
},
},
map: {
...mapStateWithCurrentlySelectedMainMapFixture,
},
models: {
...MODELS_DATA_MOCK_WITH_MAIN_MAP,
},
});
renderHook(() => useOverlayFeatures(), {
wrapper: OverlayFeaturesWrapper,
});
expect(getPolygonLatitudeCoordinatesSpy).toHaveBeenLastCalledWith({
nOverlays: 4,
overlayIndexBasedOnOrder: 0,
width: 8.923194537814197,
xMin: 4454.850442288663,
});
});
it('should not get coordinates for active overlays if no overlay is active', () => {
const { Wrapper: OverlayFeaturesWrapper } = getReduxWrapperWithStore({
configuration: {
...CONFIGURATION_INITIAL_STORE_MOCKS,
},
overlayBioEntity: {
overlaysId: [],
data: {},
},
overlays: {
...OVERLAYS_PUBLIC_FETCHED_STATE_MOCK,
userOverlays: {
data: USER_OVERLAYS_MOCK,
error: { message: '', name: '' },
loading: 'succeeded',
},
},
map: {
...mapStateWithCurrentlySelectedMainMapFixture,
},
models: {
...MODELS_DATA_MOCK_WITH_MAIN_MAP,
},
});
renderHook(() => useOverlayFeatures(), {
wrapper: OverlayFeaturesWrapper,
});
expect(getPolygonLatitudeCoordinatesSpy).not.toHaveBeenCalled();
});
});
......@@ -2,9 +2,18 @@ import { OverlayBioEntityRender } from '@/types/OLrendering';
import { createSelector } from '@reduxjs/toolkit';
import { currentSearchedBioEntityId } from '../drawer/drawer.selectors';
import { currentModelIdSelector } from '../models/models.selectors';
import { overlaysDataSelector, overlaysIdsAndOrderSelector } from '../overlays/overlays.selectors';
import {
overlaysDataSelector,
overlaysIdsAndOrderSelector,
userOverlaysDataSelector,
userOverlaysIdsAndOrderSelector,
} from '../overlays/overlays.selectors';
import { rootSelector } from '../root/root.selectors';
import { calculateOvarlaysOrder } from './overlayBioEntity.utils';
import {
calculateOvarlaysOrder,
getActiveOverlaysIdsAndOrder,
getActiveUserOverlaysIdsAndOrder,
} from './overlayBioEntity.utils';
export const overlayBioEntitySelector = createSelector(
rootSelector,
......@@ -52,19 +61,35 @@ export const isOverlayLoadingSelector = createSelector(
export const activeOverlaysSelector = createSelector(
rootSelector,
overlaysDataSelector,
(state, overlaysData) =>
overlaysData.filter(overlay => isOverlayActiveSelector(state, overlay.idObject)),
userOverlaysDataSelector,
(state, overlaysData, userOverlaysData) => {
const activeOverlays = overlaysData.filter(overlay =>
isOverlayActiveSelector(state, overlay.idObject),
);
const activeUserOverlays =
userOverlaysData?.filter(overlay => isOverlayActiveSelector(state, overlay.idObject)) || [];
return [...activeOverlays, ...activeUserOverlays];
},
);
export const getOverlayOrderSelector = createSelector(
overlaysIdsAndOrderSelector,
userOverlaysIdsAndOrderSelector,
activeOverlaysIdSelector,
(overlaysIdsAndOrder, activeOverlaysIds) => {
const activeOverlaysIdsAndOrder = overlaysIdsAndOrder.filter(({ idObject }) =>
activeOverlaysIds.includes(idObject),
(overlaysIdsAndOrder, userOverlaysIdsAndOrder, activeOverlaysIds) => {
const { activeOverlaysIdsAndOrder, maxOrderValue } = getActiveOverlaysIdsAndOrder(
overlaysIdsAndOrder,
activeOverlaysIds,
);
const activeUserOverlaysIdsAndOrder = getActiveUserOverlaysIdsAndOrder(
userOverlaysIdsAndOrder,
activeOverlaysIds,
maxOrderValue,
);
return calculateOvarlaysOrder(activeOverlaysIdsAndOrder);
return calculateOvarlaysOrder([...activeOverlaysIdsAndOrder, ...activeUserOverlaysIdsAndOrder]);
},
);
......
......@@ -13,3 +13,8 @@ export type OverlaysBioEntityState = {
export type RemoveOverlayBioEntityForGivenOverlayPayload = { overlayId: number };
export type RemoveOverlayBioEntityForGivenOverlayAction =
PayloadAction<RemoveOverlayBioEntityForGivenOverlayPayload>;
export type OverlaysIdsAndOrder = {
idObject: number;
order: number;
}[];
/* eslint-disable no-magic-numbers */
import { overlayBioEntityFixture } from '@/models/fixtures/overlayBioEntityFixture';
import { OverlayBioEntity } from '@/types/models';
import { calculateOvarlaysOrder, getValidOverlayBioEntities } from './overlayBioEntity.utils';
import {
calculateOvarlaysOrder,
getActiveOverlaysIdsAndOrder,
getActiveUserOverlaysIdsAndOrder,
getValidOverlayBioEntities,
} from './overlayBioEntity.utils';
describe('calculateOverlaysOrder', () => {
const cases = [
......@@ -98,3 +104,113 @@ describe('getValidOverlayBioEntities', () => {
expect(result).toEqual([]);
});
});
describe('getActiveUserOverlaysIdsAndOrder', () => {
const userOverlaysIdsAndOrder = [
{ idObject: 1, order: 1 },
{ idObject: 2, order: 2 },
{ idObject: 3, order: 3 },
{ idObject: 4, order: 4 },
];
const activeOverlaysIds = [2, 4];
const maxOrderValue = 10;
it('should return active user overlays with updated order values', () => {
const expectedOutput = [
{ idObject: 2, order: 12 },
{ idObject: 4, order: 14 },
];
expect(
getActiveUserOverlaysIdsAndOrder(userOverlaysIdsAndOrder, activeOverlaysIds, maxOrderValue),
).toEqual(expectedOutput);
});
it('should return an empty array when there are no active overlays', () => {
expect(getActiveUserOverlaysIdsAndOrder(userOverlaysIdsAndOrder, [], maxOrderValue)).toEqual(
[],
);
});
it('should return all user overlays with updated order values when all overlays are active', () => {
const allOverlaysActive = [1, 2, 3, 4];
const expectedOutput = [
{ idObject: 1, order: 11 },
{ idObject: 2, order: 12 },
{ idObject: 3, order: 13 },
{ idObject: 4, order: 14 },
];
expect(
getActiveUserOverlaysIdsAndOrder(userOverlaysIdsAndOrder, allOverlaysActive, maxOrderValue),
).toEqual(expectedOutput);
});
it('should return active user overlays with order values starting from 1 when maxOrderValue is 0', () => {
const maxOrderZero = 0;
const expectedOutput = [
{ idObject: 2, order: 2 },
{ idObject: 4, order: 4 },
];
expect(
getActiveUserOverlaysIdsAndOrder(userOverlaysIdsAndOrder, activeOverlaysIds, maxOrderZero),
).toEqual(expectedOutput);
});
});
describe('getActiveOverlaysIdsAndOrder', () => {
const overlaysIdsAndOrder = [
{ idObject: 1, order: 1 },
{ idObject: 2, order: 2 },
{ idObject: 3, order: 3 },
{ idObject: 4, order: 4 },
];
const activeOverlaysIds = [2, 4];
it('should return maxOrderValue and active overlays', () => {
const expectedOutput = {
maxOrderValue: 4,
activeOverlaysIdsAndOrder: [
{ idObject: 2, order: 2 },
{ idObject: 4, order: 4 },
],
};
expect(getActiveOverlaysIdsAndOrder(overlaysIdsAndOrder, activeOverlaysIds)).toEqual(
expectedOutput,
);
});
it('should return maxOrderValue as 0 when there are no active overlays', () => {
const expectedOutput = {
maxOrderValue: 0,
activeOverlaysIdsAndOrder: [],
};
expect(getActiveOverlaysIdsAndOrder(overlaysIdsAndOrder, [])).toEqual(expectedOutput);
});
it('should return maxOrderValue and all overlays when all overlays are active', () => {
const allOverlaysActive = [1, 2, 3, 4];
const expectedOutput = {
maxOrderValue: 4,
activeOverlaysIdsAndOrder: overlaysIdsAndOrder,
};
expect(getActiveOverlaysIdsAndOrder(overlaysIdsAndOrder, allOverlaysActive)).toEqual(
expectedOutput,
);
});
it('should return maxOrderValue as 0 and no active overlays when none of the overlays are active', () => {
const expectedOutput = {
maxOrderValue: 0,
activeOverlaysIdsAndOrder: [],
};
expect(getActiveOverlaysIdsAndOrder(overlaysIdsAndOrder, [])).toEqual(expectedOutput);
});
it('should return maxOrderValue as 0 and no active overlays when there are no overlays', () => {
const expectedOutput = {
maxOrderValue: 0,
activeOverlaysIdsAndOrder: [],
};
expect(getActiveOverlaysIdsAndOrder([], [])).toEqual(expectedOutput);
});
});
/* eslint-disable no-magic-numbers */
import { ONE } from '@/constants/common';
import { overlayBioEntitySchema } from '@/models/overlayBioEntitySchema';
import { OverlayBioEntityRender } from '@/types/OLrendering';
......@@ -5,6 +6,7 @@ import { OverlayBioEntity } from '@/types/models';
import { getOverlayReactionCoordsFromLine } from '@/utils/overlays/getOverlayReactionCoords';
import { isBioEntity, isReaction, isSubmapLink } from '@/utils/overlays/overlaysElementsTypeGuards';
import { z } from 'zod';
import { OverlaysIdsAndOrder } from './overlayBioEntity.types';
export const parseOverlayBioEntityToOlRenderingFormat = (
data: OverlayBioEntity[],
......@@ -142,3 +144,53 @@ export const getValidOverlayBioEntities = (
return parsedOverlayBioEntities.success ? parsedOverlayBioEntities.data : undefined;
};
type GetActiveOverlaysIdsAndOrderReturnType = {
maxOrderValue: number;
activeOverlaysIdsAndOrder: OverlaysIdsAndOrder;
};
export const getActiveOverlaysIdsAndOrder = (
overlaysIdsAndOrder: OverlaysIdsAndOrder,
activeOverlaysIds: number[],
): GetActiveOverlaysIdsAndOrderReturnType => {
let maxOrderValue = -Infinity;
const activeOverlaysIdsAndOrder = overlaysIdsAndOrder.filter(({ idObject }) =>
activeOverlaysIds.includes(idObject),
);
overlaysIdsAndOrder.forEach(({ idObject, order }) => {
const isActive = activeOverlaysIds.includes(idObject);
if (isActive && order > maxOrderValue) maxOrderValue = order;
});
if (maxOrderValue === -Infinity) {
maxOrderValue = 0;
}
return {
maxOrderValue,
activeOverlaysIdsAndOrder,
};
};
export const getActiveUserOverlaysIdsAndOrder = (
userOverlaysIdsAndOrder: OverlaysIdsAndOrder,
activeOverlaysIds: number[],
maxOrderValue: number,
): OverlaysIdsAndOrder => {
const clonedUserOverlaysIdsAndOrder: OverlaysIdsAndOrder = JSON.parse(
JSON.stringify(userOverlaysIdsAndOrder),
);
const activeUserOverlaysIdsAndOrder = clonedUserOverlaysIdsAndOrder.filter(userOverlay => {
const isActive = activeOverlaysIds.includes(userOverlay.idObject);
if (isActive) {
/* eslint-disable-next-line no-param-reassign */
userOverlay.order = maxOrderValue + userOverlay.order; // user overlays appear after general overlays, we need to get max order value of general overlays and add to it user overlay order to be ensured that user overlay will appear after general overlays
}
return isActive;
});
return activeUserOverlaysIdsAndOrder;
};
......@@ -121,3 +121,32 @@ export const ADD_OVERLAY_MOCK = {
projectId: 'pd',
type: 'GENERIC',
};
export const USER_OVERLAYS_MOCK: MapOverlay[] = [
{
name: 'PD substantia nigra',
googleLicenseConsent: false,
creator: 'appu-admin',
description:
'Differential transcriptome expression from post mortem tissue. Meta-analysis from 8 published datasets, FDR = 0.05, see PMIDs 23832570 and 25447234.',
genomeType: null,
genomeVersion: null,
idObject: 99,
publicOverlay: true,
type: 'GENERIC',
order: 1,
},
{
name: 'Ageing brain',
googleLicenseConsent: false,
creator: 'appu-admin',
description:
'Differential transcriptome expression from post mortem tissue. Source: Allen Brain Atlas datasets, see PMID 25447234.',
genomeType: null,
genomeVersion: null,
idObject: 123,
publicOverlay: true,
type: 'GENERIC',
order: 2,
},
];
......@@ -34,6 +34,11 @@ export const userOverlaysDataSelector = createSelector(
overlays => overlays.data,
);
export const userOverlaysIdsAndOrderSelector = createSelector(
userOverlaysDataSelector,
userOverlays => userOverlays?.map(({ idObject, order }) => ({ idObject, order })) || [],
);
export const userOverlaySelector = createSelector(
[userOverlaysDataSelector, (_, userOverlayId: number): number => userOverlayId],
(userOverlays, userOverlayId) =>
......
......@@ -29,6 +29,36 @@ export const getAllPublicOverlaysByProjectId = createAsyncThunk(
},
);
export const getAllUserOverlaysByCreator = createAsyncThunk(
'overlays/getAllUserOverlaysByCreator',
async (_, { getState }): Promise<MapOverlay[]> => {
const state = getState() as RootState;
const creator = state.user.login;
if (!creator) return [];
const response = await axiosInstance<MapOverlay[]>(
apiPath.getAllUserOverlaysByCreatorQuery({
creator,
publicOverlay: false,
}),
{
withCredentials: true,
},
);
const isDataValid = validateDataUsingZodSchema(response.data, z.array(mapOverlay));
const sortByOrder = (userOverlayA: MapOverlay, userOverlayB: MapOverlay): number => {
if (userOverlayA.order > userOverlayB.order) return 1;
return -1;
};
const sortedUserOverlays = response.data.sort(sortByOrder);
return isDataValid ? sortedUserOverlays : [];
},
);
/** UTILS */
type CreateFileArgs = {
......@@ -140,14 +170,10 @@ type AddOverlayArgs = {
export const addOverlay = createAsyncThunk(
'overlays/addOverlay',
async ({
filename,
content,
description,
name,
type,
projectId,
}: AddOverlayArgs): Promise<void> => {
async (
{ filename, content, description, name, type, projectId }: AddOverlayArgs,
{ dispatch },
): Promise<void> => {
const createdFile = await createFile({
filename,
content,
......@@ -165,36 +191,8 @@ export const addOverlay = createAsyncThunk(
type,
projectId,
});
},
);
export const getAllUserOverlaysByCreator = createAsyncThunk(
'overlays/getAllUserOverlaysByCreator',
async (_, { getState }): Promise<MapOverlay[]> => {
const state = getState() as RootState;
const creator = state.user.login;
if (!creator) return [];
const response = await axiosInstance<MapOverlay[]>(
apiPath.getAllUserOverlaysByCreatorQuery({
creator,
publicOverlay: false,
}),
{
withCredentials: true,
},
);
const isDataValid = validateDataUsingZodSchema(response.data, z.array(mapOverlay));
const sortByOrder = (userOverlayA: MapOverlay, userOverlayB: MapOverlay): number => {
if (userOverlayA.order > userOverlayB.order) return 1;
return -1;
};
const sortedUserOverlays = response.data.sort(sortByOrder);
return isDataValid ? sortedUserOverlays : [];
dispatch(getAllUserOverlaysByCreator());
},
);
......
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