diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 7644aa045bb..cf0479ee1d2 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -1,19 +1,13 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import OHIF, { utils } from '@ohif/core'; import { - Notification, - ViewportActionBar, - useViewportGrid, - useViewportDialog, - LoadingIndicatorProgress, + LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar } from '@ohif/ui'; - -import { useTranslation } from 'react-i18next'; - import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; -import _hydrateSEGDisplaySet from '../utils/_hydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG'; +import hydrateSEGDisplaySet from '../utils/_hydrateSEG'; import _getStatusComponent from './_getStatusComponent'; const { formatDate } = utils; @@ -307,16 +301,15 @@ function OHIFCornerstoneSEGViewport(props) { SeriesNumber, } = referencedDisplaySetRef.current.metadata; - const onPillClick = () => { - promptHydrateSEG({ - servicesManager, - viewportIndex, + const onStatusClick = async () => { + const isHydrated = await hydrateSEGDisplaySet({ segDisplaySet, - }).then(isHydrated => { - if (isHydrated) { - setIsHydrated(true); - } + viewportIndex, + toolGroupId, + servicesManager, }); + + setIsHydrated(isHydrated); }; return ( @@ -330,14 +323,13 @@ function OHIFCornerstoneSEGViewport(props) { getStatusComponent={() => { return _getStatusComponent({ isHydrated, - onPillClick, + onStatusClick, }); }} studyData={{ label: viewportLabel, useAltStyling: true, studyDate: formatDate(StudyDate), - currentSeries: SeriesNumber, seriesDescription: `SEG Viewport ${SeriesDescription}`, patientInformation: { patientName: PatientName diff --git a/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx index 3ef7d3ce11b..f9614f8066e 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx @@ -1,92 +1,56 @@ import React from 'react'; -import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { Icon, Tooltip } from '@ohif/ui'; -import _hydrateSEGDisplaySet from '../utils/_hydrateSEG'; -export default function _getStatusComponent({ isHydrated, onPillClick }) { +export default function _getStatusComponent({ isHydrated, onStatusClick }) { let ToolTipMessage = null; let StatusIcon = null; + const {t} = useTranslation("Common"); + const loadStr = t("LOAD"); + switch (isHydrated) { case true: - StatusIcon = () => ( -
- -
- ); + StatusIcon = () => ; ToolTipMessage = () => (
This Segmentation is loaded in the segmentation panel
); break; - case false: - StatusIcon = () => ( -
- -
- ); + case false: + StatusIcon = () => ; - ToolTipMessage = () =>
Click to load segmentation.
; + ToolTipMessage = () =>
Click LOAD to load segmentation.
; } - const StatusPill = () => ( -
{ - if (!isHydrated) { - if (onPillClick) { - onPillClick(); - } - } - }} - > -
- SEG + const StatusArea = () => ( +
+
+ + SEG
- + {!isHydrated && ( +
+ {loadStr} +
+ )}
); + return ( <> {ToolTipMessage && ( } position="bottom-left"> - + )} - {!ToolTipMessage && } + {!ToolTipMessage && } ); } diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index cb13803d79d..61068726c86 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -1,18 +1,17 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import OHIF, { utils } from '@ohif/core'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import OHIF, { utils } from '@ohif/core'; import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; import { + Icon, Notification, - ViewportActionBar, - useViewportGrid, - useViewportDialog, Tooltip, - Icon, + useViewportDialog, + useViewportGrid, + ViewportActionBar, } from '@ohif/ui'; -import classNames from 'classnames'; import hydrateStructuredReport from '../utils/hydrateStructuredReport'; const { formatDate } = utils; @@ -33,8 +32,6 @@ function OHIFCornerstoneSRViewport(props) { extensionManager, } = props; - const { t } = useTranslation('SRViewport'); - const { displaySetService, cornerstoneViewportService, @@ -379,7 +376,6 @@ function OHIFCornerstoneSRViewport(props) { label: viewportLabel, useAltStyling: true, studyDate: formatDate(StudyDate), - currentSeries: SeriesNumber, seriesDescription: SeriesDescription || '', patientInformation: { patientName: PatientName @@ -468,13 +464,16 @@ function _getStatusComponent({ isLocked, sendTrackedMeasurementsEvent, }) { - const onPillClick = () => { - sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', { + const handleMouseUp = () => { + sendTrackedMeasurementsEvent('HYDRATE_SR', { displaySetInstanceUID: srDisplaySet.displaySetInstanceUID, viewportIndex, }); }; + const { t } = useTranslation('Common'); + const loadStr = t('LOAD'); + // 1 - Incompatible // 2 - Locked // 3 - Rehydratable / Open @@ -485,22 +484,7 @@ function _getStatusComponent({ switch (state) { case 1: - StatusIcon = () => ( -
- -
- ); + StatusIcon = () => ; ToolTipMessage = () => (
@@ -511,20 +495,7 @@ function _getStatusComponent({ ); break; case 2: - StatusIcon = () => ( -
- -
- ); + StatusIcon = () => ; ToolTipMessage = () => (
@@ -537,48 +508,28 @@ function _getStatusComponent({ ); break; case 3: - StatusIcon = () => ( -
- -
- ); + StatusIcon = () => ; - ToolTipMessage = () =>
Click to restore measurements.
; + ToolTipMessage = () => ( +
{`Click ${loadStr} to restore measurements.`}
+ ); } - const StatusPill = () => ( -
( +
+
+ + SR +
+ {state === 3 && ( +
+ {loadStr} +
)} - style={{ - height: '24px', - width: '55px', - }} - onClick={() => { - if (state === 3) { - if (onPillClick) { - onPillClick(); - } - } - }} - > - SR -
); @@ -586,22 +537,12 @@ function _getStatusComponent({ <> {ToolTipMessage && ( } position="bottom-left"> - + )} - {!ToolTipMessage && } + {!ToolTipMessage && } ); } -// function _onDoubleClick() { -// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule( -// 'manipulatorState' -// ).setters.cancelActiveManipulatorsForElement; -// const enabledElements = cornerstoneTools.store.state.enabledElements; -// enabledElements.forEach(element => { -// cancelActiveManipulatorsForElement(element); -// }); -// } - export default OHIFCornerstoneSRViewport; diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index d150c44e0cf..e1a6bbb1b48 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -20,6 +20,7 @@ import { IVolumeViewport, } from '@cornerstonejs/core/dist/esm/types'; import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui'; const STACK = 'stack'; @@ -112,6 +113,9 @@ const OHIFCornerstoneViewport = React.memo(props => { } = props; const [scrollbarHeight, setScrollbarHeight] = useState('100px'); + const [{ isCineEnabled, cines }, cineService] = useCine(); + const [{ activeViewportIndex }] = useViewportGrid(); + const [enabledVPElement, setEnabledVPElement] = useState(null); const elementRef = useRef(); @@ -126,6 +130,73 @@ const OHIFCornerstoneViewport = React.memo(props => { viewportGridService, } = servicesManager.services; + const cineHandler = () => { + if (!cines || !cines[viewportIndex] || !enabledVPElement) { + return; + } + + const cine = cines[viewportIndex]; + const isPlaying = cine.isPlaying || false; + const frameRate = cine.frameRate || 24; + + const validFrameRate = Math.max(frameRate, 1); + + if (isPlaying) { + cineService.playClip(enabledVPElement, { + framesPerSecond: validFrameRate, + }); + } else { + cineService.stopClip(enabledVPElement); + } + }; + + useEffect(() => { + eventTarget.addEventListener( + Enums.Events.STACK_VIEWPORT_NEW_STACK, + cineHandler + ); + + return () => { + cineService.setCine({ id: viewportIndex, isPlaying: false }); + eventTarget.removeEventListener( + Enums.Events.STACK_VIEWPORT_NEW_STACK, + cineHandler + ); + }; + }, [enabledVPElement]); + + useEffect(() => { + if (!cines || !cines[viewportIndex] || !enabledVPElement) { + return; + } + + cineHandler(); + + return () => { + if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) { + cineService.stopClip(enabledVPElement); + } + }; + }, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]); + + const cine = cines[viewportIndex]; + const isPlaying = (cine && cine.isPlaying) || false; + + const handleCineClose = () => { + toolbarService.recordInteraction({ + groupId: 'MoreTools', + itemId: 'cine', + interactionType: 'toggle', + commands: [ + { + commandName: 'toggleCine', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }); + }; + // useCallback for scroll bar height calculation const setImageScrollBarHeight = useCallback(() => { const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`; @@ -176,6 +247,7 @@ const OHIFCornerstoneViewport = React.memo(props => { const viewportIndex = viewportInfo.getViewportIndex(); setEnabledElement(viewportIndex, element); + setEnabledVPElement(element); const renderingEngineId = viewportInfo.getRenderingEngineId(); const toolGroupId = viewportInfo.getToolGroupId(); @@ -360,6 +432,25 @@ const OHIFCornerstoneViewport = React.memo(props => { scrollbarHeight={scrollbarHeight} servicesManager={servicesManager} /> + {isCineEnabled && ( + + cineService.setCine({ + id: activeViewportIndex, + isPlaying, + }) + } + onFrameRateChange={frameRate => + cineService.setCine({ + id: activeViewportIndex, + frameRate, + }) + } + /> + )}
); }, areEqual); diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx index b9e542b94ce..b3ecf6e2c1d 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx @@ -129,7 +129,7 @@ function CornerstoneViewportOverlay({ } return ( -
+
W: {windowWidth.toFixed(0)} L: @@ -140,7 +140,7 @@ function CornerstoneViewportOverlay({ if (activeTools.includes('Zoom')) { return ( -
+
Zoom: {scale.toFixed(2)}x
@@ -174,7 +174,7 @@ function CornerstoneViewportOverlay({ } return ( -
+
I: {instanceNumber !== undefined diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index b35f16389ba..476e80a4055 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -12,6 +12,7 @@ import promptTrackNewSeries from './promptTrackNewSeries'; import promptTrackNewStudy from './promptTrackNewStudy'; import promptSaveReport from './promptSaveReport'; import promptHydrateStructuredReport from './promptHydrateStructuredReport'; +import hydrateStructuredReport from './hydrateStructuredReport'; const TrackedMeasurementsContext = React.createContext(); TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; @@ -106,6 +107,10 @@ function TrackedMeasurementsContextProvider( servicesManager, extensionManager, }), + hydrateStructuredReport: hydrateStructuredReport.bind(null, { + servicesManager, + extensionManager, + }), }); // TODO: IMPROVE diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx new file mode 100644 index 00000000000..5a1be430b5e --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx @@ -0,0 +1,33 @@ +import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr'; + +function hydrateStructuredReport( + { servicesManager, extensionManager }, + ctx, + evt +) { + const { displaySetService } = servicesManager.services; + const { viewportIndex, displaySetInstanceUID } = evt; + const srDisplaySet = displaySetService.getDisplaySetByUID( + displaySetInstanceUID + ); + + return new Promise((resolve, reject) => { + const hydrationResult = baseHydrateStructuredReport( + { servicesManager, extensionManager }, + displaySetInstanceUID + ); + + const StudyInstanceUID = hydrationResult.StudyInstanceUID; + const SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs; + + resolve({ + displaySetInstanceUID: evt.displaySetInstanceUID, + srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + viewportIndex, + StudyInstanceUID, + SeriesInstanceUIDs, + }); + }); +} + +export default hydrateStructuredReport; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index a6e45850e6f..08c6b8c4ad1 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -1,3 +1,4 @@ +import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr'; import { assign } from 'xstate'; const RESPONSE = { @@ -45,6 +46,7 @@ const machineConfiguration = { cond: 'hasNotIgnoredSRSeriesForHydration', }, RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport', + HYDRATE_SR: 'hydrateStructuredReport', }, }, promptBeginTracking: { @@ -232,6 +234,24 @@ const machineConfiguration = { }, }, }, + hydrateStructuredReport: { + invoke: { + src: 'hydrateStructuredReport', + onDone: [ + { + target: 'tracking', + actions: [ + 'setTrackedStudyAndMultipleSeries', + 'jumpToFirstMeasurementInActiveViewport', + 'setIsDirtyToClean', + ], + }, + ], + onError: { + target: 'idle', + }, + }, + }, }, strict: true, }; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index c732215f448..97ac7affca9 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -5,8 +5,6 @@ import OHIF, { utils } from '@ohif/core'; import { Notification, ViewportActionBar, - useCine, - useViewportGrid, useViewportDialog, Tooltip, Icon, @@ -14,7 +12,6 @@ import { import { useTranslation } from 'react-i18next'; -import { eventTarget, Enums } from '@cornerstonejs/core'; import { annotation } from '@cornerstonejs/tools'; import { useTrackedMeasurements } from './../getContextModule'; @@ -43,12 +40,9 @@ function TrackedCornerstoneViewport(props) { const displaySet = displaySets[0]; const [trackedMeasurements] = useTrackedMeasurements(); - const [{ activeViewportIndex }] = useViewportGrid(); - const [{ isCineEnabled, cines }, cineService] = useCine(); const [viewportDialogState] = useViewportDialog(); const [isTracked, setIsTracked] = useState(false); const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null); - const [element, setElement] = useState(null); const { trackedSeries } = trackedMeasurements.context; const viewportId = viewportOptions.viewportId; @@ -71,26 +65,6 @@ function TrackedCornerstoneViewport(props) { ManufacturerModelName, } = displaySet.images[0]; - const cineHandler = () => { - if (!cines || !cines[viewportIndex] || !element) { - return; - } - - const cine = cines[viewportIndex]; - const isPlaying = cine.isPlaying || false; - const frameRate = cine.frameRate || 24; - - const validFrameRate = Math.max(frameRate, 1); - - if (isPlaying) { - cineService.playClip(element, { - framesPerSecond: validFrameRate, - }); - } else { - cineService.stopClip(element); - } - }; - useEffect(() => { if (isTracked) { annotation.config.style.setViewportToolStyles(viewportId, { @@ -119,50 +93,10 @@ function TrackedCornerstoneViewport(props) { }; }, [isTracked]); - // unmount cleanup - useEffect(() => { - eventTarget.addEventListener( - Enums.Events.STACK_VIEWPORT_NEW_STACK, - cineHandler - ); - - return () => { - cineService.setCine({ id: viewportIndex, isPlaying: false }); - eventTarget.removeEventListener( - Enums.Events.STACK_VIEWPORT_NEW_STACK, - cineHandler - ); - }; - }, [element]); - - useEffect(() => { - if (!cines || !cines[viewportIndex] || !element) { - return; - } - - cineHandler(); - - return () => { - if (element && cines?.[viewportIndex]?.isPlaying) { - cineService.stopClip(element); - } - }; - }, [cines, viewportIndex, cineService, element, cineHandler]); - if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { setIsTracked(!isTracked); } - /** - * OnElementEnabled callback which is called after the cornerstoneExtension - * has enabled the element. Note: we delegate all the image rendering to - * cornerstoneExtension, so we don't need to do anything here regarding - * the image rendering, element enabling etc. - */ - const onElementEnabled = evt => { - setElement(evt.detail.element); - }; - function switchMeasurement(direction) { const newTrackedMeasurementUID = _getNextMeasurementUID( direction, @@ -188,12 +122,9 @@ function TrackedCornerstoneViewport(props) { '@ohif/extension-cornerstone.viewportModule.cornerstone' ); - return ; + return ; }; - const cine = cines[viewportIndex]; - const isPlaying = (cine && cine.isPlaying) || false; - return ( <> commandsManager.runCommand('toggleCine'), - onPlayPauseChange: isPlaying => - cineService.setCine({ - id: activeViewportIndex, - isPlaying, - }), - onFrameRateChange: frameRate => - cineService.setCine({ - id: activeViewportIndex, - frameRate, - }), - }} /> {/* TODO: Viewport interface to accept stack or layers of content like this? */}
@@ -281,17 +194,25 @@ function _getNextMeasurementUID( trackedMeasurementId, trackedMeasurements ) { - const { measurementService } = servicesManager.services; + const { measurementService, viewportGridService } = servicesManager.services; const measurements = measurementService.getMeasurements(); + const { activeViewportIndex, viewports } = viewportGridService.getState(); + const { + displaySetInstanceUIDs: activeViewportDisplaySetInstanceUIDs, + } = viewports[activeViewportIndex]; + const { trackedSeries } = trackedMeasurements.context; - // Get the potentially trackable measurements for this series, + // Get the potentially trackable measurements for the series of the + // active viewport. // The measurements to jump between are the same // regardless if this series is tracked or not. - const filteredMeasurements = measurements.filter(m => - trackedSeries.includes(m.referenceSeriesUID) + const filteredMeasurements = measurements.filter( + m => + trackedSeries.includes(m.referenceSeriesUID) && + activeViewportDisplaySetInstanceUIDs.includes(m.displaySetInstanceUID) ); if (!filteredMeasurements.length) { @@ -329,7 +250,7 @@ function _getNextMeasurementUID( } function _getStatusComponent(isTracked) { - const trackedIcon = isTracked ? 'tracked' : 'dotted-circle'; + const trackedIcon = isTracked ? 'status-tracked' : 'status-untracked'; return (
@@ -361,7 +282,7 @@ function _getStatusComponent(isTracked) {
} > - +
); diff --git a/platform/docs/tailwind.config.js b/platform/docs/tailwind.config.js index a6cfc19538d..3081fe2e228 100644 --- a/platform/docs/tailwind.config.js +++ b/platform/docs/tailwind.config.js @@ -53,11 +53,17 @@ module.exports = { customgreen: { 100: '#05D97C', + 200: '#0FD97C', }, customblue: { 100: '#c4fdff', 200: '#38daff', + 300: '#1D204D', + }, + + customgray: { + 100: '#262943', }, gray: { @@ -338,14 +344,15 @@ module.exports = { full: '100%', screen: '100vh', }), - inset: { + inset: theme => ({ + ...theme('spacing'), '0': '0', auto: 'auto', full: '100%', viewport: '0.5rem', '1/2': '50%', 'viewport-scrollbar': '1.3rem', - }, + }), letterSpacing: { tighter: '-0.05em', tight: '-0.025em', diff --git a/platform/i18n/src/locales/en-US/Common.json b/platform/i18n/src/locales/en-US/Common.json index 64c587150d6..ff2e5d3b170 100644 --- a/platform/i18n/src/locales/en-US/Common.json +++ b/platform/i18n/src/locales/en-US/Common.json @@ -2,6 +2,7 @@ "Close": "Close", "Image": "Image", "Layout": "Layout", + "LOAD": "LOAD", "Measurements": "Measurements", "More": "More", "Next": "Next", @@ -13,4 +14,4 @@ "Show": "Show", "Stop": "Stop", "StudyDate": "Study Date" -} \ No newline at end of file +} diff --git a/platform/ui/src/assets/icons/arrow-left-small.svg b/platform/ui/src/assets/icons/arrow-left-small.svg new file mode 100644 index 00000000000..4a86c50e23b --- /dev/null +++ b/platform/ui/src/assets/icons/arrow-left-small.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/arrow-right-small.svg b/platform/ui/src/assets/icons/arrow-right-small.svg new file mode 100644 index 00000000000..6d72336f60e --- /dev/null +++ b/platform/ui/src/assets/icons/arrow-right-small.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/chevron-next.svg b/platform/ui/src/assets/icons/chevron-next.svg new file mode 100644 index 00000000000..197f3f2d424 --- /dev/null +++ b/platform/ui/src/assets/icons/chevron-next.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/chevron-prev.svg b/platform/ui/src/assets/icons/chevron-prev.svg new file mode 100644 index 00000000000..dcd01a2b8c6 --- /dev/null +++ b/platform/ui/src/assets/icons/chevron-prev.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/component-slider.svg b/platform/ui/src/assets/icons/component-slider.svg new file mode 100644 index 00000000000..b5216568907 --- /dev/null +++ b/platform/ui/src/assets/icons/component-slider.svg @@ -0,0 +1,3 @@ + + + diff --git a/platform/ui/src/assets/icons/icon-close.svg b/platform/ui/src/assets/icons/icon-close.svg new file mode 100644 index 00000000000..75066f4d005 --- /dev/null +++ b/platform/ui/src/assets/icons/icon-close.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/platform/ui/src/assets/icons/icon-pause.svg b/platform/ui/src/assets/icons/icon-pause.svg new file mode 100644 index 00000000000..00f45ac2987 --- /dev/null +++ b/platform/ui/src/assets/icons/icon-pause.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/icon-play.svg b/platform/ui/src/assets/icons/icon-play.svg new file mode 100644 index 00000000000..226ae614d2c --- /dev/null +++ b/platform/ui/src/assets/icons/icon-play.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/info-action.svg b/platform/ui/src/assets/icons/info-action.svg new file mode 100644 index 00000000000..305a6c385a7 --- /dev/null +++ b/platform/ui/src/assets/icons/info-action.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/platform/ui/src/assets/icons/status-alert.svg b/platform/ui/src/assets/icons/status-alert.svg new file mode 100644 index 00000000000..3a88223e96e --- /dev/null +++ b/platform/ui/src/assets/icons/status-alert.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/status-locked.svg b/platform/ui/src/assets/icons/status-locked.svg new file mode 100644 index 00000000000..344ffe329e7 --- /dev/null +++ b/platform/ui/src/assets/icons/status-locked.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/status-tracked.svg b/platform/ui/src/assets/icons/status-tracked.svg new file mode 100644 index 00000000000..549063a4968 --- /dev/null +++ b/platform/ui/src/assets/icons/status-tracked.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/status-untracked.svg b/platform/ui/src/assets/icons/status-untracked.svg new file mode 100644 index 00000000000..cf347223f38 --- /dev/null +++ b/platform/ui/src/assets/icons/status-untracked.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.css b/platform/ui/src/components/CinePlayer/CinePlayer.css new file mode 100644 index 00000000000..ffa0a52b56c --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayer.css @@ -0,0 +1,3 @@ +.cine-fps-range-tooltip .tooltip.tooltip-top { + bottom: 85% !important; +} diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.tsx b/platform/ui/src/components/CinePlayer/CinePlayer.tsx index 6e0fe5b964c..6ede1bf11e2 100644 --- a/platform/ui/src/components/CinePlayer/CinePlayer.tsx +++ b/platform/ui/src/components/CinePlayer/CinePlayer.tsx @@ -1,11 +1,28 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; -import { IconButton, Icon } from '../'; +import { Icon, Tooltip, InputRange } from '../'; -import './CinePlayerCustomInputRange.css'; +import './CinePlayer.css'; +import classNames from 'classnames'; -const CinePlayer = ({ +export type CinePlayerProps = { + className: string; + isPlaying: boolean; + minFrameRate: number; + maxFrameRate: number; + stepFrameRate: number; + frameRate: number; + onFrameRateChange: (value: number) => void; + onPlayPauseChange: (value: boolean) => void; + onClose: () => void; +}; + +const fpsButtonClassNames = + 'cursor-pointer text-primary-active active:text-primary-light hover:bg-customblue-300 w-4 flex items-center justify-center'; + +const CinePlayer: React.FC = ({ + className, isPlaying, minFrameRate, maxFrameRate, @@ -18,52 +35,68 @@ const CinePlayer = ({ const [frameRate, setFrameRate] = useState(defaultFrameRate); const debouncedSetFrameRate = debounce(onFrameRateChange, 300); - const onFrameRateChangeHandler = ({ target }) => { - const frameRate = parseFloat(target.value); - debouncedSetFrameRate(frameRate); - setFrameRate(frameRate); - }; - - const onPlayPauseChangeHandler = () => onPlayPauseChange(!isPlaying); + const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play'); - const action = { - false: { icon: 'old-play' }, - true: { icon: 'old-stop' }, + const handleSetFrameRate = (frameRate: number) => { + if (frameRate < minFrameRate || frameRate > maxFrameRate) { + return; + } + setFrameRate(frameRate); + debouncedSetFrameRate(frameRate); }; return ( -
- + onPlayPauseChange(!isPlaying)} + /> + + } > - - -
- -

{`${frameRate.toFixed( - 1 - )} fps`}

-
- +
handleSetFrameRate(frameRate - 1)} + > + +
+
+ {`${frameRate} FPS`} +
+
handleSetFrameRate(frameRate + 1)} + > + +
+
+ + - - + />
); }; diff --git a/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx b/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx deleted file mode 100644 index f53ba7112a9..00000000000 --- a/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx +++ /dev/null @@ -1,32 +0,0 @@ -import CinePlayer from '../CinePlayer'; -import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; -import { createComponentTemplate } from '../../../storybook/functions/create-component-story'; - -export const argTypes = { - component: CinePlayer, - title: 'Components/CinePlayer', -}; - - - -export const cineTemplate = createComponentTemplate(CinePlayer); - - - -- [Overview](#overview) -- [Props](#props) -- [Contribute](#contribute) - -## Overview - - - {cineTemplate.bind({})} - - -## Props - - - -## Contribute - -
diff --git a/platform/ui/src/components/Icon/getIcon.js b/platform/ui/src/components/Icon/getIcon.js index d1b1d886c4f..b4ebdf56297 100644 --- a/platform/ui/src/components/Icon/getIcon.js +++ b/platform/ui/src/components/Icon/getIcon.js @@ -3,6 +3,8 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import arrowLeft from './../../assets/icons/arrow-left.svg'; +import arrowLeftSmall from './../../assets/icons/arrow-left-small.svg'; +import arrowRightSmall from './../../assets/icons/arrow-right-small.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; import clipboard from './../../assets/icons/clipboard.svg'; @@ -11,6 +13,8 @@ import dottedCircle from './../../assets/icons/dotted-circle.svg'; import circledCheckmark from './../../assets/icons/circled-checkmark.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; import chevronLeft from './../../assets/icons/chevron-left.svg'; +import chevronNext from './../../assets/icons/chevron-next.svg'; +import chevronPrev from './../../assets/icons/chevron-prev.svg'; import chevronRight from './../../assets/icons/chevron-right.svg'; import eyeVisible from './../../assets/icons/eye-visible.svg'; import eyeHidden from './../../assets/icons/eye-hidden.svg'; @@ -18,6 +22,7 @@ import exclamation from './../../assets/icons/exclamation.svg'; import externalLink from './../../assets/icons/external-link.svg'; import groupLayers from './../../assets/icons/group-layers.svg'; import info from './../../assets/icons/info.svg'; +import infoAction from './../../assets/icons/info-action.svg'; import infoLink from './../../assets/icons/info-link.svg'; import launchArrow from './../../assets/icons/launch-arrow.svg'; import launchInfo from './../../assets/icons/launch-info.svg'; @@ -36,12 +41,19 @@ import settings from './../../assets/icons/settings.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'; +import statusAlert from './../../assets/icons/status-alert.svg'; +import statusLocked from './../../assets/icons/status-locked.svg'; +import statusTracked from './../../assets/icons/status-tracked.svg'; +import statusUntracked from './../../assets/icons/status-untracked.svg'; import tracked from './../../assets/icons/tracked.svg'; import unlink from './../../assets/icons/unlink.svg'; import checkboxChecked from './../../assets/icons/checkbox-checked.svg'; import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg'; +import iconClose from './../../assets/icons/icon-close.svg'; import iconNextInactive from './../../assets/icons/icon-next-inactive.svg'; import iconNext from './../../assets/icons/icon-next.svg'; +import iconPlay from './../../assets/icons/icon-play.svg'; +import iconPause from './../../assets/icons/icon-pause.svg'; import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg'; import iconPrev from './../../assets/icons/icon-prev.svg'; import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg'; @@ -107,6 +119,9 @@ import oldStop from './../../assets/icons/old-stop.svg'; const ICONS = { 'arrow-down': arrowDown, + 'arrow-left': arrowLeft, + 'arrow-left-small': arrowLeftSmall, + 'arrow-right-small': arrowRightSmall, calendar: calendar, cancel: cancel, clipboard: clipboard, @@ -115,12 +130,18 @@ const ICONS = { 'circled-checkmark': circledCheckmark, 'chevron-down': chevronDown, 'chevron-left': chevronLeft, + 'chevron-next': chevronNext, + 'chevron-prev': chevronPrev, 'chevron-right': chevronRight, 'eye-visible': eyeVisible, 'eye-hidden': eyeHidden, 'external-link': externalLink, 'group-layers': groupLayers, info: info, + 'icon-close': iconClose, + 'icon-play': iconPlay, + 'icon-pause': iconPause, + 'info-action': infoAction, 'info-link': infoLink, 'arrow-left': arrowLeft, 'launch-arrow': launchArrow, @@ -140,6 +161,10 @@ const ICONS = { settings: settings, 'sorting-active-down': sortingActiveDown, 'sorting-active-up': sortingActiveUp, + 'status-alert': statusAlert, + 'status-locked': statusLocked, + 'status-tracked': statusTracked, + 'status-untracked': statusUntracked, sorting: sorting, tracked: tracked, unlink: unlink, diff --git a/platform/ui/src/components/IconButton/IconButton.tsx b/platform/ui/src/components/IconButton/IconButton.tsx index d073443d790..a32df49a6b7 100644 --- a/platform/ui/src/components/IconButton/IconButton.tsx +++ b/platform/ui/src/components/IconButton/IconButton.tsx @@ -21,7 +21,7 @@ const disabledClasses = { const variantClasses = { text: { default: - 'text-white hover:bg-primary-light hover:text-black active:opacity-80 focus:bg-primary-light focus:text-black', + 'text-white hover:bg-primary-light hover:text-black active:opacity-80 focus:!bg-primary-light focus:text-black', primary: 'text-primary-main hover:bg-primary-main hover:text-white active:opacity-80 focus:bg-primary-main focus:text-white', secondary: diff --git a/platform/ui/src/components/InputRange/InputRange.tsx b/platform/ui/src/components/InputRange/InputRange.tsx index e79dcdce3a0..28e98b06f5e 100644 --- a/platform/ui/src/components/InputRange/InputRange.tsx +++ b/platform/ui/src/components/InputRange/InputRange.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import classNames from 'classnames'; import Typography from '../Typography'; import './InputRange.css'; @@ -23,6 +23,7 @@ const InputRange: React.FC<{ inputClassName?: string; labelClassName?: string; labelVariant?: string; + showLabel: boolean; }> = ({ value, onChange, @@ -34,12 +35,16 @@ const InputRange: React.FC<{ inputClassName, labelClassName, labelVariant, + showLabel = true, }) => { const [rangeValue, setRangeValue] = useState(value); + // Allow for the value property to update the range value. + useEffect(() => setRangeValue(value), [value]); + const handleChange = useCallback( e => { - const rangeValue = e.target.value; + const rangeValue = Number(e.target.value); setRangeValue(rangeValue); onChange(rangeValue); }, @@ -71,14 +76,16 @@ const InputRange: React.FC<{ id="myRange" step={step} /> - - {rangeValue} - {unit} - + {showLabel && ( + + {rangeValue} + {unit} + + )}
); }; diff --git a/platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayer.tsx b/platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayer.tsx new file mode 100644 index 00000000000..465368888f7 --- /dev/null +++ b/platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayer.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash.debounce'; +import { IconButton, Icon } from '../'; + +import './LegacyCinePlayerCustomInputRange.css'; + +const LegacyCinePlayer = ({ + isPlaying, + minFrameRate, + maxFrameRate, + stepFrameRate, + frameRate: defaultFrameRate, + onFrameRateChange, + onPlayPauseChange, + onClose, +}) => { + const [frameRate, setFrameRate] = useState(defaultFrameRate); + const debouncedSetFrameRate = debounce(onFrameRateChange, 300); + + const onFrameRateChangeHandler = ({ target }) => { + const frameRate = parseFloat(target.value); + debouncedSetFrameRate(frameRate); + setFrameRate(frameRate); + }; + + const onPlayPauseChangeHandler = () => onPlayPauseChange(!isPlaying); + + const action = { + false: { icon: 'old-play' }, + true: { icon: 'old-stop' }, + }; + + return ( +
+ + + +
+ +

{`${frameRate.toFixed( + 1 + )} fps`}

+
+ + + +
+ ); +}; + +const noop = () => {}; + +LegacyCinePlayer.defaultProps = { + isPlaying: false, + minFrameRate: 1, + maxFrameRate: 90, + stepFrameRate: 1, + frameRate: 24, + onPlayPauseChange: noop, + onFrameRateChange: noop, + onClose: noop, +}; + +LegacyCinePlayer.propTypes = { + /** Minimum value for range slider */ + minFrameRate: PropTypes.number.isRequired, + /** Maximum value for range slider */ + maxFrameRate: PropTypes.number.isRequired, + /** Increment range slider can "step" in either direction */ + stepFrameRate: PropTypes.number.isRequired, + frameRate: PropTypes.number.isRequired, + /** 'true' if playing, 'false' if paused */ + isPlaying: PropTypes.bool.isRequired, + onPlayPauseChange: PropTypes.func, + onFrameRateChange: PropTypes.func, + onClose: PropTypes.func, +}; + +export default LegacyCinePlayer; diff --git a/platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css b/platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayerCustomInputRange.css similarity index 66% rename from platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css rename to platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayerCustomInputRange.css index 19661d2b9b9..d321b7fa79e 100644 --- a/platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css +++ b/platform/ui/src/components/LegacyCinePlayer/LegacyCinePlayerCustomInputRange.css @@ -1,9 +1,9 @@ /* - * This is a custom input style scoped specifically to CinePlayer + * This is a custom input style scoped specifically to LegacyCinePlayer * written in plain CSS with color variables from tailwind * to avoid complex compatibility configuration. */ -.CinePlayer input[type='range'] { +.LegacyCinePlayer input[type='range'] { -webkit-appearance: none; background: transparent; width: 100%; @@ -11,11 +11,11 @@ z-index: 5; } -.CinePlayer input[type='range']:focus { +.LegacyCinePlayer input[type='range']:focus { outline: none; } -.CinePlayer input[type='range']::-webkit-slider-runnable-track { +.LegacyCinePlayer input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 2px; cursor: pointer; @@ -26,7 +26,7 @@ border: 0px solid #000000; } -.CinePlayer input[type='range']::-webkit-slider-thumb { +.LegacyCinePlayer input[type='range']::-webkit-slider-thumb { box-shadow: 0px 0px 0px #000000; border: 4px solid #000000; height: 18px; @@ -38,11 +38,11 @@ margin-top: -9px; } -.CinePlayer input[type='range']:focus::-webkit-slider-runnable-track { +.LegacyCinePlayer input[type='range']:focus::-webkit-slider-runnable-track { @apply bg-primary-light; } -.CinePlayer input[type='range']::-moz-range-track { +.LegacyCinePlayer input[type='range']::-moz-range-track { width: 100%; height: 2px; cursor: pointer; @@ -53,7 +53,7 @@ border: 0px solid #000000; } -.CinePlayer input[type='range']::-moz-range-thumb { +.LegacyCinePlayer input[type='range']::-moz-range-thumb { box-shadow: 0px 0px 0px #000000; border: 2px solid #000000; height: 12px; @@ -63,7 +63,7 @@ cursor: pointer; } -.CinePlayer input[type='range']::-ms-track { +.LegacyCinePlayer input[type='range']::-ms-track { width: 100%; height: 2px; cursor: pointer; @@ -73,21 +73,21 @@ color: transparent; } -.CinePlayer input[type='range']::-ms-fill-lower { +.LegacyCinePlayer input[type='range']::-ms-fill-lower { @apply bg-primary-light; border: 0px solid #000000; border-radius: 10px; box-shadow: 0px 0px 0px #000000; } -.CinePlayer input[type='range']::-ms-fill-upper { +.LegacyCinePlayer input[type='range']::-ms-fill-upper { @apply bg-primary-light; border: 0px solid #000000; border-radius: 10px; box-shadow: 0px 0px 0px #000000; } -.CinePlayer input[type='range']::-ms-thumb { +.LegacyCinePlayer input[type='range']::-ms-thumb { margin-top: 1px; box-shadow: 0px 0px 0px #000000; border: 4px solid #000000; @@ -98,10 +98,10 @@ cursor: pointer; } -.CinePlayer input[type='range']:focus::-ms-fill-lower { +.LegacyCinePlayer input[type='range']:focus::-ms-fill-lower { @apply bg-primary-light; } -.CinePlayer input[type='range']:focus::-ms-fill-upper { +.LegacyCinePlayer input[type='range']:focus::-ms-fill-upper { @apply bg-primary-light; } diff --git a/platform/ui/src/components/LegacyCinePlayer/__stories__/legacyCinePlayer.stories.mdx b/platform/ui/src/components/LegacyCinePlayer/__stories__/legacyCinePlayer.stories.mdx new file mode 100644 index 00000000000..35ad2c7f0fe --- /dev/null +++ b/platform/ui/src/components/LegacyCinePlayer/__stories__/legacyCinePlayer.stories.mdx @@ -0,0 +1,35 @@ +import LegacyCinePlayer from '../LegacyCinePlayer'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { createComponentTemplate } from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: LegacyCinePlayer, + title: 'Components/LegacyCinePlayer', +}; + + + +export const cineTemplate = createComponentTemplate(LegacyCinePlayer); + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + + + {cineTemplate.bind({})} + + +## Props + + + +## Contribute + +
diff --git a/platform/ui/src/components/LegacyCinePlayer/index.js b/platform/ui/src/components/LegacyCinePlayer/index.js new file mode 100644 index 00000000000..6344b92c3de --- /dev/null +++ b/platform/ui/src/components/LegacyCinePlayer/index.js @@ -0,0 +1,2 @@ +import LegacyCinePlayer from './LegacyCinePlayer'; +export default LegacyCinePlayer; diff --git a/platform/ui/src/components/LegacyPatientInfo/LegacyPatientInfo.tsx b/platform/ui/src/components/LegacyPatientInfo/LegacyPatientInfo.tsx new file mode 100644 index 00000000000..9c0957630ee --- /dev/null +++ b/platform/ui/src/components/LegacyPatientInfo/LegacyPatientInfo.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { Icon, Tooltip } from '../'; +import { useTranslation } from 'react-i18next'; + +const classes = { + infoHeader: 'text-base text-primary-light', + infoText: 'text-base text-white max-w-24 truncate', + firstRow: 'flex flex-col', + row: 'flex flex-col ml-4', +}; + +function LegacyPatientInfo({ + patientName, + patientSex, + patientAge, + MRN, + thickness, + spacing, + scanner, + isOpen, + showPatientInfoRef, +}) { + const { t } = useTranslation('PatientInfo'); + + while (patientAge.charAt(0) === '0') { + patientAge = patientAge.substr(1); + } + + return ( +
+ +
+ +
+
+ + {patientName} + +
+
+ + {t('Sex')} + + + {patientSex} + +
+
+ + {t('Age')} + + + {patientAge} + +
+
+ + {t('MRN')} + + + {MRN} + +
+
+
+
+ + {t('Thickness')} + + + {thickness} + +
+
+ + {t('Spacing')} + + + {spacing} + +
+
+ + {t('Scanner')} + + + {scanner} + +
+
+
+
+ ) + } + > +
+
+ + +
+
+ +
+ ); +} + +LegacyPatientInfo.propTypes = { + patientName: PropTypes.string, + patientSex: PropTypes.string, + patientAge: PropTypes.string, + MRN: PropTypes.string, + thickness: PropTypes.string, + spacing: PropTypes.string, + scanner: PropTypes.string, + isOpen: PropTypes.bool, + showPatientInfoRef: PropTypes.object, +}; + +export default LegacyPatientInfo; diff --git a/platform/ui/src/components/LegacyPatientInfo/index.js b/platform/ui/src/components/LegacyPatientInfo/index.js new file mode 100644 index 00000000000..d4506eeab4d --- /dev/null +++ b/platform/ui/src/components/LegacyPatientInfo/index.js @@ -0,0 +1,2 @@ +import LegacyPatientInfo from './LegacyPatientInfo'; +export default LegacyPatientInfo; diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.stories.tsx b/platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.stories.tsx similarity index 82% rename from platform/ui/src/components/ViewportActionBar/ViewportActionBar.stories.tsx rename to platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.stories.tsx index ec6d1a3b12a..fb76d1feac9 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.stories.tsx +++ b/platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.stories.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import ViewportActionBar from './ViewportActionBar'; +import LegacyViewportActionBar from './LegacyViewportActionBar'; export default { - component: ViewportActionBar, - title: 'Components/ViewportActionBar', + component: LegacyViewportActionBar, + title: 'Components/LegacyViewportActionBar', }; export const Default = () => (
- alert(`Series ${direction}`)} studyData={{ label: 'A', diff --git a/platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.tsx b/platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.tsx new file mode 100644 index 00000000000..5207d179c8a --- /dev/null +++ b/platform/ui/src/components/LegacyViewportActionBar/LegacyViewportActionBar.tsx @@ -0,0 +1,180 @@ +import React, { useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Icon, ButtonGroup, Button, LegacyCinePlayer } from '../'; +import useOnClickOutside from '../../utils/useOnClickOutside'; +import LegacyPatientInfo from '../LegacyPatientInfo'; +import { StringNumber } from '../../types'; + +const LegacyViewportActionBar = ({ + studyData, + showNavArrows, + showStatus, + showCine, + cineProps, + showPatientInfo: patientInfoVisibility, + onArrowsClick, + onDoubleClick, + getStatusComponent, +}) => { + const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + + const { + label, + useAltStyling, + studyDate, + currentSeries, + seriesDescription, + patientInformation, + } = studyData; + + const { + patientName, + patientSex, + patientAge, + MRN, + thickness, + spacing, + scanner, + } = patientInformation; + + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo); + const closePatientInfo = () => setShowPatientInfo(false); + const showPatientInfoRef = useRef(null); + const clickOutsideListener = useOnClickOutside( + showPatientInfoRef, + closePatientInfo + ); + + useEffect(() => { + if (showPatientInfo) { + clickOutsideListener.add(); + } else { + clickOutsideListener.remove(); + } + + return () => clickOutsideListener.remove(); + }, [clickOutsideListener, showPatientInfo]); + + const borderColor = useAltStyling ? '#365A6A' : '#1D205A'; + + let backgroundColor = '#020424'; + if (useAltStyling) { + backgroundColor = '#031923'; + } + + return ( +
e.preventDefault()} + > +
+
+ {label} + {showStatus && getStatusComponent()} +
+
+
+ {studyDate} + + S: {currentSeries} + +
+
+ {/* TODO: + This is tricky. Our "no-wrap" in truncate means this has a hard + length. The overflow forces ellipse. If we don't set max width + appropriately, this causes the ActionBar to overflow. + Can clean up by setting percentage widths + calc on parent + containers + */} +

+ {seriesDescription} +

+
+
+
+ {showNavArrows && !showCine && ( +
+ + + + +
+ )} + {showCine && !showNavArrows && ( +
+ +
+ )} +
+ +
+
+ ); +}; + +LegacyViewportActionBar.propTypes = { + onArrowsClick: PropTypes.func.isRequired, + showNavArrows: PropTypes.bool, + showCine: PropTypes.bool, + cineProps: PropTypes.object, + showPatientInfo: PropTypes.bool, + studyData: PropTypes.shape({ + // + useAltStyling: PropTypes.bool, + // + label: PropTypes.string.isRequired, + studyDate: PropTypes.string.isRequired, + currentSeries: StringNumber.isRequired, + seriesDescription: PropTypes.string.isRequired, + patientInformation: PropTypes.shape({ + patientName: PropTypes.string.isRequired, + patientSex: PropTypes.string.isRequired, + patientAge: PropTypes.string.isRequired, + MRN: PropTypes.string.isRequired, + thickness: PropTypes.string.isRequired, + spacing: PropTypes.string.isRequired, + scanner: PropTypes.string.isRequired, + }), + }).isRequired, + getStatusComponent: PropTypes.func.isRequired, +}; + +LegacyViewportActionBar.defaultProps = { + cineProps: {}, + showCine: false, + showStatus: true, + showNavArrows: true, + showPatientInfo: false, +}; + +export default LegacyViewportActionBar; diff --git a/platform/ui/src/components/LegacyViewportActionBar/index.js b/platform/ui/src/components/LegacyViewportActionBar/index.js new file mode 100644 index 00000000000..1a1a8959ccf --- /dev/null +++ b/platform/ui/src/components/LegacyViewportActionBar/index.js @@ -0,0 +1,2 @@ +import LegacyViewportActionBar from './LegacyViewportActionBar'; +export default LegacyViewportActionBar; diff --git a/platform/ui/src/components/PatientInfo/PatientInfo.tsx b/platform/ui/src/components/PatientInfo/PatientInfo.tsx index bb09389f9bc..615950c065a 100644 --- a/platform/ui/src/components/PatientInfo/PatientInfo.tsx +++ b/platform/ui/src/components/PatientInfo/PatientInfo.tsx @@ -119,16 +119,10 @@ function PatientInfo({ ) } > -
-
- - -
-
+
); diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx index 437d0cdad49..6167ec55df3 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationConfig.tsx @@ -13,6 +13,15 @@ const ActiveSegmentationConfig = ({ setFillAlpha, usePercentage, }) => { + const [ + useOutlineOpacityPercentage, + setUseOutlineOpacityPercentage, + ] = useState(usePercentage); + + const [useFillAlphaPercentage, setUseFillAlphaPercentage] = useState( + usePercentage + ); + return (
@@ -57,9 +66,12 @@ const ActiveSegmentationConfig = ({ minValue={0} maxValue={usePercentage ? 100 : 1} value={ - usePercentage ? config.outlineOpacity * 100 : config.outlineOpacity + useOutlineOpacityPercentage + ? config.outlineOpacity * 100 + : config.outlineOpacity } onChange={value => { + setUseOutlineOpacityPercentage(false); dispatch({ type: 'SET_OUTLINE_OPACITY', payload: { @@ -78,8 +90,11 @@ const ActiveSegmentationConfig = ({ { + setUseFillAlphaPercentage(false); dispatch({ type: 'SET_FILL_ALPHA', payload: { @@ -125,6 +140,11 @@ const InactiveSegmentationConfig = ({ setFillAlphaInactive, usePercentage, }) => { + const [ + useFillAlphaInactivePercentage, + setUseFillInactivePercentage, + ] = useState(usePercentage); + return (
{ + setUseFillInactivePercentage(false); dispatch({ type: 'SET_FILL_ALPHA_INACTIVE', payload: { diff --git a/platform/ui/src/components/SplitButton/SplitButton.tsx b/platform/ui/src/components/SplitButton/SplitButton.tsx index 00c354098cc..1aec782d579 100644 --- a/platform/ui/src/components/SplitButton/SplitButton.tsx +++ b/platform/ui/src/components/SplitButton/SplitButton.tsx @@ -26,7 +26,7 @@ const classes = { baseClasses.Button, !isExpanded && !primary.isActive && - 'hover:bg-primary-dark hover:border-primary-dark' + 'hover:!bg-primary-dark hover:border-primary-dark' ), Interface: 'h-full flex flex-row items-center', Primary: ({ primary, isExpanded }) => @@ -34,7 +34,7 @@ const classes = { baseClasses.Primary, primary.isActive ? isExpanded - ? 'border-primary-dark !bg-primary-dark hover:border-primary-dark text-primary-light' + ? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light' : `${ primary.isToggle ? 'border-secondary-dark bg-secondary-light' @@ -45,7 +45,7 @@ const classes = { ${ isExpanded ? 'border-primary-dark bg-primary-dark !text-primary-light' - : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:bg-primary-dark hover:border-primary-dark focus:!text-black' + : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black' } ` ), diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx index b2fb20da691..5e9a2c14188 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -24,13 +24,13 @@ const ToolbarButton = ({ const classes = { tool: isActive ? 'text-black' - : 'text-common-bright hover:bg-primary-dark hover:text-primary-light', + : 'text-common-bright hover:!bg-primary-dark hover:text-primary-light', toggle: isActive - ? 'text-[#348CFD]' - : 'text-common-bright hover:bg-primary-dark hover:text-primary-light', + ? '!text-[#348CFD]' + : 'text-common-bright hover:!bg-primary-dark hover:text-primary-light', action: isActive ? 'text-black' - : 'text-common-bright hover:bg-primary-dark hover:text-primary-light', + : 'text-common-bright hover:!bg-primary-dark hover:text-primary-light', }; const bgClasses = { diff --git a/platform/ui/src/components/Tooltip/Tooltip.tsx b/platform/ui/src/components/Tooltip/Tooltip.tsx index d44f73409e6..7487e9bfa38 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.tsx +++ b/platform/ui/src/components/Tooltip/Tooltip.tsx @@ -23,6 +23,11 @@ const arrowPositionStyle = { right: -15, transform: 'rotate(-270deg)', }, + top: { + bottom: -15, + left: '50%', + transform: 'translateX(-50%) rotate(180deg)', + }, }; const Tooltip = ({ @@ -69,7 +74,7 @@ const Tooltip = ({ >
{typeof content === 'string' ? t(content) : content} - +
@@ -107,6 +112,7 @@ Tooltip.propTypes = { 'bottom-right', 'left', 'right', + 'top', ]), isSticky: PropTypes.bool, tight: PropTypes.bool, diff --git a/platform/ui/src/components/Tooltip/tooltip.css b/platform/ui/src/components/Tooltip/tooltip.css index 28c0b399108..06de8e4b518 100644 --- a/platform/ui/src/components/Tooltip/tooltip.css +++ b/platform/ui/src/components/Tooltip/tooltip.css @@ -13,12 +13,29 @@ transform: translateX(-50%); } +.tooltip.tooltip-top .tooltip-box::before { + @apply absolute z-10 bg-primary-dark; + content: ''; + width: 14px; + height: 1px; + bottom: -1px; + left: 50%; + transform: translateX(-50%); +} + .tooltip.tooltip-bottom { @apply pt-2 mt-1; left: 50%; transform: translateX(-50%); } +.tooltip.tooltip-top { + @apply pb-2 mb-1; + left: 50%; + transform: translateX(-50%); + bottom: 100%; +} + .tooltip.tooltip-bottom-left { @apply pt-2 mt-1; left: 0; diff --git a/platform/ui/src/components/Viewport/Viewport.tsx b/platform/ui/src/components/Viewport/Viewport.tsx index e2c599238af..a9576c1c945 100644 --- a/platform/ui/src/components/Viewport/Viewport.tsx +++ b/platform/ui/src/components/Viewport/Viewport.tsx @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ViewportActionBar, Notification } from '../'; +import { LegacyViewportActionBar, Notification } from '../'; const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => { return (
- diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx index 4490b57df01..cbb5df02291 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx @@ -1,31 +1,31 @@ -import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Icon, ButtonGroup, Button, CinePlayer } from '../'; +import React, { + MouseEventHandler, + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { Icon } from '..'; +import { useResizeObserver } from '../../hooks'; import useOnClickOutside from '../../utils/useOnClickOutside'; import PatientInfo from '../PatientInfo'; -import { StringNumber } from '../../types'; + +export type ViewportActionBarProps = { + studyData: any; + onArrowsClick: (arrow: string) => void; + onDoubleClick: MouseEventHandler; + getStatusComponent: () => ReactElement; +}; const ViewportActionBar = ({ studyData, - showNavArrows, - showStatus, - showCine, - cineProps, - showPatientInfo: patientInfoVisibility, onArrowsClick, onDoubleClick, getStatusComponent, -}) => { - const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); - - const { - label, - useAltStyling, - studyDate, - currentSeries, - seriesDescription, - patientInformation, - } = studyData; +}: ViewportActionBarProps): JSX.Element => { + const { label, studyDate, seriesDescription, patientInformation } = studyData; const { patientName, @@ -37,11 +37,43 @@ const ViewportActionBar = ({ scanner, } = patientInformation; + // The minimum width that the viewport must be to show the next/prev arrows. + const arrowsPresentViewportMinWidth = 300; + + // The space left between the study date and the patient info icon when the series description text is zero width. + // With a zero width series description what we have left is: + // - a separator (17px) + // - patient info icon left padding (4px) + // - series description right margin (4px) + const zeroWidthSeriesDescriptionSpace = 25; + + const separatorClasses = 'border-l py-2 mx-2 border-secondary-light'; + const textEllipsisClasses = 'overflow-hidden shrink text-ellipsis'; + const arrowClasses = + 'cursor-pointer shrink-0 mr-2 text-white hover:text-primary-light'; + + const componentRootElemRef = (elem: HTMLElement) => { + setComponentRootElem(elem); + }; + const studyDateElemRef = useRef(null); + const seriesDescElemRef = useRef(null); + const showPatientInfoElemRef = useRef(null); + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo); const closePatientInfo = () => setShowPatientInfo(false); - const showPatientInfoRef = useRef(null); + + const [showPatientInfo, setShowPatientInfo] = useState(false); + const [showSeriesDesc, setShowSeriesDesc] = useState(true); + const [showArrows, setShowArrows] = useState(true); + const [componentRootElem, setComponentRootElem] = useState(null); + + const studyDateClasses = () => + `text-white ${showSeriesDesc ? '' : `mr-1 ${textEllipsisClasses}`}`; + + const patientInfoClasses = () => (showArrows ? '' : 'pl-1 ml-auto'); + const clickOutsideListener = useOnClickOutside( - showPatientInfoRef, + showPatientInfoElemRef, closePatientInfo ); @@ -55,79 +87,81 @@ const ViewportActionBar = ({ return () => clickOutsideListener.remove(); }, [clickOutsideListener, showPatientInfo]); - const borderColor = useAltStyling ? '#365A6A' : '#1D205A'; + /** + * Handles what gets hidden and what gets shown during a resize of the viewport. + */ + const resizeCallback = useCallback(() => { + if (!componentRootElem) { + return; + } - let backgroundColor = '#020424'; - if (useAltStyling) { - backgroundColor = '#031923'; - } + const componentRootElemBBox = componentRootElem.getBoundingClientRect(); + + // Show or hide the arrows based on the viewport/root element width. + if (componentRootElemBBox.width < arrowsPresentViewportMinWidth) { + setShowArrows(false); + } else { + setShowArrows(true); + } + + const studyDateElemBBox = studyDateElemRef.current.getBoundingClientRect(); + const showPatientInfoElemBBox = showPatientInfoElemRef.current.getBoundingClientRect(); + + if ( + showPatientInfoElemBBox.left - studyDateElemBBox.right <= + zeroWidthSeriesDescriptionSpace + ) { + // The area to display the series description is zero, so don't show the series description element. + setShowSeriesDesc(false); + } else { + setShowSeriesDesc(true); + } + }, [componentRootElem]); + + useResizeObserver(componentRootElem, resizeCallback); return (
e.preventDefault()} > -
-
- {label} - {showStatus && getStatusComponent()} -
-
-
- {studyDate} - - S: {currentSeries} - -
-
- {/* TODO: - This is tricky. Our "no-wrap" in truncate means this has a hard - length. The overflow forces ellipse. If we don't set max width - appropriately, this causes the ActionBar to overflow. - Can clean up by setting percentage widths + calc on parent - containers - */} -

- {seriesDescription} -

-
-
-
- {showNavArrows && !showCine && ( -
- - - - -
+ {getStatusComponent()} + {!!label?.length && ( + {label} )} - {showCine && !showNavArrows && ( -
- -
+
+ + {studyDate} + + {showSeriesDesc && ( + <> +
+ + {seriesDescription} + + )} -
+ {showArrows && ( + <> + onArrowsClick('left')} + /> + onArrowsClick('right')} + /> + + )} +
+ * Care is taken to disconnect the ResizeObserver whenever either the element or the callback change. + * + * @param elem the element to listen for resizing + * @param callback the callback to invoke when the element is resized + */ +const useResizeObserver = ( + elem: HTMLElement, + callback: ResizeObserverCallback +): void => { + useEffect(() => { + if (!elem || !callback) { + return; + } + + const resizeObserver = new ResizeObserver(callback); + resizeObserver.observe(elem); + + return () => resizeObserver.disconnect(); + }, [elem, callback]); +}; + +export default useResizeObserver; diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 3e14405c7c5..9315e4c11c3 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -62,6 +62,8 @@ export { InputText, Label, LayoutSelector, + LegacyCinePlayer, + LegacyViewportActionBar, LoadingIndicatorProgress, MeasurementTable, Modal, diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index b8863ac2e91..3a5b1d84279 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -55,11 +55,17 @@ module.exports = { customgreen: { 100: '#05D97C', + 200: '#0FD97C', }, customblue: { 100: '#c4fdff', 200: '#38daff', + 300: '#1D204D', + }, + + customgray: { + 100: '#262943', }, gray: { @@ -257,14 +263,15 @@ module.exports = { full: '100%', screen: '100vh', }), - inset: { + inset: theme => ({ + ...theme('spacing'), '0': '0', auto: 'auto', full: '100%', viewport: '0.5rem', '1/2': '50%', 'viewport-scrollbar': '1.3rem', - }, + }), letterSpacing: { tighter: '-0.05em', tight: '-0.025em', diff --git a/platform/viewer/tailwind.config.js b/platform/viewer/tailwind.config.js index ca024d27397..535239ca572 100644 --- a/platform/viewer/tailwind.config.js +++ b/platform/viewer/tailwind.config.js @@ -63,11 +63,17 @@ module.exports = { customgreen: { 100: '#05D97C', + 200: '#0FD97C', }, customblue: { 100: '#c4fdff', 200: '#38daff', + 300: '#1D204D', + }, + + customgray: { + 100: '#262943', }, gray: { @@ -305,14 +311,15 @@ module.exports = { screen: '100vh', inherit: 'inherit', }), - inset: { + inset: theme => ({ + ...theme('spacing'), '0': '0', auto: 'auto', full: '100%', viewport: '0.5rem', '1/2': '50%', 'viewport-scrollbar': '1.3rem', - }, + }), minHeight: theme => ({ ...theme('spacing'), '0': '0', diff --git a/yarn.lock b/yarn.lock index 493f956c85f..8566e80789f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17919,9 +17919,27 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.2 picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.15, postcss@^8.3.11, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.18, postcss@^8.4.19: +postcss@^8.2.15, postcss@^8.3.11, postcss@^8.3.5: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8.4.14: + version "8.4.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8.4.17, postcss@^8.4.18, postcss@^8.4.19: version "8.4.21" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== dependencies: nanoid "^3.3.4" @@ -21030,7 +21048,7 @@ table@^6.0.9: tailwindcss@3.2.4: version "3.2.4" - resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250" integrity sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ== dependencies: arg "^5.0.2"