diff --git a/prettier.config.js b/prettier.config.js index eec5dca5c09d171184a2d30ee3bd9e029b5e32c2..1b959334323fcd05515f269dcfc1b6f595745f48 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -9,4 +9,4 @@ const config = { tabWidth: 2, }; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..601f8c31c777ebbf64fa7b981cbb5bbbe0da2d33 --- /dev/null +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx @@ -0,0 +1,11 @@ +describe('MapNavigation - component', () => { + it.skip('should render list of currently opened maps, main map should not have close button', () => { + expect(true).toBe(false); + }); + it.skip('should close map tab when clicking on close button while', () => { + expect(true).toBe(false); + }); + it.skip('should close map and open main map if closed currently selected map', () => { + expect(true).toBe(false); + }); +}); diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx index 9a2e9c9b58ba1d0ee3fba92c5dae6cf880027025..0bc4e1d80759f06d92e40b57f2cd8bded63ac632 100644 --- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx @@ -1,10 +1,56 @@ +import { MouseEvent } from 'react'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapModelIdSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors'; +import { closeMap, closeMapAndSetMainMapActive, setActiveMap } from '@/redux/map/map.slice'; +import { OppenedMap } from '@/redux/map/map.types'; import { Button } from '@/shared/Button'; +import { Icon } from '@/shared/Icon'; +import { MAIN_MAP } from '@/redux/map/map.constants'; +import { twMerge } from 'tailwind-merge'; -export const MapNavigation = (): JSX.Element => ( - <div className="h-10 w-full bg-white-pearl shadow-map-navigation-bar"> - {/* TODO: Button is temporary until we add tabs */} - <Button className="h-10 bg-[#EBF4FF]" variantStyles="secondary"> - Main map - </Button> - </div> -); +export const MapNavigation = (): JSX.Element => { + const dispatch = useAppDispatch(); + const openedMaps = useAppSelector(mapOpenedMapsSelector); + const currentModelId = useAppSelector(mapModelIdSelector); + + const isActive = (modelId: number): boolean => currentModelId === modelId; + const isNotMainMap = (modelName: string): boolean => modelName !== MAIN_MAP; + + const onCloseSubmap = (event: MouseEvent<HTMLDivElement>, map: OppenedMap): void => { + event.stopPropagation(); + if (isActive(map.modelId)) { + dispatch(closeMapAndSetMainMapActive({ modelId: map.modelId })); + } else { + dispatch(closeMap({ modelId: map.modelId })); + } + }; + + const onSubmapTabClick = (map: OppenedMap): void => { + dispatch(setActiveMap(map)); + }; + + return ( + <div className="flex h-10 w-full flex-row flex-nowrap justify-start overflow-y-auto bg-white-pearl shadow-map-navigation-bar"> + {openedMaps.map(map => ( + <Button + key={map.modelId} + className={twMerge( + 'h-10 whitespace-nowrap ', + isActive(map.modelId) ? 'bg-[#EBF4FF]' : 'font-normal', + )} + variantStyles={isActive(map.modelId) ? 'secondary' : 'ghost'} + onClick={(): void => onSubmapTabClick(map)} + > + {map.modelName} + {isNotMainMap(map.modelName) && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + <div onClick={(event): void => onCloseSubmap(event, map)} data-testid="close-icon"> + <Icon name="close" className="ml-3 h-5 w-5 fill-font-400" /> + </div> + )} + </Button> + ))} + </div> + ); +}; diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx index a143386895dc17b677d768ac5ee16ef8ac51e6f7..e056671a444f93c2d528489ab00c9b5074e8a214 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapItem/SubmapItem.component.tsx @@ -3,9 +3,10 @@ import { IconButton } from '@/shared/IconButton'; interface SubmapItemProps { modelName: string; + onOpenClick: () => void; } -export const SubmpamItem = ({ modelName }: SubmapItemProps): JSX.Element => ( +export const SubmpamItem = ({ modelName, onOpenClick }: SubmapItemProps): JSX.Element => ( <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"> @@ -16,6 +17,8 @@ export const SubmpamItem = ({ modelName }: SubmapItemProps): JSX.Element => ( icon="chevron-right" className="h-6 w-6 bg-white-pearl" classNameIcon="fill-font-500 h-6 w-6" + data-testid={`${modelName}-open`} + onClick={onOpenClick} /> </div> </div> diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx index f37efc2cedc5f130a0c3647433411cb167dcc778..884d1a17d2bbb9764694c9d779b6921385b015d8 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx @@ -2,10 +2,11 @@ import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { StoreType } from '@/redux/store'; import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; import { openedDrawerSubmapsFixture } from '@/redux/drawer/drawerFixture'; +import { initialMapDataFixture } from '@/redux/map/map.fixtures'; import { SubmapsDrawer } from './SubmapsDrawer'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -48,4 +49,45 @@ describe('SubmapsDrawer - component', () => { expect(isOpen).toBe(false); }); + it("should open submap and set it to active if it's not already opened", async () => { + const { store } = renderComponent({ + models: { data: MODELS_MOCK_SHORT, loading: 'succeeded', error: { name: '', message: '' } }, + map: { data: initialMapDataFixture, loading: 'succeeded', error: { name: '', message: '' } }, + }); + + const { + data: { modelId, openedMaps }, + } = store.getState().map; + + // eslint-disable-next-line no-magic-numbers + expect(modelId).toBe(0); + expect(openedMaps).not.toContainEqual({ + modelId: 5052, + modelName: 'Histamine signaling', + lastPosition: { x: 0, y: 0, z: 0 }, + }); + + const openHistamineMapButton = screen.getByTestId('Histamine signaling-open'); + await act(() => { + openHistamineMapButton.click(); + }); + + const { + data: { modelId: newModelId, openedMaps: newOpenedMaps }, + } = store.getState().map; + + expect(newOpenedMaps).toContainEqual({ + modelId: 5052, + modelName: 'Histamine signaling', + lastPosition: { x: 0, y: 0, z: 0 }, + }); + // eslint-disable-next-line no-magic-numbers + expect(newModelId).toBe(5052); + }); + it.skip("should set map active if it's already opened", () => { + // const { store } = renderComponent({ + // models: { data: MODELS_MOCK_SHORT, loading: 'succeeded', error: { name: '', message: '' } }, + // map: { data: initialMapDataFixture, loading: 'succeeded', error: { name: '', message: '' } }, + // }); + }); }); diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.tsx index 35af92d1994ef36b6de85fe734667d7344f88b75..a3b4bc1bc9e63c724abbe954915846275793bbc0 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.tsx @@ -1,17 +1,38 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { modelsDataSelector } from '@/redux/models/models.selectors'; import { DrawerHeading } from '@/shared/DrawerHeading'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; +import { MapModel } from '@/types/models'; +import { mapOpenedMapsSelector } from '@/redux/map/map.selectors'; import { SubmpamItem } from './SubmapItem/SubmapItem.component'; export const SubmapsDrawer = (): JSX.Element => { const models = useAppSelector(modelsDataSelector); + const openedMaps = useAppSelector(mapOpenedMapsSelector); + const dispatch = useAppDispatch(); + + const isMapAlreadyOpened = (modelId: number): boolean => + openedMaps.some(map => map.modelId === modelId); + + const onSubmapOpenClick = (model: MapModel): void => { + if (isMapAlreadyOpened(model.idObject)) { + dispatch(setActiveMap({ modelId: model.idObject })); + } else { + dispatch(openMapAndSetActive({ modelId: model.idObject, modelName: model.name })); + } + }; return ( <div data-testid="submap-drawer" className="h-full max-h-full"> <DrawerHeading title="Submaps" /> <ul className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto px-6"> {models.map(model => ( - <SubmpamItem key={model.idObject} modelName={model.name} /> + <SubmpamItem + key={model.idObject} + modelName={model.name} + onOpenClick={(): void => onSubmapOpenClick(model)} + /> ))} </ul> </div> diff --git a/src/models/mapOverlay.ts b/src/models/mapOverlay.ts index b76cd45abde2b9bf1c4c3314040ab37e4e48a610..a22b65aa5eed7751bc3033c30998972dadadd9c5 100644 --- a/src/models/mapOverlay.ts +++ b/src/models/mapOverlay.ts @@ -5,8 +5,8 @@ export const mapOverlay = z.object({ googleLicenseConsent: z.boolean(), creator: z.string(), description: z.string(), - genomeType: z.null(), - genomeVersion: z.null(), + genomeType: z.string().nullable(), + genomeVersion: z.string().nullable(), idObject: z.number(), publicOverlay: z.boolean(), type: z.string(), diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts index ba88a03811be7e0e0d08fe58bb13ceaff6f51d65..eaaaa21c64a98790b4700bbb41f65b1470881477 100644 --- a/src/redux/map/map.constants.ts +++ b/src/redux/map/map.constants.ts @@ -7,6 +7,8 @@ import { } from '@/constants/map'; import { MapData } from './map.types'; +export const MAIN_MAP = 'Main map'; + export const MAP_DATA_INITIAL_STATE: MapData = { projectId: PROJECT_ID, meshId: '', @@ -25,6 +27,7 @@ export const MAP_DATA_INITIAL_STATE: MapData = { minZoom: DEFAULT_MIN_ZOOM, maxZoom: DEFAULT_MAX_ZOOM, }, + openedMaps: [{ modelId: 0, modelName: MAIN_MAP, lastPosition: { x: 0, y: 0, z: 0 } }], }; export const MIDDLEWARE_ALLOWED_ACTIONS: string[] = ['map/setMapData', 'map/initMapData']; diff --git a/src/redux/map/map.fixtures.ts b/src/redux/map/map.fixtures.ts new file mode 100644 index 0000000000000000000000000000000000000000..eeed2745fbe9425a1fb12deae1e723675cb2958f --- /dev/null +++ b/src/redux/map/map.fixtures.ts @@ -0,0 +1,22 @@ +import { MapData } from './map.types'; + +export const initialMapDataFixture: MapData = { + projectId: 'pdmap', + meshId: '', + modelId: 0, + backgroundId: 0, + overlaysIds: [], + position: { x: 0, y: 0, z: 5 }, + show: { + legend: false, + comments: false, + }, + size: { + width: 0, + height: 0, + tileSize: 256, + minZoom: 2, + maxZoom: 9, + }, + openedMaps: [{ modelId: 0, modelName: 'Main map', lastPosition: { x: 0, y: 0, z: 0 } }], +}; diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts index 962704b36070701d2e763147d57f2f99c8751ae8..c21175ff91c94e9bb9e279d49f28174b50b888e4 100644 --- a/src/redux/map/map.reducers.ts +++ b/src/redux/map/map.reducers.ts @@ -1,6 +1,13 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { initMapData } from './map.thunks'; -import { MapState, SetMapDataAction } from './map.types'; +import { + CloseMapAction, + MapState, + OpenMapAndSetActiveAction, + SetActiveMapAction, + SetMapDataAction, +} from './map.types'; +import { MAIN_MAP } from './map.constants'; export const setMapDataReducer = (state: MapState, action: SetMapDataAction): void => { state.data = { ...state.data, ...action.payload }; @@ -20,3 +27,37 @@ export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void // TODO to discuss manage state of failure }); }; + +export const setActiveMapReducer = (state: MapState, action: SetActiveMapAction): void => { + state.data.modelId = action.payload.modelId; +}; + +export const openMapAndSetActiveReducer = ( + state: MapState, + action: OpenMapAndSetActiveAction, +): void => { + state.data.openedMaps.push({ + modelId: action.payload.modelId, + modelName: action.payload.modelName, + lastPosition: { x: 0, y: 0, z: 0 }, + }); + state.data.modelId = action.payload.modelId; +}; + +export const closeMapReducer = (state: MapState, action: CloseMapAction): void => { + state.data.openedMaps = state.data.openedMaps.filter( + openedMap => openedMap.modelId !== action.payload.modelId, + ); +}; + +export const closeMapAndSetMainMapActiveReducer = ( + state: MapState, + action: CloseMapAction, +): void => { + state.data.openedMaps = state.data.openedMaps.filter( + openedMap => openedMap.modelId !== action.payload.modelId, + ); + state.data.modelId = + // eslint-disable-next-line no-magic-numbers + state.data.openedMaps.find(openedMap => openedMap.modelName === MAIN_MAP)?.modelId || 0; +}; diff --git a/src/redux/map/map.selectors.ts b/src/redux/map/map.selectors.ts index e5bbcfe7dc366b0154edc21775477bb38be82973..c1cb1b43373bb87c154a8464a3c9a5bc951809c4 100644 --- a/src/redux/map/map.selectors.ts +++ b/src/redux/map/map.selectors.ts @@ -6,3 +6,7 @@ export const mapDataSelector = createSelector(rootSelector, state => state.map.d export const mapDataSizeSelector = createSelector(mapDataSelector, map => map.size); export const mapDataPositionSelector = createSelector(mapDataSelector, map => map.position); + +export const mapOpenedMapsSelector = createSelector(mapDataSelector, map => map.openedMaps); + +export const mapModelIdSelector = createSelector(mapDataSelector, map => map.modelId); diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts index 3565dc2500f9c45182b563f636f16fdf5b47f4a9..7f8864d499eb8a9b5faa3596fe1b6059b06c7a9e 100644 --- a/src/redux/map/map.slice.ts +++ b/src/redux/map/map.slice.ts @@ -1,6 +1,13 @@ import { createSlice } from '@reduxjs/toolkit'; import { MAP_DATA_INITIAL_STATE } from './map.constants'; -import { getMapReducers, setMapDataReducer } from './map.reducers'; +import { + closeMapAndSetMainMapActiveReducer, + closeMapReducer, + getMapReducers, + openMapAndSetActiveReducer, + setActiveMapReducer, + setMapDataReducer, +} from './map.reducers'; import { MapState } from './map.types'; const initialState: MapState = { @@ -14,12 +21,22 @@ const mapSlice = createSlice({ initialState, reducers: { setMapData: setMapDataReducer, + setActiveMap: setActiveMapReducer, + openMapAndSetActive: openMapAndSetActiveReducer, + closeMap: closeMapReducer, + closeMapAndSetMainMapActive: closeMapAndSetMainMapActiveReducer, }, extraReducers: builder => { getMapReducers(builder); }, }); -export const { setMapData } = mapSlice.actions; +export const { + setMapData, + setActiveMap, + openMapAndSetActive, + closeMap, + closeMapAndSetMainMapActive, +} = mapSlice.actions; export default mapSlice.reducer; diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts index 7051691ed57d56fe19e934c18e8fa020a9f9246e..159388d3c06cef95dd2fe7bae558542570b69736 100644 --- a/src/redux/map/map.types.ts +++ b/src/redux/map/map.types.ts @@ -10,6 +10,12 @@ export interface MapSize { maxZoom: number; } +export type OppenedMap = { + modelId: number; + modelName: string; + lastPosition: Point; +}; + export type MapData = { projectId: string; meshId: string; @@ -22,6 +28,7 @@ export type MapData = { legend: boolean; comments: boolean; }; + openedMaps: OppenedMap[]; }; export type MapState = FetchDataState<MapData, MapData>; @@ -30,6 +37,20 @@ export type SetMapDataActionPayload = Partial<MapData> | undefined; export type SetMapDataAction = PayloadAction<SetMapDataActionPayload>; +export type SetActiveMapActionPayload = Pick<OppenedMap, 'modelId'>; + +export type SetActiveMapAction = PayloadAction<SetActiveMapActionPayload>; + +export type SetMainMapModelIdAction = PayloadAction<SetActiveMapActionPayload>; + +export type OpenMapAndSetActivePayload = Pick<OppenedMap, 'modelId' | 'modelName'>; + +export type OpenMapAndSetActiveAction = PayloadAction<OpenMapAndSetActivePayload>; + +export type CloseMapActionPayload = Pick<OppenedMap, 'modelId'>; + +export type CloseMapAction = PayloadAction<CloseMapActionPayload>; + export type InitMapDataActionPayload = { modelId: number; backgroundId: number } | object; export type InitMapDataAction = PayloadAction<SetMapDataAction>;