diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index 7a693a8c8bf31e213fd199e87cd0141f574326eb..caba733755f14d7c6bfccb84a711a173fe4d2487 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -50,8 +50,8 @@ export const useOlMapReactionsLayer = ({ const reactions = useMemo(() => { return modelReactions.map(reaction => { - const shape = shapes.find(bioShape => bioShape.sboTerm === reaction.sboTerm); - if (!shape) { + const reactionShapes = shapes && shapes[reaction.sboTerm]; + if (!reactionShapes) { return []; } const reactionObject = new Reaction({ @@ -63,7 +63,7 @@ export const useOlMapReactionsLayer = ({ zIndex: reaction.z, lineTypes, arrowTypes, - shapes: shape.shapes, + shapes: reactionShapes, pointToProjection, mapInstance, }); @@ -130,11 +130,11 @@ export const useOlMapReactionsLayer = ({ } return; } - const shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm); - if (shape) { + const elementShapes = shapes[element.sboTerm]; + if (elementShapes) { validElements.push( new MapElement({ - shapes: shape.shapes, + shapes: elementShapes, x: element.x, y: element.y, nameX: element.nameX, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index b81bb1f06abda517599992e9670bc92c67416fcb..2717f41ff4fe5e9a0e91f31b7887c3279f25e747 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -5,7 +5,7 @@ import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/s import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import Polygon from 'ol/geom/Polygon'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; -import { BioShape, Color, LineType, Modification, Shape } from '@/types/models'; +import { Color, Modification, Shape } from '@/types/models'; import { MapInstance } from '@/types/map'; import { HorizontalAlign, @@ -20,6 +20,7 @@ import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/st import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; +import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; export type MapElementProps = { shapes: Array<Shape>; @@ -45,8 +46,8 @@ export type MapElementProps = { activity?: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; - bioShapes?: Array<BioShape>; - lineTypes?: Array<LineType>; + bioShapes?: BioShapesDict; + lineTypes?: LineTypeDict; modifications?: Array<Modification>; }; @@ -57,9 +58,9 @@ export default class MapElement extends BaseMultiPolygon { lineType: string | undefined; - bioShapes: Array<BioShape>; + bioShapes: BioShapesDict; - lineTypes: Array<LineType>; + lineTypes: LineTypeDict; homodimer: number; @@ -93,8 +94,8 @@ export default class MapElement extends BaseMultiPolygon { activity, pointToProjection, mapInstance, - bioShapes = [], - lineTypes = [], + bioShapes = {}, + lineTypes = {}, modifications = [], }: MapElementProps) { super({ @@ -135,18 +136,15 @@ export default class MapElement extends BaseMultiPolygon { return; } - const shape = this.bioShapes.find(bioShape => bioShape.sboTerm === modification.sboTerm); - if (!shape) { + const shapes = this.bioShapes[modification.sboTerm]; + if (!shapes) { return; } - this.drawModification(modification, shape.shapes); + this.drawModification(modification, shapes); }); if (this.lineType) { - const lineTypeFound = this.lineTypes.find(type => type.name === this.lineType); - if (lineTypeFound) { - this.lineDash = lineTypeFound.pattern; - } + this.lineDash = this.lineTypes[this.lineType] || []; } const homodimerOffset = (this.homodimer - 1) * 6; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts index f94790c7a4ba06ec06d4c859b4dfb8c19d95d4a6..71a6ce39bf3e34118995b962e971d2a58f1f3935 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts @@ -5,72 +5,69 @@ import { Polygon, MultiPolygon } from 'ol/geom'; import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature'; -import { ArrowType } from '@/types/models'; import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { ArrowTypeDict } from '@/redux/shapes/shapes.types'; jest.mock('../style/getStyle'); jest.mock('./getShapePolygon'); describe('getArrowFeature', () => { const props = { - arrowTypes: [ - { - arrowType: 'FULL', - shapes: [ - { - type: 'POLYGON', - fill: false, - points: [ - { - type: 'REL_ABS_POINT', - absoluteX: 0.0, - absoluteY: 0.0, - relativeX: 0.0, - relativeY: 50.0, - relativeHeightForX: null, - relativeWidthForY: null, - }, - { - type: 'REL_ABS_POINT', - absoluteX: 0.0, - absoluteY: 0.0, - relativeX: 100.0, - relativeY: 50.0, - relativeHeightForX: null, - relativeWidthForY: null, - }, - { - type: 'REL_ABS_POINT', - absoluteX: 0.0, - absoluteY: 0.0, - relativeX: 7.612046748871326, - relativeY: 50.0, - relativeHeightForX: null, - relativeWidthForY: 19.134171618254495, - }, - { - type: 'REL_ABS_POINT', - absoluteX: 0.0, - absoluteY: 0.0, - relativeX: 7.612046748871326, - relativeY: 50.0, - relativeHeightForX: null, - relativeWidthForY: -19.134171618254495, - }, - { - type: 'REL_ABS_POINT', - absoluteX: 0.0, - absoluteY: 0.0, - relativeX: 100.0, - relativeY: 50.0, - relativeHeightForX: null, - relativeWidthForY: null, - }, - ], - }, - ], - } as ArrowType, - ], + arrowTypes: { + FULL: [ + { + type: 'POLYGON', + fill: false, + points: [ + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 0.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 100.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 7.612046748871326, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: 19.134171618254495, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 7.612046748871326, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: -19.134171618254495, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 100.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + ], + }, + ], + } as ArrowTypeDict, arrow: { length: 15, arrowType: 'FULL', angle: 2.74, lineType: 'SOLID' }, x: 0, y: 0, @@ -114,7 +111,7 @@ describe('getArrowFeature', () => { }); it('should handle missing arrowType in props', () => { - const invalidProps = { ...props, arrowTypes: [] }; + const invalidProps = { ...props, arrowTypes: {} }; expect(() => getArrowFeature(invalidProps)).not.toThrow(); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts index cce7e1963badcd1e478fd497c1f7be97199fb5c5..97168e2d6b9b65dcfda1f258e4b3b61f44953101 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts @@ -3,11 +3,12 @@ import Style from 'ol/style/Style'; import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; import { Feature } from 'ol'; import { MultiPolygon } from 'ol/geom'; -import { Arrow, ArrowType, Color } from '@/types/models'; +import { Arrow, Color } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import Polygon from 'ol/geom/Polygon'; import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { ArrowTypeDict } from '@/redux/shapes/shapes.types'; export default function getArrowFeature({ arrowTypes, @@ -20,7 +21,7 @@ export default function getArrowFeature({ color, pointToProjection, }: { - arrowTypes: Array<ArrowType>; + arrowTypes: ArrowTypeDict; arrow: Arrow; x: number; y: number; @@ -30,7 +31,7 @@ export default function getArrowFeature({ color: Color; pointToProjection: UsePointToProjectionResult; }): undefined | Feature<MultiPolygon> { - const arrowShapes = arrowTypes.find(arrowType => arrowType.arrowType === arrow.arrowType)?.shapes; + const arrowShapes = arrowTypes[arrow.arrowType]; if (!arrowShapes) { return undefined; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts index 10c29480c6fe795114a0592e4e3252063ba370c5..96829031476236840553f6d7da063b086dc60eee 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts @@ -120,8 +120,8 @@ describe('Layer', () => { layerId: '23', pointToProjection: jest.fn(point => [point.x, point.y]), mapInstance, - lineTypes: [], - arrowTypes: [], + lineTypes: {}, + arrowTypes: {}, }; (getTextStyle as jest.Mock).mockReturnValue( new Style({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index 924c529612b670b7c6e6631d33eacec6157e6054..bd6fac334f19caf9c8cf03683e35fb5b8a5ad69d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -1,5 +1,5 @@ /* eslint-disable no-magic-numbers */ -import { ArrowType, LayerLine, LayerOval, LayerRect, LayerText, LineType } from '@/types/models'; +import { LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; import { MapInstance } from '@/types/map'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; @@ -18,6 +18,7 @@ import getRotation from '@/components/Map/MapViewer/MapViewerVector/utils/shapes import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature'; import { FeatureLike } from 'ol/Feature'; import Style from 'ol/style/Style'; +import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; export interface LayerProps { texts: Array<LayerText>; @@ -26,8 +27,8 @@ export interface LayerProps { lines: Array<LayerLine>; visible: boolean; layerId: string; - lineTypes: Array<LineType>; - arrowTypes: Array<ArrowType>; + lineTypes: LineTypeDict; + arrowTypes: ArrowTypeDict; mapInstance: MapInstance; pointToProjection: UsePointToProjectionResult; } @@ -41,9 +42,9 @@ export default class Layer { lines: Array<LayerLine>; - lineTypes: Array<LineType>; + lineTypes: LineTypeDict; - arrowTypes: Array<ArrowType>; + arrowTypes: ArrowTypeDict; textFeatures: Array<Feature<Point>>; @@ -262,11 +263,7 @@ export default class Layer { const lineString = new LineString(points); - let lineDash; - const lineTypeFound = this.lineTypes.find(type => type.name === line.lineType); - if (lineTypeFound) { - lineDash = lineTypeFound.pattern; - } + const lineDash = this.lineTypes[line.lineType] || []; const lineStyle = getStyle({ geometry: lineString, borderColor: line.color, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts index 06427838e0c233c6a6dd70a778965d4072ad7f16..f8c68fc15fa5ca8785aaaca1696b2c4f148a6f99 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts @@ -8,6 +8,8 @@ import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; import { shapesFixture } from '@/models/fixtures/shapesFixture'; import View from 'ol/View'; +import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import { ArrowType, LineType } from '@/types/models'; describe('Layer', () => { let props: ReactionProps; @@ -31,8 +33,14 @@ describe('Layer', () => { shapes: shapesFixture, zIndex: newReactionFixture.z, pointToProjection: jest.fn(() => [10, 10]), - lineTypes: lineTypesFixture, - arrowTypes: arrowTypesFixture, + lineTypes: lineTypesFixture.reduce((acc: LineTypeDict, line: LineType) => { + acc[line.name] = line.pattern; + return acc; + }, {}), + arrowTypes: arrowTypesFixture.reduce((acc: ArrowTypeDict, arrow: ArrowType) => { + acc[arrow.arrowType] = arrow.shapes; + return acc; + }, {}), mapInstance, }; }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts index 1d2880b8fc0dc1ee8d7ef1e634f4b090b9e47ba1..2535e785468dc63d15d90f12ad2b12262502879d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -1,5 +1,5 @@ /* eslint-disable no-magic-numbers */ -import { ArrowType, Line, LineType, Operator, ReactionProduct, Shape } from '@/types/models'; +import { Line, Operator, ReactionProduct, Shape } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; import { Circle, LineString, MultiPolygon } from 'ol/geom'; @@ -14,6 +14,7 @@ import { FeatureLike } from 'ol/Feature'; import { MapInstance } from '@/types/map'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; export interface ReactionProps { line: Line; @@ -22,8 +23,8 @@ export interface ReactionProps { modifiers: Array<ReactionProduct>; operators: Array<Operator>; zIndex: number; - lineTypes: Array<LineType>; - arrowTypes: Array<ArrowType>; + lineTypes: LineTypeDict; + arrowTypes: ArrowTypeDict; shapes: Array<Shape>; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; @@ -42,9 +43,9 @@ export default class Reaction { zIndex: number; - lineTypes: Array<LineType>; + lineTypes: LineTypeDict; - arrowTypes: Array<ArrowType>; + arrowTypes: ArrowTypeDict; shapes: Array<Shape>; @@ -185,11 +186,7 @@ export default class Reaction { const lineString = new LineString(points); - let lineDash; - const lineTypeFound = this.lineTypes.find(type => type.name === line.lineType); - if (lineTypeFound) { - lineDash = lineTypeFound.pattern; - } + const lineDash = this.lineTypes[line.lineType] || []; const lineStyle = getStyle({ geometry: lineString, borderColor: line.color, diff --git a/src/redux/shapes/shapes.mock.ts b/src/redux/shapes/shapes.mock.ts index b1ad3a5eaa627abffc1793bbb41735afa9164f6a..ed8dc98962da79e7164e57022072d0de5e0d85d2 100644 --- a/src/redux/shapes/shapes.mock.ts +++ b/src/redux/shapes/shapes.mock.ts @@ -3,17 +3,17 @@ import { ShapesState } from '@/redux/shapes/shapes.types'; export const SHAPES_STATE_INITIAL_MOCK: ShapesState = { bioShapesState: { - data: [], + data: {}, loading: 'idle', error: DEFAULT_ERROR, }, lineTypesState: { - data: [], + data: {}, loading: 'idle', error: DEFAULT_ERROR, }, arrowTypesState: { - data: [], + data: {}, loading: 'idle', error: DEFAULT_ERROR, }, diff --git a/src/redux/shapes/shapes.reducers.test.ts b/src/redux/shapes/shapes.reducers.test.ts index 9eab2b219d230f81f163b8464c5813f93a503005..fa4e3a08252859d37dea6bb2a0d63bda5704b37e 100644 --- a/src/redux/shapes/shapes.reducers.test.ts +++ b/src/redux/shapes/shapes.reducers.test.ts @@ -10,9 +10,10 @@ import { bioShapesFixture } from '@/models/fixtures/bioShapesFixture'; import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock'; import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; +import arrayToDict from '@/utils/array/arrayToDict'; import shapesReducer from './shapes.slice'; import { getArrowTypes, getLineTypes, getShapes } from './shapes.thunks'; -import { ShapesState } from './shapes.types'; +import { ArrowTypeDict, BioShapesDict, LineTypeDict, ShapesState } from './shapes.types'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -38,7 +39,8 @@ describe('shapes reducer', () => { expect(type).toBe('vectorMap/getShapes/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(bioShapesFixture); + const expectedResult = arrayToDict(bioShapesFixture, 'sboTerm', 'shapes') as BioShapesDict; + expect(data).toEqual(expectedResult); }); it('should update store after successful getLineTypes query', async () => { @@ -49,7 +51,9 @@ describe('shapes reducer', () => { expect(type).toBe('vectorMap/getLineTypes/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(lineTypesFixture); + + const expectedResult = arrayToDict(lineTypesFixture, 'name', 'pattern') as LineTypeDict; + expect(data).toEqual(expectedResult); }); it('should update store after successful getArrowTypes query', async () => { @@ -60,7 +64,8 @@ describe('shapes reducer', () => { expect(type).toBe('vectorMap/getArrowTypes/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(arrowTypesFixture); + const expectedResult = arrayToDict(arrowTypesFixture, 'arrowType', 'shapes') as ArrowTypeDict; + expect(data).toEqual(expectedResult); }); it('should update store after failed getShapes query', async () => { @@ -75,7 +80,7 @@ describe('shapes reducer', () => { ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual([]); + expect(data).toEqual({}); }); it('should update store after failed getLineTypes query', async () => { @@ -90,7 +95,7 @@ describe('shapes reducer', () => { ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual([]); + expect(data).toEqual({}); }); it('should update store after failed getArrowTypes query', async () => { @@ -105,7 +110,7 @@ describe('shapes reducer', () => { ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual([]); + expect(data).toEqual({}); }); it('should update store on loading getShapes query', async () => { @@ -114,14 +119,14 @@ describe('shapes reducer', () => { const shapesPromise = store.dispatch(getShapes()); const { data, loading } = store.getState().shapes.bioShapesState; - expect(data).toEqual([]); + expect(data).toEqual({}); expect(loading).toEqual('pending'); shapesPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().shapes.bioShapesState; - - expect(dataPromiseFulfilled).toEqual(bioShapesFixture); + const expectedResult = arrayToDict(bioShapesFixture, 'sboTerm', 'shapes') as BioShapesDict; + expect(dataPromiseFulfilled).toEqual(expectedResult); expect(promiseFulfilled).toEqual('succeeded'); }); }); @@ -132,14 +137,14 @@ describe('shapes reducer', () => { const lineTypesPromise = store.dispatch(getLineTypes()); const { data, loading } = store.getState().shapes.lineTypesState; - expect(data).toEqual([]); + expect(data).toEqual({}); expect(loading).toEqual('pending'); lineTypesPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().shapes.lineTypesState; - - expect(dataPromiseFulfilled).toEqual(lineTypesFixture); + const expectedResult = arrayToDict(lineTypesFixture, 'name', 'pattern') as LineTypeDict; + expect(dataPromiseFulfilled).toEqual(expectedResult); expect(promiseFulfilled).toEqual('succeeded'); }); }); @@ -150,14 +155,14 @@ describe('shapes reducer', () => { const arrowTypesPromise = store.dispatch(getArrowTypes()); const { data, loading } = store.getState().shapes.arrowTypesState; - expect(data).toEqual([]); + expect(data).toEqual({}); expect(loading).toEqual('pending'); arrowTypesPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().shapes.arrowTypesState; - - expect(dataPromiseFulfilled).toEqual(arrowTypesFixture); + const expectedResult = arrayToDict(arrowTypesFixture, 'arrowType', 'shapes') as ArrowTypeDict; + expect(dataPromiseFulfilled).toEqual(expectedResult); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/shapes/shapes.reducers.ts b/src/redux/shapes/shapes.reducers.ts index 525cde54ca6b47cf1e06cf7af8f3b623e8c4c8c0..ecf8486b9664de24f30853beb12fdb23f9b79c81 100644 --- a/src/redux/shapes/shapes.reducers.ts +++ b/src/redux/shapes/shapes.reducers.ts @@ -7,7 +7,7 @@ export const getShapesReducer = (builder: ActionReducerMapBuilder<ShapesState>): state.bioShapesState.loading = 'pending'; }); builder.addCase(getShapes.fulfilled, (state, action) => { - state.bioShapesState.data = action.payload || []; + state.bioShapesState.data = action.payload || {}; state.bioShapesState.loading = 'succeeded'; }); builder.addCase(getShapes.rejected, state => { @@ -20,7 +20,7 @@ export const getLineTypesReducer = (builder: ActionReducerMapBuilder<ShapesState state.lineTypesState.loading = 'pending'; }); builder.addCase(getLineTypes.fulfilled, (state, action) => { - state.lineTypesState.data = action.payload || []; + state.lineTypesState.data = action.payload || {}; state.lineTypesState.loading = 'succeeded'; }); builder.addCase(getLineTypes.rejected, state => { @@ -33,7 +33,7 @@ export const getArrowTypesReducer = (builder: ActionReducerMapBuilder<ShapesStat state.arrowTypesState.loading = 'pending'; }); builder.addCase(getArrowTypes.fulfilled, (state, action) => { - state.arrowTypesState.data = action.payload || []; + state.arrowTypesState.data = action.payload || {}; state.arrowTypesState.loading = 'succeeded'; }); builder.addCase(getArrowTypes.rejected, state => { diff --git a/src/redux/shapes/shapes.selectors.ts b/src/redux/shapes/shapes.selectors.ts index 35569752e5b3dbd9a26ccac4b22210dff147b7f6..cdd4fa160ae29d1a0197c847a0d20127c394365a 100644 --- a/src/redux/shapes/shapes.selectors.ts +++ b/src/redux/shapes/shapes.selectors.ts @@ -10,16 +10,10 @@ export const bioShapesSelector = createSelector( export const lineTypesSelector = createSelector( shapesSelector, - shapes => shapes.lineTypesState.data, + shapes => shapes.lineTypesState.data || {}, ); export const arrowTypesSelector = createSelector( shapesSelector, - shapes => shapes.arrowTypesState.data, -); - -export const shapeBySBOSelector = createSelector( - [shapesSelector, (_state, shapeSBO: string): string => shapeSBO], - (shapes, shapeSBO) => - (shapes?.bioShapesState.data || []).find(({ sboTerm }) => sboTerm === shapeSBO), + shapes => shapes.arrowTypesState.data || {}, ); diff --git a/src/redux/shapes/shapes.thunks.test.ts b/src/redux/shapes/shapes.thunks.test.ts index 7b9980bf1679f1b5aac404a44e655db53b28f1d2..1618d2ae2aa46cc60ffdb5c2b07888fa35ac4f58 100644 --- a/src/redux/shapes/shapes.thunks.test.ts +++ b/src/redux/shapes/shapes.thunks.test.ts @@ -6,9 +6,15 @@ import { import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { bioShapesFixture } from '@/models/fixtures/bioShapesFixture'; -import { ShapesState } from '@/redux/shapes/shapes.types'; +import { + ArrowTypeDict, + BioShapesDict, + LineTypeDict, + ShapesState, +} from '@/redux/shapes/shapes.types'; import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; +import arrayToDict from '@/utils/array/arrayToDict'; import shapesReducer from './shapes.slice'; import { getArrowTypes, getLineTypes, getShapes } from './shapes.thunks'; @@ -25,16 +31,17 @@ describe('shapes thunks', () => { mockedAxiosClient.onGet(apiPath.getShapes()).reply(HttpStatusCode.Ok, bioShapesFixture); const { payload } = await store.dispatch(getShapes()); - expect(payload).toEqual(bioShapesFixture); + const expectedResult = arrayToDict(bioShapesFixture, 'sboTerm', 'shapes') as BioShapesDict; + expect(payload).toEqual(expectedResult); }); - it('should return undefined when data response from API is not valid ', async () => { + it('should return empty object when data response from API is not valid ', async () => { mockedAxiosClient .onGet(apiPath.getShapes()) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); const { payload } = await store.dispatch(getShapes()); - expect(payload).toEqual(undefined); + expect(payload).toEqual({}); }); }); @@ -43,7 +50,8 @@ describe('shapes thunks', () => { mockedAxiosClient.onGet(apiPath.getLineTypes()).reply(HttpStatusCode.Ok, lineTypesFixture); const { payload } = await store.dispatch(getLineTypes()); - expect(payload).toEqual(lineTypesFixture); + const expectedResult = arrayToDict(lineTypesFixture, 'name', 'pattern') as LineTypeDict; + expect(payload).toEqual(expectedResult); }); it('should return undefined when data response from API is not valid ', async () => { @@ -52,7 +60,7 @@ describe('shapes thunks', () => { .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); const { payload } = await store.dispatch(getLineTypes()); - expect(payload).toEqual(undefined); + expect(payload).toEqual({}); }); }); @@ -61,7 +69,8 @@ describe('shapes thunks', () => { mockedAxiosClient.onGet(apiPath.getArrowTypes()).reply(HttpStatusCode.Ok, arrowTypesFixture); const { payload } = await store.dispatch(getArrowTypes()); - expect(payload).toEqual(arrowTypesFixture); + const expectedResult = arrayToDict(arrowTypesFixture, 'arrowType', 'shapes') as ArrowTypeDict; + expect(payload).toEqual(expectedResult); }); it('should return undefined when data response from API is not valid ', async () => { @@ -70,7 +79,7 @@ describe('shapes thunks', () => { .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); const { payload } = await store.dispatch(getArrowTypes()); - expect(payload).toEqual(undefined); + expect(payload).toEqual({}); }); }); }); diff --git a/src/redux/shapes/shapes.thunks.ts b/src/redux/shapes/shapes.thunks.ts index f882d2fb6f0f64acc5b703ed77309cf1a25f0f1d..f222ef0e1a875c7a985c1afa81e303ca87021a06 100644 --- a/src/redux/shapes/shapes.thunks.ts +++ b/src/redux/shapes/shapes.thunks.ts @@ -14,43 +14,53 @@ import { import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { lineTypeSchema } from '@/models/lineTypeSchema'; import { arrowTypeSchema } from '@/models/arrowTypeSchema'; +import { ArrowTypeDict, BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import arrayToDict from '@/utils/array/arrayToDict'; -export const getShapes = createAsyncThunk<BioShape[] | undefined, void, ThunkConfig>( +export const getShapes = createAsyncThunk<BioShapesDict | undefined, void, ThunkConfig>( 'vectorMap/getShapes', async () => { try { const { data } = await axiosInstanceNewAPI.get<BioShape[]>(apiPath.getShapes()); const isDataValid = validateDataUsingZodSchema(data, z.array(bioShapeSchema)); - - return isDataValid ? data : undefined; + if (!isDataValid) { + return {}; + } + return arrayToDict(data, 'sboTerm', 'shapes') as BioShapesDict; } catch (error) { return Promise.reject(getError({ error, prefix: SHAPES_FETCHING_ERROR_PREFIX })); } }, ); -export const getLineTypes = createAsyncThunk<LineType[] | undefined, void, ThunkConfig>( +export const getLineTypes = createAsyncThunk<LineTypeDict | undefined, void, ThunkConfig>( 'vectorMap/getLineTypes', async () => { try { const { data } = await axiosInstanceNewAPI.get<LineType[]>(apiPath.getLineTypes()); const isDataValid = validateDataUsingZodSchema(data, z.array(lineTypeSchema)); - return isDataValid ? data : undefined; + if (!isDataValid) { + return {}; + } + return arrayToDict(data, 'name', 'pattern') as LineTypeDict; } catch (error) { return Promise.reject(getError({ error, prefix: LINE_TYPES_FETCHING_ERROR_PREFIX })); } }, ); -export const getArrowTypes = createAsyncThunk<ArrowType[] | undefined, void, ThunkConfig>( +export const getArrowTypes = createAsyncThunk<ArrowTypeDict | undefined, void, ThunkConfig>( 'vectorMap/getArrowTypes', async () => { try { const { data } = await axiosInstanceNewAPI.get<ArrowType[]>(apiPath.getArrowTypes()); const isDataValid = validateDataUsingZodSchema(data, z.array(arrowTypeSchema)); - return isDataValid ? data : undefined; + if (!isDataValid) { + return {}; + } + return arrayToDict(data, 'arrowType', 'shapes') as ArrowTypeDict; } catch (error) { return Promise.reject(getError({ error, prefix: ARROW_TYPES_FETCHING_ERROR_PREFIX })); } diff --git a/src/redux/shapes/shapes.types.ts b/src/redux/shapes/shapes.types.ts index e597d194d1ebef945cfc49da1ca881dd9dc62664..1b22bae154e557c050449c1a34e36f4047a8271b 100644 --- a/src/redux/shapes/shapes.types.ts +++ b/src/redux/shapes/shapes.types.ts @@ -1,11 +1,23 @@ import { FetchDataState } from '@/types/fetchDataState'; -import { ArrowType, BioShape, LineType } from '@/types/models'; +import { Shape } from '@/types/models'; -export type LineTypesState = FetchDataState<LineType[], []>; +export type LineTypeDict = { + [key: string]: Array<number>; +}; + +export type ArrowTypeDict = { + [key: string]: Array<Shape>; +}; + +export type BioShapesDict = { + [key: string]: Array<Shape>; +}; + +export type LineTypesState = FetchDataState<LineTypeDict>; -export type ArrowTypesState = FetchDataState<ArrowType[], []>; +export type ArrowTypesState = FetchDataState<ArrowTypeDict>; -export type BioShapesState = FetchDataState<BioShape[], []>; +export type BioShapesState = FetchDataState<BioShapesDict>; export type ShapesState = { lineTypesState: LineTypesState; diff --git a/src/utils/array/arrayToDict.test.ts b/src/utils/array/arrayToDict.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6389bdc6a7ea34e171cc5967cc93cdfbed4f4698 --- /dev/null +++ b/src/utils/array/arrayToDict.test.ts @@ -0,0 +1,45 @@ +import arrayToDict from './arrayToDict'; + +describe('arrayToDict', () => { + interface Person { + id: number; + name: string; + age: number; + isActive: boolean; + } + + const people: Person[] = [ + { id: 1, name: 'John', age: 30, isActive: true }, + { id: 2, name: 'Anna', age: 25, isActive: false }, + { id: 3, name: 'Peter', age: 28, isActive: true }, + ]; + + it('create dict with key "id" and value "name"', () => { + const result = arrayToDict(people, 'id', 'name'); + expect(result).toEqual({ + 1: 'John', + 2: 'Anna', + 3: 'Peter', + }); + }); + + it('create dict with key "name" and value "age"', () => { + const result = arrayToDict(people, 'name', 'age'); + expect(result).toEqual({ + John: 30, + Anna: 25, + Peter: 28, + }); + }); + + it('handles duplicate keys, overwriting previous values', () => { + const duplicateData = [ + { id: 1, name: 'John', age: 30, isActive: true }, + { id: 1, name: 'Anna', age: 25, isActive: false }, + ]; + const result = arrayToDict(duplicateData, 'id', 'name'); + expect(result).toEqual({ + 1: 'Anna', + }); + }); +}); diff --git a/src/utils/array/arrayToDict.ts b/src/utils/array/arrayToDict.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1994dc4e2781f01352f9b4882e47c0b4cdb3f75 --- /dev/null +++ b/src/utils/array/arrayToDict.ts @@ -0,0 +1,13 @@ +export default function arrayToDict<T, K extends keyof T, V extends keyof T>( + array: T[], + keyKey: K, + valueKey: V, +): Record<T[K] & PropertyKey, T[V]> { + return array.reduce( + (accumulator, currentItem) => { + accumulator[currentItem[keyKey] as T[K] & PropertyKey] = currentItem[valueKey]; + return accumulator; + }, + {} as Record<T[K] & PropertyKey, T[V]>, + ); +}