diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts index ab1dcd58b..d2bc01b06 100644 --- a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -1,6 +1,15 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { const { segmentationService, toolbarService, toolGroupService } = servicesManager.services; return [ + { + name: 'evaluate.cornerstone.hasSegmentation', + evaluate: ({ viewportId }) => { + const segmentations = segmentationService.getSegmentationRepresentations(viewportId); + return { + disabled: !segmentations?.length, + }; + }, + }, { name: 'evaluate.cornerstone.segmentation', evaluate: ({ viewportId, button, toolNames, disabledText }) => { @@ -12,7 +21,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { if (!segmentations?.length) { return { disabled: true, - className: '!text-common-bright !bg-black opacity-50', disabledText: disabledText ?? 'No segmentations available', }; } @@ -22,7 +30,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { if (!toolGroup) { return { disabled: true, - className: '!text-common-bright ohif-disabled', disabledText: disabledText ?? 'Not available on the current viewport', }; } @@ -32,7 +39,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { if (!toolGroup.hasTool(toolName) && !toolNames) { return { disabled: true, - className: '!text-common-bright ohif-disabled', disabledText: disabledText ?? 'Not available on the current viewport', }; } @@ -43,12 +49,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { return { disabled: false, - className: isPrimaryActive - ? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer' - : '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black', - // Todo: isActive right now is used for nested buttons where the primary - // button needs to be fully rounded (vs partial rounded) when active - // otherwise it does not have any other use isActive: isPrimaryActive, }; }, diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index e94eb895f..26e4aa707 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -1,14 +1,8 @@ import { Enums } from '@cornerstonejs/tools'; - -const getToggledClassName = (isToggled: boolean) => { - return isToggled - ? '!text-primary-active' - : '!text-common-bright hover:!bg-primary-dark hover:text-primary-light'; -}; +import { utils } from '@ohif/ui-next'; const getDisabledState = (disabledText?: string) => ({ disabled: true, - className: '!text-common-bright ohif-disabled', disabledText: disabledText ?? 'Not available on the current viewport', }); @@ -93,9 +87,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w return { disabled: false, - className: isPrimaryActive - ? '!text-black bg-primary-light rounded' - : '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded', // Todo: isActive right now is used for nested buttons where the primary // button needs to be fully rounded (vs partial rounded) when active // otherwise it does not have any other use @@ -153,9 +144,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w }, { name: 'evaluate.action', - evaluate: ({ viewportId, button }) => { + evaluate: () => { return { - className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light', + disabled: false, }; }, }, @@ -190,7 +181,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w if (!synchronizers?.length) { return { - className: getToggledClassName(false), + className: utils.getToggledClassName(false), }; } @@ -204,7 +195,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w if (!synchronizers?.length) { return { - className: getToggledClassName(false), + className: utils.getToggledClassName(false), }; } @@ -216,7 +207,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w const isEnabled = synchronizer?._enabled; return { - className: getToggledClassName(isEnabled), + className: utils.getToggledClassName(isEnabled), }; }, }, @@ -239,14 +230,13 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w if (!prop) { return { disabled: false, - className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light', }; } const isToggled = prop; return { - className: getToggledClassName(isToggled), + className: utils.getToggledClassName(isToggled), }; }, }, @@ -275,7 +265,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w return { disabled: false, - className: getToggledClassName(isMpr), + className: utils.getToggledClassName(isMpr), }; }, }, @@ -304,6 +294,6 @@ function _evaluateToggle({ const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode); return { - className: getToggledClassName(!isOff), + className: utils.getToggledClassName(!isOff), }; } diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 70642c4be..de7c22116 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -160,7 +160,7 @@ const cornerstoneExtension: Types.Extensions.Extension = { ViewportActionCornersProvider ); - const { syncGroupService, customizationService } = servicesManager.services; + const { syncGroupService } = servicesManager.services; syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer); return init.call(this, props); diff --git a/extensions/default/src/Components/MoreDropdownMenu.tsx b/extensions/default/src/Components/MoreDropdownMenu.tsx index 5dbeb7241..62b14e3b1 100644 --- a/extensions/default/src/Components/MoreDropdownMenu.tsx +++ b/extensions/default/src/Components/MoreDropdownMenu.tsx @@ -3,6 +3,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, + DropdownMenuItem, Icons, Button, } from '@ohif/ui-next'; @@ -18,6 +19,13 @@ const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props // getMenuItems can also be replaced by providing it to the MoreDropdownMenu const menuContent = customizationService.getCustomization('ohif.menuContent'); + // Default menu item component if none is provided through customization + const DefaultMenuItem = ({ item }) => ( + {item.label || item.title} + ); + + const MenuItemComponent = menuContent?.content || DefaultMenuItem; + return ( - {items?.map((item, index) => - React.createElement(menuContent.content, { - key: item.id || `menu-item-${index}`, - item, - commandsManager, - servicesManager, - ...props, - }) - )} + {items?.map((item, index) => ( + + ))} ); }; diff --git a/extensions/default/src/Toolbar/ToolBoxWrapper.tsx b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx new file mode 100644 index 000000000..d40c9015a --- /dev/null +++ b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import classNames from 'classnames'; +import { ToolButton } from '@ohif/ui-next'; + +/** + * Wraps the ToolButtonList component to handle the OHIF toolbar button structure + * @param props - Component props + * @returns Component + */ +export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) { + if (!items || !groupId) { + return null; + } + + return ( +
+ {items.map(item => ( + + onInteraction?.({ groupId, itemId: item.id, commands: item.commands }) + } + /> + ))} +
+ ); +} + +export function ToolBoxButtonWrapper({ onInteraction, ...props }) { + return ( +
+ onInteraction?.({ itemId: props.id, commands: props.commands })} + /> +
+ ); +} diff --git a/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx b/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx new file mode 100644 index 000000000..d1477f727 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { + ToolButtonList, + ToolButton, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from '@ohif/ui-next'; + +interface ButtonItem { + id: string; + icon?: string; + label?: string; + tooltip?: string; + isActive?: boolean; + disabledText?: string; + commands?: Record; + disabled?: boolean; + className?: string; +} + +interface ToolButtonListWrapperProps { + groupId: string; + primary: ButtonItem; + items: ButtonItem[]; + onInteraction?: (details: { + groupId: string; + itemId: string; + commands?: Record; + }) => void; +} + +/** + * Wraps the ToolButtonList component to handle the OHIF toolbar button structure + * @param props - Component props + * @returns Component + * // test + */ +export default function ToolButtonListWrapper({ + groupId, + primary, + items, + onInteraction, +}: ToolButtonListWrapperProps) { + return ( + + +
+ + onInteraction?.({ groupId, itemId, commands: primary.commands }) + } + className={primary.className} + /> +
+
+ +
+ + {items.map(item => ( + + onInteraction?.({ groupId, itemId: item.id, commands: item.commands }) + } + > + {item.label || item.tooltip || item.id} + + ))} + +
+
+ ); +} diff --git a/extensions/default/src/Toolbar/Toolbar.tsx b/extensions/default/src/Toolbar/Toolbar.tsx index b2b52211d..967baf9b8 100644 --- a/extensions/default/src/Toolbar/Toolbar.tsx +++ b/extensions/default/src/Toolbar/Toolbar.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import { Tooltip } from '@ohif/ui'; -import classnames from 'classnames'; import { useToolbar } from '@ohif/core'; export function Toolbar({ servicesManager, buttonSection = 'primary' }) { @@ -15,7 +13,7 @@ export function Toolbar({ servicesManager, buttonSection = 'primary' }) { return ( <> - {toolbarButtons.map(toolDef => { + {toolbarButtons?.map(toolDef => { if (!toolDef) { return null; } diff --git a/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx b/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx index 0e6ad1cdd..300270a14 100644 --- a/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx +++ b/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx @@ -1,5 +1,5 @@ -import { ToolbarButton, ButtonGroup } from '@ohif/ui'; import React, { useCallback } from 'react'; +import { ToolbarButton, ButtonGroup } from '@ohif/ui'; function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) { const getSplitButtonItems = useCallback( diff --git a/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx index d15c16017..60dad54eb 100644 --- a/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx +++ b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx @@ -107,18 +107,18 @@ const useResizablePanels = ( // And by virtue of the dependency on the minimum size state variables, this code // is executed on the render following an update of the minimum percentage sizes // for a panel. - if (!resizableLeftPanelAPIRef.current.isCollapsed()) { + if (!resizableLeftPanelAPIRef.current?.isCollapsed()) { const leftSize = getPercentageSize( leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize ); - resizableLeftPanelAPIRef.current.resize(leftSize); + resizableLeftPanelAPIRef.current?.resize(leftSize); } - if (!resizableRightPanelAPIRef.current.isCollapsed()) { + if (!resizableRightPanelAPIRef?.current?.isCollapsed()) { const rightSize = getPercentageSize( rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize ); - resizableRightPanelAPIRef.current.resize(rightSize); + resizableRightPanelAPIRef?.current?.resize(rightSize); } // This observer kicks in when the ViewportLayout resizable panel group @@ -199,7 +199,7 @@ const useResizablePanels = ( }, [setLeftPanelClosed]); const onLeftPanelResize = useCallback(size => { - if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current.isCollapsed()) { + if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current?.isCollapsed()) { return; } @@ -228,7 +228,7 @@ const useResizablePanels = ( }, [setRightPanelClosed]); const onRightPanelResize = useCallback(size => { - if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef.current.isCollapsed()) { + if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef?.current?.isCollapsed()) { return; } @@ -248,7 +248,7 @@ const useResizablePanels = ( * Note that the width attributed to the handles must be taken into account. */ const getPercentageSize = pixelSize => { - const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect(); return (pixelSize / (panelGroupWidth - resizableHandlesWidth.current)) * 100; }; @@ -257,7 +257,7 @@ const useResizablePanels = ( * Note that the width attributed to the handles must be taken into account. */ const getExpandedPixelWidth = percentageSize => { - const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect(); const expandedWidth = (percentageSize / 100) * (panelGroupWidth - resizableHandlesWidth.current) - panelGroupDefinition.shared.expandedInsideBorderSize; diff --git a/extensions/default/src/getToolbarModule.tsx b/extensions/default/src/getToolbarModule.tsx index d84aad9f9..92ada7ef5 100644 --- a/extensions/default/src/getToolbarModule.tsx +++ b/extensions/default/src/getToolbarModule.tsx @@ -1,42 +1,61 @@ -import ToolbarDivider from './Toolbar/ToolbarDivider'; +import { ToolbarButton as ToolbarButtonLegacy } from '@ohif/ui'; +import { ToolButton, utils } from '@ohif/ui-next'; + import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector'; -import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices'; -import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices'; -import { ToolbarButton } from '@ohif/ui'; + +// legacy +import ToolbarDividerLegacy from './Toolbar/ToolbarDivider'; +import ToolbarSplitButtonWithServicesLegacy from './Toolbar/ToolbarSplitButtonWithServices'; +import ToolbarButtonGroupWithServicesLegacy from './Toolbar/ToolbarButtonGroupWithServices'; import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService'; -const getClassName = isToggled => { - return { - className: isToggled - ? '!text-primary-active' - : '!text-common-bright hover:!bg-primary-dark hover:text-primary-light', - }; -}; +// new +import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper'; +import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper'; export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) { const { cineService } = servicesManager.services; return [ + // new + { + name: 'ohif.toolButton', + defaultComponent: ToolButton, + }, + { + name: 'ohif.toolButtonList', + defaultComponent: ToolButtonListWrapper, + }, + { + name: 'ohif.toolBoxButtonGroup', + defaultComponent: ToolBoxButtonGroupWrapper, + }, + { + name: 'ohif.toolBoxButton', + defaultComponent: ToolBoxButtonWrapper, + }, + // legacy { name: 'ohif.radioGroup', - defaultComponent: ToolbarButton, + defaultComponent: ToolbarButtonLegacy, + }, + { + name: 'ohif.buttonGroup', + defaultComponent: ToolbarButtonGroupWithServicesLegacy, }, { name: 'ohif.divider', - defaultComponent: ToolbarDivider, + defaultComponent: ToolbarDividerLegacy, }, { name: 'ohif.splitButton', - defaultComponent: ToolbarSplitButtonWithServices, + defaultComponent: ToolbarSplitButtonWithServicesLegacy, }, + // others { name: 'ohif.layoutSelector', defaultComponent: props => ToolbarLayoutSelectorWithServices({ ...props, commandsManager, servicesManager }), }, - { - name: 'ohif.buttonGroup', - defaultComponent: ToolbarButtonGroupWithServices, - }, { name: 'ohif.progressDropdown', defaultComponent: ProgressDropdownWithService, @@ -66,7 +85,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w name: 'evaluate.cine', evaluate: () => { const isToggled = cineService.getState().isCineEnabled; - return getClassName(isToggled); + return { + className: utils.getToggledClassName(isToggled), + }; }, }, ]; diff --git a/extensions/default/src/hooks/usePatientInfo.tsx b/extensions/default/src/hooks/usePatientInfo.tsx index 9af2efc3f..a28d94dfc 100644 --- a/extensions/default/src/hooks/usePatientInfo.tsx +++ b/extensions/default/src/hooks/usePatientInfo.tsx @@ -13,9 +13,8 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) { PatientDOB: '', }); const [isMixedPatients, setIsMixedPatients] = useState(false); - const displaySets = displaySetService.getActiveDisplaySets(); - const checkMixedPatients = PatientID => { + const checkMixedPatients = (PatientID: string) => { const displaySets = displaySetService.getActiveDisplaySets(); let isMixedPatients = false; displaySets.forEach(displaySet => { @@ -30,8 +29,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) { setIsMixedPatients(isMixedPatients); }; - const updatePatientInfo = () => { - const displaySet = displaySets[0]; + const updatePatientInfo = ({ displaySetsAdded }) => { + if (!displaySetsAdded.length) { + return; + } + const displaySet = displaySetsAdded[0]; const instance = displaySet?.instances?.[0] || displaySet?.instance; if (!instance) { return; @@ -39,7 +41,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) { setPatientInfo({ PatientID: instance.PatientID || null, - PatientName: instance.PatientName ? formatPN(instance.PatientName ) : null, + PatientName: instance.PatientName ? formatPN(instance.PatientName) : null, PatientSex: instance.PatientSex || null, PatientDOB: formatDate(instance.PatientBirthDate) || null, }); @@ -49,15 +51,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) { useEffect(() => { const subscription = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, - () => updatePatientInfo() + props => updatePatientInfo(props) ); return () => subscription.unsubscribe(); }, []); - useEffect(() => { - updatePatientInfo(); - }, [displaySets]); - return { patientInfo, isMixedPatients }; } diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index 782184fe2..684783387 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -39,7 +39,6 @@ import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowser import * as utils from './utils'; import MoreDropdownMenu from './Components/MoreDropdownMenu'; import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy'; - const defaultExtension: Types.Extensions.Extension = { /** * Only required property. Should be a unique value across all extensions. diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 72c489242..15be51e64 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -323,7 +323,7 @@ function _getStatusComponent(isTracked, t) { - + { const { diff --git a/modes/longitudinal/src/moreTools.ts b/modes/longitudinal/src/moreTools.ts index 483cc0aa5..77265e3a4 100644 --- a/modes/longitudinal/src/moreTools.ts +++ b/modes/longitudinal/src/moreTools.ts @@ -14,7 +14,7 @@ const ReferenceLinesListeners: RunCommand = [ const moreTools = [ { id: 'MoreTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MoreTools', evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts index 08fe2598c..3a8d4d592 100644 --- a/modes/longitudinal/src/toolbarButtons.ts +++ b/modes/longitudinal/src/toolbarButtons.ts @@ -15,7 +15,7 @@ export const setToolActiveToolbar = { const toolbarButtons: Button[] = [ { id: 'MeasurementTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MeasurementTools', // group evaluate to determine which item should move to the top @@ -110,7 +110,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Zoom', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-zoom', label: 'Zoom', @@ -121,7 +121,7 @@ const toolbarButtons: Button[] = [ // Window Level { id: 'WindowLevel', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-window-level', label: 'Window Level', @@ -138,7 +138,7 @@ const toolbarButtons: Button[] = [ // Pan... { id: 'Pan', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-move', @@ -149,7 +149,7 @@ const toolbarButtons: Button[] = [ }, { id: 'TrackballRotate', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-3d-rotate', @@ -163,7 +163,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Capture', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-capture', label: 'Capture', @@ -188,7 +188,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Crosshairs', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-crosshair', diff --git a/modes/microscopy/src/toolbarButtons.js b/modes/microscopy/src/toolbarButtons.js index 82c774683..9edb10d4f 100644 --- a/modes/microscopy/src/toolbarButtons.js +++ b/modes/microscopy/src/toolbarButtons.js @@ -3,7 +3,7 @@ import { ToolbarService } from '@ohif/core'; const toolbarButtons = [ { id: 'MeasurementTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MeasurementTools', // group evaluate to determine which item should move to the top @@ -131,7 +131,7 @@ const toolbarButtons = [ }, { id: 'dragPan', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-move', label: 'Pan', @@ -147,7 +147,7 @@ const toolbarButtons = [ }, { id: 'TagBrowser', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'dicom-tag-browser', label: 'Dicom Tag Browser', @@ -161,4 +161,4 @@ const toolbarButtons = [ }, ]; -export default toolbarButtons; +export default toolbarButtons; \ No newline at end of file diff --git a/modes/preclinical-4d/src/segmentationButtons.ts b/modes/preclinical-4d/src/segmentationButtons.ts index 4a4e1a89d..ec6f60022 100644 --- a/modes/preclinical-4d/src/segmentationButtons.ts +++ b/modes/preclinical-4d/src/segmentationButtons.ts @@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types'; const toolbarButtons: Button[] = [ { id: 'BrushTools', - uiType: 'ohif.buttonGroup', + uiType: 'ohif.toolBoxButtonGroup', props: { groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', items: [ { id: 'Brush', @@ -134,7 +135,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Shapes', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolBoxButton', props: { label: 'Shapes', evaluate: { diff --git a/modes/preclinical-4d/src/toolbarButtons.tsx b/modes/preclinical-4d/src/toolbarButtons.tsx index eaabaea0b..4d8632ac6 100644 --- a/modes/preclinical-4d/src/toolbarButtons.tsx +++ b/modes/preclinical-4d/src/toolbarButtons.tsx @@ -13,7 +13,7 @@ const setToolActiveToolbar = { const toolbarButtons = [ { id: 'MeasurementTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MeasurementTools', evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', @@ -67,7 +67,7 @@ const toolbarButtons = [ }, { id: 'Zoom', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-zoom', label: 'Zoom', @@ -77,7 +77,7 @@ const toolbarButtons = [ }, { id: 'WindowLevel', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-window-level', label: 'Window Level', @@ -87,7 +87,7 @@ const toolbarButtons = [ }, { id: 'Pan', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-move', @@ -98,7 +98,7 @@ const toolbarButtons = [ }, { id: 'TrackballRotate', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-3d-rotate', @@ -134,7 +134,7 @@ const toolbarButtons = [ }, { id: 'Crosshairs', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-crosshair', diff --git a/modes/segmentation/src/segmentationButtons.ts b/modes/segmentation/src/segmentationButtons.ts index 448267f02..103a2e3bd 100644 --- a/modes/segmentation/src/segmentationButtons.ts +++ b/modes/segmentation/src/segmentationButtons.ts @@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types'; const toolbarButtons: Button[] = [ { id: 'BrushTools', - uiType: 'ohif.buttonGroup', + uiType: 'ohif.toolBoxButtonGroup', props: { groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', items: [ { id: 'Brush', @@ -172,14 +173,16 @@ const toolbarButtons: Button[] = [ }, { id: 'Shapes', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolBoxButton', props: { + id: 'Shapes', + icon: 'icon-tool-shape', label: 'Shapes', evaluate: { name: 'evaluate.cornerstone.segmentation', toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + disabledText: 'Create new segmentation to enable shapes tool.', }, - icon: 'icon-tool-shape', options: [ { name: 'Shape', diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts index a59c994eb..f03eefe7c 100644 --- a/modes/segmentation/src/toolbarButtons.ts +++ b/modes/segmentation/src/toolbarButtons.ts @@ -20,7 +20,7 @@ export const setToolActiveToolbar = { const toolbarButtons: Button[] = [ { id: 'Zoom', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-zoom', label: 'Zoom', @@ -30,7 +30,7 @@ const toolbarButtons: Button[] = [ }, { id: 'WindowLevel', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-window-level', label: 'Window Level', @@ -40,7 +40,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Pan', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-move', label: 'Pan', @@ -50,7 +50,7 @@ const toolbarButtons: Button[] = [ }, { id: 'TrackballRotate', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { type: 'tool', icon: 'tool-3d-rotate', @@ -64,7 +64,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Capture', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-capture', label: 'Capture', @@ -90,7 +90,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Crosshairs', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-crosshair', label: 'Crosshairs', @@ -108,7 +108,7 @@ const toolbarButtons: Button[] = [ }, { id: 'MoreTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MoreTools', evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', @@ -192,14 +192,6 @@ const toolbarButtons: Button[] = [ commands: 'invertViewport', evaluate: 'evaluate.viewportProperties.toggle', }), - createButton({ - id: 'Probe', - icon: 'tool-probe', - label: 'Probe', - tooltip: 'Probe', - commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', - }), createButton({ id: 'Cine', icon: 'tool-cine', @@ -214,14 +206,6 @@ const toolbarButtons: Button[] = [ }, ], }), - createButton({ - id: 'Angle', - icon: 'tool-angle', - label: 'Angle', - tooltip: 'Angle', - commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', - }), createButton({ id: 'Magnify', icon: 'tool-magnify', @@ -230,22 +214,6 @@ const toolbarButtons: Button[] = [ commands: setToolActiveToolbar, evaluate: 'evaluate.cornerstoneTool', }), - createButton({ - id: 'RectangleROI', - icon: 'tool-rectangle', - label: 'Rectangle', - tooltip: 'Rectangle', - commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', - }), - createButton({ - id: 'CalibrationLine', - icon: 'tool-calibration', - label: 'Calibration', - tooltip: 'Calibration Line', - commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', - }), createButton({ id: 'TagBrowser', icon: 'dicom-tag-browser', diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js index 26948697b..6e63b2165 100644 --- a/modes/tmtv/src/toolbarButtons.js +++ b/modes/tmtv/src/toolbarButtons.js @@ -11,7 +11,7 @@ const setToolActiveToolbar = { const toolbarButtons = [ { id: 'MeasurementTools', - uiType: 'ohif.splitButton', + uiType: 'ohif.toolButtonList', props: { groupId: 'MeasurementTools', primary: ToolbarService.createButton({ @@ -56,7 +56,7 @@ const toolbarButtons = [ }, { id: 'Zoom', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-zoom', label: 'Zoom', @@ -67,7 +67,7 @@ const toolbarButtons = [ // Window Level + Presets { id: 'WindowLevel', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-window-level', label: 'Window Level', @@ -78,7 +78,7 @@ const toolbarButtons = [ // Crosshairs Button { id: 'Crosshairs', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-crosshair', label: 'Crosshairs', @@ -89,7 +89,7 @@ const toolbarButtons = [ // Pan Button { id: 'Pan', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolButton', props: { icon: 'tool-move', label: 'Pan', @@ -100,7 +100,7 @@ const toolbarButtons = [ // Rectangle ROI Start End Threshold Button { id: 'RectangleROIStartEndThreshold', - uiType: 'ohif.radioGroup', + uiType: 'ohif.toolBoxButton', props: { icon: 'tool-create-threshold', label: 'Rectangle ROI Threshold', @@ -119,9 +119,10 @@ const toolbarButtons = [ }, { id: 'BrushTools', - uiType: 'ohif.buttonGroup', + uiType: 'ohif.toolBoxButtonGroup', props: { groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', items: [ { id: 'Brush', diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js index bf551565d..3af31119e 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js @@ -72,7 +72,7 @@ describe('OHIF Cornerstone Toolbar', () => { // Assign an alias to the button element cy.get('@wwwcBtnPrimary').as('wwwcButton'); cy.get('@wwwcButton').click(); - cy.get('@wwwcButton').should('have.class', 'bg-primary-light'); + cy.get('@wwwcButton').should('have.attr', 'data-active', 'true'); //drags the mouse inside the viewport to be able to interact with series cy.get('@viewport') @@ -97,7 +97,7 @@ describe('OHIF Cornerstone Toolbar', () => { cy.get('@panButton').click(); // Assert that the button has the 'active' class - cy.get('@panButton').should('have.class', 'bg-primary-light'); + cy.get('@panButton').should('have.attr', 'data-active', 'true'); // Trigger the pan actions on the viewport cy.get('@viewport') diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js index c31a2b76f..272e9e4fb 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js @@ -18,7 +18,7 @@ describe('OHIF General Viewer', function () { cy.get('@zoomBtn') .click() .then($zoomBtn => { - cy.wrap($zoomBtn).should('have.class', 'bg-primary-light'); + cy.wrap($zoomBtn).should('have.attr', 'data-active', 'true'); }); const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => { diff --git a/platform/app/cypress/integration/volume/MPR.spec.js b/platform/app/cypress/integration/volume/MPR.spec.js index a315394d6..f9de4e064 100644 --- a/platform/app/cypress/integration/volume/MPR.spec.js +++ b/platform/app/cypress/integration/volume/MPR.spec.js @@ -7,7 +7,7 @@ describe('OHIF MPR', () => { }); it('should not go MPR for non reconstructible displaySets', () => { - cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled'); + cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed'); }); it('should go MPR for reconstructible displaySets and come back', () => { diff --git a/platform/app/cypress/support/aliases.js b/platform/app/cypress/support/aliases.js index af1ad6c1c..7fdb27180 100644 --- a/platform/app/cypress/support/aliases.js +++ b/platform/app/cypress/support/aliases.js @@ -1,6 +1,8 @@ //Creating aliases for Cornerstone tools buttons export function initCornerstoneToolsAliases() { - cy.get('[data-cy="StackScroll"]').as('stackScrollBtn'); + // Note: stack scroll is not in the DOM when the study is loaded + // cy.get('[data-cy="StackScroll"]').as('stackScrollBtn'); + cy.get('[data-cy="Zoom"]').as('zoomBtn'); cy.get('[data-cy="WindowLevel-split-button-primary"]').as('wwwcBtnPrimary'); cy.get('[data-cy="WindowLevel-split-button-secondary"]').as('wwwcBtnSecondary'); @@ -34,7 +36,6 @@ export function initCommonElementsAliases(skipMarkers) { cy.get('[data-cy="viewport-overlay-bottom-right"]').as('viewportInfoBottomRight'); cy.get('[data-cy="viewport-overlay-bottom-left"]').as('viewportInfoBottomLeft'); - console.debug('🚀 ~ skipMarkers:', skipMarkers); if (skipMarkers) { return; } diff --git a/platform/app/cypress/support/commands.js b/platform/app/cypress/support/commands.js index 4ead24dc1..93f086435 100644 --- a/platform/app/cypress/support/commands.js +++ b/platform/app/cypress/support/commands.js @@ -265,7 +265,7 @@ Cypress.Commands.add( } }); - cy.get('@lengthButton').should('have.class', 'bg-primary-light'); + cy.get('@lengthButton').should('have.attr', 'data-active', 'true'); cy.get('@viewport').then($viewport => { const [x1, y1] = firstClick; diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 75290c53c..a92a970a5 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -21,13 +21,13 @@ import { ViewportDialogProvider, CineProvider, UserAuthenticationProvider, - ToolboxProvider, } from '@ohif/ui'; import { ThemeWrapper as ThemeWrapperNext, NotificationProvider, ViewportGridProvider, TooltipProvider, + ToolboxProvider, } from '@ohif/ui-next'; // Viewer Project // TODO: Should this influence study list? diff --git a/platform/core/src/hooks/useToolbar.tsx b/platform/core/src/hooks/useToolbar.tsx index d232c8800..33ef80665 100644 --- a/platform/core/src/hooks/useToolbar.tsx +++ b/platform/core/src/hooks/useToolbar.tsx @@ -5,7 +5,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA const { EVENTS } = toolbarService; const [toolbarButtons, setToolbarButtons] = useState( - toolbarService.getButtonSection(buttonSection) + toolbarService.getButtonSection(buttonSection as string) ); // Callback function for handling toolbar interactions @@ -25,7 +25,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA // Effect to handle toolbar modification events useEffect(() => { const handleToolbarModified = () => { - setToolbarButtons(toolbarService.getButtonSection(buttonSection)); + setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean)); }; const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => { diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index cd8b042a1..280568b20 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -236,8 +236,8 @@ export default class ToolbarService extends PubSubService { const evaluateButtonProps = (button, props, refreshProps) => { if (evaluationResults.has(button.id)) { - const { disabled, className, isActive } = evaluationResults.get(button.id); - return { ...props, disabled, className, isActive }; + const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id); + return { ...props, disabled, disabledText, className, isActive }; } else { const evaluated = props.evaluate?.({ ...refreshProps, button }); const updatedProps = { @@ -276,7 +276,9 @@ export default class ToolbarService extends PubSubService { // item in the group buttonProps = { ...buttonProps, - primary: groupEvaluated?.primary || buttonProps.primary, + primary: groupEvaluated?.primary ?? buttonProps.primary, + disabled: groupEvaluated?.disabled ?? buttonProps.disabled, + disabledText: groupEvaluated?.disabledText ?? buttonProps.disabledText, }; const { primary, items } = buttonProps; diff --git a/platform/docs/docs/development/playwright-testing.md b/platform/docs/docs/development/playwright-testing.md index 679872399..f85f02d33 100644 --- a/platform/docs/docs/development/playwright-testing.md +++ b/platform/docs/docs/development/playwright-testing.md @@ -3,12 +3,28 @@ sidebar_position: 11 sidebar_label: Playwright Testing --- + + +:::note +You might need to run the `yarn playwright install ` for the first time if you have not +::: + +# Running the tests + +```bash +# run the tests +yarn test:e2e:ui +``` + + # Writing PlayWright Tests Our Playwright tests are written using the Playwright test framework. We use these tests to test our OHIF Viewer and ensure that it is working as expected. In this guide, we will show you how to write Playwright tests for the OHIF Viewer. + + ## Using a specific study and mode If you would like to use a specific study, you can use the `studyInstanceUID` property to reference the study you would like to visit. for example, if you would like to use the study with StudyInstanceUID `2.16.840.1.114362.1.11972228.22789312658.616067305.306.2` and the mode `Basic Viewer`, you can use the following code snippet: diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-Icons.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Icons.md similarity index 100% rename from platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-Icons.md rename to platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Icons.md diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md new file mode 100644 index 000000000..768e29600 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md @@ -0,0 +1,199 @@ +--- +title: Input Number, Range, and Double Range +--- + + +# Migration Guide: Moving to `Numeric` Component + +This guide explains how to migrate from the existing `Input`, `InputRange`, and `InputDoubleRange` components to the new `Numeric` meta component. + + +## Why Migrate? + +The old components relied heavily on props, making them complex and difficult to maintain and apply custom styles. The new `Numeric` component provides a structured approach with a context-based API, reducing prop clutter and improving reusability. + + +## `Input` > `Numeric.NumberInput` + +### Basic Usage + +**Old Usage:** + +```tsx + setValue(e.target.value)} + type="number" +/> +``` + +**New Usage:** + +```tsx + + Enter a number + + +``` + + + +### `Input` with Custom Classes + +#### **Old Usage (with containerClassName, labelClassName, and className)** + +In the old implementation, we manually applied `containerClassName`, `labelClassName`, and `className` to style the `Input` component: + +```tsx + setValue(e.target.value)} + type="number" + containerClassName="flex flex-col space-y-2" + labelClassName="text-gray-500 text-sm" + className="border rounded p-2" +/> +``` + + +**New Usage (Migrating to `Numeric.NumberInput`)** + +With `Numeric`, you should wrap everything inside `Numeric.Container`, and you can directly apply class names to its subcomponents: + +```tsx + + Enter a number + + +``` + + +## `InputRange` > `Numeric.SingleRange` + +### Basic Usage + +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +```tsx + + Range + + +``` + + +### Custom Classes + +**Old Usage** + +```tsx + +``` + +**New Usage** + +```tsx + + Range + + +``` + +:::note +You now have more control over the position of the label and the slider. You can use pretty much any layout you want, whether that's flex, grid, or something else. Instead of relying on `labelPosition` to position the label, you're free to use the layout that works best for you. +::: + + +### AllowNumberEdit + +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +Using `Numeric.SingleRange` and `showNumberInput` + +```tsx + + Range + + +``` + + +## `InputDoubleRange` > `Numeric.DoubleRange` + + +### Basic Usage +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +```tsx + + Range + + +``` + + +--- + +## Summary of Changes + +| Old Component | New Component Equivalent | +|--------------------|------------------------| +| `` | `` | +| `` | `` | +| `` | `` | diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md new file mode 100644 index 000000000..17e8a9689 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md @@ -0,0 +1,68 @@ +--- +title: Tests +--- + + +## 1. ToolButton `data-active` Attribute + +- **Previous**: Checked for a `bg-primary-light` class to determine if a tool was active. +- **Now**: Check for the HTML attribute `data-active="true"`. + +### Example + +```diff +- cy.get('@wwwcButton').should('have.class', 'bg-primary-light'); ++ cy.get('@wwwcButton').should('have.attr', 'data-active', 'true'); +``` + +## 2. Additional Data Attributes + +- Each tool button now includes: + - `data-tool=""` + - `data-active=""` + +This makes it easier to identify and assert on specific tools in the DOM. + +### Example + +```diff +- ++ +``` + +## 3. MPR Button Class Change + +If you were targeting the `ohif-disabled` class, you need to update your tests to target the `cursor-not-allowed` class. + +- **Previous**: `ohif-disabled` +- **Now**: `cursor-not-allowed` + +### Example + +```diff +- cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled'); ++ cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed'); +``` + +## 4. Removal of Stack Scroll Alias + +- The `[data-cy="StackScroll"]` element is no longer reliably in the DOM at study load. +- If needed, reintroduce or conditionally assert its presence when appropriate. + +```diff +- cy.get('[data-cy="StackScroll"]').as('stackScrollBtn'); ++ // Removed due to absence in DOM at study load +``` + +--- + +## Summary + +1. **Replace** all checks for `bg-primary-light` with `data-active="true"`. +2. **Use** `data-tool` and `data-active` attributes for more robust DOM selection and assertions. +3. **Update** MPR button checks to `cursor-not-allowed`. +4. **Remove** the `[data-cy="StackScroll"]` alias (or only use it when the element is present). diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md new file mode 100644 index 000000000..9a2c79bc7 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md @@ -0,0 +1,101 @@ +--- +title: Toolbar +--- + +# Toolbar + +## New Toolbar uiType + +We have two new toolbar button types: `ohif.toolButtonList` and `ohif.toolButton`, which are intended to replace the `ohif.radioGroup` and `ohif.splitButton` types. + +Note that these are backward compatible, so if you are not ready to pick up the new ui types (which are more flexible and powerful), you can continue using the old types. + + +```js +// Old type +{ + uiType: 'ohif.radioGroup', +} + +// New type +{ + uiType: 'ohif.toolButton', +} +``` + +and + +```js +// Old type +{ + uiType: 'ohif.splitButton', +} + +// New type +{ + uiType: 'ohif.toolButtonList', +} +``` + +The `ohif.buttonGroup` and `ohif.radioGroup` types used in the Toolbox have been replaced with `ohif.toolBoxButtonGroup` and `ohif.toolBoxButton` to reflect their usage in the Toolbox, which has distinct styling. + +```js +// Old type +{ + uiType: 'ohif.buttonGroup', +} + +// New type +{ + uiType: 'ohif.toolBoxButtonGroup', +} +``` + + +```js +// Old type +{ + uiType: 'ohif.radioGroup', +} + +// New type +{ + uiType: 'ohif.toolBoxButton', +} +``` + + + +## getToolbarModule + + +The `getToolbarModule` function previously returned `disabled`, `disabledText`, and `className` as part of its evaluation process for the button state. These properties will still be returned, but common class names are now handled internally by the new UI button components, including `ToolButton`, `ToolButtonList`, `Toolbox`, and `ToolBoxGroup`. You can override the `className` if you need to. + + +## ToolBox + +Previously, the segmentation toolbox was not using an `evaluator` property. This is now taken into account + + +```js +// old +{ + id: 'BrushTools', + uiType: 'ohif.buttonGroup', + props: { + groupId: 'BrushTools', + items: [] + } +} + +// now +{ + id: 'BrushTools', + uiType: 'ohif.buttonGroup', + props: { + groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', + items: [] + } +} +``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-Tooltip.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tooltip.md similarity index 100% rename from platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-Tooltip.md rename to platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tooltip.md diff --git a/platform/docs/src/pages/components-list.tsx b/platform/docs/src/pages/components-list.tsx index 3888fc0b9..cd9a57ae9 100644 --- a/platform/docs/src/pages/components-list.tsx +++ b/platform/docs/src/pages/components-list.tsx @@ -1,651 +1,57 @@ -import React, { useState } from 'react'; +import React from 'react'; import '../css/custom.css'; - import Layout from '@theme/Layout'; -import { Label } from '../../../ui-next/src/components/Label'; -import { Input } from '../../../ui-next/src/components/Input'; -import { Separator } from '../../../ui-next/src/components/Separator'; -import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs'; -import { - Select, - SelectTrigger, - SelectContent, - SelectItem, - SelectValue, -} from '../../../ui-next/src/components/Select'; -import { Button } from '../../../ui-next/src/components/Button'; -import { Switch } from '../../../ui-next/src/components/Switch'; -import { Checkbox } from '../../../ui-next/src/components/Checkbox'; -import { Toggle } from '../../../ui-next/src/components/Toggle'; -import { Slider } from '../../../ui-next/src/components/Slider'; -import { ScrollArea } from '../../../ui-next/src/components/ScrollArea'; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from '../../../ui-next/src/components/DropdownMenu'; -import { Icons } from '../../../ui-next/src/components/Icons'; -import { Toaster, toast } from '../../../ui-next/src/components/Sonner'; -import { DataRow } from '../../../ui-next/src/components/DataRow'; -import DataRowExample from './patterns/DataRowExample'; -import { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, -} from '../../../ui-next/src/components/Card'; - -interface ShowcaseRowProps { - title: string; - description?: string; - children: React.ReactNode; - code: string; -} - -export default function ComponentShowcase() { - // Handlers to trigger different types of toasts - const triggerSuccess = () => { - toast.success('This is a success toast!'); - }; - - const triggerError = () => { - toast.error('This is an error toast!'); - }; - - const triggerInfo = () => { - toast.info('This is an info toast!'); - }; - - const triggerWarning = () => { - toast.warning('This is a warning toast!'); - }; - - // Handler to trigger a toast.promise example - const triggerPromiseToast = () => { - const promise = () => - new Promise<{ name: string }>(resolve => - setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000) - ); - - toast.promise(promise(), { - loading: 'Loading Segmentation...', - success: data => `${data.name} has been added`, - error: 'Error', - }); - }; - - // Handler to trigger a toast with description - const triggerDescriptionToast = () => { - toast.success('Completed', { - description: 'This is a detailed description of the success message.', - }); - }; - - // Handler to trigger a toast with an action button - const triggerActionButtonToast = () => { - toast.info('No active segmentation detected', { - description: 'Create a segmentation before using the Brush', - }); - }; - - // Handler to trigger a toast with a cancel button - const triggerCancelButtonToast = () => { - toast.error('No active segmentation detected', { - description: 'Create a segmentation before using the Brush', - }); - }; - - // Handler to trigger a toast with both action and cancel buttons - const triggerCombinedToast = () => { - toast.warning('Warning!', { - description: 'This is a warning with both action and cancel buttons.', - action: ( - - ), - cancel: ( - - ), - }); - }; - - // Handler to trigger a loading toast using Toaster's default loading icon - const showLoadingToast = () => { - toast.loading('Loading your data...'); - }; +import { TooltipProvider } from '../../../ui-next/src/components/Tooltip'; +// Import all showcase components +import ButtonShowcase from './components/ButtonShowcase'; +import CheckboxShowcase from './components/CheckboxShowcase'; +import DataRowShowcase from './components/DataRowShowcase'; +import DropdownMenuShowcase from './components/DropdownMenuShowcase'; +import InputShowcase from './components/InputShowcase'; +import ScrollAreaShowcase from './components/ScrollAreaShowcase'; +import SelectShowcase from './components/SelectShowcase'; +import SliderShowcase from './components/SliderShowcase'; +import SwitchShowcase from './components/SwitchShowcase'; +import TabsShowcase from './components/TabsShowcase'; +import ToastShowcase from './components/ToastShowcase'; +import ToolButtonShowcase from './components/ToolButtonShowcase'; +import ToolButtonListShowcase from './components/ToolButtonListShowcase'; +import NumericMetaShowcase from './components/NumericMetaShowcase'; +/** + * Components List page that displays all available UI components + */ +export default function ComponentsList() { return ( -
-
-
- - - - - - Colors & Typography - - - Color Palette and Typography Guidelines - - - - - - - - - - Components - - - Essential UI Components with Variants - - - - - - - - - - Patterns - - - Component-Based Layout Examples - - - - + +
+
+
+

Components

+
+ + + + + + + + + + + + + + + +
- -
-
-

Components

-
- - {/* Alphabetically Sorted ShowcaseRows */} - Primary Button - - - - - - - - - `} - > -
- - - - - -
-
- - -
-
- - - -
- -
-
- `} - > -
- -
- -
-
- - - - {/* Render the DataRowExample component */} - - - - - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - - console.debug('Item 1')}>Item 1 - console.debug('Item 2')}>Item 2 - console.debug('Item 3')}>Long name Item 3 - - - `} - > -
- - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - Item 1 - Item 2 - Long name Item 3 - - - - - - - - console.debug('Item 1')}> - Item 1 - - console.debug('Item 2')}> - Item 2 - - console.debug('Item 3')}> - Long name Item 3 - - - -
-
- - -
- -
-
- -
-
- `} - > -
-
- -
-
- -
-
- - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat - non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. - - `} - > - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt - mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - - - - - - Light - Dark - System - - - `} - > - - - - - -
- `} - > -
- -
- - - - `} - > - - - - - - - Circle - - Sphere - - Square - - - `} - > - - - Circle - - Sphere - - Square - - - - - - {/* Toast Examples Section */} - Simple message: -
- - - - - -
- Message with details: -
- - - - -
- {/* Render the Toaster component */} - -
-
- +
); } - -function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { - const [showCode, setShowCode] = useState(false); - - return ( -
-
-
-

{title}

-
- -
-
-
- {description &&

{description}

} -
-
-
{children}
-
-
- {showCode && ( -
-          {code}
-        
- )} -
- ); -} - -// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { -// const [showCode, setShowCode] = useState(false); - -// return ( -//
-//
-//
-//

{title}

-// {description &&

{description}

} -//
-// -//
-//
{children}
-// {showCode && ( -//
-//           {code}
-//         
-// )} -//
-// ); -// } diff --git a/platform/docs/src/pages/components/ButtonShowcase.tsx b/platform/docs/src/pages/components/ButtonShowcase.tsx new file mode 100644 index 000000000..2c3debdc4 --- /dev/null +++ b/platform/docs/src/pages/components/ButtonShowcase.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ButtonShowcase component displays button variants and examples + */ +export default function ButtonShowcase() { + return ( + Primary Button + + + + + + + + + `} + > +
+ + + + + +
+
+ + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/CheckboxShowcase.tsx b/platform/docs/src/pages/components/CheckboxShowcase.tsx new file mode 100644 index 000000000..427911485 --- /dev/null +++ b/platform/docs/src/pages/components/CheckboxShowcase.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Checkbox } from '../../../../ui-next/src/components/Checkbox'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * CheckboxShowcase component displays checkbox variants and examples + */ +export default function CheckboxShowcase() { + return ( + + +
+ +
+ + `} + > +
+ +
+ +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/DataRowShowcase.tsx b/platform/docs/src/pages/components/DataRowShowcase.tsx new file mode 100644 index 000000000..200b165a9 --- /dev/null +++ b/platform/docs/src/pages/components/DataRowShowcase.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import DataRowExample from '../patterns/DataRowExample'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * DataRowShowcase component displays DataRow variants and examples + */ +export default function DataRowShowcase() { + return ( + + + + ); +} diff --git a/platform/docs/src/pages/components/DropdownMenuShowcase.tsx b/platform/docs/src/pages/components/DropdownMenuShowcase.tsx new file mode 100644 index 000000000..5309847a2 --- /dev/null +++ b/platform/docs/src/pages/components/DropdownMenuShowcase.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../../../../ui-next/src/components/DropdownMenu'; +import { Button } from '../../../../ui-next/src/components/Button'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * DropdownMenuShowcase component displays DropdownMenu variants and examples + */ +export default function DropdownMenuShowcase() { + return ( + + + + + + Item 1 + Item 2 + Long name Item 3 + + + `} + > +
+ + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + console.debug('Item 1')}>Item 1 + console.debug('Item 2')}>Item 2 + console.debug('Item 3')}> + Long name Item 3 + + + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/InputShowcase.tsx b/platform/docs/src/pages/components/InputShowcase.tsx new file mode 100644 index 000000000..77c0484a8 --- /dev/null +++ b/platform/docs/src/pages/components/InputShowcase.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Input } from '../../../../ui-next/src/components/Input'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * InputShowcase component displays Input variants and examples + */ +export default function InputShowcase() { + return ( + +
+ +
+
+ +
+ + `} + > +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/NumericMetaShowcase.tsx b/platform/docs/src/pages/components/NumericMetaShowcase.tsx new file mode 100644 index 000000000..1dcc6bce8 --- /dev/null +++ b/platform/docs/src/pages/components/NumericMetaShowcase.tsx @@ -0,0 +1,260 @@ +import React from 'react'; +import Numeric from '../../../../ui-next/src/components/Numeric'; +import Icons from '../../../../ui-next/src/components/Icons'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * NumericShowcase component displays Numeric variants and examples + */ +export default function NumericShowcase() { + return ( +
+ {/* Basic Number Input */} + +
+ Width + +
+ + + + Bolder + + + + + + + With Icon + + +`} + > +
+ console.debug('Value changed:', val)} + > +
+ Width + +
+
+ + console.debug('Value changed:', val)} + > + + Bolder + + + + + console.debug('Value changed:', val)} + min={0} + value={123465789} + max={10000000000000} + > + + + With Icon + + + +
+
+ + {/* Single Range Slider */} + + Brightness + + + + + Contrast + + + + + Something Else + +`} + > +
+ console.debug('Value changed:', val)} + > + Brightness + + + + console.debug('Value changed:', val)} + > + Contrast + + + + console.debug('Value changed:', val)} + > + Something Else + + +
+
+ + {/* Double Range Slider */} + + Window Width/Level + + + + + Window Width/Level + + + + + Inline double slider + +`} + > +
+ console.debug('Values changed:', vals)} + > + Window Width/Level + + + + + Window Width/Level + + + + console.debug('Values changed:', vals)} + > + Inline double slider + + +
+
+ + {/* Combined Examples */} + + Zoom Factor + + + + + Rotation + + + + + CT Window + +`} + > +
+ + Zoom Factor + + + + + Rotation + + + + + CT Window + + +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/ScrollAreaShowcase.tsx b/platform/docs/src/pages/components/ScrollAreaShowcase.tsx new file mode 100644 index 000000000..d9eaffdad --- /dev/null +++ b/platform/docs/src/pages/components/ScrollAreaShowcase.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { ScrollArea } from '../../../../ui-next/src/components/ScrollArea'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ScrollAreaShowcase component displays ScrollArea variants and examples + */ +export default function ScrollAreaShowcase() { + return ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. + + `} + > + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in + voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem + ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + + + ); +} diff --git a/platform/docs/src/pages/components/SelectShowcase.tsx b/platform/docs/src/pages/components/SelectShowcase.tsx new file mode 100644 index 000000000..8ee244b3d --- /dev/null +++ b/platform/docs/src/pages/components/SelectShowcase.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from '../../../../ui-next/src/components/Select'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SelectShowcase component displays Select variants and examples + */ +export default function SelectShowcase() { + return ( + + + + + + Light + Dark + System + + + `} + > + + + ); +} diff --git a/platform/docs/src/pages/components/ShowcaseRow.tsx b/platform/docs/src/pages/components/ShowcaseRow.tsx new file mode 100644 index 000000000..a1c92400c --- /dev/null +++ b/platform/docs/src/pages/components/ShowcaseRow.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Icons } from '../../../../ui-next/src/components/Icons'; + +interface ShowcaseRowProps { + title: string; + description?: string; + children: React.ReactNode; + code: string; +} + +/** + * ShowcaseRow component displays a UI component example with title, description, + * and optional code snippet that can be toggled. + */ +export default function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { + const [showCode, setShowCode] = useState(false); + + return ( +
+
+
+

{title}

+
+ +
+
+
+ {description &&

{description}

} +
+
+
{children}
+
+
+ {showCode && ( +
+          {code}
+        
+ )} +
+ ); +} diff --git a/platform/docs/src/pages/components/SliderShowcase.tsx b/platform/docs/src/pages/components/SliderShowcase.tsx new file mode 100644 index 000000000..0788ceb8d --- /dev/null +++ b/platform/docs/src/pages/components/SliderShowcase.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Slider } from '../../../../ui-next/src/components/Slider'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SliderShowcase component displays Slider variants and examples + */ +export default function SliderShowcase() { + return ( + + + + `} + > +
+ +
+
+ ); +} diff --git a/platform/docs/src/pages/components/SwitchShowcase.tsx b/platform/docs/src/pages/components/SwitchShowcase.tsx new file mode 100644 index 000000000..7d814a222 --- /dev/null +++ b/platform/docs/src/pages/components/SwitchShowcase.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Switch } from '../../../../ui-next/src/components/Switch'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SwitchShowcase component displays Switch variants and examples + */ +export default function SwitchShowcase() { + return ( + + `} + > + + + + ); +} diff --git a/platform/docs/src/pages/components/TabsShowcase.tsx b/platform/docs/src/pages/components/TabsShowcase.tsx new file mode 100644 index 000000000..e88750339 --- /dev/null +++ b/platform/docs/src/pages/components/TabsShowcase.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import { Separator } from '../../../../ui-next/src/components/Separator'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * TabsShowcase component displays Tabs variants and examples + */ +export default function TabsShowcase() { + return ( + console.log(newValue)}> + + Circle + + Sphere + + Square + + + `} + > + console.log(newValue)} + > + + Circle + + Sphere + + Square + + + + ); +} diff --git a/platform/docs/src/pages/components/ToastShowcase.tsx b/platform/docs/src/pages/components/ToastShowcase.tsx new file mode 100644 index 000000000..b79ae5c33 --- /dev/null +++ b/platform/docs/src/pages/components/ToastShowcase.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Toaster, toast } from '../../../../ui-next/src/components/Sonner'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToastShowcase component displays Toast variants and examples + */ +export default function ToastShowcase() { + // Handlers to trigger different types of toasts + const triggerSuccess = () => { + toast.success('This is a success toast!'); + }; + + const triggerError = () => { + toast.error('This is an error toast!'); + }; + + const triggerInfo = () => { + toast.info('This is an info toast!'); + }; + + const triggerWarning = () => { + toast.warning('This is a warning toast!'); + }; + + // Handler to trigger a toast.promise example + const triggerPromiseToast = () => { + const promise = () => + new Promise<{ name: string }>(resolve => + setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000) + ); + + toast.promise(promise(), { + loading: 'Loading Segmentation...', + success: data => `${data.name} has been added`, + error: 'Error', + }); + }; + + // Handler to trigger a toast with description + const triggerDescriptionToast = () => { + toast.success('Completed', { + description: 'This is a detailed description of the success message.', + }); + }; + + // Handler to trigger a toast with an action button + const triggerActionButtonToast = () => { + toast.info('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with a cancel button + const triggerCancelButtonToast = () => { + toast.error('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with both action and cancel buttons + const triggerCombinedToast = () => { + toast.warning('Warning!', { + description: 'This is a warning with both action and cancel buttons.', + action: ( + + ), + cancel: ( + + ), + }); + }; + + return ( + + Simple message: +
+ + + + + +
+ Message with details: +
+ + + + +
+ +
+ ); +} diff --git a/platform/docs/src/pages/components/ToolButtonListShowcase.tsx b/platform/docs/src/pages/components/ToolButtonListShowcase.tsx new file mode 100644 index 000000000..eccae8f94 --- /dev/null +++ b/platform/docs/src/pages/components/ToolButtonListShowcase.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { + ToolButtonList, + ToolButton, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from '../../../../ui-next/src/components/ToolButton'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; + +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToolButtonListShowcase component displays ToolButtonList variants and examples + */ +export default function ToolButtonListShowcase() { + return ( + + + console.debug(\`Clicked \${itemId}\`)} + /> + + + + console.debug('Selected Length')} + > + Length + + console.debug('Selected Bidirectional')} + > + Bidirectional + + + + `} + > +
+ + + + console.debug(`Clicked ${itemId}`)} + /> + + + + console.debug('Selected Length')} + > + Length + + console.debug('Selected Bidirectional')} + > + Bidirectional + + console.debug('Selected Annotation')} + > + Annotation + + + + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/ToolButtonShowcase.tsx b/platform/docs/src/pages/components/ToolButtonShowcase.tsx new file mode 100644 index 000000000..b3dcf103e --- /dev/null +++ b/platform/docs/src/pages/components/ToolButtonShowcase.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; +import ToolButton from '../../../../ui-next/src/components/ToolButton/ToolButton'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToolButtonShowcase component displays ToolButton variants and examples + */ +export default function ToolButtonShowcase() { + return ( + console.debug(\`Clicked \${itemId}\`)} +/> + `} + > +
+ + console.debug(`Clicked ${itemId}`)} + /> + console.debug(`Clicked ${itemId}`)} + /> + console.debug(`Clicked ${itemId}`)} + /> + +
+
+ ); +} diff --git a/platform/ui-next/src/components/Button/Button.tsx b/platform/ui-next/src/components/Button/Button.tsx index 48dbcc11f..c8a2ed602 100644 --- a/platform/ui-next/src/components/Button/Button.tsx +++ b/platform/ui-next/src/components/Button/Button.tsx @@ -38,12 +38,12 @@ export interface ButtonProps } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, ...props }, forwardRef) => { const Comp = asChild ? Slot : 'button'; return ( ); diff --git a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx index 867962c22..20a5a1484 100644 --- a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx +++ b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx @@ -11,10 +11,22 @@ interface DoubleSliderProps { step?: number; defaultValue?: [number, number]; onValueChange?: (value: [number, number]) => void; + showNumberInputs?: boolean; } const DoubleSlider = React.forwardRef( - ({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => { + ( + { + className, + min, + max, + onValueChange, + step = 1, + defaultValue = [min, max], + showNumberInputs = false, + }, + ref + ) => { const [value, setValue] = React.useState<[number, number]>(defaultValue); const prevDefaultValueRef = React.useRef<[number, number] | null>(null); @@ -77,16 +89,18 @@ const DoubleSlider = React.forwardRef( ref={ref} className={cn('flex w-full items-center space-x-2', className)} > - handleInputChange(0, e.target.value)} - onBlur={() => handleInputChange(0, value[0].toString())} - className="w-14" - min={min} - max={max} - step={step} - /> + {showNumberInputs && ( + handleInputChange(0, e.target.value)} + onBlur={() => handleInputChange(0, value[0].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> + )} ( - handleInputChange(1, e.target.value)} - onBlur={() => handleInputChange(1, value[1].toString())} - className="w-14" - min={min} - max={max} - step={step} - /> + {showNumberInputs && ( + handleInputChange(1, e.target.value)} + onBlur={() => handleInputChange(1, value[1].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> + )} ); } diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx index 4f95ff831..d3c566d29 100644 --- a/platform/ui-next/src/components/Icons/Icons.tsx +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -689,6 +689,9 @@ export const Icons = { 'icon-status-alert': (props: IconProps) => Alert(props), 'info-link': (props: IconProps) => InfoLink(props), 'launch-info': (props: IconProps) => LaunchInfo(props), + 'old-trash': (props: IconProps) => Trash(props), + 'tool-point': (props: IconProps) => ToolCircle(props), + 'tool-freehand-line': (props: IconProps) => ToolFreehand(props), clipboard: (props: IconProps) => Clipboard(props), /** Adds an icon to the set of icons */ diff --git a/platform/ui-next/src/components/Icons/Sources/Tools.tsx b/platform/ui-next/src/components/Icons/Sources/Tools.tsx index 2100fed57..4140dc5f7 100644 --- a/platform/ui-next/src/components/Icons/Sources/Tools.tsx +++ b/platform/ui-next/src/components/Icons/Sources/Tools.tsx @@ -2447,8 +2447,6 @@ export const ToolSegBrush = (props: IconProps) => ( width="24px" height="24px" viewBox="0 0 24 24" - version="1.1" - xmlns="http://www.w3.org/2000/svg" {...props} > ( width="24px" height="24px" viewBox="0 0 24 24" - version="1.1" - xmlns="http://www.w3.org/2000/svg" {...props} > ( width="24px" height="24px" viewBox="0 0 24 24" - version="1.1" - xmlns="http://www.w3.org/2000/svg" {...props} > ( export const ToolWindowRegion = (props: IconProps) => ( ( id="Rectangle" x="0" y="0" - width="28" - height="28" + width="24" + height="24" > ( export const ToolBrush = (props: IconProps) => ( ( id="Rectangle" x="0" y="0" - width="28" - height="28" + width="24" + height="24" > ( export const ToolEraser = (props: IconProps) => ( ( id="Rectangle" x="0" y="0" - width="28" - height="28" + width="24" + height="24" > ( export const ToolThreshold = (props: IconProps) => ( ( id="Rectangle" x="0" y="0" - width="28" - height="28" + width="24" + height="24" > ( export const ToolShape = (props: IconProps) => ( ( id="Rectangle" x="0" y="0" - width="28" - height="28" + width="24" + height="24" > void; + setDoubleValue: (vals: [number, number]) => void; + min: number; + max: number; + step: number; +} + +const NumericMetaContext = createContext(null); + +/* ------------------------------------------------------------------------- + 1) Container +---------------------------------------------------------------------------*/ +interface NumericMetaContainerProps { + mode: 'number' | 'singleRange' | 'doubleRange'; + value?: number; // for single-value usage + values?: [number, number]; // for double-range usage + onChange?: (val: number | [number, number]) => void; + min?: number; + max?: number; + step?: number; + className?: string; +} + +function NumericMetaContainer({ + mode, + value = 0, + values = [0, 100], + onChange, + min = 0, + max = 100, + step = 1, + className, + children, +}: PropsWithChildren) { + // Initialize state with props but don't update automatically + const [internalSingleValue, setInternalSingleValue] = useState(value); + const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values); + + const handleSingleChange = useCallback( + (newVal: number) => { + setInternalSingleValue(newVal); + onChange?.(newVal); + }, + [onChange] + ); + + const handleDoubleChange = useCallback( + (newVals: [number, number]) => { + // Update internal state + setInternalDoubleValue(newVals); + // Notify parent if onChange is provided + onChange?.(newVals); + }, + [onChange] + ); + + return ( + +
{children}
+ + ); +} + +/* ------------------------------------------------------------------------- + 2) Label sub-component +---------------------------------------------------------------------------*/ +interface NumericMetaLabelProps { + showValue?: boolean; // optionally show the current numeric value(s) + className?: string; + children: React.ReactNode; +} + +function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('NumericMetaLabel must be used inside .'); + } + + const { mode, singleValue, doubleValue } = ctx; + + let displayedValue = ''; + let valueClasses = ''; + if (mode === 'number' || mode === 'singleRange') { + displayedValue = singleValue.toString(); + valueClasses = 'w-10'; + } else if (mode === 'doubleRange') { + displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`; + } + + return ( +
+ {children} + {showValue && ( + {`: ${displayedValue}`} + )} +
+ ); +} + +/* ------------------------------------------------------------------------- + 3) SingleRange sub-component +---------------------------------------------------------------------------*/ + +interface SingleRangeProps { + showNumberInput?: boolean; + sliderClassName?: string; + numberInputClassName?: string; +} + +function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('SingleRange must be used inside .'); + } + + const { mode, singleValue, setSingleValue, min, max, step } = ctx; + + const handleSliderChange = useCallback( + (val: number[]) => { + setSingleValue(val[0]); + }, + [setSingleValue] + ); + + const handleNumberChange = useCallback( + (evt: React.ChangeEvent) => { + const parsed = parseFloat(evt.target.value); + if (!isNaN(parsed)) { + setSingleValue(Math.max(min, Math.min(parsed, max))); + } + }, + [min, max, setSingleValue] + ); + + if (mode !== 'singleRange') { + return null; + } + + return ( +
+ + {showNumberInput && ( + + )} +
+ ); +} + +/* ------------------------------------------------------------------------- + 4) DoubleRange sub-component +---------------------------------------------------------------------------*/ +interface DoubleRangeProps { + showNumberInputs?: boolean; + className?: string; +} + +function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('DoubleRange must be used inside .'); + } + + const { mode, doubleValue, setDoubleValue, min, max, step } = ctx; + + const handleSliderChange = useCallback( + (values: [number, number]) => { + setDoubleValue(values); + }, + [setDoubleValue] + ); + + if (mode !== 'doubleRange') { + return null; + } + + return ( +
+ +
+ ); +} + +/* ------------------------------------------------------------------------- + 5) Basic NumberInput sub-component +---------------------------------------------------------------------------*/ +interface NumberInputProps { + className?: string; +} + +function NumberInput({ className }: NumberInputProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('NumberInput must be used inside .'); + } + + const { mode, singleValue, setSingleValue, min, max, step } = ctx; + if (mode !== 'number') { + return null; + } + + const handleChange = (evt: React.ChangeEvent) => { + const val = parseFloat(evt.target.value); + if (!isNaN(val)) { + setSingleValue(Math.max(min, Math.min(val, max))); + } + }; + + // Calculate width based on max value's length, with a minimum of 3 characters + const maxLength = Math.max(3, max?.toString().length ?? 3); + const calculatedWidth = `${maxLength + 1.5}ch`; + + return ( + + ); +} + +export const Numeric = { + Container: NumericMetaContainer, + Label: NumericMetaLabel, + SingleRange, + DoubleRange, + NumberInput, +}; + +export default Numeric; diff --git a/platform/ui-next/src/components/Numeric/index.ts b/platform/ui-next/src/components/Numeric/index.ts new file mode 100644 index 000000000..124189577 --- /dev/null +++ b/platform/ui-next/src/components/Numeric/index.ts @@ -0,0 +1,3 @@ +import Numeric from './Numeric'; + +export default Numeric; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx new file mode 100644 index 000000000..58cc3f3a0 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Numeric from '../Numeric'; +import { cn } from '../../lib/utils'; + +interface RowDoubleRangeProps { + values: [number, number]; + onChange: (values: [number, number]) => void; + minValue: number; + maxValue: number; + step: number; + showLabel?: boolean; + label?: string; + className?: string; +} + +const RowDoubleRange: React.FC = ({ + values, + onChange, + minValue, + maxValue, + step, + showLabel = false, + label = '', + className, +}) => { + return ( + + {showLabel && {label}} + + + ); +}; + +export default RowDoubleRange; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx new file mode 100644 index 000000000..395fa3a3a --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import Numeric from '../Numeric'; +import { cn } from '../../lib/utils'; + +interface RowInputRangeProps { + value: number; + onChange: (newValue: number) => void; + minValue?: number; + maxValue?: number; + step?: number; + label?: string; + showLabel?: boolean; + labelPosition?: 'left' | 'right'; + allowNumberEdit?: boolean; + showNumberInput?: boolean; + className?: string; + containerClassName?: string; +} + +const RowInputRange: React.FC = ({ + value, + onChange, + minValue = 0, + maxValue = 100, + step = 1, + label = '', + showLabel = false, + labelPosition = 'right', + allowNumberEdit = false, + showNumberInput = true, + className, + containerClassName, +}) => { + const handleChange = (newValue: number | [number, number]) => { + if (typeof newValue === 'number') { + onChange(newValue); + } else { + onChange(newValue[0]); + } + }; + + const content = ( + + {showLabel && label && labelPosition === 'left' && ( + {label} + )} + + {showLabel && label && labelPosition === 'right' && ( + {label} + )} + + ); + + return containerClassName ?
{content}
: content; +}; + +export default RowInputRange; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx new file mode 100644 index 000000000..918c3da13 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Label } from '../Label'; +import { Tabs, TabsList, TabsTrigger } from '../Tabs'; +import { cn } from '../../lib/utils'; + +interface RadioValue { + value: string; + label: string; +} + +interface RadioOption { + id: string; + name: string; + value: string; + values: RadioValue[]; + commands?: (val: string) => void; +} + +interface RowSegmentedControlProps { + option: RadioOption; + className?: string; +} + +export const RowSegmentedControl: React.FC = ({ option, className }) => { + const handleValueChange = (newVal: string) => { + if (option.commands) { + option.commands(newVal); + } + }; + + return ( +
+ +
+ + + {option.values.map(({ label, value: itemValue }, index) => ( + + {label} + + ))} + + +
+
+ ); +}; + +export default RowSegmentedControl; diff --git a/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx b/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx new file mode 100644 index 000000000..33e3c0969 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import RowInputRange from './RowInputRange'; +import RowSegmentedControl from './RowSegmentedControl'; +import RowDoubleRange from './RowDoubleRange'; + +const SETTING_TYPES = { + RANGE: 'range', + RADIO: 'radio', + CUSTOM: 'custom', + DOUBLE_RANGE: 'double-range', +}; + +function ToolSettings({ options }) { + if (!options) { + return null; + } + + if (typeof options === 'function') { + return options(); + } + + return ( +
+ {options?.map(option => { + if (option.condition && option.condition?.({ options }) === false) { + return null; + } + + switch (option.type) { + case SETTING_TYPES.RANGE: + return renderRangeSetting(option); + case SETTING_TYPES.RADIO: + return renderRadioSetting(option); + case SETTING_TYPES.DOUBLE_RANGE: + return renderDoubleRangeSetting(option); + case SETTING_TYPES.CUSTOM: + return renderCustomSetting(option); + default: + return null; + } + })} +
+ ); +} + +const renderRangeSetting = option => { + return ( +
+
{option.name}
+
+ option.commands?.(value)} + allowNumberEdit={true} + inputClassName="ml-1 w-4/5 cursor-pointer" + /> +
+
+ ); +}; + +const renderRadioSetting = option => { + return ( + + ); +}; + +function renderDoubleRangeSetting(option) { + return ( + + ); +} + +const renderCustomSetting = option => { + return ( +
+ {typeof option.children === 'function' ? option.children() : option.children} +
+ ); +}; + +export default ToolSettings; diff --git a/platform/ui-next/src/components/OHIFToolSettings/index.ts b/platform/ui-next/src/components/OHIFToolSettings/index.ts new file mode 100644 index 000000000..592221343 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/index.ts @@ -0,0 +1 @@ +export { default as ToolSettings } from './ToolSettings'; diff --git a/platform/ui-next/src/components/Toolbox/Toolbox.tsx b/platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx similarity index 90% rename from platform/ui-next/src/components/Toolbox/Toolbox.tsx rename to platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx index 5e23fb774..93871ec8e 100644 --- a/platform/ui-next/src/components/Toolbox/Toolbox.tsx +++ b/platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useRef } from 'react'; +import { ToolboxUI, useToolbox } from '../../'; import { useToolbar } from '@ohif/core'; -import { ToolboxUI } from './'; -// Migrate this file to the new UI eventually -import { useToolbox } from '@ohif/ui'; /** * A toolbox is a collection of buttons and commands that they invoke, used to provide @@ -21,6 +19,8 @@ function Toolbox({ title, ...props }: withAppTypes) { + // We should move these outside of the platform/ui-next, no file here + // should rely on the managers and services const { state: toolboxState, api } = useToolbox(buttonSectionId); const { onInteraction, toolbarButtons } = useToolbar({ servicesManager, @@ -117,6 +117,9 @@ function Toolbox({ if (items?.length) { items.forEach(({ options, id }) => { + if (!options) { + return; + } accumulator[id] = createEnhancedOptions(options, id); }); } else if (options?.length) { @@ -144,17 +147,15 @@ function Toolbox({ }, []); return ( - <> - api.handleToolSelect(id)} - handleToolOptionChange={handleToolOptionChange} - onInteraction={onInteraction} - /> - + api.handleToolSelect(id)} + handleToolOptionChange={handleToolOptionChange} + onInteraction={onInteraction} + /> ); } diff --git a/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx b/platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx similarity index 79% rename from platform/ui-next/src/components/Toolbox/ToolboxUI.tsx rename to platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx index b00079826..32b522b01 100644 --- a/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx +++ b/platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useRef } from 'react'; -import { PanelSection } from '../../components'; -// Migrate this file to the new UI eventually -import { ToolSettings } from '@ohif/ui'; import classnames from 'classnames'; +import { PanelSection } from '../../components'; +import { ToolSettings } from '../OHIFToolSettings'; + const ItemsPerRow = 4; function usePrevious(value) { @@ -55,8 +55,8 @@ function ToolboxUI(props: withAppTypes) { const render = () => { return ( <> -
-
+
+
{toolbarButtons.map((toolDef, index) => { if (!toolDef) { return null; @@ -81,20 +81,16 @@ function ToolboxUI(props: withAppTypes) { key={id} className={classnames({ [toolClasses]: true, - 'border-secondary-light flex flex-col items-center justify-center rounded-md border': - true, })} > -
- -
+
); })} diff --git a/platform/ui-next/src/components/Toolbox/index.ts b/platform/ui-next/src/components/OHIFToolbox/index.ts similarity index 100% rename from platform/ui-next/src/components/Toolbox/index.ts rename to platform/ui-next/src/components/OHIFToolbox/index.ts diff --git a/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx index 5ccb15d81..a2be669b3 100644 --- a/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx +++ b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx @@ -101,7 +101,7 @@ const Thumbnail = ({
- +
- + ; + onInteraction?: (details: { itemId: string; commands?: Record }) => void; + className?: string; +} + +function ToolButton(props: ToolButtonProps) { + const { + id, + icon = 'MissingIcon', + label, + tooltip, + size = 'default', + disabled = false, + isActive = false, + disabledText, + commands, + onInteraction, + className, + } = props; + + const { buttonSizeClass, iconSizeClass } = sizeClasses[size]; + + const buttonClasses = cn( + baseClasses, + buttonSizeClass, + disabled ? disabledClasses : isActive ? activeClasses : defaultClasses, + className + ); + + const defaultTooltip = tooltip || label; + const disabledTooltip = disabled && disabledText ? disabledText : null; + const hasTooltip = defaultTooltip || disabledTooltip; + + return ( + + + {/* TooltipTrigger is a span since a disabled button does not fire events and the tooltip + will not show. */} + + + + + + {hasTooltip && ( + <> +
{defaultTooltip}
+ {disabledTooltip &&
{disabledTooltip}
} + + )} +
+
+ ); +} + +export default ToolButton; diff --git a/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx new file mode 100644 index 000000000..e2374a470 --- /dev/null +++ b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx @@ -0,0 +1,207 @@ +import React from 'react'; +import { Button } from '../Button'; +import { Icons } from '../Icons'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../DropdownMenu'; +import { cn } from '../../lib/utils'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; + +/** + * ToolButtonList Component + * Root component that wraps the default and dropdown sections + * ----------------------------------------------- + */ +interface ToolButtonListProps extends React.HTMLAttributes { + children?: React.ReactNode; +} + +const ToolButtonList = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); +ToolButtonList.displayName = 'ToolButtonList'; + +/** + * ToolButtonListDefault Component + * Container for the default/primary tool button + * ----------------------------------------------- + */ +interface ToolButtonListDefaultProps extends React.HTMLAttributes { + children?: React.ReactNode; + tooltip?: string; + disabledText?: string; + disabled?: boolean; +} + +const ToolButtonListDefault = React.forwardRef( + ({ className, children, tooltip, disabledText, disabled, ...props }, ref) => { + const hasTooltip = tooltip || disabledText; + + const defaultContent = ( +
+ {children} +
+ ); + + if (!hasTooltip) { + return defaultContent; + } + + return ( + + + {defaultContent} + + + {tooltip &&
{tooltip}
} + {disabledText && disabled &&
{disabledText}
} +
+
+ ); + } +); +ToolButtonListDefault.displayName = 'ToolButtonListDefault'; + +/** + * ToolButtonListDropDown Component + * Container for the dropdown section with trigger and content + * ----------------------------------------------- + */ +interface ToolButtonListDropDownProps { + children: React.ReactNode; + className?: string; +} + +const ToolButtonListDropDown = React.forwardRef( + ({ children, className, ...props }, ref) => ( + + + + + + {children} + + + ) +); +ToolButtonListDropDown.displayName = 'ToolButtonListDropDown'; + +/** + * ToolButtonListItem Component + * Individual item in the dropdown menu + * ----------------------------------------------- + */ +interface ToolButtonListItemProps extends React.ComponentProps { + icon?: string; + children?: React.ReactNode; + className?: string; + disabledText?: string; + tooltip?: string; +} + +const ToolButtonListItem = React.forwardRef< + React.ElementRef, + ToolButtonListItemProps +>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => { + const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined); + const hasTooltip = defaultTooltip || disabledText; + + const menuItem = ( + + {icon && ( + + )} + {children} + + ); + + // Todo: there is a weird issue where i can't control the duration of the delay + // for the items in this list, causing the tooltip to show up too early in the + // dropdown menu. So i'm just removing the tooltip for list items unless the disabledText is set. + if (!disabled) { + return menuItem; + } + + return ( + + + {menuItem} + + + {defaultTooltip &&
{defaultTooltip}
} + {disabledText && disabled &&
{disabledText}
} +
+
+ ); +}); +ToolButtonListItem.displayName = 'ToolButtonListItem'; + +/** + * ToolButtonListDivider Component + * Divider between items in the dropdown menu + * ----------------------------------------------- + */ +const ToolButtonListDivider = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ToolButtonListDivider.displayName = 'ToolButtonListDivider'; + +export { + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +}; diff --git a/platform/ui-next/src/components/ToolButton/index.ts b/platform/ui-next/src/components/ToolButton/index.ts new file mode 100644 index 000000000..49dce1dfa --- /dev/null +++ b/platform/ui-next/src/components/ToolButton/index.ts @@ -0,0 +1,8 @@ +export { default as ToolButton } from './ToolButton'; +export { + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from './ToolButtonList'; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index c2d57b748..0183ed417 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -52,7 +52,9 @@ import { ThumbnailList } from './ThumbnailList'; import { PanelSection } from './PanelSection'; import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip'; import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip'; -import { Toolbox, ToolboxUI } from './Toolbox'; +import { ToolboxUI, Toolbox } from './OHIFToolbox'; +import Numeric from './Numeric'; + import { DropdownMenu, DropdownMenuTrigger, @@ -91,8 +93,17 @@ import { ViewportOverlay, ViewportGrid, } from './Viewport'; +import { + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from './ToolButton'; export { + Numeric, ErrorBoundary, Button, buttonVariants, @@ -157,8 +168,8 @@ export { ThumbnailList, PanelSection, DisplaySetMessageListTooltip, - Toolbox, ToolboxUI, + Toolbox, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, @@ -208,4 +219,10 @@ export { ViewportOverlay, ViewportGrid, Clipboard, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, }; diff --git a/platform/ui-next/src/contextProviders/ToolboxContext.tsx b/platform/ui-next/src/contextProviders/ToolboxContext.tsx new file mode 100644 index 000000000..23c87f80f --- /dev/null +++ b/platform/ui-next/src/contextProviders/ToolboxContext.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +export const initialState = {}; + +export const toolboxReducer = (state, action) => { + const { toolbarSectionId } = action.payload; + + if (!state[toolbarSectionId]) { + state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false }; + } + + switch (action.type) { + case 'SET_ACTIVE_TOOL': + return { + ...state, + [toolbarSectionId]: { + ...state[toolbarSectionId], + activeTool: action.payload.activeTool, + selectedEvent: true, + }, + }; + case 'UPDATE_TOOL_OPTION': + const { toolName, optionName, value } = action.payload; + return { + ...state, + [toolbarSectionId]: { + ...state[toolbarSectionId], + selectedEvent: false, + toolOptions: { + ...state[toolbarSectionId].toolOptions, + [toolName]: state[toolbarSectionId].toolOptions[toolName].map(option => + option.id === optionName ? { ...option, value } : option + ), + }, + }, + }; + case 'INITIALIZE_TOOL_OPTIONS': + // Initialize tool options for each toolbarSectionId + return { + ...state, + selectedEvent: false, + [action.toolbarSectionId]: { + ...state[action.toolbarSectionId], + toolOptions: action.payload, + }, + }; + default: + return state; + } +}; + +const ToolboxContext = createContext(); + +export const ToolboxProvider = ({ children }) => { + const [state, dispatch] = useReducer(toolboxReducer, initialState); + + const handleToolSelect = (toolbarSectionId, toolName) => { + dispatch({ + type: 'SET_ACTIVE_TOOL', + payload: { toolbarSectionId, activeTool: toolName }, + }); + }; + + const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => { + dispatch({ + type: 'UPDATE_TOOL_OPTION', + payload: { toolbarSectionId, toolName, optionName, value: newValue }, + }); + }; + + const initializeToolOptions = (toolbarSectionId, toolOptions) => { + dispatch({ + type: 'INITIALIZE_TOOL_OPTIONS', + toolbarSectionId, + payload: toolOptions, + }); + }; + + const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions }; + + const value = { state, api }; + + return {children}; +}; + +/** + * Custom hook for accessing toolbox state and actions for a specific toolbar section. + * You can use this hook to access the state and actions for a specific toolbar section ( + * defined by the toolbarSectionId) in your custom toolbar components. This hook + * helps to manage the state and actions for the tools and their options in the toolbar. + */ +export const useToolbox = toolbarSectionId => { + const context = useContext(ToolboxContext); + if (context === undefined) { + throw new Error('useToolbox must be used within a ToolboxProvider'); + } + const { state, api } = context; + + return { + state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} }, + api: { + handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName), + handleToolOptionChange: (toolName, optionName, value) => + api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value), + initializeToolOptions: toolOptions => + api.initializeToolOptions(toolbarSectionId, toolOptions), + }, + }; +}; diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts index ffe6d2fa7..088b51419 100644 --- a/platform/ui-next/src/contextProviders/index.ts +++ b/platform/ui-next/src/contextProviders/index.ts @@ -1,5 +1,7 @@ import NotificationProvider, { useNotification } from './NotificationProvider'; import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider'; +import { ToolboxProvider, useToolbox } from './ToolboxContext'; export { useNotification, NotificationProvider }; export { ViewportGridContext, ViewportGridProvider, useViewportGrid }; +export { ToolboxProvider, useToolbox }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index a37eb59ca..8d4f665b6 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -32,7 +32,6 @@ import { ThumbnailList, PanelSection, DisplaySetMessageListTooltip, - Toolbox, ToolboxUI, DoubleSlider, Label, @@ -93,11 +92,24 @@ import { ViewportActionCornersLocations, ViewportOverlay, ViewportGrid, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, + Toolbox, } from './components'; import { DataRow } from './components/DataRow'; -import { useNotification, NotificationProvider } from './contextProviders'; +import { + useNotification, + NotificationProvider, + useToolbox, + ToolboxProvider, +} from './contextProviders'; import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders'; +import * as utils from './utils'; export { ErrorBoundary, @@ -143,7 +155,6 @@ export { ThumbnailList, PanelSection, DisplaySetMessageListTooltip, - Toolbox, ToolboxUI, Label, Slider, @@ -202,4 +213,14 @@ export { ViewportActionCornersLocations, ViewportOverlay, ViewportGrid, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, + ToolboxProvider, + Toolbox, + useToolbox, + utils, }; diff --git a/platform/ui-next/src/utils/getToggledClassName.tsx b/platform/ui-next/src/utils/getToggledClassName.tsx new file mode 100644 index 000000000..9d18de015 --- /dev/null +++ b/platform/ui-next/src/utils/getToggledClassName.tsx @@ -0,0 +1,7 @@ +const getToggledClassName = isToggled => { + return isToggled + ? '!text-primary-active' + : '!text-common-bright hover:!bg-primary-dark hover:text-primary-light'; +}; + +export { getToggledClassName }; diff --git a/platform/ui-next/src/utils/index.ts b/platform/ui-next/src/utils/index.ts new file mode 100644 index 000000000..b842eb1f3 --- /dev/null +++ b/platform/ui-next/src/utils/index.ts @@ -0,0 +1,3 @@ +import { getToggledClassName } from './getToggledClassName'; + +export { getToggledClassName }; diff --git a/platform/ui/src/components/SplitButton/SplitButton.tsx b/platform/ui/src/components/SplitButton/SplitButton.tsx index 7e246a334..e453414cb 100644 --- a/platform/ui/src/components/SplitButton/SplitButton.tsx +++ b/platform/ui/src/components/SplitButton/SplitButton.tsx @@ -31,7 +31,7 @@ const classes = { isActive ? isExpanded ? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light' - : 'border-primary-light bg-primary-light rounded-md' + : 'border-primary-light bg-primary-light !text-black rounded-md' : `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light ${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'}` ), Secondary: ({ isExpanded, primary }) => diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx index ab41ac636..5e0d2f018 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -76,6 +76,7 @@ ToolbarButton.propTypes = { commands: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]), onInteraction: PropTypes.func, icon: PropTypes.string.isRequired, + disabledText: PropTypes.string, label: PropTypes.string.isRequired, /** Tooltip content can be replaced for a customized content by passing a node to this value. */ dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), diff --git a/platform/ui/src/components/Toolbox/Toolbox.tsx b/platform/ui/src/components/Toolbox/Toolbox.tsx index 79e288e6a..74574ffb4 100644 --- a/platform/ui/src/components/Toolbox/Toolbox.tsx +++ b/platform/ui/src/components/Toolbox/Toolbox.tsx @@ -31,7 +31,7 @@ function Toolbox({ useEffect(() => { const currentButtonIdsStr = JSON.stringify( - toolbarButtons.map(button => { + toolbarButtons?.map(button => { const { id, componentProps } = button; if (componentProps.items?.length) { return componentProps.items.map(item => `${item.id}-${item.disabled}`); diff --git a/platform/ui/src/components/Toolbox/ToolboxUI.tsx b/platform/ui/src/components/Toolbox/ToolboxUI.tsx index 4b1dec6ef..955176e46 100644 --- a/platform/ui/src/components/Toolbox/ToolboxUI.tsx +++ b/platform/ui/src/components/Toolbox/ToolboxUI.tsx @@ -55,7 +55,7 @@ function ToolboxUI(props: withAppTypes) { <>
- {toolbarButtons.map((toolDef, index) => { + {toolbarButtons?.map((toolDef, index) => { if (!toolDef) { return null; } diff --git a/platform/ui/src/contextProviders/Toolbox/toolboxReducer.ts b/platform/ui/src/contextProviders/Toolbox/toolboxReducer.ts deleted file mode 100644 index c97df9938..000000000 --- a/platform/ui/src/contextProviders/Toolbox/toolboxReducer.ts +++ /dev/null @@ -1,52 +0,0 @@ -export function toolboxReducer(state, action) { - let newState = { ...state }; - - const { payload } = action; - - switch (action.type) { - case 'SET_ACTIVE_TOOL': - newState = { - ...state, - activeTool: payload.activeTool, - }; - break; - case 'UPDATE_TOOL_OPTION': - newState = { - ...state, - toolOptions: { - ...state.toolOptions, - [payload.toolName]: state.toolOptions[payload.toolName].map(option => - option.id === payload.optionName ? { ...option, value: payload.value } : option - ), - }, - }; - break; - case 'INITIALIZE_TOOL_OPTIONS': - // eslint-disable-next-line no-case-declarations - const newToolOptions = Object.keys(payload?.toolOptions || {}).reduce((acc, toolId) => { - const tool = payload.toolOptions[toolId]; - if (state.toolOptions[toolId]) { - // Preserve existing options, potentially merging with new ones if necessary - acc[toolId] = state.toolOptions[toolId].map(existingOption => { - const initialOption = tool.find(option => option.id === existingOption.id); - return initialOption - ? { ...initialOption, value: existingOption.value } - : existingOption; - }); - } else { - acc[toolId] = tool; - } - return acc; - }, {}); - - newState = { - ...state, - toolOptions: newToolOptions, - }; - break; - default: - break; - } - - return newState; -} diff --git a/playwright.config.ts b/playwright.config.ts index 4f1bca940..e9cd32dfc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -38,7 +38,7 @@ export default defineConfig({ //}, ], webServer: { - command: 'yarn test:e2e:serve', + command: 'cross-env APP_CONFIG=config/e2e.js yarn start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 360_000, diff --git a/tests/3DFourUp.spec.ts b/tests/3DFourUp.spec.ts index f54e8c2fc..4b4cadc8f 100644 --- a/tests/3DFourUp.spec.ts +++ b/tests/3DFourUp.spec.ts @@ -9,7 +9,7 @@ import { test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/3DMain.spec.ts b/tests/3DMain.spec.ts index 84164e93a..3c75ef755 100644 --- a/tests/3DMain.spec.ts +++ b/tests/3DMain.spec.ts @@ -9,7 +9,7 @@ import { test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/3DOnly.spec.ts b/tests/3DOnly.spec.ts index 0f2905834..c940f4f02 100644 --- a/tests/3DOnly.spec.ts +++ b/tests/3DOnly.spec.ts @@ -9,7 +9,7 @@ import { test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/3DPrimary.spec.ts b/tests/3DPrimary.spec.ts index 4b9bb335b..ec184587c 100644 --- a/tests/3DPrimary.spec.ts +++ b/tests/3DPrimary.spec.ts @@ -9,7 +9,7 @@ import { test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Angle.spec.ts b/tests/Angle.spec.ts index 88ef1c459..a948b963b 100644 --- a/tests/Angle.spec.ts +++ b/tests/Angle.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/AxialPrimary.spec.ts b/tests/AxialPrimary.spec.ts index 8826ed031..ff2b7ec28 100644 --- a/tests/AxialPrimary.spec.ts +++ b/tests/AxialPrimary.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Bidirectional.spec.ts b/tests/Bidirectional.spec.ts index 68268480a..4e2c3db09 100644 --- a/tests/Bidirectional.spec.ts +++ b/tests/Bidirectional.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Circle.spec.ts b/tests/Circle.spec.ts index c3505f5e0..bbca3111d 100644 --- a/tests/Circle.spec.ts +++ b/tests/Circle.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/CobbAngle.spec.ts b/tests/CobbAngle.spec.ts index d142535c2..a0085939e 100644 --- a/tests/CobbAngle.spec.ts +++ b/tests/CobbAngle.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Crosshairs.spec.ts b/tests/Crosshairs.spec.ts index 6a98a91bb..461b50756 100644 --- a/tests/Crosshairs.spec.ts +++ b/tests/Crosshairs.spec.ts @@ -40,7 +40,7 @@ const increaseSlabThickness = async (page: Page, id: string, lineNumber: number, test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); await initilizeMousePositionTracker(page); }); diff --git a/tests/DicomTagBrowser.spec.ts b/tests/DicomTagBrowser.spec.ts index 1e4136c42..337b0101a 100644 --- a/tests/DicomTagBrowser.spec.ts +++ b/tests/DicomTagBrowser.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Ellipse.spec.ts b/tests/Ellipse.spec.ts index 3dca6cdd3..2a376c81f 100644 --- a/tests/Ellipse.spec.ts +++ b/tests/Ellipse.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/FlipHorizontal.spec.ts b/tests/FlipHorizontal.spec.ts index 7477a050c..437a52229 100644 --- a/tests/FlipHorizontal.spec.ts +++ b/tests/FlipHorizontal.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Invert.spec.ts b/tests/Invert.spec.ts index bbff4657c..d0b9a269c 100644 --- a/tests/Invert.spec.ts +++ b/tests/Invert.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Length.spec.ts b/tests/Length.spec.ts index 8b6c51f30..719b2192d 100644 --- a/tests/Length.spec.ts +++ b/tests/Length.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Livewire.spec.ts b/tests/Livewire.spec.ts index dae68b449..4fa08a1f5 100644 --- a/tests/Livewire.spec.ts +++ b/tests/Livewire.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/MPR.spec.ts b/tests/MPR.spec.ts index 66c711ada..4f4326855 100644 --- a/tests/MPR.spec.ts +++ b/tests/MPR.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils/index.j test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Probe.spec.ts b/tests/Probe.spec.ts index 1a4fac22f..e6256b4c5 100644 --- a/tests/Probe.spec.ts +++ b/tests/Probe.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/RTHydration.spec.ts b/tests/RTHydration.spec.ts index b3638e28d..74e881f34 100644 --- a/tests/RTHydration.spec.ts +++ b/tests/RTHydration.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Rectangle.spec.ts b/tests/Rectangle.spec.ts index 904e27a35..a4655f6e8 100644 --- a/tests/Rectangle.spec.ts +++ b/tests/Rectangle.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Reset.spec.ts b/tests/Reset.spec.ts index b4ca11963..0c6b6ae63 100644 --- a/tests/Reset.spec.ts +++ b/tests/Reset.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/RotateRight.spec.ts b/tests/RotateRight.spec.ts index 2f6971a23..9d06144e6 100644 --- a/tests/RotateRight.spec.ts +++ b/tests/RotateRight.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/SEGHydration.spec.ts b/tests/SEGHydration.spec.ts index 2077d99fc..b812290d2 100644 --- a/tests/SEGHydration.spec.ts +++ b/tests/SEGHydration.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/SRHydration.spec.ts b/tests/SRHydration.spec.ts index c47c696a2..76f532a1a 100644 --- a/tests/SRHydration.spec.ts +++ b/tests/SRHydration.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/Spline.spec.ts b/tests/Spline.spec.ts index acd4adf6a..bf36ff29a 100644 --- a/tests/Spline.spec.ts +++ b/tests/Spline.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; - const mode = 'Basic Viewer'; + const mode = 'viewer'; await visitStudy(page, studyInstanceUID, mode, 2000); }); diff --git a/tests/TMTVAlignment.spec.ts b/tests/TMTVAlignment.spec.ts index 44ed0bd75..c97832bcc 100644 --- a/tests/TMTVAlignment.spec.ts +++ b/tests/TMTVAlignment.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, scrollVolumeViewport } from './utils'; test.skip('PT should show slice closest to CT', async ({ page }) => { const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; - const mode = 'Total Metabolic Tumor Volume'; + const mode = 'tmtv'; await visitStudy(page, studyInstanceUID, mode); const vp = page.getByTestId('viewport-pane'); diff --git a/tests/TMTVModalityUnit.spec.ts b/tests/TMTVModalityUnit.spec.ts index ede9b518f..f0540900b 100644 --- a/tests/TMTVModalityUnit.spec.ts +++ b/tests/TMTVModalityUnit.spec.ts @@ -10,7 +10,7 @@ test.skip('pets where SUV cannot be calculated should show same unit in TMTV as page, }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339'; - const mode = 'Total Metabolic Tumor Volume'; + const mode = 'tmtv'; await visitStudy(page, studyInstanceUID, mode, 10000); // Change to image where SUV cannot be calculated diff --git a/tests/TMTVRecalculate.spec.ts b/tests/TMTVRecalculate.spec.ts index 1601ba627..825f1c5bd 100644 --- a/tests/TMTVRecalculate.spec.ts +++ b/tests/TMTVRecalculate.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, simulateClicksOnElement, getSUV, clearAllAnnotations } from test.skip('should update SUV values correctly.', async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339'; - const mode = 'Total Metabolic Tumor Volume'; + const mode = 'tmtv'; await visitStudy(page, studyInstanceUID, mode, 10000); // Create ROI diff --git a/tests/TMTVRendering.spec.ts b/tests/TMTVRendering.spec.ts index 918787bfb..27eb74230 100644 --- a/tests/TMTVRendering.spec.ts +++ b/tests/TMTVRendering.spec.ts @@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils/index.j test.skip('should render TMTV correctly.', async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339'; - const mode = 'Total Metabolic Tumor Volume'; + const mode = 'tmtv'; await visitStudy(page, studyInstanceUID, mode, 10000); await checkForScreenshot(page, page, screenShotPaths.tmtvRendering.tmtvDisplayedCorrectly, 100); }); diff --git a/tests/utils/visitStudy.ts b/tests/utils/visitStudy.ts index 25c9bd80d..628a9c4ad 100644 --- a/tests/utils/visitStudy.ts +++ b/tests/utils/visitStudy.ts @@ -15,9 +15,10 @@ export async function visitStudy( delay: number = 0, datasources = 'ohif' ) { - await page.goto(`/?resultsPerPage=100&datasources=${datasources}`); - await page.getByTestId(studyInstanceUID).click(); - await page.getByRole('button', { name: mode }).click(); + // await page.goto(`/?resultsPerPage=100&datasources=${datasources}`); + // await page.getByTestId(studyInstanceUID).click(); + // await page.getByRole('button', { name: mode }).click(); + await page.goto(`/${mode}/${datasources}?StudyInstanceUIDs=${studyInstanceUID}`); await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(delay);