From 4bc1dfb316e776fb236deac5aa1e53c9c9b7a4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Fri, 12 Jan 2024 14:32:44 +0100 Subject: [PATCH] feat: add submap download comp wo tests --- .../DownloadSubmap.component.tsx | 44 +++++++++ .../DownloadSubmap.constants.ts | 13 +++ .../SubmapItem/DownloadSubmap/index.ts | 1 + .../utils/useGetSubmapDownloadUrl.ts | 19 ++++ .../SubmapItem/SubmapItem.component.tsx | 6 +- src/models/configurationSchema.ts | 99 +++++++++++++++++++ src/models/mocks/configurationFormatsMock.ts | 49 +++++++++ .../configuration/configuration.constants.ts | 12 +++ src/redux/configuration/configuration.mock.ts | 2 +- .../configuration/configuration.reducers.ts | 2 +- .../configuration/configuration.selectors.ts | 51 +++++++++- .../configuration/configuration.types.ts | 17 ++++ src/redux/models/models.selectors.ts | 7 ++ src/types/models.ts | 3 + 14 files changed, 318 insertions(+), 7 deletions(-) create mode 100644 src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx create mode 100644 src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.constants.ts create mode 100644 src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/index.ts create mode 100644 src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/utils/useGetSubmapDownloadUrl.ts create mode 100644 src/models/configurationSchema.ts create mode 100644 src/models/mocks/configurationFormatsMock.ts create mode 100644 src/redux/configuration/configuration.types.ts diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx new file mode 100644 index 00000000..63bcb435 --- /dev/null +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.component.tsx @@ -0,0 +1,44 @@ +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 variantStyles="ghost" className="mr-4" {...getToggleButtonProps()}> + Download + </Button> + <ul + 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> + ); +}; diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.constants.ts b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.constants.ts new file mode 100644 index 00000000..5bd478cc --- /dev/null +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/DownloadSubmap.constants.ts @@ -0,0 +1,13 @@ +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', +}; diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/index.ts b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/index.ts new file mode 100644 index 00000000..96d65c33 --- /dev/null +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/index.ts @@ -0,0 +1 @@ +export { DownloadSubmap } from './DownloadSubmap.component'; diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/utils/useGetSubmapDownloadUrl.ts b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/utils/useGetSubmapDownloadUrl.ts new file mode 100644 index 00000000..f55e3edd --- /dev/null +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/DownloadSubmap/utils/useGetSubmapDownloadUrl.ts @@ -0,0 +1,19 @@ +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'; + +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 }) => { + return `${BASE_API_URL}/projects/${PROJECT_ID}/models/${model?.idObject}:downloadModel?backgroundOverlayId=${background?.id}&handlerClass=${handler}&zoomLevel=${mapSize.maxZoom}`; + }; + + return getSubmapDownloadUrl; +}; diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx index e056671a..a7a38dfa 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx @@ -1,5 +1,5 @@ -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" diff --git a/src/models/configurationSchema.ts b/src/models/configurationSchema.ts new file mode 100644 index 00000000..b3ec41d8 --- /dev/null +++ b/src/models/configurationSchema.ts @@ -0,0 +1,99 @@ +import { z } from 'zod'; + +export const elementTypeSchema = z.object({ + className: z.string(), + name: z.string(), + parentClass: z.string(), +}); + +export const optionSchema = z.object({ + idObject: z.number(), + type: z.string(), + valueType: z.string(), + commonName: z.string(), + isServerSide: z.boolean(), + group: z.string(), + value: z.string().optional(), +}); + +export const formatSchema = z.object({ + name: z.string(), + handler: z.string(), + extension: z.string(), +}); + +export const overlayTypeSchema = z.object({ name: z.string() }); + +export const reactionTypeSchema = z.object({ + className: z.string(), + name: z.string(), + parentClass: z.string(), +}); + +export const miriamTypesSchema = z.record( + z.string(), + z.object({ + commonName: z.string(), + homepage: z.string().nullable(), + registryIdentifier: z.string().nullable(), + uris: z.array(z.string()), + }), +); + +export const bioEntityFieldSchema = z.object({ commonName: z.string(), name: z.string() }); + +export const annotatorSchema = z.object({ + className: z.string(), + name: z.string(), + description: z.string(), + url: z.string(), + elementClassNames: z.array(z.string()), + parameters: z.array( + z.object({ + field: z.string().nullable().optional(), + annotation_type: z.string().nullable().optional(), + order: z.number(), + type: z.string(), + }), + ), +}); + +export const privilegeTypeSchema = z.record( + z.string(), + z.object({ + commonName: z.string(), + objectType: z.string().nullable(), + valueType: z.string(), + }), +); + +export const mapTypeSchema = z.object({ name: z.string(), id: z.string() }); + +export const mapCanvasTypeSchema = z.object({ name: z.string(), id: z.string() }); + +export const unitTypeSchema = z.object({ name: z.string(), id: z.string() }); + +export const modificationStateTypeSchema = z.record( + z.string(), + z.object({ commonName: z.string(), abbreviation: z.string() }), +); + +export const configurationSchema = z.object({ + elementTypes: z.array(elementTypeSchema), + options: z.array(optionSchema), + imageFormats: z.array(formatSchema), + modelFormats: z.array(formatSchema), + overlayTypes: z.array(overlayTypeSchema), + reactionTypes: z.array(reactionTypeSchema), + miriamTypes: miriamTypesSchema, + bioEntityFields: z.array(bioEntityFieldSchema), + version: z.string(), + buildDate: z.string(), + gitHash: z.string(), + annotators: z.array(annotatorSchema), + privilegeTypes: privilegeTypeSchema, + mapTypes: z.array(mapTypeSchema), + mapCanvasTypes: z.array(mapCanvasTypeSchema), + unitTypes: z.array(unitTypeSchema), + modificationStateTypes: modificationStateTypeSchema, +}); diff --git a/src/models/mocks/configurationFormatsMock.ts b/src/models/mocks/configurationFormatsMock.ts new file mode 100644 index 00000000..307271bc --- /dev/null +++ b/src/models/mocks/configurationFormatsMock.ts @@ -0,0 +1,49 @@ +import { ConfigurationFormatSchema } from '@/types/models'; + +export const CONFIGURATION_FORMATS_TYPES_MOCK: string[] = [ + 'PNG image', + 'PDF', + 'SVG image', + 'CellDesigner SBML', + 'SBGN-ML', + 'SBML', + 'GPML', +]; + +export const CONFIGURATION_FORMATS_COLOURS_MOCK: ConfigurationFormatSchema[] = [ + { + name: 'PNG image', + handler: 'lcsb.mapviewer.converter.graphics.PngImageGenerator', + extension: 'png', + }, + { + name: 'PDF', + handler: 'lcsb.mapviewer.converter.graphics.PdfImageGenerator', + extension: 'pdf', + }, + { + name: 'SVG image', + handler: 'lcsb.mapviewer.converter.graphics.SvgImageGenerator', + extension: 'svg', + }, + { + 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', + }, +]; diff --git a/src/redux/configuration/configuration.constants.ts b/src/redux/configuration/configuration.constants.ts index 765ad32a..56448d47 100644 --- a/src/redux/configuration/configuration.constants.ts +++ b/src/redux/configuration/configuration.constants.ts @@ -3,3 +3,15 @@ export const MAX_COLOR_VAL_NAME_ID = 'MAX_COLOR_VAL'; export const SIMPLE_COLOR_VAL_NAME_ID = 'SIMPLE_COLOR_VAL'; export const NEUTRAL_COLOR_VAL_NAME_ID = 'NEUTRAL_COLOR_VAL'; export const OVERLAY_OPACITY_NAME_ID = 'OVERLAY_OPACITY'; + +export const LEGEND_FILE_NAMES_IDS = [ + 'LEGEND_FILE_1', + 'LEGEND_FILE_2', + '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'; diff --git a/src/redux/configuration/configuration.mock.ts b/src/redux/configuration/configuration.mock.ts index ce8f052d..320f0155 100644 --- a/src/redux/configuration/configuration.mock.ts +++ b/src/redux/configuration/configuration.mock.ts @@ -1,8 +1,8 @@ /* 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'; diff --git a/src/redux/configuration/configuration.reducers.ts b/src/redux/configuration/configuration.reducers.ts index 01cd1fe5..4b0f87f3 100644 --- a/src/redux/configuration/configuration.reducers.ts +++ b/src/redux/configuration/configuration.reducers.ts @@ -1,6 +1,6 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getConfigurationOptions } from './configuration.thunks'; import { ConfigurationState, configurationAdapter } from './configuration.adapter'; +import { getConfigurationOptions } from './configuration.thunks'; export const getConfigurationOptionsReducer = ( builder: ActionReducerMapBuilder<ConfigurationState>, diff --git a/src/redux/configuration/configuration.selectors.ts b/src/redux/configuration/configuration.selectors.ts index 7a694a44..9164accd 100644 --- a/src/redux/configuration/configuration.selectors.ts +++ b/src/redux/configuration/configuration.selectors.ts @@ -1,15 +1,24 @@ +import { ConfigurationFormatSchema } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; -import { configurationAdapter } from './configuration.adapter'; 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(); @@ -37,3 +46,43 @@ export const simpleColorValSelector = createSelector( configurationSelector, state => configurationAdapterSelectors.selectById(state, SIMPLE_COLOR_VAL_NAME_ID)?.value, ); + +export const defaultLegendImagesSelector = createSelector(configurationOptionsSelector, state => + LEGEND_FILE_NAMES_IDS.map( + legendNameId => configurationAdapterSelectors.selectById(state, legendNameId)?.value, + ).filter(legendImage => Boolean(legendImage)), +); + +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): format is ConfigurationFormatSchema => Boolean(format)) + .map(format => [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, + }; + }, +); diff --git a/src/redux/configuration/configuration.types.ts b/src/redux/configuration/configuration.types.ts new file mode 100644 index 00000000..e797b887 --- /dev/null +++ b/src/redux/configuration/configuration.types.ts @@ -0,0 +1,17 @@ +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; +} diff --git a/src/redux/models/models.selectors.ts b/src/redux/models/models.selectors.ts index ab2cdc16..4838e521 100644 --- a/src/redux/models/models.selectors.ts +++ b/src/redux/models/models.selectors.ts @@ -2,6 +2,7 @@ import { rootSelector } from '@/redux/root/root.selectors'; import { createSelector } from '@reduxjs/toolkit'; import { MODEL_ID_DEFAULT } from '../map/map.constants'; import { mapDataSelector } from '../map/map.selectors'; +import { overlaysDataSelector } from '../overlays/overlays.selectors'; export const modelsSelector = createSelector(rootSelector, state => state.models); @@ -13,6 +14,12 @@ export const currentModelSelector = createSelector( (models, mapData) => models.find(model => model.idObject === mapData.modelId), ); +export const currentOverlaySelector = createSelector( + overlaysDataSelector, + mapDataSelector, + (models, mapData) => models.find(model => model.idObject === mapData.overlaysIds), +); + export const modelsIdsSelector = createSelector(modelsDataSelector, models => models.map(model => model.idObject), ); diff --git a/src/types/models.ts b/src/types/models.ts index a1e01c3d..7d4d591c 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -4,6 +4,7 @@ import { bioEntitySchema } from '@/models/bioEntitySchema'; import { chemicalSchema } from '@/models/chemicalSchema'; import { colorSchema } from '@/models/colorSchema'; import { configurationOptionSchema } from '@/models/configurationOptionSchema'; +import { configurationSchema, formatSchema } from '@/models/configurationSchema'; import { disease } from '@/models/disease'; import { drugSchema } from '@/models/drugSchema'; import { elementSearchResult, elementSearchResultType } from '@/models/elementSearchResult'; @@ -51,5 +52,7 @@ export type ElementSearchResultType = z.infer<typeof elementSearchResultType>; 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 Color = z.infer<typeof colorSchema>; -- GitLab