feat(SidePanel): new side panel tab look-and-feel (#3657)
This commit is contained in:
parent
48bbd6281a
commit
85c899b399
@ -45,10 +45,10 @@ module.exports = (env, argv) => {
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
// new MiniCssExtractPlugin({
|
||||
// filename: `./dist/${outputName}.css`,
|
||||
// chunkFilename: `./dist/${outputName}.css`,
|
||||
// }),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
60
extensions/default/src/Components/SidePanelWithServices.tsx
Normal file
60
extensions/default/src/Components/SidePanelWithServices.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SidePanel } from '@ohif/ui';
|
||||
import { PanelService, ServicesManager } from '@ohif/core';
|
||||
|
||||
export type SidePanelWithServicesProps = {
|
||||
servicesManager: ServicesManager;
|
||||
side: 'left' | 'right';
|
||||
className: string;
|
||||
activeTabIndex: number;
|
||||
tabs: any;
|
||||
};
|
||||
|
||||
const SidePanelWithServices = ({
|
||||
servicesManager,
|
||||
side,
|
||||
className,
|
||||
activeTabIndex: activeTabIndexProp,
|
||||
tabs,
|
||||
}) => {
|
||||
const panelService: PanelService = servicesManager?.services?.panelService;
|
||||
|
||||
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelService) {
|
||||
const activatePanelSubscription = panelService.subscribe(
|
||||
panelService.EVENTS.ACTIVATE_PANEL,
|
||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||
if (tabIndex !== -1) {
|
||||
setActiveTabIndex(tabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
activatePanelSubscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
}, [tabs, hasBeenOpened, panelService]);
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
side={side}
|
||||
className={className}
|
||||
activeTabIndex={activeTabIndex}
|
||||
tabs={tabs}
|
||||
onOpen={() => {
|
||||
setHasBeenOpened(true);
|
||||
}}
|
||||
></SidePanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidePanelWithServices;
|
||||
@ -5,6 +5,7 @@ import { SidePanel, ErrorBoundary, LoadingIndicatorProgress } from '@ohif/ui';
|
||||
import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import ViewerHeader from './ViewerHeader';
|
||||
import SidePanelWithServices from '../Components/SidePanelWithServices';
|
||||
|
||||
function ViewerLayout({
|
||||
// From Extension Module Params
|
||||
@ -119,7 +120,7 @@ function ViewerLayout({
|
||||
{/* LEFT SIDEPANELS */}
|
||||
{leftPanelComponents.length ? (
|
||||
<ErrorBoundary context="Left Panel">
|
||||
<SidePanel
|
||||
<SidePanelWithServices
|
||||
side="left"
|
||||
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
||||
tabs={leftPanelComponents}
|
||||
@ -141,7 +142,7 @@ function ViewerLayout({
|
||||
</div>
|
||||
{rightPanelComponents.length ? (
|
||||
<ErrorBoundary context="Right Panel">
|
||||
<SidePanel
|
||||
<SidePanelWithServices
|
||||
side="right"
|
||||
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
||||
tabs={rightPanelComponents}
|
||||
|
||||
@ -20,7 +20,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
return [
|
||||
{
|
||||
name: 'seriesList',
|
||||
iconName: 'group-layers',
|
||||
iconName: 'tab-studies',
|
||||
iconLabel: 'Studies',
|
||||
label: 'Studies',
|
||||
component: WrappedPanelStudyBrowser.bind(null, {
|
||||
|
||||
@ -9,7 +9,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
|
||||
return [
|
||||
{
|
||||
name: 'seriesList',
|
||||
iconName: 'group-layers',
|
||||
iconName: 'tab-studies',
|
||||
iconLabel: 'Studies',
|
||||
label: 'Studies',
|
||||
component: PanelStudyBrowserTracking.bind(null, {
|
||||
|
||||
@ -13,8 +13,12 @@ describe('OHIF Multi Study', () => {
|
||||
it('Should display 2 comparison up', () => {
|
||||
beforeSetup();
|
||||
|
||||
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4);
|
||||
cy.get('[data-cy="studyDate"]').should(studyDate => {
|
||||
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
|
||||
cy.get('@viewportPane').its('length').should('be.eq', 4);
|
||||
|
||||
cy.get('[data-cy="studyDate"]').as('studyDate');
|
||||
|
||||
cy.get('@studyDate').should(studyDate => {
|
||||
expect(studyDate.length).to.be.eq(4);
|
||||
expect(studyDate.text()).to.contain('2014').contain('2001');
|
||||
expect(studyDate.text().indexOf('2014')).to.be.lessThan(studyDate.text().indexOf('2001'));
|
||||
|
||||
@ -23,26 +23,28 @@ describe('OHIF Measurement Panel', function () {
|
||||
it('checks if measurement item can be Relabeled under Measurements panel', function () {
|
||||
// Add length measurement
|
||||
cy.addLengthMeasurement();
|
||||
cy.get('[data-cy="viewport-notification"]').should('exist');
|
||||
cy.get('[data-cy="viewport-notification"]').should('be.visible');
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click();
|
||||
cy.get('[data-cy="measurement-item"]').click();
|
||||
cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('exist');
|
||||
cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('be.visible');
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').find('svg').click();
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn').click();
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').as('measurementItem').click();
|
||||
|
||||
cy.get('[data-cy="measurement-item"]').find('svg').as('measurementItemSvg').click();
|
||||
|
||||
// enter Bone label
|
||||
cy.get('[data-cy="input-annotation"]').should('exist');
|
||||
cy.get('[data-cy="input-annotation"]').should('be.visible');
|
||||
cy.get('[data-cy="input-annotation"]').type('Bone{enter}');
|
||||
|
||||
// Verify if 'Bone' label was added
|
||||
cy.get('[data-cy="measurement-item"]').should('contain.text', 'Bone');
|
||||
cy.get('[data-cy="measurement-item"]').as('measurementItem').should('contain.text', 'Bone');
|
||||
});
|
||||
|
||||
it('checks if image would jump when clicked on a measurement item', function () {
|
||||
// Add length measurement
|
||||
cy.addLengthMeasurement();
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click();
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn');
|
||||
cy.get('@promptBeginTrackingYesBtn').click();
|
||||
|
||||
cy.scrollToIndex(13);
|
||||
|
||||
|
||||
@ -144,19 +144,22 @@ Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) =>
|
||||
* @param {number[]} secondClick - Click position [x, y]
|
||||
*/
|
||||
Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => {
|
||||
const performClick = (alias, x, y) => {
|
||||
cy.get(alias).click(x, y, { force: true, multiple: true }).wait(250);
|
||||
};
|
||||
|
||||
cy.get(viewport).as('viewportAlias');
|
||||
const [x1, y1] = firstClick;
|
||||
const [x2, y2] = secondClick;
|
||||
|
||||
// First click
|
||||
cy.get('@viewportAlias').click(x1, y1, { force: true, multiple: true }).wait(250);
|
||||
performClick('@viewportAlias', x1, y1);
|
||||
|
||||
// Move the mouse and then click again
|
||||
cy.get('@viewportAlias')
|
||||
.trigger('mousemove', { clientX: x2, clientY: y2 })
|
||||
.get('@viewportAlias')
|
||||
.click(x2, y2, { force: true, multiple: true })
|
||||
.wait(250);
|
||||
// Move the mouse
|
||||
cy.get('@viewportAlias').trigger('mousemove', { clientX: x2, clientY: y2 }).wait(250);
|
||||
|
||||
// Second click
|
||||
performClick('@viewportAlias', x2, y2);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
14
platform/ui/src/assets/icons/side-panel-close-left.svg
Normal file
14
platform/ui/src/assets/icons/side-panel-close-left.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-panel-close-left</title>
|
||||
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-370, -60)">
|
||||
<g id="icon-panel-close-left" transform="translate(380, 70) scale(-1, 1) translate(-380, -70)translate(370, 60)">
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
<line x1="12.8336364" y1="10.4061473" x2="3" y2="10.4061473" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(7.9168, 10.4061) scale(-1, 1) translate(-7.9168, -10.4061)"></line>
|
||||
<polyline id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(10.7991, 10.4061) scale(-1, 1) translate(-10.7991, -10.4061)" points="12.8336364 6.33705636 8.76454545 10.4061473 12.8336364 14.4752382"></polyline>
|
||||
<line x1="16.2418182" y1="14.4752382" x2="16.2418182" y2="6.33705636" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(16.2418, 10.4061) scale(-1, 1) translate(-16.2418, -10.4061)"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
14
platform/ui/src/assets/icons/side-panel-close-right.svg
Normal file
14
platform/ui/src/assets/icons/side-panel-close-right.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-panel-close-right</title>
|
||||
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-418, -60)">
|
||||
<g id="icon-panel-close-right" transform="translate(418, 60)">
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
<line x1="12.8336364" y1="10.4061473" x2="3" y2="10.4061473" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(7.9168, 10.4061) scale(-1, 1) translate(-7.9168, -10.4061)"></line>
|
||||
<polyline id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(10.7991, 10.4061) scale(-1, 1) translate(-10.7991, -10.4061)" points="12.8336364 6.33705636 8.76454545 10.4061473 12.8336364 14.4752382"></polyline>
|
||||
<line x1="16.2418182" y1="14.4752382" x2="16.2418182" y2="6.33705636" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(16.2418, 10.4061) scale(-1, 1) translate(-16.2418, -10.4061)"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
13
platform/ui/src/assets/icons/tab-studies.svg
Normal file
13
platform/ui/src/assets/icons/tab-studies.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="22px" height="23px" viewBox="0 0 22 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>tab-studies</title>
|
||||
<g id="Artboards-Updated" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-61, -242)">
|
||||
<g id="tab-studies" transform="translate(61, 242.5)">
|
||||
<rect id="Rectangle" x="0" y="0" width="22" height="22"></rect>
|
||||
<path d="M6.93478261,15.0652174 L3.23913043,15.0652174 C2.83091997,15.0652174 2.5,14.7342974 2.5,14.326087 L2.5,3.23913043 C2.5,2.83091997 2.83091997,2.5 3.23913043,2.5 L14.326087,2.5 C14.7342974,2.5 15.0652174,2.83091997 15.0652174,3.23913043 L15.0652174,6.93478261" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<rect id="Rectangle" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" x="6.93478261" y="6.93478261" width="12.5652174" height="12.5652174" rx="1"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -39,6 +39,8 @@ import profile from './../../assets/icons/profile.svg';
|
||||
import pushLeft from './../../assets/icons/push-left.svg';
|
||||
import pushRight from './../../assets/icons/push-right.svg';
|
||||
import settings from './../../assets/icons/settings.svg';
|
||||
import sidePanelCloseLeft from './../../assets/icons/side-panel-close-left.svg';
|
||||
import sidePanelCloseRight from './../../assets/icons/side-panel-close-right.svg';
|
||||
import sorting from './../../assets/icons/sorting.svg';
|
||||
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
|
||||
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
|
||||
@ -71,6 +73,7 @@ import tabLinear from './../../assets/icons/tab-linear.svg';
|
||||
import tabPatientInfo from './../../assets/icons/tab-patient-info.svg';
|
||||
import tabROIThreshold from './../../assets/icons/tab-roi-threshold.svg';
|
||||
import tabSegmentation from './../../assets/icons/tab-segmentation.svg';
|
||||
import tabStudies from './../../assets/icons/tab-studies.svg';
|
||||
import uiArrowDown from './../../assets/icons/ui-arrow-down.svg';
|
||||
import uiArrowUp from './../../assets/icons/ui-arrow-up.svg';
|
||||
import uiArrowLeft from './../../assets/icons/ui-arrow-left.svg';
|
||||
@ -189,6 +192,8 @@ const ICONS = {
|
||||
'push-left': pushLeft,
|
||||
'push-right': pushRight,
|
||||
settings: settings,
|
||||
'side-panel-close-left': sidePanelCloseLeft,
|
||||
'side-panel-close-right': sidePanelCloseRight,
|
||||
'sorting-active-down': sortingActiveDown,
|
||||
'sorting-active-up': sortingActiveUp,
|
||||
'status-alert': statusAlert,
|
||||
@ -259,6 +264,7 @@ const ICONS = {
|
||||
'tab-patient-info': tabPatientInfo,
|
||||
'tab-roi-threshold': tabROIThreshold,
|
||||
'tab-segmentation': tabSegmentation,
|
||||
'tab-studies': tabStudies,
|
||||
'ui-arrow-down': uiArrowDown,
|
||||
'ui-arrow-up': uiArrowUp,
|
||||
'ui-arrow-left': uiArrowLeft,
|
||||
|
||||
376
platform/ui/src/components/LegacySidePanel/LegacySidePanel.tsx
Normal file
376
platform/ui/src/components/LegacySidePanel/LegacySidePanel.tsx
Normal file
@ -0,0 +1,376 @@
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SwiperCore, { A11y, Controller, Navigation, Pagination, Scrollbar } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
|
||||
import { PanelService, ServicesManager, Types } from '@ohif/core';
|
||||
|
||||
import LegacyButton from '../LegacyButton';
|
||||
import Icon from '../Icon';
|
||||
import IconButton from '../IconButton';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import './style.css';
|
||||
|
||||
const borderSize = 4;
|
||||
const expandedWidth = 248;
|
||||
const collapsedWidth = 25;
|
||||
|
||||
const baseStyle = {
|
||||
maxWidth: `${expandedWidth}px`,
|
||||
width: `${expandedWidth}px`,
|
||||
};
|
||||
|
||||
const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
|
||||
const styleMap = {
|
||||
open: {
|
||||
left: { marginLeft: '0px' },
|
||||
right: { marginRight: '0px' },
|
||||
},
|
||||
closed: {
|
||||
left: { marginLeft: `-${collapsedHideWidth}px` },
|
||||
right: { marginRight: `-${collapsedHideWidth}px` },
|
||||
},
|
||||
};
|
||||
|
||||
const baseClasses =
|
||||
'transition-all duration-300 ease-in-out h-100 bg-black border-black justify-start box-content flex flex-col';
|
||||
|
||||
const classesMap = {
|
||||
open: {
|
||||
left: `mr-1`,
|
||||
right: `ml-1`,
|
||||
},
|
||||
closed: {
|
||||
left: `mr-2 items-end`,
|
||||
right: `ml-2 items-start`,
|
||||
},
|
||||
};
|
||||
|
||||
const openStateIconName = {
|
||||
left: 'push-left',
|
||||
right: 'push-right',
|
||||
};
|
||||
|
||||
const position = {
|
||||
left: {
|
||||
right: 5,
|
||||
},
|
||||
right: {
|
||||
left: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const LegacySidePanel = ({
|
||||
servicesManager,
|
||||
side,
|
||||
className,
|
||||
activeTabIndex: activeTabIndexProp,
|
||||
tabs,
|
||||
}) => {
|
||||
const panelService: PanelService = servicesManager?.services?.panelService;
|
||||
|
||||
const { t } = useTranslation('LegacySidePanel');
|
||||
|
||||
// Tracks whether this LegacySidePanel has been opened at least once since this LegacySidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a LegacySidePanel.
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(activeTabIndexProp !== null);
|
||||
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
|
||||
const swiperRef = useRef() as any;
|
||||
const [swiper, setSwiper] = useState<any>();
|
||||
|
||||
const prevRef = React.useRef();
|
||||
const nextRef = React.useRef();
|
||||
|
||||
const openStatus = panelOpen ? 'open' : 'closed';
|
||||
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
|
||||
|
||||
const ActiveComponent = tabs[activeTabIndex].content;
|
||||
|
||||
useEffect(() => {
|
||||
if (panelOpen && swiper) {
|
||||
swiper.slideTo(activeTabIndex, 500);
|
||||
}
|
||||
}, [panelOpen, swiper]);
|
||||
|
||||
useEffect(() => {
|
||||
if (swiper) {
|
||||
swiper.params.navigation.prevEl = prevRef.current;
|
||||
swiper.params.navigation.nextEl = nextRef.current;
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}
|
||||
}, [swiper]);
|
||||
|
||||
const updatePanelOpen = useCallback((panelOpen: boolean) => {
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen) {
|
||||
setHasBeenOpened(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateActiveTabIndex = useCallback(
|
||||
(activeTabIndex: number) => {
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
updatePanelOpen(true);
|
||||
},
|
||||
[updatePanelOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelService) {
|
||||
const activatePanelSubscription = panelService.subscribe(
|
||||
panelService.EVENTS.ACTIVATE_PANEL,
|
||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||
if (tabIndex !== -1) {
|
||||
updateActiveTabIndex(tabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
activatePanelSubscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
|
||||
|
||||
const getCloseStateComponent = () => {
|
||||
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classnames(
|
||||
'bg-secondary-dark flex h-[28px] w-full cursor-pointer items-center rounded-md',
|
||||
side === 'left' ? 'justify-end pr-2' : 'justify-start pl-2'
|
||||
)}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
<Icon
|
||||
name={'navigation-panel-right-reveal'}
|
||||
className={classnames('text-primary-active', side === 'left' && 'rotate-180 transform')}
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames('mt-3 flex flex-col space-y-3')}>
|
||||
{_childComponents.map((childComponent, index) => (
|
||||
<Tooltip
|
||||
position={side === 'left' ? 'right' : 'left'}
|
||||
key={index}
|
||||
content={`${childComponent.label}`}
|
||||
className={classnames(
|
||||
'flex items-center',
|
||||
side === 'left' ? 'justify-end ' : 'justify-start '
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
id={`${childComponent.name}-btn`}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
onClick={() => {
|
||||
updateActiveTabIndex(index);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={childComponent.iconName}
|
||||
className="text-primary-active"
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, baseClasses, classesMap[openStatus][side])}
|
||||
style={style}
|
||||
>
|
||||
{panelOpen ? (
|
||||
<React.Fragment>
|
||||
{/** Panel Header with Arrow and Close Actions */}
|
||||
<div
|
||||
className={classnames(
|
||||
'flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]',
|
||||
tabs.length === 1 && 'mb-1'
|
||||
)}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
// slideToActivePanel();
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
{/* TODO This should be redesigned to not be a button. */}
|
||||
<LegacyButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
border="none"
|
||||
rounded="none"
|
||||
className="flex-static relative flex w-full flex-row items-center px-3"
|
||||
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
|
||||
>
|
||||
<Icon
|
||||
name={openStateIconName[side]}
|
||||
className={classnames(
|
||||
'text-primary-active absolute',
|
||||
side === 'left' && 'order-last'
|
||||
)}
|
||||
style={{ ...position[side] }}
|
||||
/>
|
||||
{/* Todo: ass secondary label here */}
|
||||
<span className="text-primary-active">
|
||||
{tabs.length === 1 && (t(tabs[activeTabIndex].label) as string)}
|
||||
</span>
|
||||
</LegacyButton>
|
||||
</div>
|
||||
{tabs.length > 1 &&
|
||||
_getMoreThanOneTabLayout(
|
||||
swiperRef,
|
||||
setSwiper,
|
||||
prevRef,
|
||||
nextRef,
|
||||
tabs,
|
||||
activeTabIndex,
|
||||
updateActiveTabIndex
|
||||
)}
|
||||
{/** carousel navigation with the arrows */}
|
||||
{/** only show carousel nav if tabs are more than 3 tabs */}
|
||||
{tabs.length > 3 && (
|
||||
<div className="text-primary-active bg-primary-dark flex w-full justify-end gap-2 py-1 px-2">
|
||||
<button
|
||||
ref={prevRef}
|
||||
className="swiper-button-prev-custom"
|
||||
>
|
||||
<Icon
|
||||
name={'icon-prev'}
|
||||
className={classnames('text-primary-active')}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
ref={nextRef}
|
||||
className="swiper-button-next-custom"
|
||||
>
|
||||
<Icon
|
||||
name={'icon-next'}
|
||||
className={classnames('text-primary-active')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ActiveComponent />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LegacySidePanel.defaultProps = {
|
||||
defaultComponentOpen: null,
|
||||
};
|
||||
|
||||
LegacySidePanel.propTypes = {
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
||||
className: PropTypes.string,
|
||||
activeTabIndex: PropTypes.number,
|
||||
tabs: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
iconName: PropTypes.string.isRequired,
|
||||
iconLabel: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
|
||||
})
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
function _getMoreThanOneTabLayout(
|
||||
swiperRef: any,
|
||||
setSwiper: React.Dispatch<any>,
|
||||
prevRef: React.MutableRefObject<undefined>,
|
||||
nextRef: React.MutableRefObject<undefined>,
|
||||
tabs: any,
|
||||
activeTabIndex: any,
|
||||
updateActiveTabIndex
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className="flex-static collapse-sidebar relative"
|
||||
style={{
|
||||
backgroundColor: '#06081f',
|
||||
}}
|
||||
>
|
||||
<div className="w-full">
|
||||
<Swiper
|
||||
onInit={(core: SwiperCore) => {
|
||||
swiperRef.current = core.el;
|
||||
}}
|
||||
simulateTouch={false}
|
||||
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
|
||||
slidesPerView={3}
|
||||
spaceBetween={5}
|
||||
onSwiper={swiper => setSwiper(swiper)}
|
||||
navigation={{
|
||||
prevEl: prevRef?.current,
|
||||
nextEl: nextRef?.current,
|
||||
}}
|
||||
>
|
||||
{tabs.map((obj, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div
|
||||
className={classnames(
|
||||
index === activeTabIndex ? 'bg-secondary-main text-white' : 'text-aqua-pale',
|
||||
'flex cursor-pointer flex-col items-center justify-center rounded-[4px] px-4 py-1 text-center hover:text-white'
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
updateActiveTabIndex(index);
|
||||
}}
|
||||
data-cy={`${obj.name}-btn`}
|
||||
>
|
||||
<span>
|
||||
<Icon
|
||||
name={obj.iconName}
|
||||
className={classnames(
|
||||
index === activeTabIndex ? 'text-white' : 'text-primary-active'
|
||||
)}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-[5px] select-none whitespace-nowrap text-[10px] font-medium">
|
||||
{obj.label}
|
||||
</span>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LegacySidePanel;
|
||||
@ -0,0 +1,74 @@
|
||||
import LegacySidePanel from '../LegacySidePanel';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: LegacySidePanel,
|
||||
title: 'Components/LegacySidePanel',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/LegacySidePanel"
|
||||
component={LegacySidePanel}
|
||||
/>
|
||||
|
||||
export const LegacySidePanelTemplate = args => (
|
||||
<div className="w-96">
|
||||
<LegacySidePanel {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="LegacySidePanel"
|
||||
componentRelativePath="LegacySidePanel/LegacySidePanel.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
LegacySidePanel is a component that renders the LegacySidePanels.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
side: 'left',
|
||||
defaultComponentOpen: 'Example',
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab1',
|
||||
name: 'tab1',
|
||||
label: 'Tab 1',
|
||||
iconName: 'group-layers',
|
||||
iconLabel: 'group-layers',
|
||||
content: () => <div class="text-primary-light p-3">Hello</div>,
|
||||
},
|
||||
],
|
||||
childComponents: [
|
||||
{
|
||||
iconName: 'home',
|
||||
iconLabel: 'launch-info',
|
||||
name: 'Example',
|
||||
label: 'home',
|
||||
content: () => <div>Hello world</div>,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{LegacySidePanelTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={LegacySidePanel} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="LegacySidePanel/__stories__/legacySidePanel.stories.mdx" />
|
||||
2
platform/ui/src/components/LegacySidePanel/index.js
Normal file
2
platform/ui/src/components/LegacySidePanel/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import LegacySidePanel from './LegacySidePanel';
|
||||
export default LegacySidePanel;
|
||||
@ -1,28 +1,27 @@
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SwiperCore, { A11y, Controller, Navigation, Pagination, Scrollbar } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
|
||||
import { PanelService, ServicesManager, Types } from '@ohif/core';
|
||||
|
||||
import LegacyButton from '../LegacyButton';
|
||||
import Icon from '../Icon';
|
||||
import IconButton from '../IconButton';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import './style.css';
|
||||
|
||||
const borderSize = 4;
|
||||
const expandedWidth = 248;
|
||||
const collapsedWidth = 25;
|
||||
const closeIconWidth = 30;
|
||||
const gridHorizontalPadding = 10;
|
||||
const tabSpacerWidth = 2;
|
||||
const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
|
||||
|
||||
const baseStyle = {
|
||||
maxWidth: `${expandedWidth}px`,
|
||||
width: `${expandedWidth}px`,
|
||||
// To align the top of the side panel with the top of the viewport grid, use position relative and offset the
|
||||
// top by the same top offset as the viewport grid. Also adjust the height so that there is no overflow.
|
||||
position: 'relative',
|
||||
top: '0.2%',
|
||||
height: '99.8%',
|
||||
};
|
||||
|
||||
const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
|
||||
@ -38,7 +37,7 @@ const styleMap = {
|
||||
};
|
||||
|
||||
const baseClasses =
|
||||
'transition-all duration-300 ease-in-out h-100 bg-black border-black justify-start box-content flex flex-col';
|
||||
'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col';
|
||||
|
||||
const classesMap = {
|
||||
open: {
|
||||
@ -52,70 +51,110 @@ const classesMap = {
|
||||
};
|
||||
|
||||
const openStateIconName = {
|
||||
left: 'push-left',
|
||||
right: 'push-right',
|
||||
left: 'side-panel-close-left',
|
||||
right: 'side-panel-close-right',
|
||||
};
|
||||
|
||||
const position = {
|
||||
left: {
|
||||
right: 5,
|
||||
},
|
||||
right: {
|
||||
left: 5,
|
||||
},
|
||||
const getTabWidth = (numTabs: number) => {
|
||||
if (numTabs < 3) {
|
||||
return 68;
|
||||
} else {
|
||||
return 40;
|
||||
}
|
||||
};
|
||||
|
||||
const SidePanel = ({
|
||||
servicesManager,
|
||||
side,
|
||||
className,
|
||||
activeTabIndex: activeTabIndexProp,
|
||||
tabs,
|
||||
}) => {
|
||||
const panelService: PanelService = servicesManager?.services?.panelService;
|
||||
const getGridWidth = (numTabs: number) => {
|
||||
const spacersWidth = (numTabs - 1) * tabSpacerWidth;
|
||||
const tabsWidth = getTabWidth(numTabs) * numTabs;
|
||||
|
||||
if (gridAvailableWidth > tabsWidth + spacersWidth) {
|
||||
return tabsWidth + spacersWidth;
|
||||
}
|
||||
|
||||
return gridAvailableWidth;
|
||||
};
|
||||
|
||||
const getNumGridColumns = (numTabs: number) => {
|
||||
if (numTabs === 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Start by calculating the number of tabs assuming each tab was accompanied by a spacer.
|
||||
const tabWidth = getTabWidth(numTabs);
|
||||
const gridWidth = getGridWidth(numTabs);
|
||||
const numTabsWithOneSpacerEach = Math.floor(gridWidth / (tabWidth + tabSpacerWidth));
|
||||
|
||||
// But there is always one less spacer than tabs, so now check if an extra tab with one less spacer fits.
|
||||
if (
|
||||
(numTabsWithOneSpacerEach + 1) * tabWidth + numTabsWithOneSpacerEach * tabSpacerWidth <=
|
||||
gridWidth
|
||||
) {
|
||||
return numTabsWithOneSpacerEach + 1;
|
||||
}
|
||||
|
||||
return numTabsWithOneSpacerEach;
|
||||
};
|
||||
|
||||
const getGridStyle = (side: string, numTabs: number = 0): CSSProperties => {
|
||||
const gridWidth = getGridWidth(numTabs);
|
||||
const relativePosition = Math.max(0, Math.floor(expandedWidth - gridWidth) / 2 - closeIconWidth);
|
||||
return {
|
||||
position: 'relative',
|
||||
...(side === 'left' ? { right: `${relativePosition}px` } : { left: `${relativePosition}px` }),
|
||||
width: `${gridWidth}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const getTabClassNames = (
|
||||
numColumns: number,
|
||||
numTabs: number,
|
||||
tabIndex: number,
|
||||
isActiveTab: boolean
|
||||
) =>
|
||||
classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', {
|
||||
'hover:text-primary-active': !isActiveTab,
|
||||
'rounded-l': tabIndex % numColumns === 0,
|
||||
'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1,
|
||||
});
|
||||
|
||||
const getTabStyle = (numTabs: number) => {
|
||||
return {
|
||||
width: `${getTabWidth(numTabs)}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => {
|
||||
return classnames('h-full w-full flex items-center justify-center', {
|
||||
'bg-customblue-40': isActiveTab,
|
||||
rounded: isActiveTab,
|
||||
});
|
||||
};
|
||||
|
||||
const SidePanel = ({ side, className, activeTabIndex: activeTabIndexProp, tabs, onOpen }) => {
|
||||
const { t } = useTranslation('SidePanel');
|
||||
|
||||
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(activeTabIndexProp !== null);
|
||||
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
|
||||
const swiperRef = useRef() as any;
|
||||
const [swiper, setSwiper] = useState<any>();
|
||||
|
||||
const prevRef = React.useRef();
|
||||
const nextRef = React.useRef();
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
const openStatus = panelOpen ? 'open' : 'closed';
|
||||
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
|
||||
|
||||
const ActiveComponent = tabs[activeTabIndex].content;
|
||||
|
||||
useEffect(() => {
|
||||
if (panelOpen && swiper) {
|
||||
swiper.slideTo(activeTabIndex, 500);
|
||||
}
|
||||
}, [panelOpen, swiper]);
|
||||
|
||||
useEffect(() => {
|
||||
if (swiper) {
|
||||
swiper.params.navigation.prevEl = prevRef.current;
|
||||
swiper.params.navigation.nextEl = nextRef.current;
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}
|
||||
}, [swiper]);
|
||||
const ActiveComponent = tabs[activeTabIndex]?.content;
|
||||
|
||||
const updatePanelOpen = useCallback((panelOpen: boolean) => {
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen) {
|
||||
setHasBeenOpened(true);
|
||||
onOpen?.();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateActiveTabIndex = useCallback(
|
||||
(activeTabIndex: number) => {
|
||||
if (activeTabIndex === null) {
|
||||
updatePanelOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
updatePanelOpen(true);
|
||||
},
|
||||
@ -123,24 +162,8 @@ const SidePanel = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelService) {
|
||||
const activatePanelSubscription = panelService.subscribe(
|
||||
panelService.EVENTS.ACTIVATE_PANEL,
|
||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||
if (tabIndex !== -1) {
|
||||
updateActiveTabIndex(tabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
activatePanelSubscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
|
||||
updateActiveTabIndex(activeTabIndexProp);
|
||||
}, [activeTabIndexProp, updateActiveTabIndex]);
|
||||
|
||||
const getCloseStateComponent = () => {
|
||||
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
||||
@ -172,12 +195,10 @@ const SidePanel = ({
|
||||
side === 'left' ? 'justify-end ' : 'justify-start '
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
<div
|
||||
id={`${childComponent.name}-btn`}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
data-cy={`${childComponent.name}-btn`}
|
||||
className="text-primary-active hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
updateActiveTabIndex(index);
|
||||
}}
|
||||
@ -190,7 +211,7 @@ const SidePanel = ({
|
||||
height: '22px',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
@ -198,81 +219,116 @@ const SidePanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getCloseIcon = () => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex h-[28px] cursor-pointer items-center justify-center',
|
||||
side === 'left' ? 'order-last' : 'order-first'
|
||||
)}
|
||||
style={{ width: `${closeIconWidth}px` }}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
<Icon
|
||||
name={openStateIconName[side]}
|
||||
className="text-primary-active"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getTabGridComponent = () => {
|
||||
const numCols = getNumGridColumns(tabs.length);
|
||||
|
||||
return (
|
||||
<div className={classnames('flex grow ', side === 'right' ? 'justify-start' : 'justify-end')}>
|
||||
<div
|
||||
className={classnames('bg-primary-dark text-primary-active flex flex-wrap')}
|
||||
style={getGridStyle(side, tabs.length)}
|
||||
>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
return (
|
||||
<React.Fragment key={tabIndex}>
|
||||
{tabIndex % numCols !== 0 && (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex h-[28px] w-[2px] items-center bg-black',
|
||||
tabSpacerWidth
|
||||
)}
|
||||
>
|
||||
<div className="bg-primary-dark h-[20px] w-full"></div>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip
|
||||
position={'bottom'}
|
||||
key={tabIndex}
|
||||
content={`${tab.label}`}
|
||||
>
|
||||
<div
|
||||
className={getTabClassNames(
|
||||
numCols,
|
||||
tabs.length,
|
||||
tabIndex,
|
||||
tabIndex === activeTabIndex
|
||||
)}
|
||||
style={getTabStyle(tabs.length)}
|
||||
onClick={() => updateActiveTabIndex(tabIndex)}
|
||||
data-cy={`${tab.name}-btn`}
|
||||
>
|
||||
<div className={getTabIconClassNames(tabs.length, tabIndex === activeTabIndex)}>
|
||||
<Icon name={tab.iconName}></Icon>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOneTabComponent = () => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'text-primary-active flex grow cursor-pointer justify-center self-center text-[13px]'
|
||||
)}
|
||||
style={{
|
||||
...(side === 'left'
|
||||
? { marginLeft: `${closeIconWidth}px` }
|
||||
: { marginRight: `${closeIconWidth}px` }),
|
||||
}}
|
||||
data-cy={`${tabs[0].name}-btn`}
|
||||
onClick={() => updatePanelOpen(prev => !prev)}
|
||||
>
|
||||
<span>{tabs[0].label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOpenStateComponent = () => {
|
||||
return (
|
||||
<div className="bg-primary-dark flex rounded-t pt-1.5 pb-[2px]">
|
||||
{getCloseIcon()}
|
||||
{tabs.length === 1 ? getOneTabComponent() : getTabGridComponent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, baseClasses, classesMap[openStatus][side])}
|
||||
style={style}
|
||||
>
|
||||
{panelOpen ? (
|
||||
<React.Fragment>
|
||||
{/** Panel Header with Arrow and Close Actions */}
|
||||
<div
|
||||
className={classnames('flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]')}
|
||||
onClick={() => {
|
||||
updatePanelOpen(prev => !prev);
|
||||
// slideToActivePanel();
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
{/* TODO This should be redesigned to not be a button. */}
|
||||
<LegacyButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
border="none"
|
||||
rounded="none"
|
||||
className="flex-static relative flex w-full flex-row items-center px-3"
|
||||
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
|
||||
>
|
||||
<Icon
|
||||
name={openStateIconName[side]}
|
||||
className={classnames(
|
||||
'text-primary-active absolute',
|
||||
side === 'left' && 'order-last'
|
||||
)}
|
||||
style={{ ...position[side] }}
|
||||
/>
|
||||
{/* Todo: ass secondary label here */}
|
||||
<span className="text-primary-active">
|
||||
{tabs.length === 1 && (t(tabs[activeTabIndex].label) as string)}
|
||||
</span>
|
||||
</LegacyButton>
|
||||
</div>
|
||||
{tabs.length > 1 &&
|
||||
_getMoreThanOneTabLayout(
|
||||
swiperRef,
|
||||
setSwiper,
|
||||
prevRef,
|
||||
nextRef,
|
||||
tabs,
|
||||
activeTabIndex,
|
||||
updateActiveTabIndex
|
||||
)}
|
||||
{/** carousel navigation with the arrows */}
|
||||
{/** only show carousel nav if tabs are more than 3 tabs */}
|
||||
{tabs.length > 3 && (
|
||||
<div className="text-primary-active bg-primary-dark flex w-full justify-end gap-2 py-1 px-2">
|
||||
<button
|
||||
ref={prevRef}
|
||||
className="swiper-button-prev-custom"
|
||||
>
|
||||
<Icon
|
||||
name={'icon-prev'}
|
||||
className={classnames('text-primary-active')}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
ref={nextRef}
|
||||
className="swiper-button-next-custom"
|
||||
>
|
||||
<Icon
|
||||
name={'icon-next'}
|
||||
className={classnames('text-primary-active')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
{getOpenStateComponent()}
|
||||
<ActiveComponent />
|
||||
</React.Fragment>
|
||||
</>
|
||||
) : (
|
||||
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
|
||||
)}
|
||||
@ -282,10 +338,10 @@ const SidePanel = ({
|
||||
|
||||
SidePanel.defaultProps = {
|
||||
defaultComponentOpen: null,
|
||||
activeTabIndex: null, // the default is to close the side panel
|
||||
};
|
||||
|
||||
SidePanel.propTypes = {
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
||||
className: PropTypes.string,
|
||||
activeTabIndex: PropTypes.number,
|
||||
@ -300,74 +356,7 @@ SidePanel.propTypes = {
|
||||
})
|
||||
),
|
||||
]),
|
||||
onOpen: PropTypes.func,
|
||||
};
|
||||
|
||||
function _getMoreThanOneTabLayout(
|
||||
swiperRef: any,
|
||||
setSwiper: React.Dispatch<any>,
|
||||
prevRef: React.MutableRefObject<undefined>,
|
||||
nextRef: React.MutableRefObject<undefined>,
|
||||
tabs: any,
|
||||
activeTabIndex: any,
|
||||
updateActiveTabIndex
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className="flex-static collapse-sidebar relative"
|
||||
style={{
|
||||
backgroundColor: '#06081f',
|
||||
}}
|
||||
>
|
||||
<div className="w-full">
|
||||
<Swiper
|
||||
onInit={(core: SwiperCore) => {
|
||||
swiperRef.current = core.el;
|
||||
}}
|
||||
simulateTouch={false}
|
||||
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
|
||||
slidesPerView={3}
|
||||
spaceBetween={5}
|
||||
onSwiper={swiper => setSwiper(swiper)}
|
||||
navigation={{
|
||||
prevEl: prevRef?.current,
|
||||
nextEl: nextRef?.current,
|
||||
}}
|
||||
>
|
||||
{tabs.map((obj, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div
|
||||
className={classnames(
|
||||
index === activeTabIndex ? 'bg-secondary-main text-white' : 'text-aqua-pale',
|
||||
'flex cursor-pointer flex-col items-center justify-center rounded-[4px] px-4 py-1 text-center hover:text-white'
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
updateActiveTabIndex(index);
|
||||
}}
|
||||
data-cy={`${obj.name}-btn`}
|
||||
>
|
||||
<span>
|
||||
<Icon
|
||||
name={obj.iconName}
|
||||
className={classnames(
|
||||
index === activeTabIndex ? 'text-white' : 'text-primary-active'
|
||||
)}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-[5px] select-none whitespace-nowrap text-[10px] font-medium">
|
||||
{obj.label}
|
||||
</span>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidePanel;
|
||||
|
||||
@ -38,6 +38,7 @@ SidePanel is a component that renders the SidePanels.
|
||||
args={{
|
||||
side: 'left',
|
||||
defaultComponentOpen: 'Example',
|
||||
activeTabIndex: 0,
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab1',
|
||||
|
||||
@ -74,6 +74,7 @@ import LoadingIndicatorProgress from './LoadingIndicatorProgress';
|
||||
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
|
||||
import ViewportActionBar from './ViewportActionBar';
|
||||
import ProgressLoadingBar from './ProgressLoadingBar';
|
||||
import LegacySidePanel from './LegacySidePanel';
|
||||
import PanelSection from './PanelSection';
|
||||
import AdvancedToolbox from './AdvancedToolbox';
|
||||
import InputDoubleRange from './InputDoubleRange';
|
||||
@ -117,6 +118,7 @@ export {
|
||||
LegacyButton,
|
||||
LegacyButtonGroup,
|
||||
LegacyCinePlayer,
|
||||
LegacySidePanel,
|
||||
LegacyViewportActionBar,
|
||||
LoadingIndicatorProgress,
|
||||
LoadingIndicatorTotalPercent,
|
||||
|
||||
@ -71,6 +71,7 @@ export {
|
||||
LegacyButton,
|
||||
LegacyButtonGroup,
|
||||
LegacyCinePlayer,
|
||||
LegacySidePanel,
|
||||
LegacyViewportActionBar,
|
||||
LoadingIndicatorProgress,
|
||||
LoadingIndicatorTotalPercent,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SidePanel, StudyBrowser } from '../../components';
|
||||
import { LegacySidePanel, StudyBrowser } from '../../components';
|
||||
import { DragAndDropProvider } from '../../contextProviders';
|
||||
|
||||
import Header from './components/Header';
|
||||
@ -13,7 +13,7 @@ const Viewer = () => {
|
||||
className="flex w-full flex-1 flex-row flex-nowrap items-stretch overflow-hidden"
|
||||
style={{ height: 'calc(100vh - 52px' }}
|
||||
>
|
||||
<SidePanel
|
||||
<LegacySidePanel
|
||||
side="left"
|
||||
iconName="group-layers"
|
||||
iconLabel="Studies"
|
||||
@ -21,12 +21,12 @@ const Viewer = () => {
|
||||
defaultIsOpen={true}
|
||||
>
|
||||
<StudyBrowser />
|
||||
</SidePanel>
|
||||
</LegacySidePanel>
|
||||
<div className="h-100 bg-primary-main flex flex-1 items-center justify-center overflow-hidden text-white">
|
||||
{/* <ViewportToolbar /> */}
|
||||
<div>CONTENT</div>
|
||||
</div>
|
||||
<SidePanel
|
||||
<LegacySidePanel
|
||||
side="right"
|
||||
iconName="list-bullets"
|
||||
iconLabel="Measure"
|
||||
@ -34,7 +34,7 @@ const Viewer = () => {
|
||||
defaultIsOpen={false}
|
||||
>
|
||||
<div className="flex justify-center p-2 text-white">panel placeholder</div>
|
||||
</SidePanel>
|
||||
</LegacySidePanel>
|
||||
</div>
|
||||
</div>
|
||||
</DragAndDropProvider>
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@ -1523,6 +1523,16 @@
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
|
||||
"@cornerstonejs/core@^1.16.6":
|
||||
version "1.16.6"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.16.6.tgz#a216fc655f3c4b110196e63dcf129e02839d0890"
|
||||
integrity sha512-ysd+t2N2KVk88TLYg0Nx174uYd47wQRBdFQZ4ApHhI/IHq5lY/qrP8prrtLR/DEtyLyUERIxDyGFRr+lEBZpxg==
|
||||
dependencies:
|
||||
"@kitware/vtk.js" "27.3.1"
|
||||
detect-gpu "^5.0.22"
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
|
||||
"@cornerstonejs/dicom-image-loader@^1.16.5":
|
||||
version "1.16.5"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.16.5.tgz#23ca3e27e29b88cd05840c777578b37de60e5b87"
|
||||
@ -1544,6 +1554,15 @@
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.16.5"
|
||||
|
||||
"@cornerstonejs/tools@^1.16.4":
|
||||
version "1.16.6"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.16.6.tgz#0debc17fb9830288bff263bbf3a34cb940b726c2"
|
||||
integrity sha512-qLjtEC0OGW+bpvvKXQPQs8TEE/ze+NIiBbg1qBSXo2vm7O3YO1DRWbWthJMVL7tIWYuVxZx5OsAI2WOkydQUOA==
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.16.6"
|
||||
lodash.clonedeep "4.5.0"
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
"@cornerstonejs/tools@^1.16.5":
|
||||
version "1.16.5"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.16.5.tgz#79948216521c591a02f395e2130d72e0b9347eca"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user