diff --git a/extensions/_example/src/index.js b/extensions/_example/src/index.js index 057e1bfde..bba2a55bd 100644 --- a/extensions/_example/src/index.js +++ b/extensions/_example/src/index.js @@ -1,112 +1,138 @@ +import ImageSet from '@ohif/core/src/classes/ImageSet'; +import { IWebApiDataSource } from '@ohif/core'; + /** * */ export default { - /** - * Only required property. Should be a unique value across all extensions. - */ - id: 'example-extension', + id: 'org.ohif.*', /** * LIFECYCLE HOOKS */ - - preRegistration({ - servicesManager = {}, - commandsManager = {}, - appConfig = {}, - configuration = {}, - }) {}, + preRegistration() {}, + beforeExtInit() {}, + beforeExtDestroy() {}, /** - * MODULE GETTERS + * MODULES */ - getViewportModule() { - return '... react component ...'; - }, - getSopClassHandlerModule() { - return sopClassHandlerModule; - }, - getPanelModule() { - return panelModule; - }, - getToolbarModule() { - return panelModule; - }, - getCommandsModule(/* store */) { - return commandsModule; - }, + getCommandsModule, + getContextModule, + getDataSourcesModule, + getLayoutTemplateModule, + getPanelModule, + getSopClassHandlerModule, + getToolbarModule() {}, + getViewportModule, }; +// appConfig, +// extensionConfig, +// dataSources, +// servicesManager, +// extensionManager, +// commandsManager, + /** * */ -const commandsModule = { - actions: { - // Store Contexts + Options - exampleAction: ({ viewports, param1 }) => { - console.log(`There are ${viewports.length} viewports`); - console.log(`param1's value is: ${param1}`); - }, - }, +const getCommandsModule = () => ({ definitions: { exampleActionDef: { - commandFn: this.actions.exampleAction, - storeContexts: ['viewports'], + commandFn: ({ param1 }) => { + console.log(`param1's value is: ${param1}`); + }, + // storeContexts: ['viewports'], options: { param1: 'hello world' }, + context: 'VIEWER', // optional }, }, -}; + defaultContext: 'ACTIVE_VIEWPORT::DICOMSR', +}); -/** - * - */ -const sopClassHandlerModule = { - id: 'OHIFDicomHtmlSopClassHandler', - sopClassUIDs: Object.values({ - BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11', - ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22', - COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33', - PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40', - MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50', - CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65', - X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67', - }), - getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) { - const instance = series.getFirstInstance(); +const ExampleContext = React.createContext(); - return { - plugin: 'html', - displaySetInstanceUID: 0, //utils.guid(), - wadoRoot: study.getData().wadoRoot, - wadoUri: instance.getData().wadouri, - SOPInstanceUID: instance.getSOPInstanceUID(), - SeriesInstanceUID: series.getSeriesInstanceUID(), - StudyInstanceUID: study.getStudyInstanceUID(), - authorizationHeaders, - }; +function ExampleContextProvider({ children }) { + return ( + + {children} + + ); +} + +const getContextModule = () => [ + { + name: 'ExampleContext', + context: ExampleContext, + provider: ExampleContextProvider, }, +]; + +const getDataSourcesModule = () => [ + { + name: 'exampleDataSource', + type: 'webApi', // 'webApi' | 'local' | 'other' + createDataSource: dataSourceConfig => { + return IWebApiDataSource.create(/* */); + }, + }, +]; + +const getLayoutTemplateModule = (/* ... */) => [ + { + id: 'exampleLayout', + name: 'exampleLayout', + component: ExampleLayoutComponent, + }, +]; + +const getPanelModule = () => { + return [ + { + name: 'exampleSidePanel', + iconName: 'info-circle-o', + iconLabel: 'Example', + label: 'Hello World', + isDisabled: studies => {}, // optional + component: ExamplePanelContentComponent, + }, + ]; }; -/** - * - */ -const panelModule = { - menuOptions: [ +const getSopClassHandlerModule = (/* ... */) => { + const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11'; + + return [ { - icon: 'th-list', - label: 'Segments', - target: 'segment-panel', - isDisabled: studies => { - return false; + name: 'ExampleSopClassHandle', + sopClassUids: [BASIC_TEXT_SR], + getDisplaySetsFromSeries: instances => { + const imageSet = new ImageSet(instances); + + imageSet.setAttributes(/** */); + imageSet.sortBy((a, b) => 0); + + return imageSet; }, }, - ], - components: [ - { - id: 'segment-panel', - component: '... react component ...', - }, - ], - defaultContext: ['VIEWER'], + ]; +}; + +const getToolbarModule = () => {}; + +// displaySet, viewportIndex, dataSource +const getViewportModule = () => { + const wrappedViewport = props => { + return ( + { + commandsManager.runCommand('commandName', data); + }} + /> + ); + }; + + return [{ name: 'example', component: wrappedViewport }]; }; diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index 7582c0960..babf3bf69 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -145,4 +145,8 @@ CornerstoneViewportDownloadForm.propTypes = { activeViewportIndex: PropTypes.number.isRequired, }; -export default CornerstoneViewportDownloadForm; +// export default CornerstoneViewportDownloadForm; + +export default function HelloWorld() { + return
Hello World
; +} diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index 74c0d56f2..891e90f8f 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -148,13 +148,13 @@ const commandsModule = ({ servicesManager }) => { const enabledElement = getEnabledElement(viewports.activeViewportIndex); return enabledElement; }, - showDownloadViewportModal: ({ title, viewports }) => { - const activeViewportIndex = viewports.activeViewportIndex; + showDownloadViewportModal: () => { + const activeViewportIndex = 1; // viewports.activeViewportIndex; const { UIModalService } = servicesManager.services; if (UIModalService) { UIModalService.show({ content: CornerstoneViewportDownloadForm, - title, + title: 'Download High Quality Image', contentProps: { activeViewportIndex, onClose: UIModalService.hide, @@ -318,7 +318,7 @@ const commandsModule = ({ servicesManager }) => { }, showDownloadViewportModal: { commandFn: actions.showDownloadViewportModal, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, getActiveViewportEnabledElement: { diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index 985663238..4885d0591 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -1,7 +1,6 @@ import React from 'react'; import init from './init.js'; import commandsModule from './commandsModule.js'; -import toolbarModule from './toolbarModule.js'; import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm'; const Component = React.lazy(() => { @@ -48,9 +47,6 @@ export default { { name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport }, ]; }, - getToolbarModule() { - return toolbarModule; - }, getCommandsModule({ servicesManager }) { return commandsModule({ servicesManager }); }, diff --git a/extensions/cornerstone/src/toolbarModule.js b/extensions/cornerstone/src/toolbarModule.js index 72cd0194d..40ba576eb 100644 --- a/extensions/cornerstone/src/toolbarModule.js +++ b/extensions/cornerstone/src/toolbarModule.js @@ -1,14 +1,3 @@ -// TODO: A way to add Icons that don't already exist? -// - Register them and add -// - Include SVG Source/Inline? -// - By URL, or own component? - -// What KINDS of toolbar buttons do we have... -// - One's that dispatch commands -// - One's that set tool's active -// - More custom, like CINE -// - Built in for one's like this, or custom components? - // Visible? // Disabled? // Based on contexts or misc. criteria? @@ -17,241 +6,118 @@ // setToolActive commands should receive the button event that triggered // so we can do the "bind to this button" magic -const TOOLBAR_BUTTON_TYPES = { - COMMAND: 'command', - SET_TOOL_ACTIVE: 'setToolActive', - BUILT_IN: 'builtIn', -}; +// const definitions = [ +// // OLD +// { +// id: 'StackScroll', +// label: 'Stack Scroll', +// icon: 'bars', +// // +// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, +// commandName: 'setToolActive', +// commandOptions: { toolName: 'StackScroll' }, +// }, +// { +// id: 'Reset', +// label: 'Reset', +// icon: 'reset', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'resetViewport', +// }, +// { +// id: 'Cine', +// label: 'CINE', +// icon: 'youtube', +// // +// type: TOOLBAR_BUTTON_TYPES.BUILT_IN, +// options: { +// behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE, +// }, +// }, +// { +// id: 'More', +// label: 'More', +// icon: 'ellipse-circle', +// buttons: [ +// { +// id: 'Magnify', +// label: 'Magnify', +// icon: 'circle', +// // +// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, +// commandName: 'setToolActive', +// commandOptions: { toolName: 'Magnify' }, +// }, +// { +// id: 'WwwcRegion', +// label: 'ROI Window', +// icon: 'stop', +// // +// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, +// commandName: 'setToolActive', +// commandOptions: { toolName: 'WwwcRegion' }, +// }, +// { +// id: 'Invert', +// label: 'Invert', +// icon: 'adjust', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'invertViewport', +// }, +// { +// id: 'RotateRight', +// label: 'Rotate Right', +// icon: 'rotate-right', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'rotateViewportCW', +// }, +// { +// id: 'FlipH', +// label: 'Flip H', +// icon: 'ellipse-h', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'flipViewportHorizontal', +// }, +// { +// id: 'FlipV', +// label: 'Flip V', +// icon: 'ellipse-v', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'flipViewportVertical', +// }, +// { +// id: 'Clear', +// label: 'Clear', +// icon: 'trash', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'clearAnnotations', +// }, +// { +// id: 'Eraser', +// label: 'Eraser', +// icon: 'eraser', +// // +// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, +// commandName: 'setToolActive', +// commandOptions: { toolName: 'Eraser' }, +// }, +// ], +// }, +// { +// id: 'Exit2DMPR', +// label: 'Exit 2D MPR', +// icon: 'times', +// // +// type: TOOLBAR_BUTTON_TYPES.COMMAND, +// commandName: 'setCornerstoneLayout', +// context: 'ACTIVE_VIEWPORT::VTK', +// }, +// ]; -const TOOLBAR_BUTTON_BEHAVIORS = { - CINE: 'CINE', - DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT', -}; - -/* TODO: Export enums through a extension manager. */ -const enums = { - TOOLBAR_BUTTON_TYPES, - TOOLBAR_BUTTON_BEHAVIORS, -}; - -const definitions = [ - // UPDATED - { - id: 'Zoom', - label: 'Zoom', - icon: 'tool-zoom', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Zoom' }, - }, - { - id: 'Wwwc', - label: 'Levels', - icon: 'tool-window-level', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Wwwc' }, - }, - { - id: 'Pan', - label: 'Pan', - icon: 'tool-move', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Pan' }, - }, - { - id: 'Capture', - label: 'Capture', - icon: 'tool-capture', - // - type: TOOLBAR_BUTTON_TYPES.BUILT_IN, - options: { - behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT, - togglable: true, - }, - }, - { - id: 'Annotate', - label: 'Annotate', - icon: 'tool-annotate', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'ArrowAnnotate' }, - }, - { - id: 'Bidirectional', - label: 'Bidirectional', - icon: 'tool-bidirectional', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Bidirectional' }, - }, - { - id: 'Ellipse', - label: 'Ellipse', - icon: 'tool-elipse', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'EllipticalRoi' }, - }, - { - id: 'Length', - label: 'Length', - icon: 'tool-length', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Length' }, - }, - // OLD - { - id: 'StackScroll', - label: 'Stack Scroll', - icon: 'bars', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'StackScroll' }, - }, - - { - id: 'Angle', - label: 'Angle', - icon: 'angle-left', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Angle' }, - }, - { - id: 'Reset', - label: 'Reset', - icon: 'reset', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'resetViewport', - }, - { - id: 'Cine', - label: 'CINE', - icon: 'youtube', - // - type: TOOLBAR_BUTTON_TYPES.BUILT_IN, - options: { - behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE, - }, - }, - { - id: 'More', - label: 'More', - icon: 'ellipse-circle', - buttons: [ - { - id: 'Magnify', - label: 'Magnify', - icon: 'circle', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Magnify' }, - }, - { - id: 'WwwcRegion', - label: 'ROI Window', - icon: 'stop', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'WwwcRegion' }, - }, - { - id: 'DragProbe', - label: 'Probe', - icon: 'dot-circle', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'DragProbe' }, - }, - { - id: 'RectangleRoi', - label: 'Rectangle', - icon: 'square-o', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'RectangleRoi' }, - }, - { - id: 'Invert', - label: 'Invert', - icon: 'adjust', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'invertViewport', - }, - { - id: 'RotateRight', - label: 'Rotate Right', - icon: 'rotate-right', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'rotateViewportCW', - }, - { - id: 'FlipH', - label: 'Flip H', - icon: 'ellipse-h', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'flipViewportHorizontal', - }, - { - id: 'FlipV', - label: 'Flip V', - icon: 'ellipse-v', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'flipViewportVertical', - }, - { - id: 'Clear', - label: 'Clear', - icon: 'trash', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'clearAnnotations', - }, - { - id: 'Eraser', - label: 'Eraser', - icon: 'eraser', - // - type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, - commandName: 'setToolActive', - commandOptions: { toolName: 'Eraser' }, - }, - ], - }, - { - id: 'Exit2DMPR', - label: 'Exit 2D MPR', - icon: 'times', - // - type: TOOLBAR_BUTTON_TYPES.COMMAND, - commandName: 'setCornerstoneLayout', - context: 'ACTIVE_VIEWPORT::VTK', - }, -]; - -export default { - definitions, - defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE', -}; +export default []; diff --git a/extensions/default/src/Toolbar/ToolbarDivider.jsx b/extensions/default/src/Toolbar/ToolbarDivider.jsx new file mode 100644 index 000000000..7e73deaf7 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarDivider.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function ToolbarDivider() { + return ( + + ); +} diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx new file mode 100644 index 000000000..345db2a57 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui'; + +function LayoutSelector() { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + function LayoutSelector() { + if (isOpen) { + setIsOpen(false); + } + } + window.addEventListener('click', LayoutSelector); + return () => { + window.removeEventListener('click', LayoutSelector); + }; + }, [isOpen]); + + const dropdownContent = isOpen ? OHIFLayoutSelector : undefined; + + return ( + { + setIsOpen(!isOpen); + }} + dropdownContent={dropdownContent} + isActive={isOpen} + type="primary" + /> + ); +} + +LayoutSelector.propTypes = { + children: PropTypes.any.isRequired, +}; + +export default LayoutSelector; diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index ff1672604..e4bde8383 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -1,10 +1,9 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; // -import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui'; +import { NavBar, Svg, Icon, IconButton } from '@ohif/ui'; -function Header({ tools, moreTools }) { - const [activeTool, setActiveTool] = useState('Zoom'); +function Header({ children }) { // const dropdownContent = [ // { // name: 'Soft tissue', @@ -14,80 +13,16 @@ function Header({ tools, moreTools }) { // { name: 'Liver', value: '150 / 90' }, // { name: 'Bone', value: '2500 / 480' }, // { name: 'Brain', value: '80 / 40' }, - // ]; + // ] - // TODO -> In ToolBarManager => Consume commandName and commandOptions and create onClick? - - /* - const tools = [ - { - id: 'Zoom', - label: 'Zoom', - icon: 'tool-zoom', - commandName: 'setToolActive', - commandOptions: { toolName: 'Zoom' }, - onClick: () => setActiveTool('Zoom'), - }, - { - id: 'Wwwc', - label: 'Levels', - icon: 'tool-window-level', - commandName: 'setToolActive', - commandOptions: { toolName: 'Wwwc' }, - onClick: () => setActiveTool('Wwwc'), - dropdownContent: ( -
- {dropdownContent.map((row, i) => ( -
-
- {row.name} - - {row.value} - -
- {i} -
- ))} -
- ), - }, - { - id: 'Pan', - label: 'Pan', - icon: 'tool-move', - commandName: 'setToolActive', - commandOptions: { toolName: 'Pan' }, - onClick: () => setActiveTool('Pan'), - }, - { - id: 'Capture', - label: 'Capture', - icon: 'tool-capture', - commandName: 'setToolActive', - commandOptions: { toolName: 'Capture' }, - onClick: () => setActiveTool('Capture'), - }, - { - id: 'Layout', - label: 'Layout', - icon: 'tool-layout', - commandName: 'setToolActive', - commandOptions: { toolName: 'Layout' }, - onClick: () => setActiveTool('Layout'), - }, - ]; - */ return ( -
+
-
+
+
{children}
- -
-
- + FOR INVESTIGATIONAL USE ONLY { + function closeNestedMenu() { + if (isOpen) { + setIsOpen(false); + } + } + window.addEventListener('click', closeNestedMenu); + return () => { + window.removeEventListener('click', closeNestedMenu); + }; + }, [isOpen]); + + const dropdownContent = isOpen ? children : undefined; + + return ( + { + setIsOpen(!isOpen); + }} + dropdownContent={dropdownContent} + isActive={isOpen} + type="primary" + /> + ); +} + +NestedMenu.propTypes = { + children: PropTypes.any.isRequired, +}; + +export default NestedMenu; diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index 2f9273f52..8e410b262 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -1,8 +1,9 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; -import { SidePanel, Toolbar } from '@ohif/ui'; -// +import { SidePanel } from '@ohif/ui'; import Header from './Header.jsx'; +import NestedMenu from './ToolbarButtonNestedMenu.jsx'; function ViewerLayout({ // From Extension Module Params @@ -54,71 +55,23 @@ function ViewerLayout({ }; }; - const handleToolBarSubscription = newToolBarLayout => { - // Get buttons to pass to toolbars. - console.log(commandsManager); - - const firstTool = newToolBarLayout[0].tools[0]; - - const toolBarLayout = []; - - newToolBarLayout.forEach(newToolBar => { - const toolBar = { tools: [], moreTools: [] }; - - Object.keys(newToolBar).forEach(key => { - if (newToolBar[key].length) { - newToolBar[key].forEach(tool => { - const commandOptions = tool.commandOptions || {}; - - toolBar[key].push({ - context: tool.context, - icon: tool.icon, - id: tool.id, - label: tool.label, - type: 'setToolActive', - onClick: () => { - commandsManager.runCommand(tool.commandName, commandOptions); - }, - }); - }); - } - }); - - toolBarLayout.push(toolBar); - - // if (newToolBar.moreTools && newToolBar.moreTools.length) { - // newToolBar.moreTools.forEach(tool => { - // const commandOptions = tool.commandOptions || {}; - - // toolBar.push({ - // context: tool.context, - // icon: tool.icon, - // id: tool.id, - // label: tool.label, - // type: 'setToolActive', - // command: () => - // commandsManager.runCommand(tool.commandName, commandOptions), - // }); - // }); - // } - }); - - setToolBarLayout(toolBarLayout); - }; - - const [toolBarLayout, setToolBarLayout] = useState([ - { tools: [], moreTools: [] }, - { tools: [] }, - ]); + const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); useEffect(() => { const { unsubscribe } = ToolBarService.subscribe( ToolBarService.EVENTS.TOOL_BAR_MODIFIED, - handleToolBarSubscription + () => { + console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); + const updatedToolbars = { + primary: ToolBarService.getButtonSection('primary'), + secondary: ToolBarService.getButtonSection('secondary'), + }; + setToolbars(updatedToolbars); + } ); return unsubscribe; - }, []); + }, [ToolBarService]); const leftPanelComponents = leftPanels.map(getPanelData); const rightPanelComponents = rightPanels.map(getPanelData); @@ -126,10 +79,30 @@ function ViewerLayout({ return (
-
+
+
+ {toolbars.primary.map(toolDef => { + const isNested = Array.isArray(toolDef); + + if (!isNested) { + const { id, Component, componentProps } = toolDef; + + return ; + } else { + return ( + +
+ {toolDef.map(x => { + const { id, Component, componentProps } = x; + return ; + })} +
+
+ ); + } + })} +
+
- + {toolbars.secondary.map(toolDef => { + const { id, Component, componentProps } = toolDef; + + return ; + })}
- {/* - viewportContents={[ - alert(`Series ${direction}`)} - studyData={{ - label: 'A', - isTracked: true, - isLocked: false, - studyDate: '07-Sep-2011', - currentSeries: 1, - seriesDescription: - 'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ', - modality: 'CT', - patientInformation: { - patientName: 'Smith, Jane', - patientSex: 'F', - patientAge: '59', - MRN: '10000001', - thickness: '5.0mm', - spacing: '1.25mm', - scanner: 'Aquilion', - }, - }} - > - , - alert(`Series ${direction}`)} - studyData={{ - label: 'A', - isTracked: false, - isLocked: true, - studyDate: '07-Sep-2010', - currentSeries: 2, - seriesDescription: - 'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ', - modality: 'SR', - patientInformation: { - patientName: 'Smith, Jane', - patientSex: 'F', - patientAge: '59', - MRN: '10000001', - thickness: '2.0mm', - spacing: '1.25mm', - scanner: 'Aquilion', - }, - }} - > - , - ]} - setActiveViewportIndex={setActiveViewportIndex} - activeViewportIndex={activeViewportIndex} - />*/}
{rightPanelComponents.length && ( @@ -227,14 +150,6 @@ ViewerLayout.propTypes = { }).isRequired, commandsManager: PropTypes.object, // From modes - // TODO: Not in love with this shape, - toolBarLayout: PropTypes.arrayOf( - PropTypes.shape({ - tools: PropTypes.array, - moreTools: PropTypes.array, - }) - ).isRequired, - //displaySetInstanceUids: PropTypes.any.isRequired, leftPanels: PropTypes.array, rightPanels: PropTypes.array, /** Responsible for rendering our grid of viewports; provided by consuming application */ diff --git a/extensions/default/src/getCommandsModule.js b/extensions/default/src/getCommandsModule.js deleted file mode 100644 index 325825ef2..000000000 --- a/extensions/default/src/getCommandsModule.js +++ /dev/null @@ -1,68 +0,0 @@ -// SEE: -// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49 -import React from 'react'; -import { useViewportGrid } from '@ohif/ui'; - -function getCommandsModule({ servicesManager }) { - const { UIDialogService } = servicesManager.services; - - const definitions = { - toggleLayoutSelectionDialog: { - commandFn: () => { - if (!UIDialogService) { - window.alert( - 'Unable to show dialog; no UI Dialog Service available.' - ); - return; - } - - // TODO: use SimpleDialog component - // TODO: update position on window resize - // TODO: Expand service API to check if dialog w/ ID is already open - // TODO: Import and call `useViewportGrid` - UIDialogService.dismiss({ id: 'layoutSelection' }); - UIDialogService.create({ - id: 'layoutSelection', - centralize: true, - isDraggable: false, - showOverlay: true, - content: Test, - }); - }, - storeContexts: [], - options: {}, - context: 'VIEWER', - }, - }; - - return { - definitions, - defaultContext: 'VIEWER', - }; -} - -function Test() { - const [ - { numCols, numRows, activeViewportIndex, viewports }, - dispatch, - ] = useViewportGrid(); - - return ( -
{ - dispatch({ - type: 'SET_LAYOUT', - payload: { - numCols: 2, - numRows: 2, - }, - }); - }} - style={{ color: 'white' }} - > - Hello World! -
- ); -} - -export default getCommandsModule; diff --git a/extensions/default/src/getToolbarModule.js b/extensions/default/src/getToolbarModule.js index e6f855057..a3777a523 100644 --- a/extensions/default/src/getToolbarModule.js +++ b/extensions/default/src/getToolbarModule.js @@ -1,19 +1,98 @@ -const TOOLBAR_BUTTON_TYPES = { - COMMAND: 'command', - SET_TOOL_ACTIVE: 'setToolActive', - BUILT_IN: 'builtIn', -}; +import { ToolbarButton } from '@ohif/ui'; +import ToolbarDivider from './Toolbar/ToolbarDivider.jsx'; +import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.jsx'; -const definitions = [ - { - id: 'Layout', - label: 'Layout', - icon: 'tool-layout', - commandName: 'toggleLayoutSelectionDialog', - commandOptions: { }, - }, -]; +export default function getToolbarModule({ commandsManager, servicesManager }) { + const toolbarService = servicesManager.services.ToolBarService; -export default function getToolbarModule() { - return { definitions, defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE' }; + return [ + { + name: 'ohif.divider', + defaultComponent: ToolbarDivider, + clickHandler: () => {}, + }, + { + name: 'ohif.action', + defaultComponent: ToolbarButton, + requiredConfig: [], + optionalConfig: [], + requiredProps: [], + optionalProps: [], + clickHandler: (evt, btn, btnSectionName) => { + const { props } = btn; + commandsManager.runCommand(props.commandName, props.commandOptions); + }, + }, + { + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + requiredConfig: ['groupName'], + optionalConfig: [], + requiredProps: [], + optionalProps: [], + clickHandler: (evt, clickedBtn, btnSectionName) => { + const { props } = clickedBtn; + const allButtons = toolbarService.getButtons(); + + // Set all buttons in same group to inactive + Object.keys(allButtons).forEach(btnName => { + const btn = allButtons[btnName]; + const isRadioGroupBtn = + btn.config && + btn.config.groupName && + btn.type === 'ohif.radioGroup'; + + if ( + isRadioGroupBtn && + clickedBtn.config.groupName === btn.config.groupName + ) { + btn.props.isActive = false; + } + }); + + // Set our clicked button to active + allButtons[clickedBtn.id].props.isActive = true; + + // Run button logic/command + commandsManager.runCommand(props.commandName, props.commandOptions); + + // Set buttons & trigger notification + toolbarService.setButtons(allButtons); + }, + }, + { + name: 'ohif.layoutSelector', + defaultComponent: ToolbarLayoutSelector, + requiredConfig: [], + optionalConfig: [], + requiredProps: [], + optionalProps: [], + clickHandler: (evt, clickedBtn, btnSectionName) => {}, + }, + { + name: 'ohif.toggle', + defaultComponent: ToolbarButton, + requiredConfig: [], + optionalConfig: [], + requiredProps: [], + optionalProps: [], + clickHandler: (evt, clickedBtn, btnSectionName) => { + const { props } = clickedBtn; + const allButtons = toolbarService.getButtons(); + const thisButton = allButtons[clickedBtn.id]; + + // Set our clicked button to active + thisButton.props.isActive = !thisButton.props.isActive; + + // Run button logic/command + // MAKE SURE THIS SUPPORTS TOGGLE! + // commandsManager.runCommand(props.commandName, props.commandOptions); + // What if just toggled "content"? + // commandName OR content? + + // Set buttons & trigger notification + toolbarService.setButtons(allButtons); + }, + }, + ]; } diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index 8099cde61..a426be014 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -1,4 +1,3 @@ -import getCommandsModule from './getCommandsModule.js'; import getContextModule from './getContextModule.js'; import getDataSourcesModule from './getDataSourcesModule.js'; import getLayoutTemplateModule from './getLayoutTemplateModule.js'; @@ -12,7 +11,6 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, - getCommandsModule, getContextModule, getDataSourcesModule, getLayoutTemplateModule, diff --git a/extensions/measurement-tracking/src/getCommandsModule.js b/extensions/measurement-tracking/src/getCommandsModule.js deleted file mode 100644 index 325825ef2..000000000 --- a/extensions/measurement-tracking/src/getCommandsModule.js +++ /dev/null @@ -1,68 +0,0 @@ -// SEE: -// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49 -import React from 'react'; -import { useViewportGrid } from '@ohif/ui'; - -function getCommandsModule({ servicesManager }) { - const { UIDialogService } = servicesManager.services; - - const definitions = { - toggleLayoutSelectionDialog: { - commandFn: () => { - if (!UIDialogService) { - window.alert( - 'Unable to show dialog; no UI Dialog Service available.' - ); - return; - } - - // TODO: use SimpleDialog component - // TODO: update position on window resize - // TODO: Expand service API to check if dialog w/ ID is already open - // TODO: Import and call `useViewportGrid` - UIDialogService.dismiss({ id: 'layoutSelection' }); - UIDialogService.create({ - id: 'layoutSelection', - centralize: true, - isDraggable: false, - showOverlay: true, - content: Test, - }); - }, - storeContexts: [], - options: {}, - context: 'VIEWER', - }, - }; - - return { - definitions, - defaultContext: 'VIEWER', - }; -} - -function Test() { - const [ - { numCols, numRows, activeViewportIndex, viewports }, - dispatch, - ] = useViewportGrid(); - - return ( -
{ - dispatch({ - type: 'SET_LAYOUT', - payload: { - numCols: 2, - numRows: 2, - }, - }); - }} - style={{ color: 'white' }} - > - Hello World! -
- ); -} - -export default getCommandsModule; diff --git a/extensions/measurement-tracking/src/index.js b/extensions/measurement-tracking/src/index.js index 7aa5cc79b..bbc50e2a0 100644 --- a/extensions/measurement-tracking/src/index.js +++ b/extensions/measurement-tracking/src/index.js @@ -1,4 +1,3 @@ -import getCommandsModule from './getCommandsModule.js'; import getContextModule from './getContextModule.js'; import getPanelModule from './getPanelModule.js'; import getViewportModule from './getViewportModule.js'; @@ -8,7 +7,6 @@ export default { * Only required property. Should be a unique value across all extensions. */ id: 'org.ohif.measurement-tracking', - getCommandsModule, getContextModule, getPanelModule, getViewportModule, diff --git a/extensions/vtk/src/toolbarModule.js b/extensions/vtk/src/toolbarModule.js index 832acae34..3e7dceb76 100644 --- a/extensions/vtk/src/toolbarModule.js +++ b/extensions/vtk/src/toolbarModule.js @@ -128,7 +128,9 @@ const definitions = [ }, ]; -export default { - definitions, - defaultContext: 'ACTIVE_VIEWPORT::VTK', -}; +export default []; + +// export default { +// definitions, +// defaultContext: 'ACTIVE_VIEWPORT::VTK', +// }; diff --git a/modes/example/src/index.js b/modes/example/src/index.js index 70636f2a9..21c98e381 100644 --- a/modes/example/src/index.js +++ b/modes/example/src/index.js @@ -55,17 +55,17 @@ export default function mode({ modeConfiguration }) { ]); // Could import layout selector here from org.ohif.default (when it exists!) - ToolBarService.setToolBarLayout([ - // Primary - { - tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'], - moreTools: ['Zoom'], - }, - // Secondary - { - tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'], - }, - ]); + // ToolBarService.setToolBarLayout([ + // // Primary + // { + // tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'], + // moreTools: ['Zoom'], + // }, + // // Secondary + // { + // tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'], + // }, + // ]); }, layoutTemplate: ({ routeProps }) => { return { diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 68754c8bc..717d301e0 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -1,3 +1,5 @@ +import toolbarButtons from './toolbarButtons.js'; + const ohif = { layout: 'org.ohif.default.layoutTemplateModule.viewerLayout', sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack', @@ -27,57 +29,24 @@ export default function mode({ modeConfiguration }) { init: ({ servicesManager, extensionManager }) => { const { ToolBarService } = servicesManager.services; ToolBarService.init(extensionManager); - ToolBarService.addButtons([ - { - id: 'Zoom', - namespace: 'org.ohif.cornerstone.toolbarModule.Zoom', - }, - { - id: 'Levels', - namespace: 'org.ohif.cornerstone.toolbarModule.Wwwc', - }, - { - id: 'Pan', - namespace: 'org.ohif.cornerstone.toolbarModule.Pan', - }, - { - id: 'Capture', - namespace: 'org.ohif.cornerstone.toolbarModule.Capture', - }, - { - id: 'Layout', - namespace: 'org.ohif.default.toolbarModule.Layout', - }, - { - id: 'Annotate', - namespace: 'org.ohif.cornerstone.toolbarModule.Annotate', - }, - { - id: 'Bidirectional', - namespace: 'org.ohif.cornerstone.toolbarModule.Bidirectional', - }, - { - id: 'Ellipse', - namespace: 'org.ohif.cornerstone.toolbarModule.Ellipse', - }, - { - id: 'Length', - namespace: 'org.ohif.cornerstone.toolbarModule.Length', - }, + ToolBarService.addButtons(toolbarButtons); + ToolBarService.createButtonSection('primary', [ + 'Zoom', + 'Wwwc', + 'Pan', + 'Capture', + 'Layout', + 'Divider', + ['Zoom', 'Wwwc'], + ]); + ToolBarService.createButtonSection('secondary', [ + 'Annotate', + 'Bidirectional', + 'Ellipse', + 'Length', ]); // Could import layout selector here from org.ohif.default (when it exists!) - ToolBarService.setToolBarLayout([ - // Primary - { - tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'], - moreTools: ['Zoom'], - }, - // Secondary - { - tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'], - }, - ]); }, layoutTemplate: ({ routeProps }) => { return { diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js new file mode 100644 index 000000000..5f9b4beb4 --- /dev/null +++ b/modes/longitudinal/src/toolbarButtons.js @@ -0,0 +1,132 @@ +// TODO: torn, can either bake this here; or have to create a whole new button type +// Only ways that you can pass in a custom React component for render :l + +export default [ + // Divider + { + id: 'Divider', + type: 'ohif.divider', + }, + // ~~ Primary + { + id: 'Zoom', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-zoom', + label: 'Zoom', + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + type: 'primary', + }, + }, + { + id: 'Wwwc', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: true, + icon: 'tool-window-level', + label: 'Levels', + commandName: 'setToolActive', + commandOptions: { toolName: 'Wwwc' }, + type: 'primary', + }, + }, + { + id: 'Pan', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-move', + label: 'Pan', + commandName: 'setToolActive', + commandOptions: { toolName: 'Pan' }, + type: 'primary', + }, + }, + { + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + commandName: 'showDownloadViewportModal', + type: 'primary', + }, + }, + { + id: 'Layout', + type: 'ohif.layoutSelector', + }, + // ~~ Primary: NESTED + // ~~ Secondary + { + id: 'Annotate', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-annotate', + label: 'Annotate', + commandName: 'setToolActive', + commandOptions: { toolName: 'ArrowAnnotate' }, + type: 'secondary', + }, + }, + { + id: 'Bidirectional', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-bidirectional', + label: 'Bidirectional', + commandName: 'setToolActive', + commandOptions: { toolName: 'Bidirectional' }, + type: 'secondary', + }, + }, + { + id: 'Ellipse', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-elipse', + label: 'Ellipse', + commandName: 'setToolActive', + commandOptions: { toolName: 'EllipticalRoi' }, + type: 'secondary', + }, + }, + { + id: 'Length', + type: 'ohif.radioGroup', + config: { + groupName: 'primaryTool', + }, + props: { + isActive: false, + icon: 'tool-length', + label: 'Length', + commandName: 'setToolActive', + commandOptions: { toolName: 'Length' }, + type: 'secondary', + }, + }, +]; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index af11f1cf4..14dfde227 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -105,9 +105,6 @@ export default class ExtensionManager { ); break; case MODULE_TYPES.TOOLBAR: - this._initToolBarModule(extensionModule, extensionId); - break; - case MODULE_TYPES.VIEWPORT: case MODULE_TYPES.PANEL: case MODULE_TYPES.SOP_CLASS_HANDLER: @@ -115,7 +112,6 @@ export default class ExtensionManager { case MODULE_TYPES.LAYOUT_TEMPLATE: // Default for most extension points, // Just adds each entry ready for consumption by mode. - extensionModule.forEach(element => { this.modulesMap[ `${extensionId}.${moduleType}.${element.name}` @@ -148,27 +144,6 @@ export default class ExtensionManager { return this.dataSourceMap[dataSourceName]; }; - _initToolBarModule = (extensionModule, extensionId) => { - let { definitions, defaultContext } = extensionModule; - if (!definitions || Object.keys(definitions).length === 0) { - log.warn('Commands Module contains no command definitions'); - return; - } - - defaultContext = defaultContext || 'VIEWER'; - - definitions.forEach(definition => { - console.log(`${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}`); - - // TODO -> Deep copy instead of mutation? We only do this once, but would be better. - definition.context = definition.context || defaultContext; - - this.modulesMap[ - `${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}` - ] = definition; - }); - }; - /** * @private * @param {string} moduleType @@ -185,13 +160,13 @@ export default class ExtensionManager { try { const extensionModule = getModuleFn({ - getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`? - servicesManager: this._servicesManager, - commandsManager: this._commandsManager, appConfig: this._appConfig, + getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`? + commandsManager: this._commandsManager, + extensionManager: this, + servicesManager: this._servicesManager, configuration, api: this._api, - extensionManager: this, }); if (!extensionModule) { diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 1c3757c9e..95cc1b431 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -6,32 +6,116 @@ const EVENTS = { export default class ToolBarService { constructor() { - this.displaySets = {}; this.EVENTS = EVENTS; this.listeners = {}; + this.buttons = {}; + this.buttonSections = { + /** + * primary: ['Zoom', 'Wwwc'], + * secondary: ['Length', 'RectangleRoi'] + */ + }; Object.assign(this, pubSubServiceInterface); } init(extensionManager) { - this.buttons = {}; this.extensionManager = extensionManager; } - addButtons(buttons) { - buttons.forEach(button => { - const buttonDefinition = this.extensionManager.getModuleEntry( - button.namespace + getButtons() { + return this.buttons; + } + + setButtons(buttons) { + this.buttons = buttons; + this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); + } + + _buttonTypes() { + console.log(this.extensionManager.modules); + const buttonTypes = {}; + const registeredToolbarModules = this.extensionManager.modules[ + 'toolbarModule' + ]; + + if ( + Array.isArray(registeredToolbarModules) && + registeredToolbarModules.length + ) { + registeredToolbarModules.forEach(toolbarModule => + toolbarModule.module.forEach(def => { + buttonTypes[def.name] = def; + }) ); + } - const id = button.id || buttonDefinition.id; + return buttonTypes; + } - this.buttons[id] = buttonDefinition; + + createButtonSection(key, buttons) { + // Maybe do this mapping at time of return, instead of time of create + // Props check important for validation here... + + this.buttonSections[key] = buttons; + this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); + } + + getButtonSection(key) { + const buttonSectionIds = this.buttonSections[key]; + const buttonsInSection = []; + + if (!buttonSectionIds) { + return buttonsInSection; + } + + buttonSectionIds.forEach(btnIdOrArray => { + const isNested = Array.isArray(btnIdOrArray); + + if (isNested) { + const btnIds = btnIdOrArray; + const nestedButtons = []; + + btnIds.forEach(nestedBtnId => { + const nestedBtn = this.buttons[nestedBtnId]; + const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key); + + nestedButtons.push(mappedNestedBtn); + }); + + if (nestedButtons.length) { + buttonsInSection.push(nestedButtons); + } + } else { + const btnId = btnIdOrArray; + const btn = this.buttons[btnId]; + const mappedBtn = this._mapButtonToDisplay(btn, key); + + buttonsInSection.push(mappedBtn); + } }); + + return buttonsInSection; } /** - * Broadcasts displaySetService changes. + * + * @param {object[]} buttons + * @param {string} buttons[].id + */ + addButtons(buttons) { + buttons.forEach(button => { + if (!this.buttons[button.id]) { + this.buttons[button.id] = button; + } + }); + + this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); + } + + /** + * Broadcasts toolbarService changes. * * @param {string} eventName The event name * @return void @@ -47,33 +131,35 @@ export default class ToolBarService { } }; - setToolBarLayout(layouts) { - const toolBarLayout = []; + /** + * + * @param {*} btn + * @param {*} btnSection + */ + _mapButtonToDisplay(btn, btnSection) { + const { id, type, component, props } = btn; + const buttonType = this._buttonTypes()[type]; - layouts.forEach(layout => { - const toolBarDefinitions = { tools: [], moreTools: [] }; + if (!buttonType) { + return; + } - const { tools, moreTools } = layout; + const onClick = evt => { + if (buttonType.clickHandler) { + buttonType.clickHandler(evt, btn, btnSection); + } + if (btn.props.onClick) { + btn.onClick(evt, btn, btnSection); + } + if (btn.props.clickHandler) { + btn.clickHandler(evt, btn, btnSection); + } + }; - tools && - tools.forEach(element => { - const button = this.buttons[element]; - - toolBarDefinitions.tools.push(button); - }); - - moreTools && - moreTools.forEach(element => { - const button = this.buttons[element]; - - toolBarDefinitions.moreTools.push(button); - }); - - toolBarLayout.push(toolBarDefinitions); - }); - - this.toolBarLayout = toolBarLayout; - - this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, toolBarLayout); + return { + id, + Component: component || buttonType.defaultComponent, + componentProps: Object.assign({}, props, { onClick }), // + }; } } diff --git a/platform/ui/index.js b/platform/ui/index.js index 94ff8071c..db85039d9 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -41,6 +41,7 @@ export { InputMultiSelect, InputText, Label, + LayoutSelector, MeasurementTable, Modal, NavBar, @@ -67,7 +68,6 @@ export { ThumbnailNoImage, ThumbnailTracked, ThumbnailList, - Toolbar, ToolbarButton, Tooltip, Typography, diff --git a/platform/ui/src/components/LayoutSelector/LayoutSelector.jsx b/platform/ui/src/components/LayoutSelector/LayoutSelector.jsx new file mode 100644 index 000000000..4226b1b56 --- /dev/null +++ b/platform/ui/src/components/LayoutSelector/LayoutSelector.jsx @@ -0,0 +1,80 @@ +// // SEE: +// // https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49 +// import React from 'react'; +// import { useViewportGrid } from '@ohif/ui'; + +// function getCommandsModule({ servicesManager }) { +// const { UIDialogService } = servicesManager.services; + +// const definitions = { +// toggleLayoutSelectionDialog: { +// commandFn: () => { +// if (!UIDialogService) { +// window.alert( +// 'Unable to show dialog; no UI Dialog Service available.' +// ); +// return; +// } + +// // TODO: use SimpleDialog component +// // TODO: update position on window resize +// // TODO: Expand service API to check if dialog w/ ID is already open +// // TODO: Import and call `useViewportGrid` +// UIDialogService.dismiss({ id: 'layoutSelection' }); +// UIDialogService.create({ +// id: 'layoutSelection', +// centralize: true, +// isDraggable: false, +// showOverlay: true, +// content: Test, +// }); +// }, +// storeContexts: [], +// options: {}, +// context: 'VIEWER', +// }, +// }; + +// return { +// definitions, +// defaultContext: 'VIEWER', +// }; +// } + +// function Test() { +// const [ +// { numCols, numRows, activeViewportIndex, viewports }, +// dispatch, +// ] = useViewportGrid(); + +// return ( +//
{ +// dispatch({ +// type: 'SET_LAYOUT', +// payload: { +// numCols: 2, +// numRows: 2, +// }, +// }); +// }} +// style={{ color: 'white' }} +// > +// Hello World! +//
+// ); +// } + +// export default getCommandsModule; + +import React from 'react'; + +function LayoutSelector() { + return ( + <> +
LAYOUT SELECTOR PLACEHOLDER!
+ + ); +} + +export default LayoutSelector; diff --git a/platform/ui/src/components/LayoutSelector/index.js b/platform/ui/src/components/LayoutSelector/index.js new file mode 100644 index 000000000..1b6044375 --- /dev/null +++ b/platform/ui/src/components/LayoutSelector/index.js @@ -0,0 +1,2 @@ +import LayoutSelector from './LayoutSelector'; +export default LayoutSelector; diff --git a/platform/ui/src/components/Toolbar/Toolbar.jsx b/platform/ui/src/components/Toolbar/Toolbar.jsx deleted file mode 100644 index dd4c11f5b..000000000 --- a/platform/ui/src/components/Toolbar/Toolbar.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { ToolbarButton, IconButton, Icon, Tooltip } from '@ohif/ui'; - -const classes = { - type: { - primary: '', - secondary: 'w-full items-center bg-primary-dark px-3', - }, -}; - -const Toolbar = ({ activeTool, tools, moreTools, type }) => { - return ( -
- {tools.map((tool) => { - const { id, onClick, icon, label, dropdownContent } = tool; - const isActive = activeTool === tool.id; - return ( -
- -
- ); - })} - {!!moreTools.length && ( - <> - - - - - - - - )} -
- ); -}; - -Toolbar.defaultProps = { - activeTool: '', - moreTools: [], - type: 'primary', -}; - -Toolbar.propTypes = { - type: PropTypes.oneOf(['primary', 'secondary']), - activeTool: PropTypes.string, - tools: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - label: PropTypes.string, - icon: PropTypes.string, - commandName: PropTypes.string, - commandOptions: PropTypes.shape({ - toolName: PropTypes.string, - }), - onClick: PropTypes.func, - dropdownContent: PropTypes.node, - }) - ).isRequired, - moreTools: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - label: PropTypes.string, - icon: PropTypes.string, - commandName: PropTypes.string, - commandOptions: PropTypes.shape({ - toolName: PropTypes.string, - }), - onClick: PropTypes.func, - }) - ), -}; - -export default Toolbar; diff --git a/platform/ui/src/components/Toolbar/Toolbar.mdx b/platform/ui/src/components/Toolbar/Toolbar.mdx deleted file mode 100644 index f6837e83c..000000000 --- a/platform/ui/src/components/Toolbar/Toolbar.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -name: Toolbar -menu: Data Display -route: components/toolbar ---- - -import { useState } from 'react'; -import { Playground, Props } from 'docz'; -import classnames from 'classnames'; -import { Toolbar, Typography } from '@ohif/ui'; - -# Toolbar - -A Toolbar is used to create a list of tools. - -## Import - -```javascript -import { Toolbar } from '@ohif/ui'; -``` - - - {() => { - const [activeTool, setActiveTool] = useState('Zoom'); - const dropdownContent = [ - { - name: 'Soft tissue', - value: '400/40', - }, - { name: 'Lung', value: '1500 / -600' }, - { name: 'Liver', value: '150 / 90' }, - { name: 'Bone', value: '2500 / 480' }, - { name: 'Brain', value: '80 / 40' }, - ]; - const tools = [ - { - id: 'Zoom', - label: 'Zoom', - icon: 'tool-zoom', - commandName: 'setToolActive', - commandOptions: { toolName: 'Zoom' }, - onClick: () => setActiveTool('Zoom'), - }, - { - id: 'Wwwc', - label: 'Levels', - icon: 'tool-window-level', - commandName: 'setToolActive', - commandOptions: { toolName: 'Wwwc' }, - onClick: () => setActiveTool('Wwwc'), - dropdownContent: ( -
- {dropdownContent.map((row, i) => ( -
-
- {row.name} - - {row.value} - -
- {i} -
- ))} -
- ), - }, - { - id: 'Pan', - label: 'Pan', - icon: 'tool-move', - commandName: 'setToolActive', - commandOptions: { toolName: 'Pan' }, - onClick: () => setActiveTool('Pan'), - }, - { - id: 'Capture', - label: 'Capture', - icon: 'tool-capture', - commandName: 'setToolActive', - commandOptions: { toolName: 'Capture' }, - onClick: () => setActiveTool('Capture'), - }, - { - id: 'Layout', - label: 'Layout', - icon: 'tool-layout', - commandName: 'setToolActive', - commandOptions: { toolName: 'Layout' }, - onClick: () => setActiveTool('Layout'), - }, - ]; - const moreTools = [ - { - id: 'Zoom 2', - label: 'Zoom', - icon: 'tool-zoom', - commandName: 'setToolActive', - commandOptions: { toolName: 'Zoom' }, - onClick: () => setActiveTool('Zoom 2'), - }, - { - id: 'Layout 2', - label: 'Layout', - icon: 'tool-layout', - commandName: 'setToolActive', - commandOptions: { toolName: 'Layout' }, - onClick: () => setActiveTool('Layout 2'), - }, - ]; - const secondaryTools = [ - { - id: 'Annotate', - label: 'Annotate', - icon: 'tool-annotate', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Annotate' }, - onClick: () => console.log('Activate Annotate'), - }, - { - id: 'Bidirectional', - label: 'Bidirectional', - icon: 'tool-bidirectional', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Bidirectional' }, - onClick: () => console.log('Activate Bidirectional'), - }, - { - id: 'Elipse', - label: 'Elipse', - icon: 'tool-elipse', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Elipse' }, - onClick: () => console.log('Activate Elipse'), - }, - { - id: 'Length', - label: 'Length', - icon: 'tool-length', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Length' }, - onClick: () => console.log('Activate Length'), - }, - ]; - return ( -
-
- Primary Toolbar - -
-
- Secondary Toolbar - -
-
- ); - }} -
- -## Properties - - diff --git a/platform/ui/src/components/Toolbar/index.js b/platform/ui/src/components/Toolbar/index.js deleted file mode 100644 index 7bd64e01f..000000000 --- a/platform/ui/src/components/Toolbar/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import Toolbar from './Toolbar'; -export default Toolbar; diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx index fb44a3a24..904d0dcf3 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx @@ -28,6 +28,7 @@ const ToolbarButton = ({ return (
@@ -51,6 +52,7 @@ ToolbarButton.defaultProps = { }; ToolbarButton.propTypes = { + /* Influences background/hover styling */ type: PropTypes.oneOf(['primary', 'secondary']), id: PropTypes.string.isRequired, isActive: PropTypes.bool, @@ -58,7 +60,7 @@ ToolbarButton.propTypes = { icon: PropTypes.string.isRequired, label: PropTypes.string.isRequired, /** Tooltip content can be replaced for a customized content by passing a node to this value. */ - dropdownContent: PropTypes.node, + dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), }; export default ToolbarButton; diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index b4a1a48d4..115646a78 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -24,7 +24,7 @@ const arrowPositionStyle = { }, }; -const Tooltip = ({ position, content, tight, children }) => { +const Tooltip = ({ content, isSticky, position, tight, children }) => { const [isActive, setIsActive] = useState(false); const handleMouseOver = () => { @@ -39,6 +39,8 @@ const Tooltip = ({ position, content, tight, children }) => { } }; + const isOpen = isSticky || isActive; + return (
{ {children}
{ } )} > - {content} + {typeof content === 'function' ? content() : content} { Tooltip.defaultProps = { tight: false, + isSticky: false, position: 'bottom', }; Tooltip.propTypes = { - tight: PropTypes.bool, + content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, position: PropTypes.oneOf([ 'bottom', 'bottom-left', @@ -92,8 +95,9 @@ Tooltip.propTypes = { 'left', 'right', ]), + isSticky: PropTypes.bool, + tight: PropTypes.bool, children: PropTypes.node.isRequired, - content: PropTypes.node.isRequired, }; export default Tooltip; diff --git a/platform/ui/src/components/Tooltip/tooltip.css b/platform/ui/src/components/Tooltip/tooltip.css index 1c388a835..28c0b3991 100644 --- a/platform/ui/src/components/Tooltip/tooltip.css +++ b/platform/ui/src/components/Tooltip/tooltip.css @@ -1,10 +1,10 @@ .tooltip { - @apply z-10 absolute; + @apply absolute z-10; } /* TOOLTIP WORKAROUND FOR ARROW UP */ .tooltip.tooltip-bottom .tooltip-box::before { - @apply bg-primary-dark absolute z-10; + @apply absolute z-10 bg-primary-dark; content: ''; width: 14px; height: 1px; @@ -44,7 +44,7 @@ } .tooltip.tooltip-right .tooltip-box::before { - @apply bg-primary-dark absolute z-10; + @apply absolute z-10 bg-primary-dark; content: ''; width: 2px; height: 15px; @@ -54,7 +54,7 @@ } .tooltip.tooltip-left .tooltip-box::before { - @apply bg-primary-dark absolute z-10; + @apply absolute z-10 bg-primary-dark; content: ''; width: 2px; height: 15px; @@ -64,7 +64,7 @@ } .tooltip.tooltip-bottom-right .tooltip-box::before { - @apply bg-primary-dark absolute z-10; + @apply absolute z-10 bg-primary-dark; content: ''; width: 15px; height: 2px; @@ -73,19 +73,10 @@ } .tooltip.tooltip-bottom-left .tooltip-box::before { - @apply bg-primary-dark absolute z-10; + @apply absolute z-10 bg-primary-dark; content: ''; width: 15px; height: 2px; left: 5px; top: -1px; } - -/* TODO: REMOVE BELOW CLASS "showTooltipOnHover" AFTER TOOLTIP REPLACEMENT */ -.showTooltipOnHover .tooltip { - display: none; -} - -.showTooltipOnHover:hover .tooltip { - display: block; -} diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index fcf5f4745..139fe9927 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -184,7 +184,7 @@ const ViewportActionBar = ({ studyData, onSeriesChange }) => {
} > -
+
{ return ( @@ -16,7 +9,7 @@ const Viewer = () => {
{ > -
- +
+ {/* */}
CONTENT
{ componentLabel="Measurements" defaultIsOpen={false} > -
+
panel placeholder
diff --git a/platform/ui/src/views/Viewer/Viewer.mdx b/platform/ui/src/views/Viewer/Viewer.mdx index 3fe5ea9a1..a0f37581a 100644 --- a/platform/ui/src/views/Viewer/Viewer.mdx +++ b/platform/ui/src/views/Viewer/Viewer.mdx @@ -25,7 +25,6 @@ import { } from '@ohif/ui'; import Header from './components/Header'; -import ViewportToolbar from './components/ViewportToolBar'; import { tabs } from './studyBrowserMockData'; @@ -76,7 +75,7 @@ import { tabs } from './studyBrowserMockData'; {/* TOOLBAR + GRID */}
- + {/* Secondary Toolbar */}
{/* VIEWPORT GRID CONTAINER */}
diff --git a/platform/ui/src/views/Viewer/components/Header.js b/platform/ui/src/views/Viewer/components/Header.js index fb90b06e4..b41739755 100644 --- a/platform/ui/src/views/Viewer/components/Header.js +++ b/platform/ui/src/views/Viewer/components/Header.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui'; +import { NavBar, Svg, Icon, IconButton } from '@ohif/ui'; const Header = () => { const [activeTool, setActiveTool] = useState('Zoom'); @@ -34,15 +34,15 @@ const Header = () => { {dropdownContent.map((row, i) => (
{row.name} - + {row.value}
- {i} + {i}
))}
@@ -75,12 +75,12 @@ const Header = () => { ]; return ( -
+
-
+
alert('Navigate to previous page')} /> @@ -89,10 +89,10 @@ const Header = () => {
- + {/* */}
- + FOR INVESTIGATIONAL USE ONLY { - const tools = [ - { - id: 'Annotate', - label: 'Annotate', - icon: 'tool-annotate', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Annotate' }, - onClick: () => console.log('Activate Annotate'), - }, - { - id: 'Bidirectional', - label: 'Bidirectional', - icon: 'tool-bidirectional', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Bidirectional' }, - onClick: () => console.log('Activate Bidirectional'), - }, - { - id: 'Elipse', - label: 'Elipse', - icon: 'tool-elipse', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Elipse' }, - onClick: () => console.log('Activate Elipse'), - }, - { - id: 'Length', - label: 'Length', - icon: 'tool-length', - type: null, - commandName: 'setToolActive', - commandOptions: { toolName: 'Length' }, - onClick: () => console.log('Activate Length'), - }, - ]; - return ; -}; - -export default ViewportToolbar; diff --git a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js b/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js deleted file mode 100644 index 624837c67..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js +++ /dev/null @@ -1,57 +0,0 @@ -import OHIF from '@ohif/core'; -import { connect } from 'react-redux'; -import { StudyBrowser } from '@ohif/ui'; -import cloneDeep from 'lodash.clonedeep'; -import findDisplaySetByUID from './findDisplaySetByUID'; - -const { setActiveViewportSpecificData } = OHIF.redux.actions; - -// TODO -// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData) -// - Pass in errors and stack loading progress from Redux -const mapStateToProps = (state, ownProps) => { - // If we know that the stack loading progress details have changed, - // we can try to update the component state so that the thumbnail - // progress bar is updated - const stackLoadingProgressMap = state.loading.progress; - const studiesWithLoadingData = cloneDeep(ownProps.studies); - - studiesWithLoadingData.forEach(study => { - study.thumbnails.forEach(data => { - const { displaySetInstanceUID } = data; - const stackId = `StackProgress:${displaySetInstanceUID}`; - const stackProgressData = stackLoadingProgressMap[stackId]; - - let stackPercentComplete = 0; - if (stackProgressData) { - stackPercentComplete = stackProgressData.percentComplete; - } - - data.stackPercentComplete = stackPercentComplete; - }); - }); - - return { - studies: studiesWithLoadingData, - }; -}; - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onThumbnailClick: displaySetInstanceUID => { - const displaySet = findDisplaySetByUID( - ownProps.studyMetadata, - displaySetInstanceUID - ); - - dispatch(setActiveViewportSpecificData(displaySet)); - }, - }; -}; - -const ConnectedStudyBrowser = connect( - mapStateToProps, - mapDispatchToProps -)(StudyBrowser); - -export default ConnectedStudyBrowser; diff --git a/platform/viewer/src/connectedComponents/ConnectedViewerMain.js b/platform/viewer/src/connectedComponents/ConnectedViewerMain.js deleted file mode 100644 index bb121bcb4..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedViewerMain.js +++ /dev/null @@ -1,37 +0,0 @@ -import OHIF from '@ohif/core'; -import ViewerMain from './ViewerMain'; -import { connect } from 'react-redux'; - -const { - setViewportSpecificData, - clearViewportSpecificData, -} = OHIF.redux.actions; - -const mapStateToProps = state => { - const { activeViewportIndex, layout, viewportSpecificData } = state.viewports; - - return { - activeViewportIndex, - layout, - viewportSpecificData, - viewports: state.viewports, - }; -}; - -const mapDispatchToProps = dispatch => { - return { - setViewportSpecificData: (viewportIndex, data) => { - dispatch(setViewportSpecificData(viewportIndex, data)); - }, - clearViewportSpecificData: () => { - dispatch(clearViewportSpecificData()); - }, - }; -}; - -const ConnectedViewerMain = connect( - mapStateToProps, - mapDispatchToProps -)(ViewerMain); - -export default ConnectedViewerMain; diff --git a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js deleted file mode 100644 index 387bea839..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js'; -import OHIF from '@ohif/core'; - -const { clearViewportSpecificData } = OHIF.redux.actions; -const isActive = a => a.active === true; - -const mapStateToProps = (state, ownProps) => { - const activeServer = state.servers.servers.find(isActive); - - return { - server: ownProps.server || activeServer, - }; -}; -const mapDispatchToProps = dispatch => { - return { - clearViewportSpecificData: () => { - dispatch(clearViewportSpecificData()); - }, - }; -}; - -const ConnectedViewerRetrieveStudyData = connect( - mapStateToProps, - mapDispatchToProps -)(ViewerRetrieveStudyData); - -export default ConnectedViewerRetrieveStudyData; diff --git a/platform/viewer/src/connectedComponents/ToolContextMenu.js b/platform/viewer/src/connectedComponents/ToolContextMenu.js deleted file mode 100644 index 13dc3aeca..000000000 --- a/platform/viewer/src/connectedComponents/ToolContextMenu.js +++ /dev/null @@ -1,114 +0,0 @@ -import { ContextMenu } from '@ohif/ui'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { commandsManager } from './../App.jsx'; - -const toolTypes = [ - 'Angle', - 'Bidirectional', - 'Length', - 'FreehandMouse', - 'EllipticalRoi', - 'CircleRoi', - 'RectangleRoi', -]; - -const ToolContextMenu = ({ - onSetLabel, - onSetDescription, - isTouchEvent, - eventData, - onClose, - onDelete, -}) => { - const defaultDropdownItems = [ - { - label: 'Delete measurement', - actionType: 'Delete', - action: ({ nearbyToolData, eventData }) => - onDelete(nearbyToolData, eventData), - }, - { - label: 'Relabel', - actionType: 'setLabel', - action: ({ nearbyToolData, eventData }) => { - const { tool: measurementData } = nearbyToolData; - onSetLabel(eventData, measurementData); - }, - }, - { - actionType: 'setDescription', - action: ({ nearbyToolData, eventData }) => { - const { tool: measurementData } = nearbyToolData; - onSetDescription(eventData, measurementData); - }, - }, - ]; - - const getDropdownItems = (eventData, isTouchEvent = false) => { - const nearbyToolData = commandsManager.runCommand('getNearbyToolData', { - element: eventData.element, - canvasCoordinates: eventData.currentPoints.canvas, - availableToolTypes: toolTypes, - }); - - /* - * Annotate tools for touch events already have a press handle to edit it, - * has a better UX for deleting it. - */ - if ( - isTouchEvent && - nearbyToolData && - nearbyToolData.toolType === 'arrowAnnotate' - ) { - return; - } - - let dropdownItems = []; - if (nearbyToolData) { - defaultDropdownItems.forEach(item => { - item.params = { eventData, nearbyToolData }; - - if (item.actionType === 'setDescription') { - item.label = `${ - nearbyToolData.tool.description ? 'Edit' : 'Add' - } Description`; - } - - dropdownItems.push(item); - }); - } - - return dropdownItems; - }; - - const onClickHandler = ({ action, params }) => { - action(params); - if (onClose) { - onClose(); - } - }; - - const dropdownItems = getDropdownItems(eventData, isTouchEvent); - - return ( -
- -
- ); -}; - -ToolContextMenu.propTypes = { - isTouchEvent: PropTypes.bool.isRequired, - eventData: PropTypes.object, - onClose: PropTypes.func, - onSetDescription: PropTypes.func, - onSetLabel: PropTypes.func, - onDelete: PropTypes.func, -}; - -ToolContextMenu.defaultProps = { - isTouchEvent: false, -}; - -export default ToolContextMenu; diff --git a/platform/viewer/src/connectedComponents/ToolbarRow.js b/platform/viewer/src/connectedComponents/ToolbarRow.js deleted file mode 100644 index 3a9d7ad9e..000000000 --- a/platform/viewer/src/connectedComponents/ToolbarRow.js +++ /dev/null @@ -1,386 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; - -import { MODULE_TYPES } from '@ohif/core'; -import { - ExpandableToolMenu, - RoundedButtonGroup, - ToolbarButton, - withModal, - withDialog, -} from '@ohif/ui'; - -import './ToolbarRow.css'; -import { commandsManager, extensionManager } from './../App.jsx'; - -import ConnectedCineDialog from './ConnectedCineDialog'; -import ConnectedLayoutButton from './ConnectedLayoutButton'; -import { withAppContext } from '../context/AppContext'; - -class ToolbarRow extends Component { - // TODO: Simplify these? isOpen can be computed if we say "any" value for selected, - // closed if selected is null/undefined - static propTypes = { - isLeftSidePanelOpen: PropTypes.bool.isRequired, - isRightSidePanelOpen: PropTypes.bool.isRequired, - selectedLeftSidePanel: PropTypes.string.isRequired, - selectedRightSidePanel: PropTypes.string.isRequired, - handleSidePanelChange: PropTypes.func.isRequired, - activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired, - studies: PropTypes.array, - t: PropTypes.func.isRequired, - // NOTE: withDialog, withModal HOCs - dialog: PropTypes.any, - modal: PropTypes.any, - }; - - static defaultProps = { - studies: [], - }; - - constructor(props) { - super(props); - - const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this); - // TODO: - // If it's a tool that can be active... Mark it as active? - // - Tools that are on/off? - // - Tools that can be bound to multiple buttons? - - // Normal ToolbarButtons... - // Just how high do we need to hoist this state? - // Why ToolbarRow instead of just Toolbar? Do we have any others? - this.state = { - toolbarButtons: toolbarButtonDefinitions, - activeButtons: [], - }; - - this.seriesPerStudyCount = []; - - this._handleBuiltIn = _handleBuiltIn.bind(this); - - this.updateButtonGroups(); - } - - updateButtonGroups() { - const panelModules = extensionManager.modules[MODULE_TYPES.PANEL]; - - this.buttonGroups = { - left: [], - right: [], - }; - - // ~ FIND MENU OPTIONS - panelModules.forEach(panelExtension => { - const panelModule = panelExtension.module; - const defaultContexts = Array.from(panelModule.defaultContext); - - panelModule.menuOptions.forEach(menuOption => { - const contexts = Array.from(menuOption.context || defaultContexts); - const hasActiveContext = this.props.activeContexts.some(actx => - contexts.includes(actx) - ); - - // It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport? - // Note: This does not cleanly handle `studies` prop updating with panel open - const isDisabled = - typeof menuOption.isDisabled === 'function' && - menuOption.isDisabled(this.props.studies); - - if (hasActiveContext && !isDisabled) { - const menuOptionEntry = { - value: menuOption.target, - icon: menuOption.icon, - bottomLabel: menuOption.label, - }; - const from = menuOption.from || 'right'; - - this.buttonGroups[from].push(menuOptionEntry); - } - }); - }); - - // TODO: This should come from extensions, instead of being baked in - this.buttonGroups.left.unshift({ - value: 'studies', - icon: 'th-large', - bottomLabel: this.props.t('Series'), - }); - } - - componentDidUpdate(prevProps) { - const activeContextsChanged = - prevProps.activeContexts !== this.props.activeContexts; - - const prevStudies = prevProps.studies; - const studies = this.props.studies; - const seriesPerStudyCount = this.seriesPerStudyCount; - - let studiesUpdated = false; - - if (prevStudies.length !== studies.length) { - studiesUpdated = true; - } else { - for (let i = 0; i < studies.length; i++) { - if (studies[i].series.length !== seriesPerStudyCount[i]) { - seriesPerStudyCount[i] = studies[i].series.length; - - studiesUpdated = true; - break; - } - } - } - - if (studiesUpdated) { - this.updateButtonGroups(); - } - - if (activeContextsChanged) { - this.setState( - { - toolbarButtons: _getVisibleToolbarButtons.call(this), - }, - this.closeCineDialogIfNotApplicable - ); - } - } - - closeCineDialogIfNotApplicable = () => { - const { dialog } = this.props; - let { dialogId, activeButtons, toolbarButtons } = this.state; - if (dialogId) { - const cineButtonPresent = toolbarButtons.find( - button => button.options && button.options.behavior === 'CINE' - ); - if (!cineButtonPresent) { - dialog.dismiss({ id: dialogId }); - activeButtons = activeButtons.filter( - button => button.options && button.options.behavior !== 'CINE' - ); - this.setState({ dialogId: null, activeButtons }); - } - } - }; - - render() { - const buttonComponents = _getButtonComponents.call( - this, - this.state.toolbarButtons, - this.state.activeButtons - ); - - const onPress = (side, value) => { - this.props.handleSidePanelChange(side, value); - }; - const onPressLeft = onPress.bind(this, 'left'); - const onPressRight = onPress.bind(this, 'right'); - - return ( - <> -
-
- -
- {buttonComponents} - -
- {this.buttonGroups.right.length && ( - - )} -
-
- - ); - } -} - -function _getCustomButtonComponent(button, activeButtons) { - const CustomComponent = button.CustomComponent; - const isValidComponent = typeof CustomComponent === 'function'; - - // Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented. - if (isValidComponent) { - const parentContext = this; - const activeButtonsIds = activeButtons.map(button => button.id); - const isActive = activeButtonsIds.includes(button.id); - - return ( - - ); - } -} - -function _getExpandableButtonComponent(button, activeButtons) { - // Iterate over button definitions and update `onClick` behavior - let activeCommand; - const childButtons = button.buttons.map(childButton => { - childButton.onClick = _handleToolbarButtonClick.bind(this, childButton); - - if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) { - activeCommand = childButton.id; - } - - return childButton; - }); - - return ( - - ); -} - -function _getDefaultButtonComponent(button, activeButtons) { - return ( - button.id).includes(button.id)} - /> - ); -} -/** - * Determine which extension buttons should be showing, if they're - * active, and what their onClick behavior should be. - */ -function _getButtonComponents(toolbarButtons, activeButtons) { - const _this = this; - return toolbarButtons.map(button => { - const hasCustomComponent = button.CustomComponent; - const hasNestedButtonDefinitions = button.buttons && button.buttons.length; - - if (hasCustomComponent) { - return _getCustomButtonComponent.call(_this, button, activeButtons); - } - - if (hasNestedButtonDefinitions) { - return _getExpandableButtonComponent.call(_this, button, activeButtons); - } - - return _getDefaultButtonComponent.call(_this, button, activeButtons); - }); -} - -/** - * TODO: DEPRECATE - * This is used exclusively in `extensions/cornerstone/src` - * We have better ways with new UI Services to trigger "builtin" behaviors - * - * A handy way for us to handle different button types. IE. firing commands for - * buttons, or initiation built in behavior. - * - * @param {*} button - * @param {*} evt - * @param {*} props - */ -function _handleToolbarButtonClick(button, evt, props) { - const { activeButtons } = this.state; - - if (button.commandName) { - const options = Object.assign({ evt }, button.commandOptions); - commandsManager.runCommand(button.commandName, options); - } - - // TODO: Use Types ENUM - // TODO: We can update this to be a `getter` on the extension to query - // For the active tools after we apply our updates? - if (button.type === 'setToolActive') { - const toggables = activeButtons.filter( - ({ options }) => options && !options.togglable - ); - this.setState({ activeButtons: [...toggables, button] }); - } else if (button.type === 'builtIn') { - this._handleBuiltIn(button); - } -} - -/** - * - */ -function _getVisibleToolbarButtons() { - const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR]; - const toolbarButtonDefinitions = []; - - toolbarModules.forEach(extension => { - const { definitions, defaultContext } = extension.module; - definitions.forEach(definition => { - const context = definition.context || defaultContext; - - if (this.props.activeContexts.includes(context)) { - toolbarButtonDefinitions.push(definition); - } - }); - }); - - return toolbarButtonDefinitions; -} - -function _handleBuiltIn(button) { - /* TODO: Keep cine button active until its unselected. */ - const { dialog, t } = this.props; - const { dialogId } = this.state; - const { id, options } = button; - - if (options.behavior === 'CINE') { - if (dialogId) { - dialog.dismiss({ id: dialogId }); - this.setState(state => ({ - dialogId: null, - activeButtons: [ - ...state.activeButtons.filter(button => button.id !== id), - ], - })); - } else { - const spacing = 20; - const { x, y } = document - .querySelector(`.ViewerMain`) - .getBoundingClientRect(); - const newDialogId = dialog.create({ - content: ConnectedCineDialog, - defaultPosition: { - x: x + spacing || 0, - y: y + spacing || 0, - }, - }); - this.setState(state => ({ - dialogId: newDialogId, - activeButtons: [...state.activeButtons, button], - })); - } - } - - if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') { - commandsManager.runCommand('showDownloadViewportModal', { - title: t('Download High Quality Image'), - }); - } -} - -export default withTranslation(['Common', 'ViewportDownloadForm'])( - withModal(withDialog(withAppContext(ToolbarRow))) -); diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js deleted file mode 100644 index ee85b8b9b..000000000 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ /dev/null @@ -1,400 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { MODULE_TYPES } from '@ohif/core'; -import OHIF, { DICOMSR } from '@ohif/core'; -import { withDialog } from '@ohif/ui'; -import moment from 'moment'; -import ConnectedHeader from './ConnectedHeader.js'; -import ToolbarRow from './ToolbarRow.js'; -import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; -import ConnectedViewerMain from './ConnectedViewerMain.js'; -import SidePanel from './../components/SidePanel.js'; -import { extensionManager } from './../App.jsx'; - -// Contexts -import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; -import UserManagerContext from '../context/UserManagerContext'; -import AppContext from '../context/AppContext'; - -import './Viewer.css'; - -class Viewer extends Component { - static propTypes = { - studies: PropTypes.arrayOf( - PropTypes.shape({ - StudyInstanceUID: PropTypes.string.isRequired, - StudyDate: PropTypes.string, - displaySets: PropTypes.arrayOf( - PropTypes.shape({ - displaySetInstanceUID: PropTypes.string.isRequired, - SeriesDescription: PropTypes.string, - SeriesNumber: PropTypes.number, - InstanceNumber: PropTypes.number, - numImageFrames: PropTypes.number, - Modality: PropTypes.string.isRequired, - images: PropTypes.arrayOf( - PropTypes.shape({ - getImageId: PropTypes.func.isRequired, - }) - ), - }) - ), - }) - ), - studyInstanceUIDs: PropTypes.array, - activeServer: PropTypes.shape({ - type: PropTypes.string, - wadoRoot: PropTypes.string, - }), - onTimepointsUpdated: PropTypes.func, - onMeasurementsUpdated: PropTypes.func, - // window.store.getState().viewports.viewportSpecificData - viewports: PropTypes.object.isRequired, - // window.store.getState().viewports.activeViewportIndex - activeViewportIndex: PropTypes.number.isRequired, - isStudyLoaded: PropTypes.bool, - dialog: PropTypes.object, - }; - - constructor(props) { - super(props); - - const { activeServer } = this.props; - const server = Object.assign({}, activeServer); - - OHIF.measurements.MeasurementApi.setConfiguration({ - dataExchange: { - retrieve: DICOMSR.retrieveMeasurements, - store: DICOMSR.storeMeasurements, - }, - server, - }); - - OHIF.measurements.TimepointApi.setConfiguration({ - dataExchange: { - retrieve: this.retrieveTimepoints, - store: this.storeTimepoints, - remove: this.removeTimepoint, - update: this.updateTimepoint, - disassociate: this.disassociateStudy, - }, - }); - } - - state = { - isLeftSidePanelOpen: true, - isRightSidePanelOpen: false, - selectedRightSidePanel: '', - selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this - thumbnails: [], - }; - - componentWillUnmount() { - if (this.props.dialog) { - this.props.dialog.dismissAll(); - } - } - - retrieveTimepoints = filter => { - OHIF.log.info('retrieveTimepoints'); - - // Get the earliest and latest study date - let earliestDate = new Date().toISOString(); - let latestDate = new Date().toISOString(); - if (this.props.studies) { - latestDate = new Date('1000-01-01').toISOString(); - this.props.studies.forEach(study => { - const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString(); - if (StudyDate < earliestDate) { - earliestDate = StudyDate; - } - if (StudyDate > latestDate) { - latestDate = StudyDate; - } - }); - } - - // Return a generic timepoint - return Promise.resolve([ - { - timepointType: 'baseline', - timepointId: 'TimepointId', - studyInstanceUIDs: this.props.studyInstanceUIDs, - PatientID: filter.PatientID, - earliestDate, - latestDate, - isLocked: false, - }, - ]); - }; - - storeTimepoints = timepointData => { - OHIF.log.info('storeTimepoints'); - return Promise.resolve(); - }; - - updateTimepoint = (timepointData, query) => { - OHIF.log.info('updateTimepoint'); - return Promise.resolve(); - }; - - removeTimepoint = timepointId => { - OHIF.log.info('removeTimepoint'); - return Promise.resolve(); - }; - - disassociateStudy = (timepointIds, StudyInstanceUID) => { - OHIF.log.info('disassociateStudy'); - return Promise.resolve(); - }; - - onTimepointsUpdated = timepoints => { - if (this.props.onTimepointsUpdated) { - this.props.onTimepointsUpdated(timepoints); - } - }; - - onMeasurementsUpdated = measurements => { - if (this.props.onMeasurementsUpdated) { - this.props.onMeasurementsUpdated(measurements); - } - }; - - componentDidMount() { - const { studies, isStudyLoaded } = this.props; - const { TimepointApi, MeasurementApi } = OHIF.measurements; - const currentTimepointId = 'TimepointId'; - - const timepointApi = new TimepointApi(currentTimepointId, { - onTimepointsUpdated: this.onTimepointsUpdated, - }); - - const measurementApi = new MeasurementApi(timepointApi, { - onMeasurementsUpdated: this.onMeasurementsUpdated, - }); - - this.currentTimepointId = currentTimepointId; - this.timepointApi = timepointApi; - this.measurementApi = measurementApi; - - if (studies) { - const PatientID = studies[0] && studies[0].PatientID; - - timepointApi.retrieveTimepoints({ PatientID }); - if (isStudyLoaded) { - this.measurementApi.retrieveMeasurements(PatientID, [ - currentTimepointId, - ]); - } - this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), - }); - } - } - - componentDidUpdate(prevProps) { - const { studies, isStudyLoaded } = this.props; - if (studies !== prevProps.studies) { - this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), - }); - } - if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) { - const PatientID = studies[0] && studies[0].PatientID; - const { currentTimepointId } = this; - - this.timepointApi.retrieveTimepoints({ PatientID }); - this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]); - } - } - - render() { - let VisiblePanelLeft, VisiblePanelRight; - const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL]; - - panelExtensions.forEach(panelExt => { - panelExt.module.components.forEach(comp => { - if (comp.id === this.state.selectedRightSidePanel) { - VisiblePanelRight = comp.component; - } else if (comp.id === this.state.selectedLeftSidePanel) { - VisiblePanelLeft = comp.component; - } - }); - }); - - return ( - <> - {/* HEADER */} - - {whiteLabeling => ( - - {userManager => ( - - {appContext => ( - - {whiteLabeling && - whiteLabeling.createLogoComponentFn && - whiteLabeling.createLogoComponentFn(React)} - - )} - - )} - - )} - - - {/* TOOLBAR */} - { - const sideClicked = side && side[0].toUpperCase() + side.slice(1); - const openKey = `is${sideClicked}SidePanelOpen`; - const selectedKey = `selected${sideClicked}SidePanel`; - const updatedState = Object.assign({}, this.state); - - const isOpen = updatedState[openKey]; - const prevSelectedPanel = updatedState[selectedKey]; - // RoundedButtonGroup returns `null` if selected button is clicked - const isSameSelectedPanel = - prevSelectedPanel === selectedPanel || selectedPanel === null; - - updatedState[selectedKey] = selectedPanel || prevSelectedPanel; - - const isClosedOrShouldClose = !isOpen || isSameSelectedPanel; - if (isClosedOrShouldClose) { - updatedState[openKey] = !updatedState[openKey]; - } - - this.setState(updatedState); - }} - studies={this.props.studies} - /> - - {/**/} - {/**/} - - {/* VIEWPORTS + SIDEPANELS */} -
- {/* LEFT */} - - {VisiblePanelLeft ? ( - - ) : ( - - )} - - - {/* MAIN */} -
- -
- - {/* RIGHT */} - - {VisiblePanelRight && ( - - )} - -
- - ); - } -} - -export default withDialog(Viewer); - -/** - * What types are these? Why do we have "mapping" dropped in here instead of in - * a mapping layer? - * - * TODO[react]: - * - Add sorting of display sets - * - Add showStackLoadingProgressBar option - * - * @param {Study[]} studies - * @param {DisplaySet[]} studies[].displaySets - */ -const _mapStudiesToThumbnails = function(studies) { - return studies.map(study => { - const { StudyInstanceUID } = study; - - const thumbnails = study.displaySets.map(displaySet => { - const { - displaySetInstanceUID, - SeriesDescription, - SeriesNumber, - InstanceNumber, - numImageFrames, - } = displaySet; - - let imageId; - let altImageText; - - if (displaySet.Modality && displaySet.Modality === 'SEG') { - // TODO: We want to replace this with a thumbnail showing - // the segmentation map on the image, but this is easier - // and better than what we have right now. - altImageText = 'SEG'; - } else if (displaySet.images && displaySet.images.length) { - const imageIndex = Math.floor(displaySet.images.length / 2); - - imageId = displaySet.images[imageIndex].getImageId(); - } else { - altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; - } - - return { - imageId, - altImageText, - displaySetInstanceUID, - SeriesDescription, - SeriesNumber, - InstanceNumber, - numImageFrames, - }; - }); - - return { - StudyInstanceUID, - thumbnails, - }; - }); -}; diff --git a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js b/platform/viewer/src/connectedComponents/ViewerLocalFileData.js deleted file mode 100644 index f2b6b116d..000000000 --- a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js +++ /dev/null @@ -1,151 +0,0 @@ -import React, { Component } from 'react'; -import { metadata, utils } from '@ohif/core'; - -import ConnectedViewer from './ConnectedViewer.js'; -import PropTypes from 'prop-types'; -import { extensionManager } from './../App.jsx'; -import Dropzone from 'react-dropzone'; -import filesToStudies from '../lib/filesToStudies'; -import './ViewerLocalFileData.css'; -import { withTranslation } from 'react-i18next'; - -const { OHIFStudyMetadata } = metadata; -const { studyMetadataManager } = utils; - -const dropZoneLinkDialog = (onDrop, i18n, dir) => { - return ( - - {({ getRootProps, getInputProps }) => ( - - {dir ? ( - - {i18n('Load folders')} - - - ) : ( - - {i18n('Load files')} - - - )} - - )} - - ); -}; - -const linksDialogMessage = (onDrop, i18n) => { - return ( - <> - {i18n('Or click to ')} - {dropZoneLinkDialog(onDrop, i18n)} - {i18n(' or ')} - {dropZoneLinkDialog(onDrop, i18n, true)} - {i18n(' from dialog')} - - ); -}; - -class ViewerLocalFileData extends Component { - static propTypes = { - studies: PropTypes.array, - }; - - state = { - studies: null, - loading: false, - error: null, - }; - - updateStudies = studies => { - // Render the viewer when the data is ready - studyMetadataManager.purge(); - - // Map studies to new format, update metadata manager? - const updatedStudies = studies.map(study => { - const studyMetadata = new OHIFStudyMetadata( - study, - study.StudyInstanceUID - ); - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - - study.displaySets = - study.displaySets || - studyMetadata.createDisplaySets(sopClassHandlerModules); - studyMetadata.setDisplaySets(study.displaySets); - - studyMetadata.forEachDisplaySet(displayset => { - displayset.localFile = true; - }); - - studyMetadataManager.add(studyMetadata); - - return study; - }); - - this.setState({ - studies: updatedStudies, - }); - }; - - render() { - const onDrop = async acceptedFiles => { - this.setState({ loading: true }); - - const studies = await filesToStudies(acceptedFiles); - const updatedStudies = this.updateStudies(studies); - - if (!updatedStudies) { - return; - } - - this.setState({ studies: updatedStudies, loading: false }); - }; - - if (this.state.error) { - return
Error: {JSON.stringify(this.state.error)}
; - } - - return ( - - {({ getRootProps, getInputProps }) => ( -
- {this.state.studies ? ( - a.StudyInstanceUID) - } - /> - ) : ( -
-
- {this.state.loading ? ( -

{this.props.t('Loading...')}

- ) : ( - <> -

- {this.props.t( - 'Drag and Drop DICOM files here to load them in the Viewer' - )} -

-

{linksDialogMessage(onDrop, this.props.t)}

- - )} -
-
- )} -
- )} -
- ); - } -} - -export default withTranslation('Common')(ViewerLocalFileData); diff --git a/platform/viewer/src/connectedComponents/ViewerMain.js b/platform/viewer/src/connectedComponents/ViewerMain.js deleted file mode 100644 index 6a9589495..000000000 --- a/platform/viewer/src/connectedComponents/ViewerMain.js +++ /dev/null @@ -1,193 +0,0 @@ -import './ViewerMain.css'; - -import { Component } from 'react'; -import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js'; -import PropTypes from 'prop-types'; -import React from 'react'; -import memoize from 'lodash/memoize'; -import _values from 'lodash/values'; - -var values = memoize(_values); - -class ViewerMain extends Component { - static propTypes = { - activeViewportIndex: PropTypes.number.isRequired, - studies: PropTypes.array, - viewportSpecificData: PropTypes.object.isRequired, - layout: PropTypes.object.isRequired, - setViewportSpecificData: PropTypes.func.isRequired, - clearViewportSpecificData: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - displaySets: [], - }; - } - - getDisplaySets(studies) { - const displaySets = []; - studies.forEach(study => { - study.displaySets.forEach(dSet => { - if (!dSet.plugin) { - dSet.plugin = 'cornerstone'; - } - displaySets.push(dSet); - }); - }); - - return displaySets; - } - - findDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) { - const study = studies.find(study => { - return study.StudyInstanceUID === StudyInstanceUID; - }); - - if (!study) { - return; - } - - return study.displaySets.find(displaySet => { - return displaySet.displaySetInstanceUID === displaySetInstanceUID; - }); - } - - componentDidMount() { - // Add beforeUnload event handler to check for unsaved changes - //window.addEventListener('beforeunload', unloadHandlers.beforeUnload); - - // Get all the display sets for the viewer studies - if (this.props.studies) { - const displaySets = this.getDisplaySets(this.props.studies); - this.setState({ displaySets }, this.fillEmptyViewportPanes); - } - } - - componentDidUpdate(prevProps) { - const prevViewportAmount = prevProps.layout.viewports.length; - const viewportAmount = this.props.layout.viewports.length; - const isVtk = this.props.layout.viewports.some(vp => !!vp.vtk); - - if ( - this.props.studies !== prevProps.studies || - (viewportAmount !== prevViewportAmount && !isVtk) - ) { - const displaySets = this.getDisplaySets(this.props.studies); - this.setState({ displaySets }, this.fillEmptyViewportPanes); - } - } - - fillEmptyViewportPanes = () => { - // TODO: Here is the entry point for filling viewports on load. - const dirtyViewportPanes = []; - const { layout, viewportSpecificData } = this.props; - const { displaySets } = this.state; - - if (!displaySets || !displaySets.length) { - return; - } - - for (let i = 0; i < layout.viewports.length; i++) { - const viewportPane = viewportSpecificData[i]; - const isNonEmptyViewport = - viewportPane && - viewportPane.StudyInstanceUID && - viewportPane.displaySetInstanceUID; - - if (isNonEmptyViewport) { - dirtyViewportPanes.push({ - StudyInstanceUID: viewportPane.StudyInstanceUID, - displaySetInstanceUID: viewportPane.displaySetInstanceUID, - }); - - continue; - } - - const foundDisplaySet = - displaySets.find( - ds => - !dirtyViewportPanes.some( - v => v.displaySetInstanceUID === ds.displaySetInstanceUID - ) - ) || displaySets[displaySets.length - 1]; - - dirtyViewportPanes.push(foundDisplaySet); - } - - dirtyViewportPanes.forEach((vp, i) => { - if (vp && vp.StudyInstanceUID) { - this.setViewportData({ - viewportIndex: i, - StudyInstanceUID: vp.StudyInstanceUID, - displaySetInstanceUID: vp.displaySetInstanceUID, - }); - } - }); - }; - - setViewportData = ({ - viewportIndex, - StudyInstanceUID, - displaySetInstanceUID, - }) => { - const displaySet = this.findDisplaySet( - this.props.studies, - StudyInstanceUID, - displaySetInstanceUID - ); - - this.props.setViewportSpecificData(viewportIndex, displaySet); - }; - - render() { - const { viewportSpecificData } = this.props; - const viewportData = values(viewportSpecificData); - - return ( -
- {this.state.displaySets.length && ( - - {/* Children to add to each viewport that support children */} - - )} -
- ); - } - - componentWillUnmount() { - // Clear the entire viewport specific data - const { viewportSpecificData } = this.props; - Object.keys(viewportSpecificData).forEach(viewportIndex => { - this.props.clearViewportSpecificData(viewportIndex); - }); - - // TODO: These don't have to be viewer specific? - // Could qualify for other routes? - // hotkeys.destroy(); - - // Remove beforeUnload event handler... - //window.removeEventListener('beforeunload', unloadHandlers.beforeUnload); - // Destroy the synchronizer used to update reference lines - //OHIF.viewer.updateImageSynchronizer.destroy(); - // TODO: Instruct all plugins to clean up themselves - // - // Clear references to all stacks in the StackManager - //StackManager.clearStacks(); - // @TypeSafeStudies - // Clears OHIF.viewer.Studies collection - //OHIF.viewer.Studies.removeAll(); - // @TypeSafeStudies - // Clears OHIF.viewer.StudyMetadataList collection - //OHIF.viewer.StudyMetadataList.removeAll(); - } -} - -export default ViewerMain; diff --git a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js deleted file mode 100644 index 63fc55093..000000000 --- a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js +++ /dev/null @@ -1,375 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { metadata, studies, utils, log } from '@ohif/core'; -import usePrevious from '../customHooks/usePrevious'; - -import ConnectedViewer from './ConnectedViewer.js'; -import PropTypes from 'prop-types'; -import { extensionManager } from './../App.jsx'; -import { useSnackbarContext } from '@ohif/ui'; - -const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata; -const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies; -const { studyMetadataManager, makeCancelable } = utils; - -// Contexts -import AppContext from '../context/AppContext'; - -const _promoteToFront = (list, value, searchMethod) => { - let response = [...list]; - let promoted = false; - const index = response.findIndex(searchMethod.bind(undefined, value)); - - if (index > 0) { - const first = response.splice(index, 1); - response = [...first, ...response]; - } - - if (index >= 0) { - promoted = true; - } - - return { - promoted, - data: response, - }; -}; - -/** - * Promote series to front if find found equivalent on filters object - * @param {Object} study - study reference to promote series against - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @param {boolean} isFilterStrategy - if filtering by query param strategy ON - */ -const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => { - let promoted = false; - // Promote only if no filter should be applied - if (!isFilterStrategy) { - _sortStudyDisplaySet(study, studyMetadata); - promoted = _promoteStudyDisplaySet(study, studyMetadata, filters); - } - - return promoted; -}; - -const _promoteStudyDisplaySet = (study, studyMetadata, filters) => { - let promoted = false; - const queryParamsLength = Object.keys(filters).length; - const shouldPromoteToFront = queryParamsLength > 0; - - if (shouldPromoteToFront) { - const { seriesInstanceUID } = filters; - - const _seriesLookup = (valueToCompare, displaySet) => { - return displaySet.SeriesInstanceUID === valueToCompare; - }; - const promotedResponse = _promoteToFront( - studyMetadata.getDisplaySets(), - seriesInstanceUID, - _seriesLookup - ); - - study.displaySets = promotedResponse.data; - promoted = promotedResponse.promoted; - } - - return promoted; -}; - -/** - * Method to identify if query param (from url) was applied to given list - * @param {Object} study - study reference to promote series against - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @param {boolean} isFilterStrategy - if filtering by query param strategy ON - */ -const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => { - const { seriesInstanceUID } = filters; - let applied = true; - // skip in case no filter or no toast manager - if (!seriesInstanceUID) { - return applied; - } - - const { series = [], displaySets = [] } = study; - const firstSeries = isFilterStrategy ? series[0] : displaySets[0]; - - if (!firstSeries || firstSeries.SeriesInstanceUID !== seriesInstanceUID) { - applied = false; - } - - return applied; -}; -const _showUserMessage = (queryParamApplied, message, dialog = {}) => { - if (queryParamApplied) { - return; - } - - const { show: showUserMessage = () => {} } = dialog; - showUserMessage({ - message, - }); -}; - -const _addSeriesToStudy = (studyMetadata, series) => { - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - const study = studyMetadata.getData(); - const seriesMetadata = new OHIFSeriesMetadata(series, study); - const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID); - if (existingSeries) { - studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata); - } else { - studyMetadata.addSeries(seriesMetadata); - } - - studyMetadata.createAndAddDisplaySetsForSeries( - sopClassHandlerModules, - seriesMetadata - ); - study.displaySets = studyMetadata.getDisplaySets(); - _updateStudyMetadataManager(study, studyMetadata); -}; - -const _updateStudyMetadataManager = (study, studyMetadata) => { - const { StudyInstanceUID } = study; - - if (!studyMetadataManager.get(StudyInstanceUID)) { - studyMetadataManager.add(studyMetadata); - } -}; - -const _updateStudyDisplaySets = (study, studyMetadata) => { - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - - if (!study.displaySets) { - study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules); - } - - studyMetadata.setDisplaySets(study.displaySets); -}; - -const _sortStudyDisplaySet = (study, studyMetadata) => { - studyMetadata.sortDisplaySets(study.displaySets); -}; - -function ViewerRetrieveStudyData({ - server, - studyInstanceUIDs, - seriesInstanceUIDs, - clearViewportSpecificData, -}) { - // hooks - const [error, setError] = useState(false); - const [studies, setStudies] = useState([]); - const [isStudyLoaded, setIsStudyLoaded] = useState(false); - const snackbarContext = useSnackbarContext(); - const { appConfig = {} } = useContext(AppContext); - const { - filterQueryParam: isFilterStrategy = false, - maxConcurrentMetadataRequests, - } = appConfig; - - let cancelableSeriesPromises; - let cancelableStudiesPromises; - /** - * Callback method when study is totally loaded - * @param {object} study study loaded - * @param {object} studyMetadata studyMetadata for given study - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - */ - const studyDidLoad = (study, studyMetadata, filters) => { - // User message - const promoted = _promoteList( - study, - studyMetadata, - filters, - isFilterStrategy - ); - - // Clear viewport to allow new promoted one to be displayed - if (promoted) { - clearViewportSpecificData(0); - } - - const isQueryParamApplied = _isQueryParamApplied( - study, - filters, - isFilterStrategy - ); - // Show message in case not promoted neither filtered but should to - _showUserMessage( - isQueryParamApplied, - 'Query parameters were not applied. Using original series list for given study.', - snackbarContext - ); - - setStudies([...studies, study]); - setIsStudyLoaded(true); - }; - - /** - * Method to process studies. It will update displaySet, studyMetadata, load remaining series, ... - * @param {Array} studiesData Array of studies retrieved from server - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - */ - const processStudies = (studiesData, filters) => { - if (Array.isArray(studiesData) && studiesData.length > 0) { - // Map studies to new format, update metadata manager? - const studies = studiesData.map(study => { - const studyMetadata = new OHIFStudyMetadata( - study, - study.StudyInstanceUID - ); - - _updateStudyDisplaySets(study, studyMetadata); - _updateStudyMetadataManager(study, studyMetadata); - - // Attempt to load remaning series if any - cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable( - loadRemainingSeries(studyMetadata) - ) - .then(result => { - if (result && !result.isCanceled) { - studyDidLoad(study, studyMetadata, filters); - } - }) - .catch(error => { - if (error && !error.isCanceled) { - setError(true); - log.error(error); - } - }); - - return study; - }); - - setStudies(studies); - } - }; - - const forceRerender = () => setStudies(studies => [...studies]); - - const loadRemainingSeries = async studyMetadata => { - const { seriesLoader } = studyMetadata.getData(); - if (!seriesLoader) return; - - const loadNextSeries = async () => { - if (!seriesLoader.hasNext()) return; - const series = await seriesLoader.next(); - _addSeriesToStudy(studyMetadata, series); - forceRerender(); - return loadNextSeries(); - }; - - const concurrentRequestsAllowed = - maxConcurrentMetadataRequests || studyMetadata.getSeriesCount(); - const promises = Array(concurrentRequestsAllowed) - .fill(null) - .map(loadNextSeries); - - return await Promise.all(promises); - }; - - const loadStudies = async () => { - try { - const filters = {}; - // Use the first, discard others - const seriesInstanceUID = seriesInstanceUIDs && seriesInstanceUIDs[0]; - - const retrieveParams = [server, studyInstanceUIDs]; - - if (seriesInstanceUID) { - filters.seriesInstanceUID = seriesInstanceUID; - // Query param filtering controlled by appConfig property - if (isFilterStrategy) { - retrieveParams.push(filters); - } - } - - cancelableStudiesPromises[studyInstanceUIDs] = makeCancelable( - retrieveStudiesMetadata(...retrieveParams) - ) - .then(result => { - if (result && !result.isCanceled) { - processStudies(result, filters); - } - }) - .catch(error => { - if (error && !error.isCanceled) { - setError(true); - log.error(error); - } - }); - } catch (error) { - if (error) { - setError(true); - log.error(error); - } - } - }; - - const purgeCancellablePromises = () => { - for (let studyInstanceUIDs in cancelableStudiesPromises) { - if ('cancel' in cancelableStudiesPromises[studyInstanceUIDs]) { - cancelableStudiesPromises[studyInstanceUIDs].cancel(); - } - } - - for (let studyInstanceUIDs in cancelableSeriesPromises) { - if ('cancel' in cancelableSeriesPromises[studyInstanceUIDs]) { - cancelableSeriesPromises[studyInstanceUIDs].cancel(); - deleteStudyMetadataPromise(studyInstanceUIDs); - studyMetadataManager.remove(studyInstanceUIDs); - } - } - }; - - const prevStudyInstanceUIDs = usePrevious(studyInstanceUIDs); - - useEffect(() => { - const hasStudyInstanceUIDsChanged = !( - prevStudyInstanceUIDs && - prevStudyInstanceUIDs.every(e => studyInstanceUIDs.includes(e)) - ); - - if (hasStudyInstanceUIDsChanged) { - studyMetadataManager.purge(); - purgeCancellablePromises(); - } - }, [studyInstanceUIDs]); - - useEffect(() => { - cancelableSeriesPromises = {}; - cancelableStudiesPromises = {}; - loadStudies(); - - return () => { - purgeCancellablePromises(); - }; - }, []); - - if (error) { - return
Error: {JSON.stringify(error)}
; - } - - return ( - - ); -} - -ViewerRetrieveStudyData.propTypes = { - studyInstanceUIDs: PropTypes.array.isRequired, - seriesInstanceUIDs: PropTypes.array, - server: PropTypes.object, - clearViewportSpecificData: PropTypes.func.isRequired, -}; - -export default ViewerRetrieveStudyData; diff --git a/platform/viewer/src/connectedComponents/findDisplaySetByUID.js b/platform/viewer/src/connectedComponents/findDisplaySetByUID.js deleted file mode 100644 index d50e437ae..000000000 --- a/platform/viewer/src/connectedComponents/findDisplaySetByUID.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Finds displaySet by UID across all displaySets inside studyMetadata - * @param {Array} studyMetadata - * @param {string} displaySetInstanceUID - */ -export default function findDisplaySetByUID( - studyMetadata, - displaySetInstanceUID -) { - if (!Array.isArray(studyMetadata)) return null; - - const allDisplaySets = studyMetadata.reduce((all, current) => { - let currentDisplaySet = []; - if (current && Array.isArray(current.displaySets)) { - currentDisplaySet = current.displaySets; - } - return all.concat(currentDisplaySet); - }, []); - - const bySetInstanceUID = ds => - ds.displaySetInstanceUID === displaySetInstanceUID; - - const displaySet = allDisplaySets.find(bySetInstanceUID); - return displaySet || null; -} diff --git a/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js b/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js deleted file mode 100644 index a53d1c544..000000000 --- a/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import findDisplaySetByUID from './findDisplaySetByUID'; - -describe('findDisplaySetByUID', () => { - test('returns null when studyMetadata isnt an array', () => { - const result = findDisplaySetByUID(undefined, 'hello'); - expect(result).toBeNull(); - }); - - test('returns null when no match found', () => { - const result = findDisplaySetByUID([], 'no-match'); - expect(result).toBeNull(); - }); - - test('it handles missing displaySet arrays', () => { - const expected = '9388-2291-a8fe'; - const studyMetadata = [ - { displaySets: null }, - { - displaySets: [{ displaySetInstanceUID: expected }], - }, - null, - 7, - ]; - const result = findDisplaySetByUID(studyMetadata, expected); - expect(result.displaySetInstanceUID).toBe(expected); - }); - - test('returns correct displaySet by UID', () => { - const expected = '1234-5678'; - const studyMetadata = [ - { displaySets: [{ displaySetInstanceUID: '0011-2239' }] }, - { - displaySets: [ - { displaySetInstanceUID: '0392-2211' }, - { displaySetInstanceUID: expected }, - ], - }, - { displaySets: [{ displaySetInstanceUID: '3384-9933' }] }, - ]; - const result = findDisplaySetByUID(studyMetadata, expected); - expect(result.displaySetInstanceUID).toBe(expected); - }); -});