Skip to content
Snippets Groups Projects
Commit 820ea6d3 authored by Piotr Gawron's avatar Piotr Gawron
Browse files

Merge branch '327-export-graphics-add-current-view' into 'development'

Resolve "Export graphics - add "Current view""

Closes #327

See merge request !352
parents 0b9489fd 186ea0c7
No related branches found
No related tags found
1 merge request!352Resolve "Export graphics - add "Current view""
Pipeline #99564 passed
Showing
with 82 additions and 97 deletions
......@@ -5,7 +5,7 @@ minerva-front (19.0.0~alpha.0) stable; urgency=medium
* Feature: allow plugin to access info about opened panel (#309)
* Feature: allow plugin to hide opened panel (#309)
* Feature: allow plugin to open left panel (#309)
* Feature: allow to export current view for graphics export (#327)
* Feature: allow to export current view (#327)
* Small improvement: reaction element annotations use the same styling as
elements (#187)
* Small improvement: styling of annotation links improved (#186)
......
import { Export } from '../ExportCompound';
export const CurrentView = (): React.ReactNode => {
return (
<div data-testid="current-view-tab">
<Export>
<Export.ImageFormat />
<Export.DownloadCurrentView />
</Export>
</div>
);
};
import { useContext } from 'react';
import { ExportContext } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/ExportCompound.context';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { currentModelIdSelector, currentModelNameSelector } from '@/redux/models/models.selectors';
export const CurrentView = (): React.ReactNode => {
const { setCurrentView, data, setModels } = useContext(ExportContext);
const currentMapModelId = useAppSelector(currentModelIdSelector);
const currentMapModelName = useAppSelector(currentModelNameSelector);
return (
<div className="flex flex-col gap-4 border-b">
<label className="flex h-9 items-center gap-4">
<span>Current view:&nbsp;</span>
<input
className="rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none hover:border-greyscale-600 focus:border-greyscale-600"
name="width"
checked={data.currentView.value}
type="checkbox"
aria-label="export graphics width input"
onChange={(e): void => {
setCurrentView({ value: e.target.checked });
if (e.target.checked) {
setModels([
{
id: `${currentMapModelId}`,
label: currentMapModelName,
},
]);
}
}}
/>
</label>
</div>
);
};
import { CurrentView } from './CurrentView.types';
export const DEFAULT_CURRENT_VIEW: CurrentView = {
value: false,
};
export interface CurrentView {
value: boolean;
}
import { Button } from '@/shared/Button';
import { useContext } from 'react';
import { ExportContext } from '../ExportCompound.context';
export const DownloadCurrentView = (): React.ReactNode => {
const { handleDownloadCurrentView } = useContext(ExportContext);
return (
<div className="mt-6">
<Button onClick={handleDownloadCurrentView}>Download</Button>
</div>
);
};
export { DownloadCurrentView } from './DownloadCurrentView.component';
......@@ -3,11 +3,15 @@ import { currentBackgroundSelector } from '@/redux/backgrounds/background.select
import { downloadElements, downloadNetwork } from '@/redux/export/export.thunks';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsDataSelector, modelsIdsSelector } from '@/redux/models/models.selectors';
import {
currentModelIdSelector,
modelsDataSelector,
modelsIdsSelector,
} from '@/redux/models/models.selectors';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { activeOverlaysIdSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { CurrentView } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/CurrentView';
import { DEFAULT_CURRENT_VIEW } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/CurrentView/CurrentView.constants';
import { DownloadCurrentView } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/DownloadCurrentView/DownloadCurrentView.component';
import { getZoom } from '@/services/pluginsManager/map/zoom/getZoom';
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.types';
import { Annotations } from './Annotations';
import { DownloadElements } from './DownloadElements/DownloadElements';
......@@ -20,7 +24,6 @@ import { ImageFormat } from './ImageFormat';
import { ImageSize } from './ImageSize';
import { DEFAULT_IMAGE_SIZE } from './ImageSize/ImageSize.constants';
import { ImageSize as ImageSizeType } from './ImageSize/ImageSize.types';
import { CurrentView as CurrentViewType } from './CurrentView/CurrentView.types';
import { IncludedCompartmentPathways } from './IncludedCompartmentPathways ';
import { Submap } from './Submap';
import { getDownloadElementsBodyRequest } from './utils/getDownloadElementsBodyRequest';
......@@ -35,6 +38,8 @@ type ExportProps = {
export const Export = ({ children }: ExportProps): JSX.Element => {
const dispatch = useAppDispatch();
const modelIds = useAppSelector(modelsIdsSelector);
const selectedModelId = useAppSelector(currentModelIdSelector);
const currentModels = useAppSelector(modelsDataSelector);
const currentBackground = useAppSelector(currentBackgroundSelector);
const overlays = useAppSelector(activeOverlaysIdSelector);
......@@ -48,7 +53,6 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
const [models, setModels] = useState<CheckboxItem[]>([]);
const [imageSize, setImageSize] = useState<ImageSizeType>(DEFAULT_IMAGE_SIZE);
const [imageFormats, setImageFormats] = useState<CheckboxItem[]>([]);
const [currentView, setCurrentView] = useState<CurrentViewType>(DEFAULT_CURRENT_VIEW);
const handleDownloadElements = useCallback(async () => {
const body = getDownloadElementsBodyRequest({
......@@ -83,7 +87,7 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
handler: imageFormats?.[FIRST_ARRAY_ELEMENT]?.id,
zoom: getModelExportZoom(imageSize.width, model),
overlayIds: overlays.map(overlayId => `${overlayId}`),
currentView: currentView.value,
currentView: false,
});
if (url) {
......@@ -91,6 +95,21 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
}
}, [models, imageFormats, currentBackground, currentModels, imageSize.width]);
const handleDownloadCurrentView = useCallback(async () => {
const url = getGraphicsDownloadUrl({
backgroundId: currentBackground?.id,
modelId: `${selectedModelId}`,
handler: imageFormats?.[FIRST_ARRAY_ELEMENT]?.id,
zoom: getZoom(),
overlayIds: overlays.map(overlayId => `${overlayId}`),
currentView: true,
});
if (url) {
window.open(url);
}
}, [selectedModelId, imageFormats, currentBackground]);
const globalContextDataValue = useMemo(
() => ({
annotations,
......@@ -99,7 +118,6 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
models,
imageSize,
imageFormats,
currentView,
}),
[
annotations,
......@@ -108,7 +126,6 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
models,
imageSize,
imageFormats,
currentView,
],
);
......@@ -120,10 +137,10 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
setModels,
setImageSize,
setImageFormats,
setCurrentView,
handleDownloadElements,
handleDownloadNetwork,
handleDownloadGraphics,
handleDownloadCurrentView,
data: globalContextDataValue,
}),
[handleDownloadElements, handleDownloadNetwork, globalContextDataValue, handleDownloadGraphics],
......@@ -141,4 +158,4 @@ Export.ImageSize = ImageSize;
Export.ImageFormat = ImageFormat;
Export.DownloadNetwork = DownloadNetwork;
Export.DownloadGraphics = DownloadGraphics;
Export.CurrentView = CurrentView;
Export.DownloadCurrentView = DownloadCurrentView;
import { DEFAULT_CURRENT_VIEW } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/CurrentView/CurrentView.constants';
import { ExportContextType } from './ExportCompound.types';
import { DEFAULT_IMAGE_SIZE } from './ImageSize/ImageSize.constants';
......@@ -55,10 +54,10 @@ export const EXPORT_CONTEXT_DEFAULT_VALUE: ExportContextType = {
setModels: () => {},
setImageSize: () => {},
setImageFormats: () => {},
setCurrentView: () => {},
handleDownloadElements: () => Promise.resolve(),
handleDownloadNetwork: () => Promise.resolve(),
handleDownloadGraphics: () => {},
handleDownloadCurrentView: () => {},
data: {
annotations: [],
includedCompartmentPathways: [],
......@@ -66,6 +65,5 @@ export const EXPORT_CONTEXT_DEFAULT_VALUE: ExportContextType = {
models: [],
imageFormats: [],
imageSize: DEFAULT_IMAGE_SIZE,
currentView: DEFAULT_CURRENT_VIEW,
},
};
import { CurrentView } from '@/components/Map/Drawer/ExportDrawer/ExportCompound/CurrentView/CurrentView.types';
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.types';
import { ImageSize } from './ImageSize/ImageSize.types';
......@@ -7,18 +6,17 @@ export type ExportContextType = {
setIncludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setExcludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setModels: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setCurrentView: React.Dispatch<React.SetStateAction<CurrentView>>;
setImageSize: React.Dispatch<React.SetStateAction<ImageSize>>;
setImageFormats: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
handleDownloadElements: () => Promise<void>;
handleDownloadNetwork: () => Promise<void>;
handleDownloadGraphics: () => void;
handleDownloadCurrentView: () => void;
data: {
annotations: CheckboxItem[];
includedCompartmentPathways: CheckboxItem[];
excludedCompartmentPathways: CheckboxItem[];
models: CheckboxItem[];
currentView: CurrentView;
imageSize: ImageSize;
imageFormats: CheckboxItem[];
};
......
import { MapModel } from '@/types/models';
import { numberToSafeInt } from '@/utils/number/numberToInt';
import { useCallback, useContext, useEffect } from 'react';
import {
getScreenAspectRatios,
getScreenDimension,
} from '@/components/Map/Drawer/ExportDrawer/ExportCompound/ImageSize/utils/useScreenAspectRatios';
import { ExportContext } from '../../ExportCompound.context';
import { DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '../ImageSize.constants';
import { ImageSize, ModelAspectRatios } from '../ImageSize.types';
......@@ -20,14 +16,9 @@ interface UseImageSizeResults {
export const useImageSize = (): UseImageSizeResults => {
const selectedModel = useExportGraphicsSelectedModel();
const { data, setImageSize, setCurrentView } = useContext(ExportContext);
const { imageSize, currentView } = data;
let aspectRatios: ModelAspectRatios;
if (currentView.value) {
aspectRatios = getScreenAspectRatios();
} else {
aspectRatios = getModelAspectRatios(selectedModel);
}
const { data, setImageSize } = useContext(ExportContext);
const { imageSize } = data;
const aspectRatios: ModelAspectRatios = getModelAspectRatios(selectedModel);
const maxWidth = selectedModel?.width || DEFAULT_IMAGE_WIDTH;
const maxHeight = selectedModel?.height || DEFAULT_IMAGE_HEIGHT;
......@@ -37,8 +28,8 @@ export const useImageSize = (): UseImageSizeResults => {
const newWidth = newImageSize.width;
const newHeight = newImageSize.height;
const widthMinMax = currentView.value ? newWidth : Math.min(maxWidth, newWidth);
const heightMinMax = currentView.value ? newHeight : Math.min(maxHeight, newHeight);
const widthMinMax = Math.min(maxWidth, newWidth);
const heightMinMax = Math.min(maxHeight, newHeight);
const widthInt = numberToSafeInt(widthMinMax);
const heightInt = numberToSafeInt(heightMinMax);
......@@ -53,9 +44,7 @@ export const useImageSize = (): UseImageSizeResults => {
const setDefaultModelImageSize = useCallback(
(model: MapModel): void => {
const screenDimension = getScreenDimension();
const width = currentView.value ? screenDimension.width : model.width;
const height = currentView.value ? screenDimension.height : model.height;
const { width, height } = model;
const newImageSize = getNormalizedImageSize({
width,
height,
......@@ -63,7 +52,7 @@ export const useImageSize = (): UseImageSizeResults => {
setImageSize(newImageSize);
},
[getNormalizedImageSize, setImageSize, setCurrentView, currentView],
[getNormalizedImageSize, setImageSize],
);
const handleChangeWidth = (width: number): void => {
......@@ -90,7 +79,7 @@ export const useImageSize = (): UseImageSizeResults => {
}
setDefaultModelImageSize(selectedModel);
}, [setDefaultModelImageSize, selectedModel, setCurrentView, currentView]);
}, [setDefaultModelImageSize, selectedModel]);
return {
handleChangeWidth,
......
......@@ -30,7 +30,7 @@ describe('ExportDrawer - component', () => {
expect(screen.getByText('Export')).toBeInTheDocument();
Object.keys(TAB_NAMES).forEach(label => {
Object.values(TAB_NAMES).forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
......
......@@ -4,6 +4,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsIdsSelector } from '@/redux/models/models.selectors';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useEffect, useState } from 'react';
import { CurrentView } from './CurrentView';
import { Elements } from './Elements';
import { Graphics } from './Graphics';
import { Network } from './Network';
......@@ -32,6 +33,7 @@ export const ExportDrawer = (): React.ReactNode => {
{activeTab === TAB_NAMES.ELEMENTS && <Elements />}
{activeTab === TAB_NAMES.NETWORK && <Network />}
{activeTab === TAB_NAMES.GRAPHICS && <Graphics />}
{activeTab === TAB_NAMES.CURRENT_VIEW && <CurrentView />}
</div>
</div>
);
......
......@@ -4,7 +4,6 @@ export const Graphics = (): React.ReactNode => {
return (
<div data-testid="graphics-tab">
<Export>
<Export.CurrentView />
<Export.Submap />
<Export.ImageSize />
<Export.ImageFormat />
......
......@@ -9,27 +9,27 @@ describe('TabNavigator - component', () => {
mockOnTabChange.mockReset();
});
it('should render TabNavigator with correct tabs', () => {
render(<TabNavigator activeTab="elements" onTabChange={mockOnTabChange} />);
render(<TabNavigator activeTab="ELEMENTS" onTabChange={mockOnTabChange} />);
Object.keys(TAB_NAMES).forEach(label => {
Object.values(TAB_NAMES).forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
it('should change tabs correctly', () => {
render(<TabNavigator activeTab="elements" onTabChange={mockOnTabChange} />);
render(<TabNavigator activeTab="ELEMENTS" onTabChange={mockOnTabChange} />);
fireEvent.click(screen.getByText(/network/i));
expect(mockOnTabChange).toHaveBeenCalledWith('network');
fireEvent.click(screen.getByText(/NETWORK/i));
expect(mockOnTabChange).toHaveBeenCalledWith('NETWORK');
fireEvent.click(screen.getByText(/graphics/i));
expect(mockOnTabChange).toHaveBeenCalledWith('graphics');
fireEvent.click(screen.getByText(/GRAPHICS/i));
expect(mockOnTabChange).toHaveBeenCalledWith('GRAPHICS');
});
it('should set initial active tab', () => {
render(<TabNavigator activeTab="network" onTabChange={mockOnTabChange} />);
render(<TabNavigator activeTab="NETWORK" onTabChange={mockOnTabChange} />);
const currentTab = screen.getByRole('button', { current: true });
const networkTab = screen.getByText(/network/i);
const networkTab = screen.getByText(/NETWORK/i);
expect(currentTab).toBe(networkTab);
});
......
......@@ -9,11 +9,11 @@ type TabNavigatorProps = {
export const TabNavigator = ({ activeTab, onTabChange }: TabNavigatorProps): React.ReactNode => (
<div className="flex gap-5">
{Object.entries(TAB_NAMES).map(([label, tabName]) => (
{Object.entries(TAB_NAMES).map(([, tabName]) => (
<TabButton
key={tabName}
handleChangeTab={(): void => onTabChange(tabName)}
label={label}
label={tabName}
active={activeTab === tabName}
/>
))}
......
export const TAB_NAMES = {
ELEMENTS: 'elements',
NETWORK: 'network',
GRAPHICS: 'graphics',
ELEMENTS: 'ELEMENTS',
NETWORK: 'NETWORK',
GRAPHICS: 'GRAPHICS',
CURRENT_VIEW: 'CURRENT VIEW',
} as const;
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