diff --git a/.circleci/config.yml b/.circleci/config.yml index ef8cf92ec..de35a810f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -462,7 +462,7 @@ workflows: - BUILD - DOCKER_BETA_PUBLISH: requires: - - NPM_PUBLISH + - DOCKER_BETA_PUBLISH_ARM - DOCKER_BETA_PUBLISH_ARM: requires: - NPM_PUBLISH @@ -487,7 +487,7 @@ workflows: - HOLD_FOR_APPROVAL - DOCKER_RELEASE_PUBLISH: requires: - - NPM_PUBLISH + - DOCKER_RELEASE_PUBLISH_ARM - DOCKER_RELEASE_PUBLISH_ARM: requires: - NPM_PUBLISH diff --git a/bun.lockb b/bun.lockb index 5d927c60c..b92e3aedb 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx index 959d8dbfe..59a3609a4 100644 --- a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx +++ b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui'; +import { LoadingIndicatorTotalPercent } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; function OHIFCornerstonePMAPViewport(props: withAppTypes) { const { diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 662541ff1..64b233252 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui'; +import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import promptHydrateRT from '../utils/promptHydrateRT'; import _getStatusComponent from './_getStatusComponent'; diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 340afad16..69c0d4f6f 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui'; +import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import promptHydrateSEG from '../utils/promptHydrateSEG'; import _getStatusComponent from './_getStatusComponent'; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx index f028b225e..bd6e7d0e8 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -5,9 +5,10 @@ import { ExtensionManager, useToolbar } from '@ohif/core'; import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; +import { ViewportActionArrows } from '@ohif/ui'; import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; -import { useViewportGrid, ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'; diff --git a/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx b/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx index aaf6e78ca..4b5a93eb7 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; -import { useCine, useViewportGrid } from '@ohif/ui'; +import { useCine } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core'; import { utilities as cstUtils } from '@cornerstonejs/tools'; import DynamicVolumeControls from './DynamicVolumeControls'; diff --git a/extensions/cornerstone/src/components/ActiveViewportWindowLevel/ActiveViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ActiveViewportWindowLevel/ActiveViewportWindowLevel.tsx index 295d6bd60..404c9cf57 100644 --- a/extensions/cornerstone/src/components/ActiveViewportWindowLevel/ActiveViewportWindowLevel.tsx +++ b/extensions/cornerstone/src/components/ActiveViewportWindowLevel/ActiveViewportWindowLevel.tsx @@ -1,6 +1,6 @@ import React, { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel'; const ActiveViewportWindowLevel = ({ servicesManager }: withAppTypes): ReactElement => { diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx index c4196f9d7..77dd1dd89 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx @@ -1,7 +1,8 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { AllInOneMenu, useViewportGrid } from '@ohif/ui'; +import { AllInOneMenu } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { Colormap } from './Colormap'; import { Colorbar } from './Colorbar'; import { setViewportColorbar } from './Colorbar'; diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 3652b31bb..ed86b2ef7 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { MeasurementTable } from '@ohif/ui-next'; import debounce from 'lodash.debounce'; import { useMeasurements } from '../hooks/useMeasurements'; diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 4dd7225b2..81440b4f8 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { useImageViewer, useViewportGrid } from '@ohif/ui'; +import { useImageViewer } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { StudyBrowser } from '@ohif/ui-next'; import { utils } from '@ohif/core'; import { useNavigate } from 'react-router-dom'; diff --git a/extensions/dicom-microscopy/src/getPanelModule.tsx b/extensions/dicom-microscopy/src/getPanelModule.tsx index 8cbceb52b..6500e8df9 100644 --- a/extensions/dicom-microscopy/src/getPanelModule.tsx +++ b/extensions/dicom-microscopy/src/getPanelModule.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { Types } from '@ohif/core'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import MicroscopyPanel from './components/MicroscopyPanel/MicroscopyPanel'; // TODO: diff --git a/extensions/dicom-microscopy/src/index.tsx b/extensions/dicom-microscopy/src/index.tsx index 3491b9a5c..4efd61610 100644 --- a/extensions/dicom-microscopy/src/index.tsx +++ b/extensions/dicom-microscopy/src/index.tsx @@ -4,7 +4,7 @@ import getPanelModule from './getPanelModule'; import getCommandsModule from './getCommandsModule'; import { Types } from '@ohif/core'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler'; import MicroscopyService from './services/MicroscopyService'; import { useResizeDetector } from 'react-resize-detector'; @@ -43,7 +43,6 @@ const extension: Types.Extensions.Extension = { * that is provided by the Cornerstone extension in OHIF. */ getViewportModule({ servicesManager, extensionManager, commandsManager }) { - /** * * @param props {*} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index cd5b6f22b..45d248ec4 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Machine } from 'xstate'; import { useMachine } from '@xstate/react'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default'; import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine'; import promptBeginTracking from './promptBeginTracking'; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index 619c5d653..147e9c651 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { PanelMeasurement } from '@ohif/extension-cornerstone'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { StudySummary } from '@ohif/ui-next'; import { Button, Icons } from '@ohif/ui-next'; import { DicomMetadataStore, utils } from '@ohif/core'; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 9ed5e1e91..699f013b4 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import { utils } from '@ohif/core'; -import { useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui'; +import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import { StudyBrowser } from '@ohif/ui-next'; import { useTrackedMeasurements } from '../../getContextModule'; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 1d08250fe..72c489242 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { ViewportActionArrows, useViewportGrid } from '@ohif/ui'; -import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; +import { ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; import { annotation } from '@cornerstonejs/tools'; import { useTrackedMeasurements } from './../getContextModule'; diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 253080bd7..75290c53c 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -19,7 +19,6 @@ import { ModalProvider, ThemeWrapper, ViewportDialogProvider, - ViewportGridProvider, CineProvider, UserAuthenticationProvider, ToolboxProvider, @@ -27,6 +26,7 @@ import { import { ThemeWrapper as ThemeWrapperNext, NotificationProvider, + ViewportGridProvider, TooltipProvider, } from '@ohif/ui-next'; // Viewer Project diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 0c0f37947..66d7d2e60 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useCallback, useRef } from 'react'; import { useResizeDetector } from 'react-resize-detector'; import { Types, MeasurementService } from '@ohif/core'; -import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; +import { ViewportGrid, ViewportPane } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; import EmptyViewport from './EmptyViewport'; import classNames from 'classnames'; import { useAppConfig } from '@state'; diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json index d030bb5b0..f43bd2ce5 100644 --- a/platform/ui-next/package.json +++ b/platform/ui-next/package.json @@ -29,6 +29,7 @@ "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", diff --git a/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx b/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 000000000..149596b47 --- /dev/null +++ b/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,187 @@ +import * as React from 'react'; +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; + +import { cn } from '../../lib/utils'; +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = 'ContextMenuShortcut'; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/ContextMenu/index.ts b/platform/ui-next/src/components/ContextMenu/index.ts new file mode 100644 index 000000000..36ed0d4cc --- /dev/null +++ b/platform/ui-next/src/components/ContextMenu/index.ts @@ -0,0 +1,35 @@ +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} from './ContextMenu'; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/Viewport/PatientInfo.tsx b/platform/ui-next/src/components/Viewport/PatientInfo.tsx new file mode 100644 index 000000000..5be179878 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/PatientInfo.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '../../components'; + +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', +}; + +/** + * A small info icon that, when clicked, can reveal additional patient details in a tooltip overlay. + */ +function PatientInfo({ + patientName, + patientSex, + patientAge, + MRN, + thickness, + thicknessUnits, + spacing, + scanner, + isOpen, + showPatientInfoRef, +}) { + const { t } = useTranslation('PatientInfo'); + + // strip leading '0' from age if present + while (patientAge.charAt(0) === '0') { + patientAge = patientAge.substr(1); + } + + return ( +
+ + + + + {isOpen && ( + +
+
+ +
+
+ + {patientName} + +
+
+ {t('Sex')} + + {patientSex} + +
+
+ {t('Age')} + + {patientAge} + +
+
+ {t('MRN')} + + {MRN} + +
+
+
+
+ {t('Thickness')} + + {thicknessUnits ? `${thickness}${thicknessUnits}` : `${thickness}`} + +
+
+ {t('Spacing')} + + {spacing} + +
+
+ {t('Scanner')} + + {scanner} + +
+
+
+
+
+ )} +
+
+ ); +} + +PatientInfo.propTypes = { + patientName: PropTypes.string, + patientSex: PropTypes.string, + patientAge: PropTypes.string, + MRN: PropTypes.string, + thickness: PropTypes.string, + thicknessUnits: PropTypes.string, + spacing: PropTypes.string, + scanner: PropTypes.string, + isOpen: PropTypes.bool, + showPatientInfoRef: PropTypes.object, +}; + +export { PatientInfo }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx b/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx new file mode 100644 index 000000000..96b41ee44 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import { Icons } from '@ohif/ui-next'; + +const arrowClasses = + 'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded'; + +/** + * A small set of left/right arrow icons for stepping through slices or series. + */ +function ViewportActionArrows({ onArrowsClick, className }) { + return ( +
+
+ onArrowsClick(-1)} /> +
+
+ onArrowsClick(1)} /> +
+
+ ); +} + +ViewportActionArrows.propTypes = { + onArrowsClick: PropTypes.func.isRequired, + className: PropTypes.string, +}; + +export { ViewportActionArrows }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx b/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx new file mode 100644 index 000000000..0de1d45fe --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx @@ -0,0 +1,128 @@ +import React, { + MouseEventHandler, + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; + +import { Icons } from '@ohif/ui-next'; +import PatientInfo from './PatientInfo'; + +/** + * This is the modern Viewport Action Bar, showing patient info, series date, + * series description, and optional next/prev arrows if there's enough screen width. + */ +type ViewportActionBarProps = { + studyData: any; + onArrowsClick: (arrow: string) => void; + onDoubleClick: MouseEventHandler; + getStatusComponent: () => ReactElement; +}; + +function ViewportActionBar({ + studyData, + onArrowsClick, + onDoubleClick, + getStatusComponent, +}: ViewportActionBarProps): JSX.Element { + const { label, studyDate, seriesDescription, patientInformation } = studyData; + const { patientName, patientSex, patientAge, MRN, thickness, thicknessUnits, spacing, scanner } = + patientInformation; + + const [showPatientInfo, setShowPatientInfo] = useState(false); + const showPatientInfoElemRef = useRef(null); + const { t } = useTranslation(); + + // handle click outside to close patient info + const handleClickOutside = useCallback( + (evt: MouseEvent) => { + if ( + showPatientInfo && + showPatientInfoElemRef.current && + !showPatientInfoElemRef.current.contains(evt.target as Node) + ) { + setShowPatientInfo(false); + } + }, + [showPatientInfoElemRef, showPatientInfo] + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return ( +
+ {getStatusComponent()} + {!!label?.length && {label}} +
+ + {studyDate} + +
+ {seriesDescription} + {/* Prev/Next icons */} + onArrowsClick('left')} + /> + onArrowsClick('right')} + /> + {/* Patient Info */} +
setShowPatientInfo(!showPatientInfo)}> + +
+
+ ); +} + +ViewportActionBar.propTypes = { + onArrowsClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func, + studyData: PropTypes.shape({ + label: PropTypes.string.isRequired, + studyDate: PropTypes.string.isRequired, + seriesDescription: PropTypes.string.isRequired, + patientInformation: PropTypes.shape({ + patientName: PropTypes.string, + patientSex: PropTypes.string, + patientAge: PropTypes.string, + MRN: PropTypes.string, + thickness: PropTypes.string, + thicknessUnits: PropTypes.string, + spacing: PropTypes.string, + scanner: PropTypes.string, + }), + }).isRequired, + getStatusComponent: PropTypes.func.isRequired, +}; + +export { ViewportActionBar }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx b/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx new file mode 100644 index 000000000..d99421b94 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * A button that can trigger commands when clicked. + */ +function ViewportActionButton({ onInteraction, commands, id, children }) { + return ( +
{ + onInteraction({ + itemId: id, + commands, + }); + }} + > + {children} +
+ ); +} + +ViewportActionButton.propTypes = { + id: PropTypes.string, + onInteraction: PropTypes.func.isRequired, + commands: PropTypes.array, + children: PropTypes.node, +}; + +export { ViewportActionButton }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx new file mode 100644 index 000000000..f02d4ff70 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +/** + * A small container that can render multiple "corner" items (like icons, status) + * in each corner of the viewport: top-left, top-right, bottom-left, bottom-right. + */ +export enum ViewportActionCornersLocations { + topLeft, + topRight, + bottomLeft, + bottomRight, +} + +const commonClasses = 'pointer-events-auto flex items-center gap-1'; +const locationClasses = { + [ViewportActionCornersLocations.topLeft]: classNames( + commonClasses, + 'absolute top-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.topRight]: classNames( + commonClasses, + 'absolute top-[4px] right-[4px] right-viewport-scrollbar' + ), + [ViewportActionCornersLocations.bottomLeft]: classNames( + commonClasses, + 'absolute bottom-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.bottomRight]: classNames( + commonClasses, + 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' + ), +}; + +function ViewportActionCorners({ cornerComponents }) { + if (!cornerComponents) { + return null; + } + + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + }} + > + {Object.entries(cornerComponents).map(([location, locationArray]) => ( +
+ {locationArray.map(componentInfo => ( +
{componentInfo.component}
+ ))} +
+ ))} +
+ ); +} + +ViewportActionCorners.propTypes = { + cornerComponents: PropTypes.object.isRequired, +}; + +export { ViewportActionCorners }; diff --git a/platform/ui-next/src/components/Viewport/ViewportGrid.tsx b/platform/ui-next/src/components/Viewport/ViewportGrid.tsx new file mode 100644 index 000000000..4c00a102a --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportGrid.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * A minimal top-level container that organizes multiple + * children in a grid. Typically driven by a layout config. + */ +function ViewportGrid({ numRows, numCols, layoutType, children }) { + return ( +
+ {children} +
+ ); +} + +ViewportGrid.propTypes = { + numRows: PropTypes.number.isRequired, + numCols: PropTypes.number.isRequired, + layoutType: PropTypes.string, + children: PropTypes.arrayOf(PropTypes.node).isRequired, +}; + +export { ViewportGrid }; diff --git a/platform/ui-next/src/components/Viewport/ViewportOverlay.css b/platform/ui-next/src/components/Viewport/ViewportOverlay.css new file mode 100644 index 000000000..e8aebd0be --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportOverlay.css @@ -0,0 +1,22 @@ +.imageViewerViewport.empty ~ .ViewportOverlay { + display: none; +} +.ViewportOverlay { + color: #9ccef9; +} +.ViewportOverlay .overlay-element { + position: absolute; + font-weight: 400; + text-shadow: 1px 1px #000; + pointer-events: none; +} +.overlay-top { + top: 28px; +} +.overlay-bottom { + bottom: 3px; +} + +.overlay-text { + text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75); +} diff --git a/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx b/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx new file mode 100644 index 000000000..5164142ad --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import './ViewportOverlay.css'; + +/** + * Renders text overlays (top-left, top-right, bottom-left, bottom-right) + * around the active viewport for metadata or status messages. + * + * The parent is responsible for styling offsets. + */ +const classes = { + topLeft: 'overlay-top left-viewport', + topRight: 'overlay-top right-viewport-scrollbar', + bottomRight: 'overlay-bottom right-viewport-scrollbar', + bottomLeft: 'overlay-bottom left-viewport', +}; + +function ViewportOverlay({ + topLeft, + topRight, + bottomRight, + bottomLeft, + color = 'text-primary-light', +}) { + const overlay = 'absolute pointer-events-none viewport-overlay'; + + return ( +
+
+ {topLeft} +
+
+ {topRight} +
+
+ {bottomRight} +
+
+ {bottomLeft} +
+
+ ); +} + +ViewportOverlay.propTypes = { + topLeft: PropTypes.node, + topRight: PropTypes.node, + bottomRight: PropTypes.node, + bottomLeft: PropTypes.node, + color: PropTypes.string, +}; + +export { ViewportOverlay }; diff --git a/platform/ui-next/src/components/Viewport/ViewportPane.tsx b/platform/ui-next/src/components/Viewport/ViewportPane.tsx new file mode 100644 index 000000000..398b84546 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportPane.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { useDrop } from 'react-dnd'; + +/** + * The "pane" that encloses a Cornerstone or other type of Viewport. This handles + * drag-and-drop for display sets, activation on click, etc. + */ +function ViewportPane({ + children, + className, + customStyle, + isActive, + onDrop, + onDoubleClick, + onInteraction = () => {}, + acceptDropsFor, +}) { + let dropElement = null; + + const [{ isHovered, isHighlighted }, drop] = useDrop({ + accept: acceptDropsFor, + drop: (droppedItem, monitor) => { + if (monitor.canDrop() && monitor.isOver() && onDrop) { + onInteraction(); + onDrop(droppedItem); + } + }, + collect: monitor => ({ + isHighlighted: monitor.canDrop(), + isHovered: monitor.isOver(), + }), + }); + + const focus = () => { + if (dropElement) { + dropElement.focus(); + } + }; + + const onInteractionHandler = event => { + focus(); + onInteraction(event); + }; + + const refHandler = element => { + drop(element); + dropElement = element; + }; + + return ( +
+
+ {children} +
+
+ ); +} + +ViewportPane.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + isActive: PropTypes.bool.isRequired, + acceptDropsFor: PropTypes.string.isRequired, + onDrop: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func, + customStyle: PropTypes.object, +}; + +export { ViewportPane }; diff --git a/platform/ui-next/src/components/Viewport/index.ts b/platform/ui-next/src/components/Viewport/index.ts new file mode 100644 index 000000000..feb9a0a95 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/index.ts @@ -0,0 +1,8 @@ +export { ViewportActionButton } from './ViewportActionButton'; +export { PatientInfo } from './PatientInfo'; +export { ViewportActionBar } from './ViewportActionBar'; +export { ViewportActionArrows } from './ViewportActionArrows'; +export { ViewportPane } from './ViewportPane'; +export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners'; +export { ViewportOverlay } from './ViewportOverlay'; +export { ViewportGrid } from './ViewportGrid'; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index 42ef70024..20e613921 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -79,6 +79,17 @@ import { StudySummary } from './StudySummary'; import { ErrorBoundary } from './Errorboundary'; import { Header } from './Header'; import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card'; +import { + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, +} from './Viewport'; export { ErrorBoundary, @@ -183,5 +194,14 @@ export { CardTitle, CardDescription, CardContent, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, Clipboard, }; diff --git a/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx new file mode 100644 index 000000000..3a63c9684 --- /dev/null +++ b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx @@ -0,0 +1,522 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useReducer, + ReactNode, +} from 'react'; +import merge from 'lodash.merge'; + +import PropTypes from 'prop-types'; +import { ViewportGridService, utils } from '@ohif/core'; + +const DEFAULT_STATE: AppTypes.ViewportGrid.State = { + activeViewportId: null, + layout: { + numRows: 0, + numCols: 0, + layoutType: 'grid', + }, + // this flag is used to determine if the hanging protocol layout is active + // so that we can inherit the viewport options from the previous state + // otherwise we will not allow that. Basically the issue is that we need + // to be able to come out of the hanging protocol layout and go back to the + // regular layout e.g., if we are in the MPR hanging protocol, and someone use + // 1x1 layout by custom layout selector, there is no way to drag and drop + // a non-reconstructible series to the viewport since it will always + // inherit the hanging protocol layout options (volume viewport), + // so we need to be able to switch back to the regular layout. + isHangingProtocolLayout: false, + // Viewports structure has been changed to Map (previously it was + // tied to the viewportIndex which caused multiple issues. Now we have + // moved completely to viewportId which is unique for each viewport. + viewports: new Map( + Object.entries({ + default: { + viewportId: 'default', + displaySetInstanceUIDs: [], + isReady: false, + viewportOptions: { + viewportId: 'default', + }, + displaySetSelectors: [], + displaySetOptions: [{}], + x: 0, // left + y: 0, // top + width: 100, + height: 100, + viewportLabel: null, + }, + }) + ), +}; + +const determineActiveViewportId = ( + state: AppTypes.ViewportGrid.State, + newViewports: Map +) => { + const { activeViewportId } = state; + const currentActiveViewport = state.viewports.get(activeViewportId); + + if (!currentActiveViewport) { + // if there is no active viewport, we should just return the first viewport + const firstViewport = newViewports.values().next().value; + return firstViewport.viewportOptions.viewportId; + } + + // for the new viewports, we should rank them by the displaySetInstanceUIDs + // they are displaying and the orientation then we can find the active viewport + const currentActiveDisplaySetInstanceUIDs = currentActiveViewport.displaySetInstanceUIDs; + + // This doesn't take into account where stack viewport is converting to volumeViewport + // since in stack viewport we don't have a concept of "orientation" as a string + // maybe we should calculate the orientation based on the active imageId + // so that we can compare it with the new viewports (which might be volume viewports) + // and find the best match + const currentOrientation = currentActiveViewport.viewportOptions.orientation; + + const filteredNewViewports = Array.from(newViewports.values()).filter( + viewport => viewport.displaySetInstanceUIDs?.length > 0 + ); + + const sortedViewports = Array.from(filteredNewViewports.values()).sort((a, b) => { + // Compare orientations + const aOrientationMatch = a.viewportOptions.orientation === currentOrientation; + const bOrientationMatch = b.viewportOptions.orientation === currentOrientation; + if (aOrientationMatch !== bOrientationMatch) { + return bOrientationMatch - aOrientationMatch; + } + + // Compare displaySetInstanceUIDs + const aMatch = a.displaySetInstanceUIDs.some(uid => + currentActiveDisplaySetInstanceUIDs.includes(uid) + ); + const bMatch = b.displaySetInstanceUIDs.some(uid => + currentActiveDisplaySetInstanceUIDs.includes(uid) + ); + if (aMatch !== bMatch) { + return bMatch - aMatch; + } + + return 0; // Return 0 if no differences found + }); + + if (!sortedViewports?.length) { + return null; + } + + return sortedViewports[0].viewportId; +}; + +// Define the API interface +interface ViewportGridApi { + getState: () => AppTypes.ViewportGrid.State; + setActiveViewportId: (index: string) => void; + setDisplaySetsForViewport: (props: any) => void; + setDisplaySetsForViewports: (props: any[]) => void; + setLayout: (layout: AppTypes.ViewportGrid.Layout) => void; + reset: () => void; + set: (gridLayoutState: Partial) => void; + getNumViewportPanes: () => number; + setViewportIsReady: (viewportId: string, isReady: boolean) => void; + getGridViewportsReady: () => boolean; + getActiveViewportOptionByKey: (key: string) => any; + setViewportGridSizeChanged: (props: any) => void; + publishViewportsReady: () => void; +} + +// Update the context type +export const ViewportGridContext = createContext<[AppTypes.ViewportGrid.State, ViewportGridApi]>([ + DEFAULT_STATE, + {} as ViewportGridApi, +]); + +// Update the provider props type +interface ViewportGridProviderProps { + children: ReactNode; + service: ViewportGridService; +} + +export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) { + const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => { + switch (action.type) { + case 'SET_ACTIVE_VIEWPORT_ID': { + return { ...state, ...{ activeViewportId: action.payload } }; + } + + /** + * Sets the display sets for multiple viewports. + * This is a replacement for the older set display set for viewport (single) + * because the old one had race conditions wherein the viewports could + * render partially in various ways causing exceptions. + */ + case 'SET_DISPLAYSETS_FOR_VIEWPORTS': { + const { payload } = action; + const viewports = new Map(state.viewports); + + payload.forEach(updatedViewport => { + const { viewportId, displaySetInstanceUIDs } = updatedViewport; + + if (!viewportId) { + throw new Error('ViewportId is required to set display sets for viewport'); + } + + const previousViewport = viewports.get(viewportId); + + // remove options that were meant for one time usage + if (previousViewport?.viewportOptions?.initialImageOptions) { + const { useOnce } = previousViewport.viewportOptions.initialImageOptions; + if (useOnce) { + previousViewport.viewportOptions.initialImageOptions = null; + } + } + + // Use the newly provide viewportOptions and display set options + // when provided, and otherwise fall back to the previous ones. + // That allows for easy updates of just the display set. + let viewportOptions = merge( + {}, + previousViewport?.viewportOptions, + updatedViewport?.viewportOptions + ); + + const displaySetOptions = updatedViewport?.displaySetOptions || []; + if (!displaySetOptions.length) { + // Copy all the display set options, assuming a full set of displaySet UID's is provided. + if (state.isHangingProtocolLayout) { + displaySetOptions.push(...(previousViewport.displaySetOptions || [])); + } + if (!displaySetOptions.length) { + displaySetOptions.push({}); + } + } + + // if it is not part of the hanging protocol layout, we should remove the toolGroupId + // and viewportType from the viewportOptions so that it doesn't + // inherit the hanging protocol layout options, only when + // the viewport options is not provided (e.g., when drag and drop) + // otherwise, programmatically set options should be preserved + if (!updatedViewport.viewportOptions && !state.isHangingProtocolLayout) { + viewportOptions = { + viewportId: viewportOptions.viewportId, + }; + } + + const newViewport = { + ...previousViewport, + displaySetInstanceUIDs, + viewportOptions, + displaySetOptions, + // viewportLabel: getViewportLabel(viewports, viewportId), + }; + + viewportOptions.presentationIds = service.getPresentationIds({ + viewport: newViewport, + viewports, + }); + + viewports.set(viewportId, { + ...viewports.get(viewportId), + ...newViewport, + }); + }); + + return { ...state, viewports }; + } + case 'SET_LAYOUT': { + const { + numCols, + numRows, + layoutOptions, + layoutType = 'grid', + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + } = action.payload; + + // If empty viewportOptions, we use numRow and numCols to calculate number of viewports + const hasOptions = layoutOptions?.length; + const viewports = new Map(); + // Options is a temporary state store which can be used by the + // findOrCreate to store state about already found viewports. Typically, + // it will be used to store the display set UID's which are already + // in view so that the find or create can decide which display sets + // haven't been viewed yet, and add them in the appropriate order. + const options = {}; + + let activeViewportIdToSet = activeViewportId; + for (let row = 0; row < numRows; row++) { + for (let col = 0; col < numCols; col++) { + const position = col + row * numCols; + const layoutOption = layoutOptions[position]; + + let xPos, yPos, w, h; + if (layoutOptions && layoutOptions[position]) { + ({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[position]); + } else { + w = 1 / numCols; + h = 1 / numRows; + xPos = col * w; + yPos = row * h; + } + + const colIndex = Math.round(xPos * numCols); + const rowIndex = Math.round(yPos * numRows); + + const positionId = layoutOption?.positionId || `${colIndex}-${rowIndex}`; + + if (hasOptions && position >= layoutOptions.length) { + continue; + } + + const viewport = findOrCreateViewport(position, positionId, options); + + if (!viewport) { + continue; + } + + viewport.positionId = positionId; + + // If the viewport doesn't have a viewportId, we create one + if (!viewport.viewportOptions?.viewportId) { + const randomUID = utils.uuidv4().substring(0, 8); + viewport.viewportOptions = viewport.viewportOptions || {}; + viewport.viewportOptions.viewportId = `viewport-${randomUID}`; + } + + viewport.viewportId = viewport.viewportOptions.viewportId; + + // Create a new viewport object as it is getting updated here + // and it is part of the read only state + viewports.set(viewport.viewportId, viewport); + + Object.assign(viewport, { + width: w, + height: h, + x: xPos, + y: yPos, + }); + + viewport.isReady = false; + + if (!viewport.viewportOptions.presentationIds) { + const presentationIds = service.getPresentationIds({ + viewport, + viewports, + }); + viewport.viewportOptions.presentationIds = presentationIds; + } + } + } + + activeViewportIdToSet = + activeViewportIdToSet ?? determineActiveViewportId(state, viewports); + + const ret = { + ...state, + activeViewportId: activeViewportIdToSet, + layout: { + ...state.layout, + numCols, + numRows, + layoutType, + }, + viewports, + isHangingProtocolLayout, + }; + return ret; + } + case 'RESET': { + return DEFAULT_STATE; + } + + case 'SET': { + return { + ...state, + ...action.payload, + }; + } + + case 'VIEWPORT_IS_READY': { + const { viewportId, isReady } = action.payload; + const viewports = new Map(state.viewports); + const viewport = viewports.get(viewportId); + if (!viewport) { + return; + } + + viewports.set(viewportId, { + ...viewport, + isReady, + }); + + return { + ...state, + viewports, + }; + } + + default: + return action.payload; + } + }; + + const [viewportGridState, dispatch] = useReducer(viewportGridReducer, DEFAULT_STATE); + + const getState = useCallback(() => { + return viewportGridState; + }, [viewportGridState]); + + const getActiveViewportOptionByKey = (key: string) => { + const { viewports, activeViewportId } = viewportGridState; + return viewports.get(activeViewportId)?.viewportOptions?.[key]; + }; + + const setActiveViewportId = useCallback( + index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }), + [dispatch] + ); + + const setDisplaySetsForViewports = useCallback( + viewports => + dispatch({ + type: 'SET_DISPLAYSETS_FOR_VIEWPORTS', + payload: viewports, + }), + [dispatch] + ); + + const setViewportIsReady = useCallback( + (viewportId, isReady) => { + dispatch({ + type: 'VIEWPORT_IS_READY', + payload: { + viewportId, + isReady, + }, + }); + }, + [dispatch, viewportGridState] + ); + + const getGridViewportsReady = useCallback(() => { + const { viewports } = viewportGridState; + const readyViewports = Array.from(viewports.values()).filter(viewport => viewport.isReady); + return readyViewports.length === viewports.size; + }, [viewportGridState]); + + const setLayout = useCallback( + ({ + layoutType, + numRows, + numCols, + layoutOptions = [], + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + }) => + dispatch({ + type: 'SET_LAYOUT', + payload: { + layoutType, + numRows, + numCols, + layoutOptions, + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + }, + }), + [dispatch] + ); + + const reset = useCallback( + () => + dispatch({ + type: 'RESET', + payload: {}, + }), + [dispatch] + ); + + const set = useCallback( + payload => + dispatch({ + type: 'SET', + payload, + }), + [dispatch] + ); + + const getNumViewportPanes = useCallback(() => { + const { layout, viewports } = viewportGridState; + const { numRows, numCols } = layout; + return Math.min(viewports.size, numCols * numRows); + }, [viewportGridState]); + + /** + * Sets the implementation of ViewportGridService that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + getState, + setActiveViewportId, + setDisplaySetsForViewports, + setLayout, + reset, + onModeExit: reset, + set, + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + }); + } + }, [ + getState, + service, + setActiveViewportId, + setDisplaySetsForViewports, + setLayout, + reset, + set, + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + ]); + + // run many of the calls through the service itself since we want to publish events + const api = { + getState, + setActiveViewportId: index => service.setActiveViewportId(index), + setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]), + setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props), + setLayout: layout => service.setLayout(layout), + reset: () => service.reset(), + set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + getActiveViewportOptionByKey, + setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props), + publishViewportsReady: () => service.publishViewportsReady(), + }; + + return ( + + {children} + + ); +} + +ViewportGridProvider.propTypes = { + children: PropTypes.any, + service: PropTypes.instanceOf(ViewportGridService).isRequired, +}; + +// Update the useViewportGrid hook +export const useViewportGrid = (): [AppTypes.ViewportGrid.State, ViewportGridApi] => + useContext(ViewportGridContext); diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts index 57d3d90ec..ffe6d2fa7 100644 --- a/platform/ui-next/src/contextProviders/index.ts +++ b/platform/ui-next/src/contextProviders/index.ts @@ -1,3 +1,5 @@ import NotificationProvider, { useNotification } from './NotificationProvider'; +import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider'; export { useNotification, NotificationProvider }; +export { ViewportGridContext, ViewportGridProvider, useViewportGrid }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index 597f3540e..874f222da 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -81,10 +81,20 @@ import { StudySummary, ErrorBoundary, Header, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, } from './components'; import { DataRow } from './components/DataRow'; import { useNotification, NotificationProvider } from './contextProviders'; +import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders'; export { ErrorBoundary, @@ -104,6 +114,9 @@ export { // contextProviders NotificationProvider, useNotification, + ViewportGridContext, + ViewportGridProvider, + useViewportGrid, Separator, Tabs, TabsContent, @@ -174,4 +187,13 @@ export { TooltipContent, StudySummary, Header, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, }; diff --git a/yarn.lock b/yarn.lock index 5070887cf..cdcbd6e03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4670,6 +4670,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== +"@radix-ui/primitive@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" + integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== + "@radix-ui/react-accordion@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.1.tgz#5c942c42c24267376b26204ec6847b17d15659b3" @@ -4700,6 +4705,13 @@ dependencies: "@radix-ui/react-primitive" "2.0.0" +"@radix-ui/react-arrow@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7" + integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-checkbox@^1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" @@ -4749,6 +4761,16 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-collection@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0" + integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" @@ -4761,6 +4783,23 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== +"@radix-ui/react-compose-refs@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" + integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== + +"@radix-ui/react-context-menu@^2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.4.tgz#8fe70ffdfec25ada35a60d100496cace1b75a977" + integrity sha512-ap4wdGwK52rJxGkwukU1NrnEodsUFQIooANKu+ey7d6raQ2biTcEf8za1zr0mgFHieevRTB2nK4dJeN8pTAZGQ== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-menu" "2.1.4" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-context@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" @@ -4866,6 +4905,17 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0" +"@radix-ui/react-dismissable-layer@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8" + integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/react-dropdown-menu@^2.1.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" @@ -4920,6 +4970,15 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-focus-scope@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb" + integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-icons@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" @@ -4971,6 +5030,30 @@ aria-hidden "^1.1.1" react-remove-scroll "2.6.0" +"@radix-ui/react-menu@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.4.tgz#ac7aec296f29608206a7c6ef6335d8f102edaa95" + integrity sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-roving-focus" "1.1.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "^2.6.1" + "@radix-ui/react-popover@^1.0.7": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz#a0cab25f69aa49ad0077d91e9e9dcd323758020c" @@ -5025,6 +5108,22 @@ "@radix-ui/react-use-size" "1.1.0" "@radix-ui/rect" "1.1.0" +"@radix-ui/react-popper@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a" + integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + "@radix-ui/react-portal@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" @@ -5049,6 +5148,14 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-portal@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" + integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -5066,6 +5173,14 @@ "@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-presence@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" + integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-primitive@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" @@ -5081,6 +5196,13 @@ dependencies: "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-primitive@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" + integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== + dependencies: + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-roving-focus@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" @@ -5096,6 +5218,21 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-controllable-state" "1.1.0" +"@radix-ui/react-roving-focus@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520" + integrity sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-scroll-area@^1.1.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz#d09fd693728b09c50145935bec6f91efc2661729" @@ -5205,6 +5342,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.0" +"@radix-ui/react-slot@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" + integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-switch@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a" @@ -20916,6 +21060,14 @@ react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.6: react-style-singleton "^2.2.1" tslib "^2.0.0" +react-remove-scroll-bar@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== + dependencies: + react-style-singleton "^2.2.2" + tslib "^2.0.0" + react-remove-scroll@2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" @@ -20938,6 +21090,17 @@ react-remove-scroll@2.6.0: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-remove-scroll@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97" + integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw== + dependencies: + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.3" + use-sidecar "^1.1.2" + react-resize-detector@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-10.0.1.tgz#ae9a8c5b6b93c4c11e03b3eb87e57fd7b62f1020" @@ -21034,6 +21197,14 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" +react-style-singleton@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== + dependencies: + get-nonce "^1.0.0" + tslib "^2.0.0" + react-test-renderer@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.3.1.tgz#e693608a1f96283400d4a3afead6893f958b80b4" @@ -24206,6 +24377,13 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== + dependencies: + tslib "^2.0.0" + use-editable@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/use-editable/-/use-editable-2.3.3.tgz#a292fe9ba4c291cd28d1cc2728c75a5fc8d9a33f"