Skip to content
Snippets Groups Projects
Commit e1eda2b8 authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

feat(submaps tabs): open submap on click

parent 9515488f
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!47feat(submaps tabs): open submap on click
Pipeline #80602 failed
Showing
with 248 additions and 17 deletions
......@@ -9,4 +9,4 @@ const config = {
tabWidth: 2,
};
module.exports = config;
module.exports = config;
\ No newline at end of file
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);
});
});
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>
);
};
......@@ -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>
......
......@@ -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: '' } },
// });
});
});
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>
......
......@@ -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(),
......
......@@ -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'];
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 } }],
};
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;
};
......@@ -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);
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;
......@@ -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>;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment