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