Skip to content
Snippets Groups Projects
Commit ba85bc5c authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-122-submap-is-downloadable-to-pc' into 'development'

feat: add submap download component (MIN-122)

Closes MIN-122

See merge request !106
parents 42bfbca0 0853904a
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...,!106feat: add submap download component (MIN-122)
Pipeline #84579 passed
Showing
with 1392 additions and 577 deletions
source diff could not be displayed: it is too large. Options to address this: view the blob.
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture';
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import {
CONFIGURATION_FORMATS_MOCK,
CONFIGURATION_FORMATS_TYPES_MOCK,
} from '@/models/mocks/configurationFormatsMock';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { RootState, StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { act, render, renderHook, screen } from '@testing-library/react';
import { DownloadSubmap } from './DownloadSubmap.component';
import { GetSubmapDownloadUrl, useGetSubmapDownloadUrl } from './utils/useGetSubmapDownloadUrl';
const VALID_MODEL_ID = 5052;
const VALID_BACKGROUND_ID = 53;
const VALID_MAX_ZOOM = 9;
const getState = (): RootState => ({
...INITIAL_STORE_STATE_MOCK,
map: {
...INITIAL_STORE_STATE_MOCK.map,
data: {
...INITIAL_STORE_STATE_MOCK.map.data,
modelId: VALID_MODEL_ID,
backgroundId: VALID_BACKGROUND_ID,
size: {
...INITIAL_STORE_STATE_MOCK.map.data.size,
maxZoom: VALID_MAX_ZOOM,
},
},
},
models: {
...INITIAL_STORE_STATE_MOCK.models,
data: [
{
...modelsFixture[FIRST_ARRAY_ELEMENT],
idObject: VALID_MODEL_ID,
},
],
},
backgrounds: {
...INITIAL_STORE_STATE_MOCK.backgrounds,
data: [
{
...backgroundsFixture[FIRST_ARRAY_ELEMENT],
id: VALID_BACKGROUND_ID,
},
],
},
configuration: {
...INITIAL_STORE_STATE_MOCK.configuration,
main: {
...INITIAL_STORE_STATE_MOCK.configuration.main,
data: {
...configurationFixture,
modelFormats: CONFIGURATION_FORMATS_MOCK,
},
},
},
});
const toggleListByButtonClick = (): void => {
const button = screen.getByTestId('download-submap-button');
act(() => {
button.click();
});
};
const getUtilGetSubmapDownloadUrl = (): GetSubmapDownloadUrl => {
const { Wrapper } = getReduxWrapperWithStore(getState());
const {
result: { current: getSubmapDownloadUrl },
} = renderHook(() => useGetSubmapDownloadUrl(), { wrapper: Wrapper });
return getSubmapDownloadUrl;
};
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<DownloadSubmap />
</Wrapper>,
),
{
store,
}
);
};
describe('DownloadSubmap - component', () => {
it('should render download button', () => {
renderComponent(getState());
const button = screen.getByTestId('download-submap-button');
expect(button).toBeInTheDocument();
});
it('should open list on button click', () => {
renderComponent(getState());
toggleListByButtonClick();
const list = screen.getByTestId('download-submap-list');
expect(list).not.toHaveClass('hidden');
});
it('should close list on button click twice', () => {
renderComponent(getState());
const list = screen.getByTestId('download-submap-list');
// list should be opened
toggleListByButtonClick();
expect(list).not.toHaveClass('hidden');
// list should be closed
toggleListByButtonClick();
expect(list).toHaveClass('hidden');
});
it('should not show list when closed (default state)', () => {
renderComponent(getState());
const list = screen.getByTestId('download-submap-list');
expect(list).toHaveClass('hidden');
});
it('should render list elements with href and names when opened', () => {
const getSubmapDownloadUrl = getUtilGetSubmapDownloadUrl();
renderComponent(getState());
const list = screen.getByTestId('download-submap-list');
const validHrefs = CONFIGURATION_FORMATS_MOCK.map(({ handler }) =>
getSubmapDownloadUrl({ handler }),
);
const validNames = CONFIGURATION_FORMATS_TYPES_MOCK;
const allAnchors = [...list.getElementsByTagName('a')];
allAnchors.forEach(anchor => {
expect(validHrefs.includes(anchor.href)).toBeTruthy();
expect(validNames.includes(anchor.innerText)).toBeTruthy();
});
});
});
import { formatsHandlersSelector } from '@/redux/configuration/configuration.selectors';
import { Button } from '@/shared/Button';
import { useSelect } from 'downshift';
import { useSelector } from 'react-redux';
import { SUBMAP_DOWNLOAD_HANDLERS_NAMES } from './DownloadSubmap.constants';
import { useGetSubmapDownloadUrl } from './utils/useGetSubmapDownloadUrl';
export const DownloadSubmap = (): JSX.Element => {
const formatsHandlers = useSelector(formatsHandlersSelector);
const formatsHandlersItems = Object.entries(formatsHandlers);
const getSubmapDownloadUrl = useGetSubmapDownloadUrl();
const { isOpen, getToggleButtonProps, getMenuProps } = useSelect({
items: formatsHandlersItems,
});
return (
<div className="relative">
<Button
data-testid="download-submap-button"
variantStyles="ghost"
className="mr-4"
{...getToggleButtonProps()}
>
Download
</Button>
<ul
data-testid="download-submap-list"
className={`absolute left-[-50%] z-10 max-h-80 w-48 overflow-scroll rounded-sm border bg-white p-0 ps-0 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
formatsHandlersItems.map(([formatId, handler]) => (
<li key={formatId}>
<a
className="flex flex-col border-t px-4 py-2 shadow-sm"
href={getSubmapDownloadUrl({ handler })}
target="_blank"
download
>
<span>{SUBMAP_DOWNLOAD_HANDLERS_NAMES[formatId]}</span>
</a>
</li>
))}
</ul>
</div>
);
};
import {
CELL_DESIGNER_SBML_HANDLER_NAME_ID,
GPML_HANDLER_NAME_ID,
SBGN_ML_HANDLER_NAME_ID,
SBML_HANDLER_NAME_ID,
} from '@/redux/configuration/configuration.constants';
export const SUBMAP_DOWNLOAD_HANDLERS_NAMES: Record<string, string> = {
[GPML_HANDLER_NAME_ID]: 'GPML',
[SBML_HANDLER_NAME_ID]: 'SBML',
[CELL_DESIGNER_SBML_HANDLER_NAME_ID]: 'CellDesigner SBML',
[SBGN_ML_HANDLER_NAME_ID]: 'SBGN-ML',
};
export { DownloadSubmap } from './DownloadSubmap.component';
import { BASE_API_URL, PROJECT_ID } from '@/constants';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { RootState } from '@/redux/store';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { useGetSubmapDownloadUrl } from './useGetSubmapDownloadUrl';
const VALID_HANDLER = 'lcsb.mapviewer.wikipathway.GpmlParser';
const VALID_MODEL_ID = 5052;
const VALID_BACKGROUND_ID = 53;
const VALID_MAX_ZOOM = 9;
const getState = ({
modelId,
backgroundId,
mapSizeMaxZoom,
}: {
modelId: number;
backgroundId: number;
mapSizeMaxZoom: number;
}): RootState => ({
...INITIAL_STORE_STATE_MOCK,
map: {
...INITIAL_STORE_STATE_MOCK.map,
data: {
...INITIAL_STORE_STATE_MOCK.map.data,
modelId,
backgroundId,
size: {
...INITIAL_STORE_STATE_MOCK.map.data.size,
maxZoom: mapSizeMaxZoom,
},
},
},
models: {
...INITIAL_STORE_STATE_MOCK.models,
data: [
{
...modelsFixture[FIRST_ARRAY_ELEMENT],
idObject: VALID_MODEL_ID,
},
],
},
backgrounds: {
...INITIAL_STORE_STATE_MOCK.backgrounds,
data: [
{
...backgroundsFixture[FIRST_ARRAY_ELEMENT],
id: VALID_BACKGROUND_ID,
},
],
},
});
describe('useGetSubmapDownloadUrl - hook', () => {
describe('when not all params valid', () => {
const cases = [
{
modelId: 0,
backgroundId: VALID_BACKGROUND_ID,
mapSizeMaxZoom: VALID_MAX_ZOOM,
handler: VALID_HANDLER,
},
{
modelId: VALID_MODEL_ID,
backgroundId: 0,
mapSizeMaxZoom: VALID_MAX_ZOOM,
handler: VALID_HANDLER,
},
{
modelId: VALID_MODEL_ID,
backgroundId: VALID_BACKGROUND_ID,
mapSizeMaxZoom: 0,
handler: VALID_HANDLER,
},
{
modelId: VALID_MODEL_ID,
backgroundId: VALID_BACKGROUND_ID,
mapSizeMaxZoom: VALID_MAX_ZOOM,
handler: '',
},
];
it.each(cases)('should return empty string', ({ handler, ...stateParams }) => {
const { Wrapper } = getReduxWrapperWithStore(getState(stateParams));
const {
result: { current: getSubmapDownloadUrl },
} = renderHook(() => useGetSubmapDownloadUrl(), { wrapper: Wrapper });
expect(getSubmapDownloadUrl({ handler })).toBe('');
});
});
describe('when all params valid', () => {
it('should return valid string', () => {
const { Wrapper } = getReduxWrapperWithStore(
getState({
modelId: VALID_MODEL_ID,
backgroundId: VALID_BACKGROUND_ID,
mapSizeMaxZoom: VALID_MAX_ZOOM,
}),
);
const {
result: { current: getSubmapDownloadUrl },
} = renderHook(() => useGetSubmapDownloadUrl(), { wrapper: Wrapper });
expect(getSubmapDownloadUrl({ handler: VALID_HANDLER })).toBe(
`${BASE_API_URL}/projects/${PROJECT_ID}/models/5052:downloadModel?backgroundOverlayId=53&handlerClass=lcsb.mapviewer.wikipathway.GpmlParser&zoomLevel=9`,
);
});
});
});
import { BASE_API_URL, PROJECT_ID } from '@/constants';
import { currentBackgroundSelector } from '@/redux/backgrounds/background.selectors';
import { mapDataSizeSelector } from '@/redux/map/map.selectors';
import { currentModelSelector } from '@/redux/models/models.selectors';
import { useSelector } from 'react-redux';
export type GetSubmapDownloadUrl = ({ handler }: { handler: string }) => string;
export const useGetSubmapDownloadUrl = (): GetSubmapDownloadUrl => {
const model = useSelector(currentModelSelector);
const background = useSelector(currentBackgroundSelector);
const mapSize = useSelector(mapDataSizeSelector);
const getSubmapDownloadUrl: GetSubmapDownloadUrl = ({ handler }) => {
const allParamsValid = [model?.idObject, background?.id, mapSize.maxZoom, handler].reduce(
(a, b) => Boolean(a) && Boolean(b),
true,
);
if (!allParamsValid) {
return '';
}
return `${BASE_API_URL}/projects/${PROJECT_ID}/models/${model?.idObject}:downloadModel?backgroundOverlayId=${background?.id}&handlerClass=${handler}&zoomLevel=${mapSize.maxZoom}`;
};
return getSubmapDownloadUrl;
};
import { Button } from '@/shared/Button';
import { IconButton } from '@/shared/IconButton';
import { DownloadSubmap } from './DownloadSubmap';
interface SubmapItemProps {
modelName: string;
......@@ -10,9 +10,7 @@ export const SubmpamItem = ({ modelName, onOpenClick }: SubmapItemProps): JSX.El
<div className="flex flex-row flex-nowrap items-center justify-between border-b py-6">
{modelName}
<div className="flex flex-row flex-nowrap items-center">
<Button variantStyles="ghost" className="mr-4">
Download
</Button>
<DownloadSubmap />
<IconButton
icon="chevron-right"
className="h-6 w-6 bg-white-pearl"
......
......@@ -16,13 +16,7 @@ export const optionSchema = z.object({
value: z.string().optional(),
});
export const imageFormatSchema = z.object({
name: z.string(),
handler: z.string(),
extension: z.string(),
});
export const modelFormatSchema = z.object({
export const formatSchema = z.object({
name: z.string(),
handler: z.string(),
extension: z.string(),
......@@ -87,8 +81,8 @@ export const modificationStateTypeSchema = z.record(
export const configurationSchema = z.object({
elementTypes: z.array(elementTypeSchema),
options: z.array(optionSchema),
imageFormats: z.array(imageFormatSchema),
modelFormats: z.array(modelFormatSchema),
imageFormats: z.array(formatSchema),
modelFormats: z.array(formatSchema),
overlayTypes: z.array(overlayTypeSchema),
reactionTypes: z.array(reactionTypeSchema),
miriamTypes: miriamTypesSchema,
......
import { ZOD_SEED } from '@/constants';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFixture } from 'zod-fixture';
import { configurationSchema } from '../configurationSchema';
export const configurationFixture = createFixture(configurationSchema, {
seed: ZOD_SEED,
array: { min: 1, max: 1 },
});
import { ConfigurationFormatSchema } from '@/types/models';
export const CONFIGURATION_FORMATS_TYPES_MOCK: string[] = [
'CellDesigner SBML',
'SBGN-ML',
'SBML',
'GPML',
];
export const CONFIGURATION_FORMATS_MOCK: ConfigurationFormatSchema[] = [
{
name: 'CellDesigner SBML',
handler: 'lcsb.mapviewer.converter.model.celldesigner.CellDesignerXmlParser',
extension: 'xml',
},
{
name: 'SBGN-ML',
handler: 'lcsb.mapviewer.converter.model.sbgnml.SbgnmlXmlConverter',
extension: 'sbgn',
},
{
name: 'SBML',
handler: 'lcsb.mapviewer.converter.model.sbml.SbmlParser',
extension: 'xml',
},
{
name: 'GPML',
handler: 'lcsb.mapviewer.wikipathway.GpmlParser',
extension: 'gpml',
},
];
......@@ -10,3 +10,8 @@ export const LEGEND_FILE_NAMES_IDS = [
'LEGEND_FILE_3',
'LEGEND_FILE_4',
];
export const GPML_HANDLER_NAME_ID = 'GPML';
export const SBML_HANDLER_NAME_ID = 'SBML';
export const CELL_DESIGNER_SBML_HANDLER_NAME_ID = 'CellDesigner SBML';
export const SBGN_ML_HANDLER_NAME_ID = 'SBGN-ML';
/* eslint-disable no-magic-numbers */
import { DEFAULT_ERROR } from '@/constants/errors';
import {
CONFIGURATION_OPTIONS_TYPES_MOCK,
CONFIGURATION_OPTIONS_COLOURS_MOCK,
CONFIGURATION_OPTIONS_TYPES_MOCK,
} from '@/models/mocks/configurationOptionMock';
import { ConfigurationState } from './configuration.adapter';
......
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getConfiguration, getConfigurationOptions } from './configuration.thunks';
import { ConfigurationState, configurationAdapter } from './configuration.adapter';
import { getConfiguration, getConfigurationOptions } from './configuration.thunks';
export const getConfigurationOptionsReducer = (
builder: ActionReducerMapBuilder<ConfigurationState>,
......
import { ConfigurationFormatSchema } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
import { configurationAdapter } from './configuration.adapter';
import {
CELL_DESIGNER_SBML_HANDLER_NAME_ID,
GPML_HANDLER_NAME_ID,
LEGEND_FILE_NAMES_IDS,
MAX_COLOR_VAL_NAME_ID,
MIN_COLOR_VAL_NAME_ID,
NEUTRAL_COLOR_VAL_NAME_ID,
OVERLAY_OPACITY_NAME_ID,
SBGN_ML_HANDLER_NAME_ID,
SBML_HANDLER_NAME_ID,
SIMPLE_COLOR_VAL_NAME_ID,
} from './configuration.constants';
import { ConfigurationHandlersIds } from './configuration.types';
const configurationSelector = createSelector(rootSelector, state => state.configuration);
const configurationOptionsSelector = createSelector(configurationSelector, state => state.options);
const configurationMainSelector = createSelector(configurationSelector, state => state.main.data);
const configurationAdapterSelectors = configurationAdapter.getSelectors();
......@@ -46,12 +53,38 @@ export const defaultLegendImagesSelector = createSelector(configurationOptionsSe
).filter(legendImage => Boolean(legendImage)),
);
export const configurationMainSelector = createSelector(
configurationSelector,
state => state.main.data,
);
export const elementTypesSelector = createSelector(
configurationMainSelector,
state => state?.elementTypes,
);
export const modelFormatsSelector = createSelector(
configurationMainSelector,
state => state?.modelFormats,
);
export const formatsEntriesSelector = createSelector(
modelFormatsSelector,
(modelFormats): Record<string, ConfigurationFormatSchema> => {
return Object.fromEntries(
(modelFormats || [])
.flat()
.filter((format: ConfigurationFormatSchema): format is ConfigurationFormatSchema =>
Boolean(format),
)
.map((format: ConfigurationFormatSchema) => [format.name, format]),
);
},
);
export const formatsHandlersSelector = createSelector(
formatsEntriesSelector,
(formats): ConfigurationHandlersIds => {
return {
[GPML_HANDLER_NAME_ID]: formats[GPML_HANDLER_NAME_ID]?.handler,
[SBML_HANDLER_NAME_ID]: formats[SBML_HANDLER_NAME_ID]?.handler,
[CELL_DESIGNER_SBML_HANDLER_NAME_ID]: formats[CELL_DESIGNER_SBML_HANDLER_NAME_ID]?.handler,
[SBGN_ML_HANDLER_NAME_ID]: formats[SBGN_ML_HANDLER_NAME_ID]?.handler,
};
},
);
import { createSlice } from '@reduxjs/toolkit';
import { getConfigurationOptionsReducer, getConfigurationReducer } from './configuration.reducers';
import { CONFIGURATION_INITIAL_STATE } from './configuration.adapter';
import { getConfigurationOptionsReducer, getConfigurationReducer } from './configuration.reducers';
export const configurationSlice = createSlice({
name: 'configuration',
......
import { FetchDataState } from '@/types/fetchDataState';
import { Configuration } from '@/types/models';
import {
CELL_DESIGNER_SBML_HANDLER_NAME_ID,
GPML_HANDLER_NAME_ID,
SBGN_ML_HANDLER_NAME_ID,
SBML_HANDLER_NAME_ID,
} from './configuration.constants';
export type ConfigurationMainState = FetchDataState<Configuration>;
export interface ConfigurationHandlersIds {
[GPML_HANDLER_NAME_ID]?: string;
[SBML_HANDLER_NAME_ID]?: string;
[CELL_DESIGNER_SBML_HANDLER_NAME_ID]?: string;
[SBGN_ML_HANDLER_NAME_ID]?: string;
}
......@@ -8,7 +8,7 @@ import {
compartmentPathwaySchema,
} from '@/models/compartmentPathwaySchema';
import { configurationOptionSchema } from '@/models/configurationOptionSchema';
import { configurationSchema } from '@/models/configurationSchema';
import { configurationSchema, formatSchema } from '@/models/configurationSchema';
import { disease } from '@/models/disease';
import { drugSchema } from '@/models/drugSchema';
import { elementSearchResult, elementSearchResultType } from '@/models/elementSearchResult';
......@@ -63,6 +63,7 @@ export type SessionValid = z.infer<typeof sessionSchemaValid>;
export type Login = z.infer<typeof loginSchema>;
export type ConfigurationOption = z.infer<typeof configurationOptionSchema>;
export type Configuration = z.infer<typeof configurationSchema>;
export type ConfigurationFormatSchema = z.infer<typeof formatSchema>;
export type OverlayBioEntity = z.infer<typeof overlayBioEntitySchema>;
export type CreatedOverlayFile = z.infer<typeof createdOverlayFileSchema>;
export type UploadedOverlayFileContent = z.infer<typeof uploadedOverlayFileContentSchema>;
......
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