From 59d4fd60d874bf1f4babe0103b567e37a813306a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com>
Date: Fri, 8 Dec 2023 09:36:38 +0100
Subject: [PATCH] feat(configuration options): fetch configuration at start of
 the app

---
 src/constants/overlayParticleMock.ts          | 137 ++++++++++++++++++
 src/models/configurationOptionSchema.ts       |  11 ++
 src/models/mocks/configurationOptionMock.ts   |  47 ++++++
 src/redux/apiPath.ts                          |   1 +
 .../configuration/configuration.adapter.ts    |  18 +++
 src/redux/configuration/configuration.mock.ts |  27 ++++
 .../configuration/configuration.reducers.ts   |  21 +++
 .../configuration/configuration.slice.ts      |  14 ++
 .../configuration/configuration.thunks.ts     |  23 +++
 src/redux/root/init.thunks.ts                 |   2 +
 src/redux/root/root.fixtures.ts               |   2 +
 src/redux/store.ts                            |   2 +
 src/types/loadingState.ts                     |   3 +
 src/types/models.ts                           |   2 +
 todo.txt                                      |   2 +
 15 files changed, 312 insertions(+)
 create mode 100644 src/constants/overlayParticleMock.ts
 create mode 100644 src/models/configurationOptionSchema.ts
 create mode 100644 src/models/mocks/configurationOptionMock.ts
 create mode 100644 src/redux/configuration/configuration.adapter.ts
 create mode 100644 src/redux/configuration/configuration.mock.ts
 create mode 100644 src/redux/configuration/configuration.reducers.ts
 create mode 100644 src/redux/configuration/configuration.slice.ts
 create mode 100644 src/redux/configuration/configuration.thunks.ts
 create mode 100644 todo.txt

diff --git a/src/constants/overlayParticleMock.ts b/src/constants/overlayParticleMock.ts
new file mode 100644
index 00000000..8ec5ab00
--- /dev/null
+++ b/src/constants/overlayParticleMock.ts
@@ -0,0 +1,137 @@
+// const overlayParticleMock = {
+//   left: {
+//     // molekuła na której jest prostokąt
+//     id: 38929,
+//     model: 52,
+//     glyph: null,
+//     submodel: null,
+//     compartment: 46645,
+//     elementId: 'path_0_sa8410',
+//     x: 15181.0, // wspolrzedne do lewego gornego naroznika
+//     y: 6976.0, // wspolrzedne do lewego gornego naroznika
+//     z: 4528,
+//     width: 80.0,
+//     height: 40.0,
+//     fontSize: 12.0,
+//     fontColor: {
+//       alpha: 255,
+//       rgb: -16777216,
+//     },
+//     fillColor: {
+//       alpha: 255,
+//       rgb: -3342388, // wartosc hexa przeksztalcona na wartosc binarna? rgb
+//     },
+//     borderColor: {
+//       alpha: 255,
+//       rgb: -16777216,
+//     },
+//     visibilityLevel: '3',
+//     transparencyLevel: '0',
+//     notes: '',
+//     symbol: 'ATP6V0B',
+//     fullName: 'ATPase H+ transporting V0 subunit b',
+//     abbreviation: null,
+//     formula: null,
+//     name: 'ATP6V0B',
+//     nameX: 15181.0,
+//     nameY: 6976.0,
+//     nameWidth: 80.0,
+//     nameHeight: 40.0,
+//     nameVerticalAlign: 'MIDDLE',
+//     nameHorizontalAlign: 'CENTER',
+//     synonyms: ['HATPL', 'VMA16'],
+//     formerSymbols: ['ATP6F'],
+//     activity: false,
+//     lineWidth: 1.0,
+//     complex: 38928,
+//     initialAmount: null,
+//     charge: null,
+//     initialConcentration: 0.0,
+//     onlySubstanceUnits: false,
+//     homodimer: 1,
+//     hypothetical: null,
+//     boundaryCondition: false,
+//     constant: false,
+//     modificationResidues: [],
+//     stringType: 'Protein',
+//     substanceUnits: null,
+//     references: [
+//       {
+//         link: 'https://www.ncbi.nlm.nih.gov/gene/533',
+//         type: 'ENTREZ',
+//         resource: '533',
+//         id: 178289,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//       {
+//         link: 'https://www.ncbi.nlm.nih.gov/protein/NM_004047',
+//         type: 'REFSEQ',
+//         resource: 'NM_004047',
+//         id: 178290,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//       {
+//         link: 'https://www.genenames.org/cgi-bin/gene_symbol_report?match=ATP6V0B',
+//         type: 'HGNC_SYMBOL',
+//         resource: 'ATP6V0B',
+//         id: 178291,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//       {
+//         link: 'https://www.ensembl.org/id/ENSG00000117410',
+//         type: 'ENSEMBL',
+//         resource: 'ENSG00000117410',
+//         id: 178292,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//       {
+//         link: 'https://purl.uniprot.org/uniprot/Q99437',
+//         type: 'UNIPROT',
+//         resource: 'Q99437',
+//         id: 178293,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//       {
+//         link: 'https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/861',
+//         type: 'HGNC',
+//         resource: '861',
+//         id: 178294,
+//         annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
+//       },
+//     ],
+//   },
+//   right: {
+//     // definiuje prostokat, na jakiej podstawie zostalo  matchowane, moze byc nazwa, identyfikator, nazwa+mapa
+//     id: 9558,
+//     name: 'ATP6V0B',
+//     modelName: null,
+//     elementId: null,
+//     reverseReaction: null,
+//     lineWidth: null,
+//     // bede mial albo wartosc albo kolor, jak bede mial oba (raczej nie bedzie) to jest to bug
+//     value: -0.493438352, // musze wartość zmapowac na kolor -> -1 to np czerwony a 1 to niebieski. Nie mam koloru. W configuracji minervy jest legenda z kolorem dla -1, 0 i 1
+//     color: null,
+//     // kiedy nie ma wartości value i color to z configuracji jest brany element "no color val" dla koloru
+//     description: null,
+//   },
+// };
+
+// const color = {
+//   // definiuje prostokat, na jakiej podstawie zostalo  matchowane, moze byc nazwa, identyfikator, nazwa+mapa
+//   id: 9558,
+//   name: 'ATP6V0B',
+//   modelName: null,
+//   elementId: null,
+//   reverseReaction: null,
+//   lineWidth: null,
+//   // bede mial albo wartosc albo kolor, jak bede mial oba (raczej nie bedzie) to jest to bug
+//   value: null,
+//   color: {
+//     alpha: 255,
+//     rgb: -16711936,
+//   },
+//   description: null,
+// };
+
+// // pole lines jest od reakcji - polegac na tym polu zeby odroznic molekuly od reakcji
+// // porozmawiac z Markiem o sumowaniu overlays na przycisku "go to submap"
diff --git a/src/models/configurationOptionSchema.ts b/src/models/configurationOptionSchema.ts
new file mode 100644
index 00000000..9268478c
--- /dev/null
+++ b/src/models/configurationOptionSchema.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod';
+
+export const configurationOptionSchema = z.object({
+  idObject: z.number(),
+  type: z.string(),
+  valueType: z.string(),
+  commonName: z.string(),
+  isServerSide: z.boolean(),
+  value: z.string().optional(),
+  group: z.string(),
+});
diff --git a/src/models/mocks/configurationOptionMock.ts b/src/models/mocks/configurationOptionMock.ts
new file mode 100644
index 00000000..f658402e
--- /dev/null
+++ b/src/models/mocks/configurationOptionMock.ts
@@ -0,0 +1,47 @@
+import { ConfigurationOption } from '@/types/models';
+
+export const CONFIGURATION_OPTIONS_TYPES_MOCK: string[] = [
+  'MIN_COLOR_VAL',
+  'MAX_COLOR_VAL',
+  'SIMPLE_COLOR_VAL',
+  'NEUTRAL_COLOR_VAL',
+];
+
+export const CONFIGURATION_OPTIONS_COLOURS_MOCK: ConfigurationOption[] = [
+  {
+    idObject: 29,
+    type: 'MIN_COLOR_VAL',
+    valueType: 'COLOR',
+    commonName: 'Overlay color for negative values',
+    isServerSide: false,
+    value: 'FF0000',
+    group: 'Overlays',
+  },
+  {
+    idObject: 30,
+    type: 'MAX_COLOR_VAL',
+    valueType: 'COLOR',
+    commonName: 'Overlay color for postive values',
+    isServerSide: false,
+    value: '0000FF',
+    group: 'Overlays',
+  },
+  {
+    idObject: 31,
+    type: 'SIMPLE_COLOR_VAL',
+    valueType: 'COLOR',
+    commonName: 'Overlay color when no values are defined',
+    isServerSide: false,
+    value: '00FF00',
+    group: 'Overlays',
+  },
+  {
+    idObject: 32,
+    type: 'NEUTRAL_COLOR_VAL',
+    valueType: 'COLOR',
+    commonName: 'Overlay color for value=0',
+    isServerSide: false,
+    value: 'FFFFFF',
+    group: 'Overlays',
+  },
+];
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 42711404..70ad3379 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -29,4 +29,5 @@ export const apiPath = {
   getAllBackgroundsByProjectIdQuery: (projectId: string): string =>
     `projects/${projectId}/backgrounds/`,
   getProjectById: (projectId: string): string => `projects/${projectId}`,
+  getConfigurationOptions: (): string => 'configuration/options/',
 };
diff --git a/src/redux/configuration/configuration.adapter.ts b/src/redux/configuration/configuration.adapter.ts
new file mode 100644
index 00000000..cb3c59be
--- /dev/null
+++ b/src/redux/configuration/configuration.adapter.ts
@@ -0,0 +1,18 @@
+import { DEFAULT_ERROR } from '@/constants/errors';
+import { Loading } from '@/types/loadingState';
+import { ConfigurationOption } from '@/types/models';
+import { createEntityAdapter } from '@reduxjs/toolkit';
+
+export const configurationAdapter = createEntityAdapter<ConfigurationOption>({
+  selectId: option => option.type,
+});
+
+const REQUEST_INITIAL_STATUS: { loading: Loading; error: Error } = {
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+};
+
+export const CONFIGURATION_INITIAL_STATE =
+  configurationAdapter.getInitialState(REQUEST_INITIAL_STATUS);
+
+export type ConfigurationState = typeof CONFIGURATION_INITIAL_STATE;
diff --git a/src/redux/configuration/configuration.mock.ts b/src/redux/configuration/configuration.mock.ts
new file mode 100644
index 00000000..ce8f052d
--- /dev/null
+++ b/src/redux/configuration/configuration.mock.ts
@@ -0,0 +1,27 @@
+/* eslint-disable no-magic-numbers */
+import { DEFAULT_ERROR } from '@/constants/errors';
+import {
+  CONFIGURATION_OPTIONS_TYPES_MOCK,
+  CONFIGURATION_OPTIONS_COLOURS_MOCK,
+} from '@/models/mocks/configurationOptionMock';
+import { ConfigurationState } from './configuration.adapter';
+
+export const CONFIGURATION_INITIAL_STORE_MOCK: ConfigurationState = {
+  ids: [],
+  entities: {},
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+};
+
+/** IMPORTANT MOCK IDS MUST MATCH KEYS IN ENTITIES  */
+export const CONFIGURATION_INITIAL_STORE_MOCKS: ConfigurationState = {
+  ids: CONFIGURATION_OPTIONS_TYPES_MOCK,
+  entities: {
+    [CONFIGURATION_OPTIONS_TYPES_MOCK[0]]: CONFIGURATION_OPTIONS_COLOURS_MOCK[0],
+    [CONFIGURATION_OPTIONS_TYPES_MOCK[1]]: CONFIGURATION_OPTIONS_COLOURS_MOCK[1],
+    [CONFIGURATION_OPTIONS_TYPES_MOCK[2]]: CONFIGURATION_OPTIONS_COLOURS_MOCK[2],
+    [CONFIGURATION_OPTIONS_TYPES_MOCK[3]]: CONFIGURATION_OPTIONS_COLOURS_MOCK[3],
+  },
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+};
diff --git a/src/redux/configuration/configuration.reducers.ts b/src/redux/configuration/configuration.reducers.ts
new file mode 100644
index 00000000..01cd1fe5
--- /dev/null
+++ b/src/redux/configuration/configuration.reducers.ts
@@ -0,0 +1,21 @@
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { getConfigurationOptions } from './configuration.thunks';
+import { ConfigurationState, configurationAdapter } from './configuration.adapter';
+
+export const getConfigurationOptionsReducer = (
+  builder: ActionReducerMapBuilder<ConfigurationState>,
+): void => {
+  builder.addCase(getConfigurationOptions.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(getConfigurationOptions.fulfilled, (state, action) => {
+    if (action.payload) {
+      state.loading = 'succeeded';
+      configurationAdapter.addMany(state, action.payload);
+    }
+  });
+  builder.addCase(getConfigurationOptions.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
+};
diff --git a/src/redux/configuration/configuration.slice.ts b/src/redux/configuration/configuration.slice.ts
new file mode 100644
index 00000000..4bf43488
--- /dev/null
+++ b/src/redux/configuration/configuration.slice.ts
@@ -0,0 +1,14 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { getConfigurationOptionsReducer } from './configuration.reducers';
+import { CONFIGURATION_INITIAL_STATE } from './configuration.adapter';
+
+export const configurationSlice = createSlice({
+  name: 'configuration',
+  initialState: CONFIGURATION_INITIAL_STATE,
+  reducers: {},
+  extraReducers: builder => {
+    getConfigurationOptionsReducer(builder);
+  },
+});
+
+export default configurationSlice.reducer;
diff --git a/src/redux/configuration/configuration.thunks.ts b/src/redux/configuration/configuration.thunks.ts
new file mode 100644
index 00000000..ad3812bb
--- /dev/null
+++ b/src/redux/configuration/configuration.thunks.ts
@@ -0,0 +1,23 @@
+import { ConfigurationOption } from '@/types/models';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { z } from 'zod';
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { configurationOptionSchema } from '@/models/configurationOptionSchema';
+import { apiPath } from '../apiPath';
+
+export const getConfigurationOptions = createAsyncThunk(
+  'configuration/getConfigurationOptions',
+  async (): Promise<ConfigurationOption[] | undefined> => {
+    const response = await axiosInstance.get<ConfigurationOption[]>(
+      apiPath.getConfigurationOptions(),
+    );
+
+    const isDataValid = validateDataUsingZodSchema(
+      response.data,
+      z.array(configurationOptionSchema),
+    );
+
+    return isDataValid ? response.data : undefined;
+  },
+);
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index b2c3ef2a..6ac3a819 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -16,6 +16,7 @@ import {
 } from '../map/map.thunks';
 import { getSearchData } from '../search/search.thunks';
 import { setPerfectMatch } from '../search/search.slice';
+import { getConfigurationOptions } from '../configuration/configuration.thunks';
 
 interface InitializeAppParams {
   queryData: QueryData;
@@ -28,6 +29,7 @@ export const fetchInitialAppData = createAsyncThunk<
 >('appInit/fetchInitialAppData', async ({ queryData }, { dispatch }): Promise<void> => {
   /** Fetch all data required for renderin map */
   await Promise.all([
+    dispatch(getConfigurationOptions()),
     dispatch(getProjectById(PROJECT_ID)),
     dispatch(getAllBackgroundsByProjectId(PROJECT_ID)),
     dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)),
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index bf70be50..2f5c7a4d 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -1,6 +1,7 @@
 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';
+import { CONFIGURATION_INITIAL_STATE } from '../configuration/configuration.adapter';
 import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture';
 import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock';
 import { initialMapStateFixture } from '../map/map.fixtures';
@@ -23,4 +24,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   map: initialMapStateFixture,
   overlays: OVERLAYS_INITIAL_STATE_MOCK,
   reactions: REACTIONS_STATE_INITIAL_MOCK,
+  configuration: CONFIGURATION_INITIAL_STATE,
 };
diff --git a/src/redux/store.ts b/src/redux/store.ts
index d98c85c3..7596b8cd 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -9,6 +9,7 @@ import overlaysReducer from '@/redux/overlays/overlays.slice';
 import projectReducer from '@/redux/project/project.slice';
 import reactionsReducer from '@/redux/reactions/reactions.slice';
 import searchReducer from '@/redux/search/search.slice';
+import configurationReducer from '@/redux/configuration/configuration.slice';
 import {
   AnyAction,
   ListenerEffectAPI,
@@ -30,6 +31,7 @@ export const reducers = {
   overlays: overlaysReducer,
   models: modelsReducer,
   reactions: reactionsReducer,
+  configuration: configurationReducer,
 };
 
 export const middlewares = [mapListenerMiddleware.middleware];
diff --git a/src/types/loadingState.ts b/src/types/loadingState.ts
index 12859c95..d21ff588 100644
--- a/src/types/loadingState.ts
+++ b/src/types/loadingState.ts
@@ -1 +1,4 @@
 export type Loading = 'idle' | 'pending' | 'succeeded' | 'failed';
+export interface LoadingInterface {
+  loading: 'idle' | 'pending' | 'succeeded' | 'failed';
+}
diff --git a/src/types/models.ts b/src/types/models.ts
index f83079f1..f8e25e6c 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -2,6 +2,7 @@ import { bioEntityContentSchema } from '@/models/bioEntityContentSchema';
 import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema';
 import { bioEntitySchema } from '@/models/bioEntitySchema';
 import { chemicalSchema } from '@/models/chemicalSchema';
+import { configurationOptionSchema } from '@/models/configurationOptionSchema';
 import { disease } from '@/models/disease';
 import { drugSchema } from '@/models/drugSchema';
 import { elementSearchResult, elementSearchResultType } from '@/models/elementSearchResult';
@@ -35,3 +36,4 @@ export type Reference = z.infer<typeof referenceSchema>;
 export type ReactionLine = z.infer<typeof reactionLineSchema>;
 export type ElementSearchResult = z.infer<typeof elementSearchResult>;
 export type ElementSearchResultType = z.infer<typeof elementSearchResultType>;
+export type ConfigurationOption = z.infer<typeof configurationOptionSchema>;
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 00000000..4f1c4bba
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,2 @@
+ - render overlays for map for single element 
+ - 
\ No newline at end of file
-- 
GitLab