From 55a580659ecb74ca6433461d8f9a05c2a2b69533 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 9 Dec 2019 14:47:23 -0300 Subject: [PATCH 1/2] feat!: Ability to configure cornerstone tools via extension configuration (#1229) * Fix ExtensionManager bug and add test to bandaid * Add tools configuration to extension manager preinit * Fix reducing of configs * Merge internal with external configs * Merge internal with external configs * Remove dialog from init in measurementstable * Testing injected configuration * New way to set config * Add new prop to dialog provider to allow disabling last position * Remove code from preinit in cornerstone * Add new prop to dialog provider to allow disabling last position * Add centralize to dialogs * Reorder dialogs when adding them * Fix draggable styles (cursor) * Remove repositioning methods from labelling flow and remove overlay from labelling manager * Fix empty array being set in bringToFront * Add new command to update table and pass commands manager to modules/preinit hook * Ad UIContextMenu service / factory * Use new contextmenu service in measurementspanel extension * Use dialogs for arrow annotate in default * Remove positioning funcionality from tool context menu * Add context menu service * Pass commandsModule to extension * Update edit description dialog and simple dialog to position relative * Remove style code from labelling flow and manager * Remove eventdata from labelling * Remove labelling code from measurement init * Add commandsmanager to provider * Update contextmenu provider and service * Add touchstart and mouseclick to hide contextmenu * Hide labelling if click/touch * Remove labelling and context menu dead code * Fix undefined bug if ViewerMain grid has no children * Fix broken prop on context menu * Update commandsmodule based on master * Fix broken configuration * Update script tag config * Remove cornerstone from toolcontextmenu * Remove cornerstone from toolcontextmenu * Split labelling and context menu providers * Split labelling and context menu providers * Update test * Destructure extensions into new array * CR Update: Move default arrow config to cornerstone instead of default * CR Update: Fix app configuration props structure * CR Update: Fix app configuration prop in script tag and extract commands manager from providers * CR Update: Create custom providers to use commandsManager * CR Update: Use services directly in measurementspanel * CR Update: Pass components to providers * CR Update: Remove position from dialog * CR Update: fix dialog prop check * CR Update: Fix comments * CR Update: Update documentation * CR Update: Add test default configuration * CR Update: Add default empy array to extensions * CR Update: Update i18n configuration all ot match current function configuration * CR Update: Add defaults to injected dependencies in configuration and extension configuration * CR Update: Add defaults to configuration with no args * Update documentation * CR Update: Add default for tools * CR Update: Update config object to i18n * CR Update: spread defaults * CR Update: Add tool configuration example to cornerstone extension * CR Update: Add tool configuration to netlify (testing) * CR Update: Remove netlify config for tools * CR Update: Rollback changes to i18n to be fixed later * CR Update: Update documentation and pass whole cornerstone config object instead of tools key SEE: https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with-both-and-breaking-change-footer BREAKING CHANGE: modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. --- docs/latest/configuring/index.md | 46 +++ .../deployment/recipes/embedded-viewer.md | 7 +- extensions/_example/src/index.js | 5 +- extensions/cornerstone/README.md | 18 ++ extensions/cornerstone/package.json | 1 + .../src/OHIFCornerstoneViewport.js | 11 +- extensions/cornerstone/src/commandsModule.js | 114 ++++++- extensions/cornerstone/src/init.js | 134 ++++---- extensions/vtk/src/OHIFVTKViewport.js | 11 +- platform/core/src/classes/CommandsManager.js | 2 +- .../core/src/extensions/ExtensionManager.js | 2 +- .../src/extensions/ExtensionManager.test.js | 18 ++ platform/core/src/index.js | 6 + platform/core/src/index.test.js | 2 + .../services/UIContextMenuService/index.js | 63 ++++ .../src/services/UIDialogService/index.js | 11 +- .../services/UILabellingFlowService/index.js | 68 ++++ platform/core/src/services/index.js | 4 + .../components/simpleDialog/SimpleDialog.styl | 4 +- .../contextProviders/ContextMenuProvider.js | 147 +++++++++ .../ui/src/contextProviders/DialogProvider.js | 60 +++- .../src/contextProviders/DialogProvider.styl | 4 +- .../contextProviders/LabellingFlowProvider.js | 136 ++++++++ platform/ui/src/contextProviders/index.js | 12 + platform/ui/src/index.js | 16 + platform/viewer/public/config/default.js | 2 + platform/viewer/public/config/demo.js | 1 + .../public/config/docker_nginx-orthanc.js | 1 + .../docker_openresty-orthanc-keycloak.js | 1 + .../public/config/docker_openresty-orthanc.js | 1 + platform/viewer/public/config/google.js | 6 +- .../viewer/public/config/local_dcm4chee.js | 1 + platform/viewer/public/config/netlify.js | 1 + .../viewer/public/config/public_dicomweb.js | 1 + .../viewer/public/html-templates/index.html | 148 +++++---- .../viewer/public/html-templates/rollbar.html | 171 ++++++---- platform/viewer/src/App.js | 164 +++++++--- .../ContextMenuProvider.js | 51 +++ .../LabellingFlowProvider.js | 49 +++ .../viewer/src/appCustomProviders/index.js | 6 + .../ConnectedMeasurementTable.js | 46 +-- .../MeasurementsPanel/actions.js | 21 -- .../getMeasurementLocationCallback.js | 33 -- .../appExtensions/MeasurementsPanel/index.js | 37 ++- .../appExtensions/MeasurementsPanel/init.js | 161 +++------ .../labelingFlowCallbacks.js | 144 --------- .../updateTableWithNewMeasurementData.js | 29 -- .../EditDescriptionDialog.css | 2 +- .../EditDescriptionDialog.js | 23 +- .../src/components/Labelling/LabellingFlow.js | 112 +------ .../components/Labelling/LabellingManager.css | 17 +- .../components/Labelling/LabellingManager.js | 35 +- .../Labelling/labellingPositionUtils.js | 53 --- .../ConnectedLabellingOverlay.js | 24 -- .../ConnectedToolContextMenu.js | 24 -- .../ConnectedUserPreferencesForm.js | 5 +- .../connectedComponents/LabellingOverlay.js | 23 -- .../connectedComponents/ToolContextMenu.css | 2 +- .../connectedComponents/ToolContextMenu.js | 306 ++++++------------ .../viewer/src/connectedComponents/Viewer.js | 2 - .../src/connectedComponents/ViewerMain.js | 2 - platform/viewer/src/index-umd.js | 4 +- platform/viewer/src/index.js | 29 +- platform/viewer/src/store/index.js | 4 +- platform/viewer/src/store/layout/reducers.js | 33 -- yarn.lock | 2 +- 66 files changed, 1460 insertions(+), 1219 deletions(-) create mode 100644 platform/core/src/services/UIContextMenuService/index.js create mode 100644 platform/core/src/services/UILabellingFlowService/index.js create mode 100644 platform/ui/src/contextProviders/ContextMenuProvider.js create mode 100644 platform/ui/src/contextProviders/LabellingFlowProvider.js create mode 100644 platform/viewer/src/appCustomProviders/ContextMenuProvider/ContextMenuProvider.js create mode 100644 platform/viewer/src/appCustomProviders/LabellingFlowProvider/LabellingFlowProvider.js create mode 100644 platform/viewer/src/appCustomProviders/index.js delete mode 100644 platform/viewer/src/appExtensions/MeasurementsPanel/actions.js delete mode 100644 platform/viewer/src/appExtensions/MeasurementsPanel/getMeasurementLocationCallback.js delete mode 100644 platform/viewer/src/appExtensions/MeasurementsPanel/labelingFlowCallbacks.js delete mode 100644 platform/viewer/src/appExtensions/MeasurementsPanel/updateTableWithNewMeasurementData.js delete mode 100644 platform/viewer/src/components/Labelling/labellingPositionUtils.js delete mode 100644 platform/viewer/src/connectedComponents/ConnectedLabellingOverlay.js delete mode 100644 platform/viewer/src/connectedComponents/ConnectedToolContextMenu.js delete mode 100644 platform/viewer/src/connectedComponents/LabellingOverlay.js delete mode 100644 platform/viewer/src/store/layout/reducers.js diff --git a/docs/latest/configuring/index.md b/docs/latest/configuring/index.md index 8501ad2e843..73b65e4a8a3 100644 --- a/docs/latest/configuring/index.md +++ b/docs/latest/configuring/index.md @@ -66,6 +66,52 @@ window.config = { }; ``` +The configuration can also be written as a JS Function in case you need to inject dependencies like external services: + +```js +window.config = ({ servicesManager } = {}) => { + const { UIDialogService } = servicesManager.services; + return { + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => UIDialogService.create({... + } + } + }, + }, + routerBasename: '/', + servers: { + dicomWeb: [ + { + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + }, + ], + }, + }; +}; +``` + +You can also create a new config file and specify its path relative to the build +output's root by setting the `APP_CONFIG` environment variable. You can set the +value of this environment variable a few different ways: + +- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- Using the `cross-env` package in an npm script: + - `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"` + After updating the configuration, `yarn run build` to generate updated build output. diff --git a/docs/latest/deployment/recipes/embedded-viewer.md b/docs/latest/deployment/recipes/embedded-viewer.md index 3a299f9ea01..9bc64ed3f43 100644 --- a/docs/latest/deployment/recipes/embedded-viewer.md +++ b/docs/latest/deployment/recipes/embedded-viewer.md @@ -24,12 +24,12 @@ include tags. Here's how it works:
    -
  1. Create a JS Object to hold the OHIF Viewer's configuration. Here are some +
  2. Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some example values that would allow the viewer to hit our public PACS:
```js -// Set before importing `ohif-viewer` +// Set before importing `ohif-viewer` (JS Object) window.config = { // default: '/' routerBasename: '/', @@ -49,6 +49,9 @@ window.config = { }; ``` +To learn more about how you can configure the OHIF Viewer, check out our +[Configuration Guide](./index.md). +
  1. Render the viewer in the web page's target div
diff --git a/extensions/_example/src/index.js b/extensions/_example/src/index.js index c061a44af74..4827f8f949c 100644 --- a/extensions/_example/src/index.js +++ b/extensions/_example/src/index.js @@ -12,8 +12,9 @@ export default { */ preRegistration({ - servicesManager, - configuration: extensionConfiguration, + servicesManager = {}, + commandsManager = {}, + configuration = {}, }) {}, /** diff --git a/extensions/cornerstone/README.md b/extensions/cornerstone/README.md index cc3d8c10c3e..88294c54d4f 100644 --- a/extensions/cornerstone/README.md +++ b/extensions/cornerstone/README.md @@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport] and is connected the redux store. This module is the most prone to change as we hammer out our Viewport interface. +## Tool Configuration + +Tools can be configured through extension configuration using the tools key: + +```js + ... + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')), + }, + }, + }, + }, + ... +``` + ## Resources ### Repositories diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 15b93a5e0eb..cf08da0ad7d 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -47,6 +47,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "classnames": "^2.2.6", + "lodash.merge": "^4.6.2", "lodash.throttle": "^4.1.1", "query-string": "^6.8.3", "react-cornerstone-viewport": "2.x.x" diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index e04b742a0cc..23e684b7665 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component { // TODO: Does it make more sense to use Context? if (this.props.children && this.props.children.length) { childrenWithProps = this.props.children.map((child, index) => { - return React.cloneElement(child, { - viewportIndex: this.props.viewportIndex, - key: index, - }); + return ( + child && + React.cloneElement(child, { + viewportIndex: this.props.viewportIndex, + key: index, + }) + ); }); } diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index 2977c624725..49b856c81f2 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => { showDownloadViewportModal: ({ title, viewports }) => { const activeViewportIndex = viewports.activeViewportIndex; const { UIModalService } = servicesManager.services; - UIModalService.show({ - content: CornerstoneViewportDownloadForm, - title, - contentProps: { - activeViewportIndex, - onClose: UIModalService.hide, - }, + if (UIModalService) { + UIModalService.show({ + content: CornerstoneViewportDownloadForm, + title, + contentProps: { + activeViewportIndex, + onClose: UIModalService.hide, + }, + }); + } + }, + updateTableWithNewMeasurementData({ + toolType, + measurementNumber, + location, + description, + }) { + // Update all measurements by measurement number + const measurementApi = OHIF.measurements.MeasurementApi.Instance; + const measurements = measurementApi.tools[toolType].filter( + m => m.measurementNumber === measurementNumber + ); + + measurements.forEach(measurement => { + measurement.location = location; + measurement.description = description; + + measurementApi.updateMeasurement(measurement.toolType, measurement); + }); + + measurementApi.syncMeasurementsAndToolData(); + + // Update images in all active viewports + cornerstone.getEnabledElements().forEach(enabledElement => { + cornerstone.updateImage(enabledElement.element); }); }, + getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) { + const nearbyTool = {}; + let pointNearTool = false; + + availableToolTypes.forEach(toolType => { + const elementToolData = cornerstoneTools.getToolState( + element, + toolType + ); + + if (!elementToolData) { + return; + } + + elementToolData.data.forEach((toolData, index) => { + let elementToolInstance = cornerstoneTools.getToolForElement( + element, + toolType + ); + + if (!elementToolInstance) { + elementToolInstance = cornerstoneTools.getToolForElement( + element, + `${toolType}Tool` + ); + } + + if (!elementToolInstance) { + console.warn('Tool not found.'); + return undefined; + } + + if ( + elementToolInstance.pointNearTool( + element, + toolData, + canvasCoordinates + ) + ) { + pointNearTool = true; + nearbyTool.tool = toolData; + nearbyTool.index = index; + nearbyTool.toolType = toolType; + } + }); + + if (pointNearTool) { + return false; + } + }); + + return pointNearTool ? nearbyTool : undefined; + }, + removeToolState: ({ element, toolType, tool }) => { + cornerstoneTools.removeToolState(element, toolType, tool); + cornerstone.updateImage(element); + }, }; const definitions = { + getNearbyToolData: { + commandFn: actions.getNearbyToolData, + storeContexts: [], + options: {}, + }, + removeToolState: { + commandFn: actions.removeToolState, + storeContexts: [], + options: {}, + }, + updateTableWithNewMeasurementData: { + commandFn: actions.updateTableWithNewMeasurementData, + storeContexts: [], + options: {}, + }, showDownloadViewportModal: { commandFn: actions.showDownloadViewportModal, storeContexts: ['viewports'], diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index b7ba2218925..06ba0a1c52e 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -4,6 +4,7 @@ import csTools from 'cornerstone-tools'; import initCornerstoneTools from './initCornerstoneTools.js'; import queryString from 'query-string'; import { SimpleDialog } from '@ohif/ui'; +import merge from 'lodash.merge'; function fallbackMetaDataProvider(type, imageId) { if (!imageId.includes('wado?requestType=WADO')) { @@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1); /** * - * @param {object} configuration + * @param {Object} servicesManager + * @param {Object} configuration * @param {Object|Array} configuration.csToolsConfig */ -export default function init({ servicesManager, configuration = {} }) { - const { UIDialogService } = servicesManager.services; +export default function init({ servicesManager, configuration }) { const callInputDialog = (data, event, callback) => { - let dialogId = UIDialogService.create({ - content: SimpleDialog.InputDialog, - defaultPosition: { - x: (event && event.currentPoints.canvas.x) || 0, - y: (event && event.currentPoints.canvas.y) || 0, - }, - showOverlay: true, - contentProps: { - title: 'Enter your annotation', - label: 'New label', - measurementData: data ? { description: data.text } : {}, - onClose: () => UIDialogService.dismiss({ id: dialogId }), - onSubmit: value => { - callback(value); - UIDialogService.dismiss({ id: dialogId }); + const { UIDialogService } = servicesManager.services; + + if (UIDialogService) { + let dialogId = UIDialogService.create({ + centralize: true, + isDraggable: false, + content: SimpleDialog.InputDialog, + useLastPosition: false, + showOverlay: true, + contentProps: { + title: 'Enter your annotation', + label: 'New label', + measurementData: data ? { description: data.text } : {}, + onClose: () => UIDialogService.dismiss({ id: dialogId }), + onSubmit: value => { + callback(value); + UIDialogService.dismiss({ id: dialogId }); + }, }, - }, - }); + }); + } }; const { csToolsConfig } = configuration; @@ -73,61 +77,55 @@ export default function init({ servicesManager, configuration = {} }) { initCornerstoneTools(defaultCsToolsConfig); // ~~ Toooools 🙌 - const { - PanTool, - ZoomTool, - WwwcTool, - MagnifyTool, - StackScrollTool, - StackScrollMouseWheelTool, - // Touch - PanMultiTouchTool, - ZoomTouchPinchTool, - // Annotations - EraserTool, - BidirectionalTool, - LengthTool, - AngleTool, - FreehandRoiTool, - EllipticalRoiTool, - DragProbeTool, - RectangleRoiTool, - // Segmentation - BrushTool, - } = csTools; const tools = [ - PanTool, - ZoomTool, - WwwcTool, - MagnifyTool, - StackScrollTool, - StackScrollMouseWheelTool, + csTools.PanTool, + csTools.ZoomTool, + csTools.WwwcTool, + csTools.MagnifyTool, + csTools.StackScrollTool, + csTools.StackScrollMouseWheelTool, // Touch - PanMultiTouchTool, - ZoomTouchPinchTool, + csTools.PanMultiTouchTool, + csTools.ZoomTouchPinchTool, // Annotations - EraserTool, - BidirectionalTool, - LengthTool, - AngleTool, - FreehandRoiTool, - EllipticalRoiTool, - DragProbeTool, - RectangleRoiTool, + csTools.ArrowAnnotateTool, + csTools.EraserTool, + csTools.BidirectionalTool, + csTools.LengthTool, + csTools.AngleTool, + csTools.FreehandRoiTool, + csTools.EllipticalRoiTool, + csTools.DragProbeTool, + csTools.RectangleRoiTool, // Segmentation - BrushTool, + csTools.BrushTool, ]; - tools.forEach(tool => csTools.addTool(tool)); - - csTools.addTool(csTools.ArrowAnnotateTool, { - configuration: { - getTextCallback: (callback, eventDetails) => - callInputDialog(null, eventDetails, callback), - changeTextCallback: (data, eventDetails, callback) => - callInputDialog(data, eventDetails, callback), + /* Add extension tools configuration here. */ + const extensionToolsConfiguration = { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => + callInputDialog(null, eventDetails, callback), + changeTextCallback: (data, eventDetails, callback) => + callInputDialog(data, eventDetails, callback), + }, }, - }); + }; + + const isEmpty = obj => Object.keys(obj).length < 1; + if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) { + /* Add tools with its custom props through extension configuration. */ + tools.forEach(tool => { + const toolName = tool.name.replace('Tool', ''); + const configurationToolProps = configuration.tools[toolName] || {}; + const extensionToolProps = extensionToolsConfiguration[toolName]; + let props = merge(extensionToolProps, configurationToolProps); + csTools.addTool(tool, props); + }); + } else { + tools.forEach(tool => csTools.addTool(tool)); + } csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); diff --git a/extensions/vtk/src/OHIFVTKViewport.js b/extensions/vtk/src/OHIFVTKViewport.js index 8926bdcf90a..32732e5cdfc 100644 --- a/extensions/vtk/src/OHIFVTKViewport.js +++ b/extensions/vtk/src/OHIFVTKViewport.js @@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component { // TODO: Does it make more sense to use Context? if (this.props.children && this.props.children.length) { childrenWithProps = this.props.children.map((child, index) => { - return React.cloneElement(child, { - viewportIndex: this.props.viewportIndex, - key: index, - }); + return ( + child && + React.cloneElement(child, { + viewportIndex: this.props.viewportIndex, + key: index, + }) + ); }); } diff --git a/platform/core/src/classes/CommandsManager.js b/platform/core/src/classes/CommandsManager.js index e6efdaa67f9..52e70da112e 100644 --- a/platform/core/src/classes/CommandsManager.js +++ b/platform/core/src/classes/CommandsManager.js @@ -59,7 +59,7 @@ export class CommandsManager { * * @method * @param {string} contextName - Namespace for commands - * @returs {Object} - the matched context + * @returns {Object} - the matched context */ getContext(contextName) { const context = this.contexts[contextName]; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 01fb88a6f69..7b9cba74ae6 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -26,7 +26,7 @@ export default class ExtensionManager { const hasConfiguration = Array.isArray(extension); if (hasConfiguration) { - const [ohifExtension, configuration] = extensions; + const [ohifExtension, configuration] = extension; this.registerExtension(ohifExtension, configuration); } else { this.registerExtension(extension); diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js index 503e4ccdbb5..1a506b0c0bf 100644 --- a/platform/core/src/extensions/ExtensionManager.test.js +++ b/platform/core/src/extensions/ExtensionManager.test.js @@ -37,6 +37,24 @@ describe('ExtensionManager.js', () => { // Assert expect(extensionManager.registerExtension.mock.calls.length).toBe(3); }); + + it('calls registerExtension() for each extension passing its configuration if tuple', () => { + const fakeConfiguration = { testing: true }; + extensionManager.registerExtension = jest.fn(); + + // SUT + const fakeExtensions = [ + { one: '1' }, + [{ two: '2' }, fakeConfiguration], + { three: '3 ' }, + ]; + extensionManager.registerExtensions(fakeExtensions); + + // Assert + expect(extensionManager.registerExtension.mock.calls[1]).toContain( + fakeConfiguration + ); + }); }); describe('registerExtension()', () => { diff --git a/platform/core/src/index.js b/platform/core/src/index.js index 678758eda34..ca45bdcea47 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -23,6 +23,8 @@ import { createUINotificationService, createUIModalService, createUIDialogService, + createUIContextMenuService, + createUILabellingFlowService, } from './services'; const OHIF = { @@ -53,6 +55,8 @@ const OHIF = { createUINotificationService, createUIModalService, createUIDialogService, + createUIContextMenuService, + createUILabellingFlowService, }; export { @@ -82,6 +86,8 @@ export { createUINotificationService, createUIModalService, createUIDialogService, + createUIContextMenuService, + createUILabellingFlowService, }; export { OHIF }; diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index c67e27a5e28..2623b10782d 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -13,6 +13,8 @@ describe('Top level exports', () => { 'createUINotificationService', 'createUIModalService', 'createUIDialogService', + 'createUIContextMenuService', + 'createUILabellingFlowService', // 'utils', 'studies', diff --git a/platform/core/src/services/UIContextMenuService/index.js b/platform/core/src/services/UIContextMenuService/index.js new file mode 100644 index 00000000000..999e150523f --- /dev/null +++ b/platform/core/src/services/UIContextMenuService/index.js @@ -0,0 +1,63 @@ +/** + * UI Context Menu + * + * @typedef {Object} ContextMenuProps + * @property {Event} event The event with tool information. + */ + +const uiContextMenuServicePublicAPI = { + name: 'UIContextMenuService', + hide, + show, + setServiceImplementation, +}; + +const uiContextMenuServiceImplementation = { + _show: () => console.warn('show() NOT IMPLEMENTED'), + _hide: () => console.warn('hide() NOT IMPLEMENTED'), +}; + +function createUIContextMenuService() { + return uiContextMenuServicePublicAPI; +} + +/** + * Show a new UI ContextMenu dialog; + * + * @param {ContextMenuProps} props { event } + */ +function show({ event }) { + return uiContextMenuServiceImplementation._show({ + event, + }); +} + +/** + * Hide a UI ContextMenu dialog; + * + */ +function hide() { + return uiContextMenuServiceImplementation._hide(); +} + +/** + * + * + * @param {*} { + * show: showImplementation, + * hide: hideImplementation, + * } + */ +function setServiceImplementation({ + show: showImplementation, + hide: hideImplementation, +}) { + if (showImplementation) { + uiContextMenuServiceImplementation._show = showImplementation; + } + if (hideImplementation) { + uiContextMenuServiceImplementation._hide = hideImplementation; + } +} + +export default createUIContextMenuService; diff --git a/platform/core/src/services/UIDialogService/index.js b/platform/core/src/services/UIDialogService/index.js index 44e8c02971b..561e214a521 100644 --- a/platform/core/src/services/UIDialogService/index.js +++ b/platform/core/src/services/UIDialogService/index.js @@ -17,8 +17,9 @@ * @property {Object} contentProps The dialog content props. * @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not. * @property {boolean} [showOverlay=false] Controls dialog overlay. + * @property {boolean} [centralize=false] Center the dialog on the screen. + * @property {boolean} [preservePosition=true] Use last position instead of default. * @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at. - * @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input. * @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel. * @property {Function} onStop Called when dragging stops. * @property {Function} onDrag Called while dragging. @@ -45,7 +46,7 @@ function createUIDialogService() { /** * Show a new UI dialog; * - * @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position } + * @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition } */ function create({ id, @@ -54,10 +55,11 @@ function create({ onStart, onDrag, onStop, + centralize = false, + preservePosition = true, isDraggable = true, showOverlay = false, defaultPosition, - position, }) { return uiDialogServiceImplementation._create({ id, @@ -66,10 +68,11 @@ function create({ onStart, onDrag, onStop, + centralize, + preservePosition, isDraggable, showOverlay, defaultPosition, - position, }); } diff --git a/platform/core/src/services/UILabellingFlowService/index.js b/platform/core/src/services/UILabellingFlowService/index.js new file mode 100644 index 00000000000..3fbc5ef0b4b --- /dev/null +++ b/platform/core/src/services/UILabellingFlowService/index.js @@ -0,0 +1,68 @@ +/** + * UI Labelling Flow + * + * @typedef {Object} LabellingFlowProps + * @property {Object} defaultPosition The position of the labelling dialog. + * @property {boolean} centralize conditional to center the labelling dialog. + * @property {Object} props The labelling props. + * + */ + +const uiLabellingFlowServicePublicAPI = { + name: 'UILabellingFlowService', + show, + hide, + setServiceImplementation, +}; + +const uiLabellingFlowServiceImplementation = { + _show: () => console.warn('show() NOT IMPLEMENTED'), + _hide: () => console.warn('hide() NOT IMPLEMENTED'), +}; + +function createUILabellingFlowService() { + return uiLabellingFlowServicePublicAPI; +} + +/** + * Hide a UI LabellingFlow dialog; + * + */ +function hide() { + return uiLabellingFlowServiceImplementation._hide(); +} + +/** + * Show a new UI LabellingFlow dialog; + * + * @param {LabellingFlowProps} props { defaultPosition, centralize, props } + */ +function show({ defaultPosition, centralize, props }) { + return uiLabellingFlowServiceImplementation._show({ + defaultPosition, + centralize, + props, + }); +} + +/** + * + * + * @param {*} { + * show: showImplementation, + * hide: hideImplementation, + * } + */ +function setServiceImplementation({ + show: showImplementation, + hide: hideImplementation, +}) { + if (showImplementation) { + uiLabellingFlowServiceImplementation._show = showImplementation; + } + if (hideImplementation) { + uiLabellingFlowServiceImplementation._hide = hideImplementation; + } +} + +export default createUILabellingFlowService; diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 0d2f5a525e7..0feacc3a5d4 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -2,10 +2,14 @@ import ServicesManager from './ServicesManager.js'; import createUINotificationService from './UINotificationService'; import createUIModalService from './UIModalService'; import createUIDialogService from './UIDialogService'; +import createUIContextMenuService from './UIContextMenuService'; +import createUILabellingFlowService from './UILabellingFlowService'; export { createUINotificationService, createUIModalService, createUIDialogService, + createUIContextMenuService, + createUILabellingFlowService, ServicesManager, }; diff --git a/platform/ui/src/components/simpleDialog/SimpleDialog.styl b/platform/ui/src/components/simpleDialog/SimpleDialog.styl index 3e52f3d26d7..5df1de18a9e 100644 --- a/platform/ui/src/components/simpleDialog/SimpleDialog.styl +++ b/platform/ui/src/components/simpleDialog/SimpleDialog.styl @@ -6,9 +6,7 @@ position: relative .simpleDialog - position: fixed; - top: 0px; - left: 0px; + position: relative; z-index: 1000; border: 0; border-radius: 6px; diff --git a/platform/ui/src/contextProviders/ContextMenuProvider.js b/platform/ui/src/contextProviders/ContextMenuProvider.js new file mode 100644 index 00000000000..1d1ee22c30c --- /dev/null +++ b/platform/ui/src/contextProviders/ContextMenuProvider.js @@ -0,0 +1,147 @@ +import React, { + createContext, + useContext, + useEffect, + useCallback, +} from 'react'; +import PropTypes from 'prop-types'; + +import { useDialog } from '@ohif/ui'; +import { useLabellingFlow } from '@ohif/ui'; + +const ContextMenuContext = createContext(null); +const { Provider } = ContextMenuContext; + +export const useContextMenu = () => useContext(ContextMenuContext); + +const ContextMenuProvider = ({ + children, + service, + contextMenuComponent: ContextMenuComponent, + onDelete, +}) => { + const { create, dismiss } = useDialog(); + const { show: showLabellingFlow } = useLabellingFlow(); + + /** + * Sets the implementation of a context menu service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + show, + hide, + }); + } + }, [hide, service, show]); + + const hide = useCallback(() => dismiss({ id: 'context-menu' }), [dismiss]); + + /** + * Show the context menu and override its configuration props. + * + * @param {ContextMenuProps} props { eventData, isTouchEvent, onClose, visible } + * @returns void + */ + const show = useCallback( + ({ event }) => { + hide(); + create({ + id: 'context-menu', + isDraggable: false, + preservePosition: false, + content: ContextMenuComponent, + contentProps: { + eventData: event, + onDelete: (nearbyToolData, eventData) => + onDelete(nearbyToolData, eventData), + onClose: () => dismiss({ id: 'context-menu' }), + onSetLabel: (eventData, measurementData) => + showLabellingFlow({ + event: eventData, + centralize: true, + props: { + measurementData, + skipAddLabelButton: true, + editLocation: true, + }, + }), + onSetDescription: (eventData, measurementData) => + showLabellingFlow({ + event: eventData, + centralize: false, + defaultPosition: _getDefaultPosition(eventData), + props: { + measurementData, + editDescriptionOnDialog: true, + }, + }), + }, + defaultPosition: _getDefaultPosition(event), + }); + }, + [ContextMenuComponent, create, dismiss, hide, onDelete, showLabellingFlow] + ); + + const _getDefaultPosition = event => ({ + x: (event && event.currentPoints.client.x) || 0, + y: (event && event.currentPoints.client.y) || 0, + }); + + return ( + + {children} + + ); +}; + +/** + * Higher Order Component to use the context menu methods through a Class Component. + * + * @returns + */ +export const withContextMenu = Component => { + return function WrappedComponent(props) { + const { show, hide } = useContextMenu(); + return ( + + ); + }; +}; + +ContextMenuProvider.defaultProps = { + service: null, +}; + +ContextMenuProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), + contextMenuComponent: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]).isRequired, + onDelete: PropTypes.func.isRequired, +}; + +export default ContextMenuProvider; + +export const ContextMenuConsumer = ContextMenuContext.Consumer; diff --git a/platform/ui/src/contextProviders/DialogProvider.js b/platform/ui/src/contextProviders/DialogProvider.js index f8388d1487e..b1d1b681c49 100644 --- a/platform/ui/src/contextProviders/DialogProvider.js +++ b/platform/ui/src/contextProviders/DialogProvider.js @@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext); const DialogProvider = ({ children, service }) => { const [isDragging, setIsDragging] = useState(false); const [dialogs, setDialogs] = useState([]); + const [lastDialogId, setLastDialogId] = useState(null); const [lastDialogPosition, setLastDialogPosition] = useState(null); + const [centerPositions, setCenterPositions] = useState([]); + + useEffect(() => { + setCenterPositions( + dialogs.map(dialog => ({ + id: dialog.id, + ...getCenterPosition(dialog.id), + })) + ); + }, [dialogs]); + + const getCenterPosition = id => { + const root = document.querySelector('#root'); + const centerX = root.offsetLeft + root.offsetWidth / 2; + const centerY = root.offsetTop + root.offsetHeight / 2; + const item = document.querySelector(`#draggableItem-${id}`); + const itemBounds = item.getBoundingClientRect(); + return { + x: centerX - itemBounds.width / 2, + y: centerY - itemBounds.height / 2, + }; + }; /** * Sets the implementation of a dialog service that can be used by extensions. @@ -42,13 +65,16 @@ const DialogProvider = ({ children, service }) => { * @property {Object} contentProps The dialog content props. * @property {boolean} isDraggable Controls if dialog content is draggable or not. * @property {boolean} showOverlay Controls dialog overlay. + * @property {boolean} centralize Center the dialog on the screen. + * @property {boolean} preservePosition Use last position instead of default. * @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at. - * @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input. * @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel. * @property {Function} onStop Called when dragging stops. * @property {Function} onDrag Called while dragging. */ + useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]); + /** * Creates a new dialog and return its id. * @@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => { } setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]); + setLastDialogId(dialogId); return dialogId; }, []); @@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => { * @property {string} props.id The dialog id. * @returns void */ - const dismiss = useCallback(({ id }) => { - setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)); - }, []); + const dismiss = useCallback( + ({ id }) => + setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)), + [] + ); /** * Dismisses all dialogs. @@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => { * @param {string} id The dialog id. * @returns void */ - const _bringToFront = id => { + const _bringToFront = useCallback(id => { setDialogs(dialogs => { const topDialog = dialogs.find(dialog => dialog.id === id); return topDialog ? [...dialogs.filter(dialog => dialog.id !== id), topDialog] - : []; + : dialogs; }); - }; + }, []); const renderDialogs = () => dialogs.map(dialog => { @@ -116,20 +145,27 @@ const DialogProvider = ({ children, service }) => { id, content: DialogContent, contentProps, - position, defaultPosition, + centralize = false, + preservePosition = true, isDraggable = true, onStart, onStop, onDrag, } = dialog; + let position = + (preservePosition && lastDialogPosition) || defaultPosition; + if (centralize) { + position = centerPositions.find(position => position.id === id); + } + return ( { const e = event || window.event; @@ -169,7 +205,11 @@ const DialogProvider = ({ children, service }) => { >
_bringToFront(id)} > diff --git a/platform/ui/src/contextProviders/DialogProvider.styl b/platform/ui/src/contextProviders/DialogProvider.styl index e3a45c4814e..13db9a19ba6 100644 --- a/platform/ui/src/contextProviders/DialogProvider.styl +++ b/platform/ui/src/contextProviders/DialogProvider.styl @@ -1,8 +1,8 @@ -.DraggableItem +.DraggableItem.draggable div cursor: grab !important -.DraggableItem.dragging +.DraggableItem.draggable.dragging div cursor: grabbing !important diff --git a/platform/ui/src/contextProviders/LabellingFlowProvider.js b/platform/ui/src/contextProviders/LabellingFlowProvider.js new file mode 100644 index 00000000000..a5c266d8f40 --- /dev/null +++ b/platform/ui/src/contextProviders/LabellingFlowProvider.js @@ -0,0 +1,136 @@ +import React, { + createContext, + useContext, + useEffect, + useCallback, +} from 'react'; +import PropTypes from 'prop-types'; + +import { useDialog } from './DialogProvider'; + +const LabellingFlowContext = createContext(null); +const { Provider } = LabellingFlowContext; + +export const useLabellingFlow = () => useContext(LabellingFlowContext); + +const LabellingFlowProvider = ({ + children, + service, + labellingComponent: LabellingComponent, + onUpdateLabelling, +}) => { + const { create, dismiss } = useDialog(); + + /** + * Sets the implementation of a labelling flow service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + show, + hide, + }); + } + }, [hide, service, show]); + + const hide = useCallback(() => dismiss({ id: 'labelling' }), [dismiss]); + + const show = useCallback( + ({ centralize, defaultPosition, props }) => { + hide(); + create({ + id: 'labelling', + centralize, + isDraggable: false, + showOverlay: true, + content: LabellingComponent, + defaultPosition, + contentProps: { + visible: true, + measurementData: props.measurementData, + labellingDoneCallback: () => dismiss({ id: 'labelling' }), + updateLabelling: labellingData => + _updateLabellingHandler(labellingData, props.measurementData), + ...props, + }, + }); + }, + [LabellingComponent, _updateLabellingHandler, create, dismiss, hide] + ); + + const _updateLabellingHandler = useCallback( + (labellingData, measurementData) => { + const { location, description, response } = labellingData; + + if (location) { + measurementData.location = location; + } + + measurementData.description = description || ''; + + if (response) { + measurementData.response = response; + } + + onUpdateLabelling(labellingData, measurementData); + }, + [onUpdateLabelling] + ); + + return ( + + {children} + + ); +}; + +/** + * Higher Order Component to use the labelling flow methods through a Class Component. + * + * @returns + */ +export const withLabellingFlow = Component => { + return function WrappedComponent(props) { + const { show, hide } = useLabellingFlow(); + return ( + + ); + }; +}; + +LabellingFlowProvider.defaultProps = { + service: null, +}; + +LabellingFlowProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), + labellingComponent: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]).isRequired, + onUpdateLabelling: PropTypes.func.isRequired, +}; + +export default LabellingFlowProvider; + +export const LabellingFlowConsumer = LabellingFlowContext.Consumer; diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index e4ca0614345..821b6ce9e6c 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -18,3 +18,15 @@ export { withDialog, useDialog, } from './DialogProvider.js'; +export { + default as ContextMenuProvider, + withContextMenu, + useContextMenu, + ContextMenuConsumer, +} from './ContextMenuProvider.js'; +export { + default as LabellingFlowProvider, + withLabellingFlow, + useLabellingFlow, + LabellingFlowConsumer, +} from './LabellingFlowProvider.js'; diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 6af64c47d36..1672d4b136e 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -60,6 +60,14 @@ import { ModalConsumer, useModal, withModal, + ContextMenuProvider, + ContextMenuConsumer, + useContextMenu, + withContextMenu, + LabellingFlowProvider, + LabellingFlowConsumer, + useLabellingFlow, + withLabellingFlow, } from './contextProviders'; export { @@ -117,6 +125,14 @@ export { DialogProvider, withDialog, useDialog, + ContextMenuProvider, + ContextMenuConsumer, + useContextMenu, + withContextMenu, + LabellingFlowProvider, + LabellingFlowConsumer, + useLabellingFlow, + withLabellingFlow, // Hooks useDebounce, useMedia, diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index 37d8077384c..a8d44e7ef37 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -1,6 +1,7 @@ window.config = { // default: '/' routerBasename: '/', + whiteLabelling: {}, extensions: [], showStudyList: true, filterQueryParam: false, @@ -69,4 +70,5 @@ window.config = { // ~ Cornerstone Tools { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, ], + cornerstoneExtensionConfig: { tools: {} }, }; diff --git a/platform/viewer/public/config/demo.js b/platform/viewer/public/config/demo.js index 7cdf103ca2a..72272ed2435 100644 --- a/platform/viewer/public/config/demo.js +++ b/platform/viewer/public/config/demo.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, extensions: [], showStudyList: true, servers: { diff --git a/platform/viewer/public/config/docker_nginx-orthanc.js b/platform/viewer/public/config/docker_nginx-orthanc.js index 81297247560..5caddff2ba4 100644 --- a/platform/viewer/public/config/docker_nginx-orthanc.js +++ b/platform/viewer/public/config/docker_nginx-orthanc.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, showStudyList: true, servers: { dicomWeb: [ diff --git a/platform/viewer/public/config/docker_openresty-orthanc-keycloak.js b/platform/viewer/public/config/docker_openresty-orthanc-keycloak.js index a872f4a44c8..c6fd6596e89 100644 --- a/platform/viewer/public/config/docker_openresty-orthanc-keycloak.js +++ b/platform/viewer/public/config/docker_openresty-orthanc-keycloak.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, showStudyList: true, servers: { // This is an array, but we'll only use the first entry for now diff --git a/platform/viewer/public/config/docker_openresty-orthanc.js b/platform/viewer/public/config/docker_openresty-orthanc.js index d23915fc78f..32f6aec217b 100644 --- a/platform/viewer/public/config/docker_openresty-orthanc.js +++ b/platform/viewer/public/config/docker_openresty-orthanc.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, showStudyList: true, servers: { // This is an array, but we'll only use the first entry for now diff --git a/platform/viewer/public/config/google.js b/platform/viewer/public/config/google.js index 64e991d5e4a..b5d0c5904e8 100644 --- a/platform/viewer/public/config/google.js +++ b/platform/viewer/public/config/google.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, enableGoogleCloudAdapter: true, servers: { // This is an array, but we'll only use the first entry for now @@ -14,7 +15,8 @@ window.config = { client_id: 'YOURCLIENTID.apps.googleusercontent.com', redirect_uri: '/callback', // `OHIFStandaloneViewer.js` response_type: 'id_token token', - scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + scope: + 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid // ~ OPTIONAL post_logout_redirect_uri: '/logout-redirect.html', revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', @@ -23,4 +25,4 @@ window.config = { }, ], studyListFunctionsEnabled: true, -} +}; diff --git a/platform/viewer/public/config/local_dcm4chee.js b/platform/viewer/public/config/local_dcm4chee.js index 1dc87f52854..afc9429ce61 100644 --- a/platform/viewer/public/config/local_dcm4chee.js +++ b/platform/viewer/public/config/local_dcm4chee.js @@ -1,6 +1,7 @@ window.config = { // default: '/' routerBasename: '/', + whiteLabelling: {}, // default: '' showStudyList: true, servers: { diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js index 0365b414fd6..41e969760e3 100644 --- a/platform/viewer/public/config/netlify.js +++ b/platform/viewer/public/config/netlify.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/pwa', + whiteLabelling: {}, showStudyList: true, servers: { dicomWeb: [ diff --git a/platform/viewer/public/config/public_dicomweb.js b/platform/viewer/public/config/public_dicomweb.js index 0569b504598..c6c30463aad 100644 --- a/platform/viewer/public/config/public_dicomweb.js +++ b/platform/viewer/public/config/public_dicomweb.js @@ -1,5 +1,6 @@ window.config = { routerBasename: '/', + whiteLabelling: {}, showStudyList: true, servers: { dicomWeb: [ diff --git a/platform/viewer/public/html-templates/index.html b/platform/viewer/public/html-templates/index.html index de7f23d4fb0..a01ca7aa3c4 100644 --- a/platform/viewer/public/html-templates/index.html +++ b/platform/viewer/public/html-templates/index.html @@ -1,74 +1,102 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - OHIF Viewer + + + + + + - - + OHIF Viewer - - + + + + - + + + + - - +
+ -
- diff --git a/platform/viewer/public/html-templates/rollbar.html b/platform/viewer/public/html-templates/rollbar.html index 7101cf87548..ae8e9c695e2 100644 --- a/platform/viewer/public/html-templates/rollbar.html +++ b/platform/viewer/public/html-templates/rollbar.html @@ -1,86 +1,117 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - OHIF Viewer + + + + + + - - + OHIF Viewer - - + + + + - - - + + + + -
+
- - + + diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js index 3ae9dd4c844..b1cf9273900 100644 --- a/platform/viewer/src/App.js +++ b/platform/viewer/src/App.js @@ -4,9 +4,13 @@ import { I18nextProvider } from 'react-i18next'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; -import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; import { hot } from 'react-hot-loader/root'; +import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; + +import ToolContextMenu from './connectedComponents/ToolContextMenu'; +import LabellingManager from './components/Labelling/LabellingManager'; + import { SnackbarProvider, ModalProvider, @@ -14,6 +18,11 @@ import { OHIFModal, } from '@ohif/ui'; +import { + LabellingFlowProvider, + ContextMenuProvider, +} from './appCustomProviders'; + import { CommandsManager, ExtensionManager, @@ -22,8 +31,10 @@ import { createUINotificationService, createUIModalService, createUIDialogService, + createUIContextMenuService, + createUILabellingFlowService, utils, - redux as reduxOHIF + redux as reduxOHIF, } from '@ohif/core'; import i18n from '@ohif/i18n'; @@ -46,12 +57,12 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer'; /** Store */ import { getActiveContexts } from './store/layout/selectors.js'; import store from './store'; -const { setUserPreferences } = reduxOHIF.actions; /** Contexts */ import WhiteLabellingContext from './context/WhiteLabellingContext'; import UserManagerContext from './context/UserManagerContext'; import AppContext from './context/AppContext'; +const { setUserPreferences } = reduxOHIF.actions; /** ~~~~~~~~~~~~~ Application Setup */ const commandsManagerConfig = { @@ -63,6 +74,8 @@ const commandsManagerConfig = { const UINotificationService = createUINotificationService(); const UIModalService = createUIModalService(); const UIDialogService = createUIDialogService(); +const UIContextMenuService = createUIContextMenuService(); +const UILabellingFlowService = createUILabellingFlowService(); /** Managers */ const commandsManager = new CommandsManager(commandsManagerConfig); @@ -79,23 +92,25 @@ window.store = store; class App extends Component { static propTypes = { - routerBasename: PropTypes.string.isRequired, - servers: PropTypes.object.isRequired, - // - oidc: PropTypes.array, - whiteLabelling: PropTypes.object, - extensions: PropTypes.arrayOf( + config: PropTypes.oneOfType([ + PropTypes.func, PropTypes.shape({ - id: PropTypes.string.isRequired, - }) - ), - hotkeys: PropTypes.array, + routerBasename: PropTypes.string.isRequired, + oidc: PropTypes.array, + whiteLabelling: PropTypes.object, + extensions: PropTypes.array, + }), + ]).isRequired, + defaultExtensions: PropTypes.array, }; static defaultProps = { - whiteLabelling: {}, - oidc: [], - extensions: [], + config: { + whiteLabelling: {}, + oidc: [], + extensions: [], + }, + defaultExtensions: [], }; _appConfig; @@ -104,31 +119,59 @@ class App extends Component { constructor(props) { super(props); - this._appConfig = props; + const { config, defaultExtensions } = props; - const { servers, extensions, hotkeys, oidc } = props; + const appDefaultConfig = { + cornerstoneExtensionConfig: {}, + extensions: [], + routerBasename: '/', + whiteLabelling: {}, + }; + + this._appConfig = { + ...appDefaultConfig, + ...(typeof config === 'function' ? config({ servicesManager }) : config), + }; + + const { + servers, + hotkeys, + cornerstoneExtensionConfig, + extensions, + oidc, + } = this._appConfig; this.initUserManager(oidc); - _initServices([UINotificationService, UIModalService, UIDialogService]); - _initExtensions(extensions, hotkeys); + _initServices([ + UINotificationService, + UIModalService, + UIDialogService, + UIContextMenuService, + UILabellingFlowService, + ]); + _initExtensions( + [...defaultExtensions, ...extensions], + cornerstoneExtensionConfig + ); + + /* + * Must run after extension commands are registered + * if there is no hotkeys from localStorage set up from config. + */ + _initHotkeys(hotkeys); _initServers(servers); initWebWorkers(); } render() { - const { whiteLabelling, routerBasename } = this.props; - const userManager = this._userManager; - const config = { - appConfig: this._appConfig, - }; - - if (userManager) { + const { whiteLabelling, routerBasename } = this._appConfig; + if (this._userManager) { return ( - + - - + + @@ -137,7 +180,21 @@ class App extends Component { modal={OHIFModal} service={UIModalService} > - + + + + + @@ -152,7 +209,7 @@ class App extends Component { } return ( - + @@ -160,7 +217,19 @@ class App extends Component { - + + + + + @@ -174,10 +243,10 @@ class App extends Component { initUserManager(oidc) { if (oidc && !!oidc.length) { - const firstOpenIdClient = this.props.oidc[0]; + const firstOpenIdClient = this._appConfig.oidc[0]; const { protocol, host } = window.location; - const { routerBasename } = this.props; + const { routerBasename } = this._appConfig; const baseUri = `${protocol}//${host}${routerBasename}`; const redirect_uri = firstOpenIdClient.redirect_uri || '/callback'; @@ -213,22 +282,22 @@ function _initServices(services) { /** * @param */ -function _initExtensions(extensions, hotkeys) { - const defaultExtensions = [ +function _initExtensions(extensions, cornerstoneExtensionConfig) { + const requiredExtensions = [ GenericViewerCommands, - OHIFCornerstoneExtension, - // WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION + [OHIFCornerstoneExtension, cornerstoneExtensionConfig], + /* WARNING: MUST BE REGISTERED _AFTER_ OHIFCornerstoneExtension */ MeasurementsPanel, ]; - const mergedExtensions = defaultExtensions.concat(extensions); + const mergedExtensions = requiredExtensions.concat(extensions); extensionManager.registerExtensions(mergedExtensions); +} +function _initHotkeys(hotkeys) { const { hotkeyDefinitions = {} } = store.getState().preferences || {}; let updateStore = false; let hotkeysToUse = hotkeyDefinitions; - // Must run after extension commands are registered - // if there is no hotkeys from localStorate set up from config if (!Object.keys(hotkeyDefinitions).length) { hotkeysToUse = hotkeys; updateStore = true; @@ -236,7 +305,8 @@ function _initExtensions(extensions, hotkeys) { if (hotkeysToUse) { hotkeysManager.setHotkeys(hotkeysToUse); - // set default based on app config + + /* Set hotkeys default based on app config. */ hotkeysManager.setDefaultHotKeys(hotkeys); if (updateStore) { @@ -264,7 +334,9 @@ function _makeAbsoluteIfNecessary(url, base_url) { return url; } - // Make sure base_url and url are not duplicating slashes + /* + * Make sure base_url and url are not duplicating slashes. + */ if (base_url[base_url.length - 1] === '/') { base_url = base_url.slice(0, base_url.length - 1); } @@ -272,7 +344,9 @@ function _makeAbsoluteIfNecessary(url, base_url) { return base_url + url; } -// Only wrap/use hot if in dev +/* + * Only wrap/use hot if in dev. + */ const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App; export default ExportedApp; diff --git a/platform/viewer/src/appCustomProviders/ContextMenuProvider/ContextMenuProvider.js b/platform/viewer/src/appCustomProviders/ContextMenuProvider/ContextMenuProvider.js new file mode 100644 index 00000000000..4cd75d9f8ee --- /dev/null +++ b/platform/viewer/src/appCustomProviders/ContextMenuProvider/ContextMenuProvider.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ContextMenuProvider } from '@ohif/ui'; + +const CustomContextMenuProvider = ({ + children, + service, + contextMenuComponent, + commandsManager, +}) => { + const onDeleteHandler = (nearbyToolData, eventData) => { + const element = eventData.element; + commandsManager.runCommand('removeToolState', { + element, + toolType: nearbyToolData.toolType, + tool: nearbyToolData.tool, + }); + }; + + return ( + + {children} + + ); +}; + +CustomContextMenuProvider.defaultProps = { + service: null, +}; + +CustomContextMenuProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), + contextMenuComponent: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]).isRequired, + commandsManager: PropTypes.object.isRequired, +}; + +export default CustomContextMenuProvider; diff --git a/platform/viewer/src/appCustomProviders/LabellingFlowProvider/LabellingFlowProvider.js b/platform/viewer/src/appCustomProviders/LabellingFlowProvider/LabellingFlowProvider.js new file mode 100644 index 00000000000..097a0350f96 --- /dev/null +++ b/platform/viewer/src/appCustomProviders/LabellingFlowProvider/LabellingFlowProvider.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { LabellingFlowProvider } from '@ohif/ui'; + +const CustomLabellingFlowProvider = ({ + children, + service, + labellingComponent, + commandsManager, +}) => { + const onUpdateLabellingHandler = (labellingData, measurementData) => { + commandsManager.runCommand( + 'updateTableWithNewMeasurementData', + measurementData + ); + }; + + return ( + + {children} + + ); +}; + +CustomLabellingFlowProvider.defaultProps = { + service: null, +}; + +CustomLabellingFlowProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), + labellingComponent: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]).isRequired, + commandsManager: PropTypes.object.isRequired, +}; + +export default CustomLabellingFlowProvider; diff --git a/platform/viewer/src/appCustomProviders/index.js b/platform/viewer/src/appCustomProviders/index.js new file mode 100644 index 00000000000..f66c8e462e5 --- /dev/null +++ b/platform/viewer/src/appCustomProviders/index.js @@ -0,0 +1,6 @@ +export { + default as LabellingFlowProvider, +} from './LabellingFlowProvider/LabellingFlowProvider.js'; +export { + default as ContextMenuProvider, +} from './ContextMenuProvider/ContextMenuProvider.js'; diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js b/platform/viewer/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js index c4092c45585..265a2e58579 100644 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js +++ b/platform/viewer/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js @@ -4,9 +4,7 @@ import OHIF from '@ohif/core'; import moment from 'moment'; import cornerstone from 'cornerstone-core'; -// import jumpToRowItem from './jumpToRowItem.js'; -import getMeasurementLocationCallback from './getMeasurementLocationCallback'; const { setViewportSpecificData } = OHIF.redux.actions; const { MeasurementApi } = OHIF.measurements; @@ -155,9 +153,11 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch, ownProps) => { return { dispatchRelabel: (event, measurementData, viewportsState) => { + event.persist(); + const activeViewportIndex = (viewportsState && viewportsState.activeViewportIndex) || 0; @@ -167,31 +167,21 @@ const mapDispatchToProps = dispatch => { return; } - const { element } = enabledElements[activeViewportIndex]; - - const eventData = { - event: { - clientX: event.clientX, - clientY: event.clientY, - }, - element, - }; - const { toolType, measurementId } = measurementData; const tool = MeasurementApi.Instance.tools[toolType].find(measurement => { return measurement._id === measurementId; }); - const options = { - skipAddLabelButton: true, - editLocation: true, - }; - // Clone the tool not to set empty location initially const toolForLocation = Object.assign({}, tool, { location: null }); - getMeasurementLocationCallback(eventData, toolForLocation, options); + + if (ownProps.onRelabel) { + ownProps.onRelabel(toolForLocation); + } }, dispatchEditDescription: (event, measurementData, viewportsState) => { + event.persist(); + const activeViewportIndex = (viewportsState && viewportsState.activeViewportIndex) || 0; @@ -201,26 +191,14 @@ const mapDispatchToProps = dispatch => { return; } - const { element } = enabledElements[activeViewportIndex]; - - const eventData = { - event: { - clientX: event.clientX, - clientY: event.clientY, - }, - element, - }; - const { toolType, measurementId } = measurementData; const tool = MeasurementApi.Instance.tools[toolType].find(measurement => { return measurement._id === measurementId; }); - const options = { - editDescriptionOnDialog: true, - }; - - getMeasurementLocationCallback(eventData, tool, options); + if (ownProps.onEditDescription) { + ownProps.onEditDescription(tool); + } }, dispatchJumpToRowItem: ( measurementData, diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/actions.js b/platform/viewer/src/appExtensions/MeasurementsPanel/actions.js deleted file mode 100644 index 7f978e3189d..00000000000 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/actions.js +++ /dev/null @@ -1,21 +0,0 @@ -const setLabellingFlowDataAction = labellingFlowData => ({ - type: 'SET_LABELLING_FLOW_DATA', - labellingFlowData, -}); - -const resetLabellingAndContextMenuAction = state => ({ - type: 'RESET_LABELLING_AND_CONTEXT_MENU', - state, -}); - -const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({ - type: 'SET_TOOL_CONTEXT_MENU_DATA', - viewportIndex, - toolContextMenuData, -}); - -export { - resetLabellingAndContextMenuAction, - setLabellingFlowDataAction, - setToolContextMenuDataAction, -}; diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/getMeasurementLocationCallback.js b/platform/viewer/src/appExtensions/MeasurementsPanel/getMeasurementLocationCallback.js deleted file mode 100644 index 4e77e94fbcb..00000000000 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/getMeasurementLocationCallback.js +++ /dev/null @@ -1,33 +0,0 @@ -import cornerstoneTools from 'cornerstone-tools'; -import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js'; - -export default function getMeasurementLocationCallback( - eventData, - tool, - options -) { - const { toolType } = tool; - const { element } = eventData; - const doneCallback = updateTableWithNewMeasurementData; - - const ToolInstance = cornerstoneTools.getToolForElement(element, toolType); - - if ( - !ToolInstance || - !ToolInstance.configuration || - !ToolInstance.configuration.getMeasurementLocationCallback - ) { - console.warn( - 'Tool instance configuration is missing: getMeasurementLocationCallback' - ); - - return; - } - - ToolInstance.configuration.getMeasurementLocationCallback( - tool, - eventData, - doneCallback, - options - ); -} diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/index.js b/platform/viewer/src/appExtensions/MeasurementsPanel/index.js index 3e4ab428809..d240971daaa 100644 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/index.js +++ b/platform/viewer/src/appExtensions/MeasurementsPanel/index.js @@ -1,3 +1,4 @@ +import React from 'react'; import ConnectedMeasurementTable from './ConnectedMeasurementTable.js'; import init from './init.js'; @@ -7,10 +8,38 @@ export default { */ id: 'measurements-table', - preRegistration({ servicesManager, configuration = {} }) { - init({ servicesManager, configuration }); + preRegistration({ servicesManager, commandsManager, configuration = {} }) { + init({ servicesManager, commandsManager, configuration }); }, - getPanelModule({ servicesManager }) { + getPanelModule({ servicesManager, commandsManager }) { + const { UILabellingFlowService } = servicesManager.services; + const ExtendedConnectedMeasurementTable = () => ( + { + if (UILabellingFlowService) { + UILabellingFlowService.show({ + centralize: true, + props: { + skipAddLabelButton: true, + editLocation: true, + measurementData: tool, + }, + }); + } + }} + onEditDescription={tool => { + if (UILabellingFlowService) { + UILabellingFlowService.show({ + centralize: true, + props: { + editDescriptionOnDialog: true, + measurementData: tool, + }, + }); + } + }} + /> + ); return { menuOptions: [ { @@ -22,7 +51,7 @@ export default { components: [ { id: 'measurement-panel', - component: ConnectedMeasurementTable, + component: ExtendedConnectedMeasurementTable, }, ], defaultContext: ['VIEWER'], diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/init.js b/platform/viewer/src/appExtensions/MeasurementsPanel/init.js index 559cff1540e..0c299b81a63 100644 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/init.js +++ b/platform/viewer/src/appExtensions/MeasurementsPanel/init.js @@ -1,19 +1,7 @@ import OHIF from '@ohif/core'; import cornerstone from 'cornerstone-core'; import csTools from 'cornerstone-tools'; -import { - getToolLabellingFlowCallback, - getOnRightClickCallback, - getOnTouchPressCallback, - getResetLabellingAndContextMenu, -} from './labelingFlowCallbacks.js'; import throttle from 'lodash.throttle'; -import { SimpleDialog } from '@ohif/ui'; - -// TODO: This only works because we have a hard dependency on this extension -// We need to decouple and make stuff like this possible w/o bundling this at -// build time -import store from './../../store'; const { onAdded, @@ -33,92 +21,18 @@ const MEASUREMENT_ACTION_MAP = { * * * @export - * @param {*} configuration + * @param {Object} servicesManager + * @param {Object} configuration */ -export default function init({ servicesManager, configuration = {} }) { - const { UIDialogService } = servicesManager.services; - const callInputDialog = (data, event, callback) => { - let dialogId = UIDialogService.create({ - content: SimpleDialog.InputDialog, - defaultPosition: { - x: (event && event.currentPoints.canvas.x) || 0, - y: (event && event.currentPoints.canvas.y) || 0, - }, - showOverlay: true, - contentProps: { - title: 'Enter your annotation', - label: 'New label', - defaultValue: data ? data.text : '', - onClose: () => UIDialogService.dismiss({ id: dialogId }), - onSubmit: value => { - callback(value); - UIDialogService.dismiss({ id: dialogId }); - }, - }, - }); - }; - - // If these tools were already added by a different extension, we want to replace - // them with the same tools that have an alternative configuration. By passing in - // our custom `getMeasurementLocationCallback`, we can... - const toolLabellingFlowCallback = getToolLabellingFlowCallback(store); - - // Removes all tools from all enabled elements w/ provided name - // Not commonly used API, so :eyes: for unknown side-effects - csTools.removeTool('Bidirectional'); - csTools.removeTool('Length'); - csTools.removeTool('Angle'); - csTools.removeTool('FreehandRoi'); - csTools.removeTool('EllipticalRoi'); - csTools.removeTool('CircleRoi'); - csTools.removeTool('RectangleRoi'); - csTools.removeTool('ArrowAnnotate'); - - // Re-add each tool w/ our custom configuration - csTools.addTool(csTools.BidirectionalTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.LengthTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.AngleTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.FreehandRoiTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.EllipticalRoiTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.CircleRoiTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.RectangleRoiTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - }, - }); - csTools.addTool(csTools.ArrowAnnotateTool, { - configuration: { - getMeasurementLocationCallback: toolLabellingFlowCallback, - getTextCallback: (callback, eventDetails) => - callInputDialog(null, eventDetails, callback), - changeTextCallback: (data, eventDetails, callback) => - callInputDialog(data, eventDetails, callback), - }, - }); +export default function init({ + servicesManager, + commandsManager, + configuration, +}) { + const { + UIContextMenuService, + UILabellingFlowService, + } = servicesManager.services; // TODO: MEASUREMENT_COMPLETED (not present in initial implementation) const onMeasurementsChanged = (action, event) => { @@ -131,15 +45,43 @@ export default function init({ servicesManager, configuration = {} }) { this, 'labelmapModified' ); - // - const onRightClick = getOnRightClickCallback(store); - const onTouchPress = getOnTouchPressCallback(store); - const onNewImage = getResetLabellingAndContextMenu(store); - const onMouseClick = getResetLabellingAndContextMenu(store); - const onTouchStart = getResetLabellingAndContextMenu(store); - - // Because click gives us the native "mouse up", buttons will always be `0` - // Need to fallback to event.which; + + const onRightClick = event => { + if (UIContextMenuService) { + UIContextMenuService.show({ event: event.detail }); + } + }; + + const onTouchPress = event => { + if (UIContextMenuService) { + UIContextMenuService.show({ + event: event.detail, + props: { + isTouchEvent: true, + }, + }); + } + }; + + const onTouchStart = () => resetLabelligAndContextMenu(); + + const onMouseClick = () => resetLabelligAndContextMenu(); + + const resetLabelligAndContextMenu = () => { + if (UILabellingFlowService && UIContextMenuService) { + UILabellingFlowService.hide(); + UIContextMenuService.hide(); + } + }; + + // TODO: This makes scrolling painfully slow + // const onNewImage = ... + + /* + * Because click gives us the native "mouse up", buttons will always be `0` + * Need to fallback to event.which; + * + */ const handleClick = cornerstoneMouseClickEvent => { const mouseUpEvent = cornerstoneMouseClickEvent.detail.event; const isRightClick = mouseUpEvent.which === 3; @@ -170,10 +112,11 @@ export default function init({ servicesManager, configuration = {} }) { csTools.EVENTS.LABELMAP_MODIFIED, onLabelmapModified ); - // + element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress); element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick); element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart); + // TODO: This makes scrolling painfully slow // element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage); } @@ -197,10 +140,12 @@ export default function init({ servicesManager, configuration = {} }) { csTools.EVENTS.LABELMAP_MODIFIED, onLabelmapModified ); - // + element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress); element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick); element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart); + + // TODO: This makes scrolling painfully slow // element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage); } diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/labelingFlowCallbacks.js b/platform/viewer/src/appExtensions/MeasurementsPanel/labelingFlowCallbacks.js deleted file mode 100644 index 81fae1f0b32..00000000000 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/labelingFlowCallbacks.js +++ /dev/null @@ -1,144 +0,0 @@ -import { - resetLabellingAndContextMenuAction, - setToolContextMenuDataAction, - setLabellingFlowDataAction, -} from './actions.js'; -import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js'; - -const VIEWPORT_INDEX = 0; - -function getOnRightClickCallback(store) { - const setToolContextMenuData = (viewportIndex, toolContextMenuData) => { - store.dispatch(resetLabellingAndContextMenuAction()); - store.dispatch( - setToolContextMenuDataAction(viewportIndex, toolContextMenuData) - ); - }; - - const getOnCloseCallback = viewportIndex => { - return function onClose() { - const toolContextMenuData = { - visible: false, - }; - - store.dispatch( - setToolContextMenuDataAction(viewportIndex, toolContextMenuData) - ); - }; - }; - - return function onRightClick(event) { - const eventData = event.detail; - const viewportIndex = VIEWPORT_INDEX; // parseInt(eventData.element.dataset.viewportIndex, 10); - - const toolContextMenuData = { - eventData, - isTouchEvent: false, - onClose: getOnCloseCallback(viewportIndex), - }; - - // setToolContextMenuData(viewportIndex, toolContextMenuData); - setToolContextMenuData(0, toolContextMenuData); - }; -} - -function getOnTouchPressCallback(store) { - const setToolContextMenuData = (viewportIndex, toolContextMenuData) => { - store.dispatch(resetLabellingAndContextMenuAction()); - store.dispatch( - setToolContextMenuDataAction(viewportIndex, toolContextMenuData) - ); - }; - - const getOnCloseCallback = viewportIndex => { - return function onClose() { - const toolContextMenuData = { - visible: false, - }; - - store.dispatch( - setToolContextMenuDataAction(viewportIndex, toolContextMenuData) - ); - }; - }; - - return function onTouchPress(event) { - const eventData = event.detail; - const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10); - - const toolContextMenuData = { - eventData, - isTouchEvent: true, - onClose: getOnCloseCallback(viewportIndex), - }; - - setToolContextMenuData(viewportIndex, toolContextMenuData); - }; -} - -function getResetLabellingAndContextMenu(store) { - return function resetLabellingAndContextMenu() { - store.dispatch(resetLabellingAndContextMenuAction()); - }; -} - -/** - * - * - * @param {*} store - * @returns - */ -function getToolLabellingFlowCallback(store) { - const setLabellingFlowData = labellingFlowData => { - store.dispatch(setLabellingFlowDataAction(labellingFlowData)); - }; - - return function toolLabellingFlowCallback( - measurementData, - eventData, - doneCallback, - options = {} - ) { - const updateLabelling = ({ location, response, description }) => { - // Update the measurement data with the labelling parameters - - if (location) { - measurementData.location = location; - } - - measurementData.description = description || ''; - - if (response) { - measurementData.response = response; - } - - updateTableWithNewMeasurementData(measurementData); - }; - - const labellingDoneCallback = () => { - setLabellingFlowData({ visible: false }); - }; - - const labellingFlowData = { - visible: true, - eventData, - measurementData, - skipAddLabelButton: options.skipAddLabelButton, - editLocation: options.editLocation, - editDescription: options.editDescription, - editResponse: options.editResponse, - editDescriptionOnDialog: options.editDescriptionOnDialog, - labellingDoneCallback, - updateLabelling, - }; - - setLabellingFlowData(labellingFlowData); - }; -} - -export { - getToolLabellingFlowCallback, - getOnRightClickCallback, - getOnTouchPressCallback, - getResetLabellingAndContextMenu, -}; diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/updateTableWithNewMeasurementData.js b/platform/viewer/src/appExtensions/MeasurementsPanel/updateTableWithNewMeasurementData.js deleted file mode 100644 index 7339cd219fd..00000000000 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/updateTableWithNewMeasurementData.js +++ /dev/null @@ -1,29 +0,0 @@ -import OHIF from '@ohif/core'; -import cornerstone from 'cornerstone-core'; - -export default function updateTableWithNewMeasurementData({ - toolType, - measurementNumber, - location, - description, -}) { - // Update all measurements by measurement number - const measurementApi = OHIF.measurements.MeasurementApi.Instance; - const measurements = measurementApi.tools[toolType].filter( - m => m.measurementNumber === measurementNumber - ); - - measurements.forEach(measurement => { - measurement.location = location; - measurement.description = description; - - measurementApi.updateMeasurement(measurement.toolType, measurement); - }); - - measurementApi.syncMeasurementsAndToolData(); - - // Update images in all active viewports - cornerstone.getEnabledElements().forEach(enabledElement => { - cornerstone.updateImage(enabledElement.element); - }); -} diff --git a/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.css b/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.css index 9d3de80fdb2..53a6e294aaa 100644 --- a/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.css +++ b/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.css @@ -1,5 +1,5 @@ .editDescriptionDialog { - position: absolute; + position: relative; z-index: 300; width: 320px; transition: all 300ms linear; diff --git a/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.js b/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.js index ff8e16bdca3..9bea7235913 100644 --- a/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.js +++ b/platform/viewer/src/components/EditDescriptionDialog/EditDescriptionDialog.js @@ -1,24 +1,15 @@ import { Component } from 'react'; import React from 'react'; import PropTypes from 'prop-types'; -import SimpleDialog from '../SimpleDialog/SimpleDialog.js'; - -import bounding from '../../lib/utils/bounding.js'; -import { getDialogStyle } from './../Labelling/labellingPositionUtils.js'; +import SimpleDialog from '../SimpleDialog/SimpleDialog.js'; import './EditDescriptionDialog.css'; export default class EditDescriptionDialog extends Component { - static defaultProps = { - componentRef: React.createRef(), - componentStyle: {}, - }; - static propTypes = { + description: PropTypes.string, measurementData: PropTypes.object.isRequired, onCancel: PropTypes.func.isRequired, - componentRef: PropTypes.object, - componentStyle: PropTypes.object, onUpdate: PropTypes.func.isRequired, }; @@ -28,14 +19,8 @@ export default class EditDescriptionDialog extends Component { this.state = { description: props.measurementData.description || '', }; - - this.mainElement = React.createRef(); } - componentDidMount = () => { - bounding(this.mainElement); - }; - componentDidUpdate(prevProps) { if (this.props.description !== prevProps.description) { this.setState({ @@ -45,16 +30,12 @@ export default class EditDescriptionDialog extends Component { } render() { - const style = getDialogStyle(this.props.componentStyle); - return ( { - this.repositionComponent(); if (this.state.editDescription) { this.descriptionInput.current.focus(); } @@ -63,36 +51,14 @@ export default class LabellingFlow extends Component { mainElementClassName += ' editDescription'; } - const style = Object.assign({}, this.state.componentStyle); - if (this.state.skipAddLabelButton) { - if (style.left - 160 < 0) { - style.left = 0; - } else { - style.left -= 160; - } - } - - if (this.state.editLocation) { - style.maxHeight = '70vh'; - if (!this.initialTopDistance) { - this.initialTopDistance = window.innerHeight - window.innerHeight * 0.3; - style.top = `${this.state.componentStyle.top - - this.initialTopDistance / 2}px`; - } else { - style.top = `${this.state.componentStyle.top}px`; - } - } - return ( <> -
); } else { @@ -195,33 +160,17 @@ export default class LabellingFlow extends Component { } }; - relabel = event => { - const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop; - const componentStyle = { - top: event.nativeEvent.y - viewportTopPosition - 55, - left: event.nativeEvent.x, - }; - this.setState({ - editLocation: true, - componentStyle, - }); - }; + relabel = event => this.setState({ editLocation: true }); setDescriptionUpdateMode = () => { this.descriptionInput.current.focus(); - - this.setState({ - editDescription: true, - }); + this.setState({ editDescription: true }); }; descriptionCancel = () => { const { description = '' } = cloneDeep(this.state); this.descriptionInput.current.value = description; - - this.setState({ - editDescription: false, - }); + this.setState({ editDescription: false }); }; handleKeyPress = e => { @@ -240,22 +189,15 @@ export default class LabellingFlow extends Component { }); }; - selectTreeSelectCalback = (event, itemSelected) => { + selectTreeSelectCallback = (event, itemSelected) => { const location = itemSelected.value; this.props.updateLabelling({ location }); - const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop; - const componentStyle = { - top: event.nativeEvent.y - viewportTopPosition - 25, - left: event.nativeEvent.x, - }; - this.setState({ editLocation: false, confirmationState: true, location: itemSelected.value, locationLabel: itemSelected.label, - componentStyle, }); if (this.isTouchScreen) { @@ -276,18 +218,13 @@ export default class LabellingFlow extends Component { fadeOutAndLeave = () => { // Wait for 1 sec to dismiss the labelling component - this.fadeOutTimer = setTimeout(() => { - this.setState({ - displayComponent: false, - }); - }, 1000); + this.fadeOutTimer = setTimeout( + () => this.setState({ displayComponent: false }), + 1000 + ); }; - fadeOutAndLeaveFast = () => { - this.setState({ - displayComponent: false, - }); - }; + fadeOutAndLeaveFast = () => this.setState({ displayComponent: false }); clearFadeOutTimer = () => { if (!this.fadeOutTimer) { @@ -296,29 +233,4 @@ export default class LabellingFlow extends Component { clearTimeout(this.fadeOutTimer); }; - - calculateTopDistance = () => { - const height = window.innerHeight - window.innerHeight * 0.3; - let top = this.state.componentStyle.top - height / 2 + 55; - if (top < 0) { - top = 0; - } else { - if (top + height > window.innerHeight) { - top -= top + height - window.innerHeight; - } - } - return top; - }; - - repositionComponent = () => { - // SetTimeout for the css animation to end. - setTimeout(() => { - bounding(this.mainElement); - if (this.state.editLocation) { - this.mainElement.current.style.maxHeight = '70vh'; - const top = this.calculateTopDistance(); - this.mainElement.current.style.top = `${top}px`; - } - }, 200); - }; } diff --git a/platform/viewer/src/components/Labelling/LabellingManager.css b/platform/viewer/src/components/Labelling/LabellingManager.css index dde9058c9f9..6842648703a 100644 --- a/platform/viewer/src/components/Labelling/LabellingManager.css +++ b/platform/viewer/src/components/Labelling/LabellingManager.css @@ -1,18 +1,9 @@ -.labellingComponent-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - background-color: rgba(0, 0, 0, 0.8); -} - .labellingComponent { - position: absolute; + position: relative; text-align: center; z-index: 999; transition: all 200ms linear; + max-height: 500px; } .labellingComponent .selectedLabel, @@ -87,9 +78,7 @@ border: none; } -.labellingComponent.editDescription - .locationDescriptionWrapper - #descriptionInput { +.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput { visibility: visible; } diff --git a/platform/viewer/src/components/Labelling/LabellingManager.js b/platform/viewer/src/components/Labelling/LabellingManager.js index e573eb79325..8b014888663 100644 --- a/platform/viewer/src/components/Labelling/LabellingManager.js +++ b/platform/viewer/src/components/Labelling/LabellingManager.js @@ -1,21 +1,16 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - import cloneDeep from 'lodash.clonedeep'; import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js'; import LabellingFlow from './LabellingFlow.js'; - import './LabellingManager.css'; export default class LabellingManager extends Component { static propTypes = { - eventData: PropTypes.object.isRequired, measurementData: PropTypes.object.isRequired, - labellingDoneCallback: PropTypes.func.isRequired, updateLabelling: PropTypes.func.isRequired, - skipAddLabelButton: PropTypes.bool, editLocation: PropTypes.bool, editDescription: PropTypes.bool, @@ -41,7 +36,6 @@ export default class LabellingManager extends Component { } this.state = { - componentStyle: getComponentPosition(props.eventData), skipAddLabelButton: props.skipAddLabelButton, editLocation: editLocation, editDescription: props.editDescription, @@ -75,20 +69,13 @@ export default class LabellingManager extends Component { ); } if (editLocation || editDescription) { - return ( - - ); + return ; } }; @@ -98,33 +85,19 @@ export default class LabellingManager extends Component { if (editDescription) { measurementData.description = undefined; } + if (editLocation) { measurementData.location = undefined; } }; responseDialogUpdate = response => { - this.props.updateLabelling({ - response, - }); + this.props.updateLabelling({ response }); this.props.labellingDoneCallback(); }; descriptionDialogUpdate = description => { - this.props.updateLabelling({ - description, - }); + this.props.updateLabelling({ description }); this.props.labellingDoneCallback(); }; } - -function getComponentPosition(eventData) { - const { - event: { clientX: left, clientY: top }, - } = eventData; - - return { - left, - top, - }; -} diff --git a/platform/viewer/src/components/Labelling/labellingPositionUtils.js b/platform/viewer/src/components/Labelling/labellingPositionUtils.js deleted file mode 100644 index f581791f7b6..00000000000 --- a/platform/viewer/src/components/Labelling/labellingPositionUtils.js +++ /dev/null @@ -1,53 +0,0 @@ -import cornerstone from 'cornerstone-core'; - -const buttonSize = { - width: 96, - height: 28, -}; - -export function getAddLabelButtonStyle(measurementData, eventData) { - const { start, end } = measurementData.handles; - const { client } = eventData.currentPoints; - const clientStart = cornerstone.pixelToCanvas(eventData.element, start); - const clientEnd = cornerstone.pixelToCanvas(eventData.element, end); - const canvasOffSetLeft = client.x - clientStart.x; - const canvasOffSetTop = client.y - clientStart.y; - const position = { - left: clientEnd.x + canvasOffSetLeft, - top: clientEnd.y + canvasOffSetTop, - }; - - if (start.y > end.y) { - position.top -= buttonSize.height; - } - if (start.x > end.x) { - position.left -= buttonSize.width; - } - - return position; -} - -export function getDialogStyle(componentStyle) { - const style = Object.assign({}, componentStyle); - const dialogProps = { - width: 320, - height: 230, - }; - - // Get max values to avoid position out of the screen - const maxLeft = window.innerWidth - dialogProps.width; - const maxTop = window.innerHeight - dialogProps.height; - - // Positioning the dialog with its center on the click event - style.left -= dialogProps.width / 2; - style.top -= dialogProps.height / 2; - - if (style.left > maxLeft) { - style.left = maxLeft; - } - if (style.top > maxTop) { - style.top = maxTop; - } - - return style; -} diff --git a/platform/viewer/src/connectedComponents/ConnectedLabellingOverlay.js b/platform/viewer/src/connectedComponents/ConnectedLabellingOverlay.js deleted file mode 100644 index 0354e1159ce..00000000000 --- a/platform/viewer/src/connectedComponents/ConnectedLabellingOverlay.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import LabellingOverlay from './LabellingOverlay'; - -const mapStateToProps = state => { - if (!state.ui || !state.ui.labelling) { - return { - visible: false, - }; - } - - const labellingFlowData = state.ui.labelling; - - return { - visible: false, - ...labellingFlowData, - }; -}; - -const ConnectedLabellingOverlay = connect( - mapStateToProps, - null -)(LabellingOverlay); - -export default ConnectedLabellingOverlay; diff --git a/platform/viewer/src/connectedComponents/ConnectedToolContextMenu.js b/platform/viewer/src/connectedComponents/ConnectedToolContextMenu.js deleted file mode 100644 index 8ab97531a5f..00000000000 --- a/platform/viewer/src/connectedComponents/ConnectedToolContextMenu.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import ToolContextMenu from './ToolContextMenu'; - -const mapStateToProps = (state, ownProps) => { - if (!state.ui || !state.ui.contextMenu) { - return { - visible: false, - }; - } - - const { viewportIndex } = ownProps; - const toolContextMenuData = state.ui.contextMenu[viewportIndex]; - - return { - ...toolContextMenuData, - }; -}; - -const ConnectedToolContextMenu = connect( - mapStateToProps, - null -)(ToolContextMenu); - -export default ConnectedToolContextMenu; diff --git a/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js b/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js index a0c29857d74..26c6a237d39 100644 --- a/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js +++ b/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js @@ -35,7 +35,10 @@ const mapDispatchToProps = (dispatch, ownProps) => { // set new language i18n.changeLanguage(language); - ownProps.hide(); + if (ownProps.hide) { + ownProps.hide(); + } + dispatch( setUserPreferences({ windowLevelData, diff --git a/platform/viewer/src/connectedComponents/LabellingOverlay.js b/platform/viewer/src/connectedComponents/LabellingOverlay.js deleted file mode 100644 index cf976424a39..00000000000 --- a/platform/viewer/src/connectedComponents/LabellingOverlay.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import LabellingManager from '../components/Labelling/LabellingManager'; - -class LabellingOverlay extends Component { - static propTypes = { - visible: PropTypes.bool.isRequired, - }; - - static defaultProps = { - visible: false, - }; - - render() { - if (!this.props.visible) { - return null; - } - - return ; - } -} - -export default LabellingOverlay; diff --git a/platform/viewer/src/connectedComponents/ToolContextMenu.css b/platform/viewer/src/connectedComponents/ToolContextMenu.css index 42bd2ca6f8d..66fa1749549 100644 --- a/platform/viewer/src/connectedComponents/ToolContextMenu.css +++ b/platform/viewer/src/connectedComponents/ToolContextMenu.css @@ -1,5 +1,5 @@ .ToolContextMenu { - position: absolute; + position: relative; background-color: white; border: 1px solid white; border-radius: 5px; diff --git a/platform/viewer/src/connectedComponents/ToolContextMenu.js b/platform/viewer/src/connectedComponents/ToolContextMenu.js index ee92bdbd916..18bb5768463 100644 --- a/platform/viewer/src/connectedComponents/ToolContextMenu.js +++ b/platform/viewer/src/connectedComponents/ToolContextMenu.js @@ -1,9 +1,6 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import cornerstone from 'cornerstone-core'; -import cornerstoneTools from 'cornerstone-tools'; -// This whole component should live in the Measurements Extension :thinking: -import getMeasurementLocationCallback from '../appExtensions/MeasurementsPanel/getMeasurementLocationCallback'; +import { commandsManager } from './../App.js'; import './ToolContextMenu.css'; @@ -17,232 +14,119 @@ const toolTypes = [ 'RectangleRoi', ]; -let defaultDropdownItems = [ - { - actionType: 'Delete', - action: ({ nearbyToolData, eventData }) => { - const element = eventData.element; - - cornerstoneTools.removeToolState( - element, - nearbyToolData.toolType, - nearbyToolData.tool - ); - cornerstone.updateImage(element); +const ToolContextMenu = ({ + onSetLabel, + onSetDescription, + isTouchEvent, + eventData, + onClose, + onDelete, +}) => { + const defaultDropdownItems = [ + { + actionType: 'Delete', + action: ({ nearbyToolData, eventData }) => + onDelete(nearbyToolData, eventData), }, - }, - { - actionType: 'setLabel', - action: ({ nearbyToolData, eventData }) => { - const { tool } = nearbyToolData; - - const options = { - skipAddLabelButton: true, - editLocation: true, - }; - - getMeasurementLocationCallback(eventData, tool, options); + { + actionType: 'setLabel', + action: ({ nearbyToolData, eventData }) => { + const { tool: measurementData } = nearbyToolData; + onSetLabel(eventData, measurementData); + }, }, - }, - { - actionType: 'setDescription', - action: ({ nearbyToolData, eventData }) => { - const { tool } = nearbyToolData; - - const options = { - editDescriptionOnDialog: true, - }; - - getMeasurementLocationCallback(eventData, tool, options); + { + actionType: 'setDescription', + action: ({ nearbyToolData, eventData }) => { + const { tool: measurementData } = nearbyToolData; + onSetDescription(eventData, measurementData); + }, }, - }, -]; - -function getNearbyToolData(element, coords, toolTypes) { - const nearbyTool = {}; - let pointNearTool = false; - - toolTypes.forEach(toolType => { - const toolData = cornerstoneTools.getToolState(element, toolType); - if (!toolData) { - return; - } + ]; - toolData.data.forEach(function(data, index) { - // TODO: Fix this, it's ugly - let toolInterface = cornerstoneTools.getToolForElement(element, toolType); - if (!toolInterface) { - toolInterface = cornerstoneTools.getToolForElement( - element, - `${toolType}Tool` - ); - } - - if (!toolInterface) { - throw new Error('Tool not found.'); - } - - if (toolInterface.pointNearTool(element, data, coords)) { - pointNearTool = true; - nearbyTool.tool = data; - nearbyTool.index = index; - nearbyTool.toolType = toolType; - } + const getDropdownItems = (eventData, isTouchEvent = false) => { + const nearbyToolData = commandsManager.runCommand('getNearbyToolData', { + element: eventData.element, + canvasCoordinates: eventData.currentPoints.canvas, + availableToolTypes: toolTypes, }); - if (pointNearTool) { - return false; - } - }); - - return pointNearTool ? nearbyTool : undefined; -} - -function getDropdownItems(eventData, isTouchEvent = false) { - const nearbyToolData = getNearbyToolData( - eventData.element, - eventData.currentPoints.canvas, - 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(function(item) { - item.params = { - eventData, - nearbyToolData, - }; - - if (item.actionType === 'Delete') { - item.text = 'Delete measurement'; - } - - if (item.actionType === 'setLabel') { - item.text = 'Relabel'; - } - - if (item.actionType === 'setDescription') { - item.text = `${ - nearbyToolData.tool.description ? 'Edit' : 'Add' - } Description`; - } - - dropdownItems.push(item); - }); - } - - return dropdownItems; -} - -class ToolContextMenu extends Component { - static propTypes = { - isTouchEvent: PropTypes.bool.isRequired, - eventData: PropTypes.object, - onClose: PropTypes.func, - visible: PropTypes.bool.isRequired, - }; - - static defaultProps = { - visible: true, - isTouchEvent: false, - }; - - constructor(props) { - super(props); - - this.mainElement = React.createRef(); - } - - render() { - if (!this.props.eventData) { - return null; + // 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; } - const { isTouchEvent, eventData } = this.props; - const dropdownItems = getDropdownItems(eventData, isTouchEvent); - - // Skip if there is no dropdown item - if (!dropdownItems.length) { - return ''; - } + let dropdownItems = []; + if (nearbyToolData) { + defaultDropdownItems.forEach(item => { + item.params = { + eventData, + nearbyToolData, + }; - const dropdownComponents = dropdownItems.map(item => { - const itemOnClick = event => { - item.action(item.params); - if (this.props.onClose) { - this.props.onClose(); + if (item.actionType === 'Delete') { + item.text = 'Delete measurement'; } - }; - - return ( -
  • - -
  • - ); - }); - const position = { - top: `${eventData.currentPoints.canvas.y}px`, - left: `${eventData.currentPoints.canvas.x}px`, - }; + if (item.actionType === 'setLabel') { + item.text = 'Relabel'; + } - return ( -
    -
      {dropdownComponents}
    -
    - ); - } + if (item.actionType === 'setDescription') { + item.text = `${ + nearbyToolData.tool.description ? 'Edit' : 'Add' + } Description`; + } - componentDidMount = () => { - if (this.mainElement.current) { - this.updateElementPosition(); + dropdownItems.push(item); + }); } + + return dropdownItems; }; - componentDidUpdate = () => { - if (this.mainElement.current) { - this.updateElementPosition(); + const itemOnClickHandler = (action, params, onClose) => { + action(params); + if (onClose) { + onClose(); } }; - updateElementPosition = () => { - const { - offsetParent, - offsetTop, - offsetHeight, - offsetWidth, - offsetLeft, - } = this.mainElement.current; - - const { eventData } = this.props; - - if (offsetTop + offsetHeight > offsetParent.offsetHeight) { - const offBoundPixels = - offsetTop + offsetHeight - offsetParent.offsetHeight; - const top = eventData.currentPoints.canvas.y - offBoundPixels; - - this.mainElement.current.style.top = `${top > 0 ? top : 0}px`; - } + const dropdownItems = getDropdownItems(eventData, isTouchEvent); + + return ( + dropdownItems.length && + eventData && ( +
    +
      + {dropdownItems.map(({ params, action, text, actionType }) => ( +
    • + +
    • + ))} +
    +
    + ) + ); +}; - if (offsetLeft + offsetWidth > offsetParent.offsetWidth) { - const offBoundPixels = - offsetLeft + offsetWidth - offsetParent.offsetWidth; - const left = eventData.currentPoints.canvas.x - offBoundPixels; +ToolContextMenu.propTypes = { + isTouchEvent: PropTypes.bool.isRequired, + eventData: PropTypes.object, + onClose: PropTypes.func, +}; - this.mainElement.current.style.left = `${left > 0 ? left : 0}px`; - } - }; -} +ToolContextMenu.defaultProps = { + isTouchEvent: false, +}; export default ToolContextMenu; diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index 3cc112c63d0..d112ac5021f 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -7,7 +7,6 @@ import OHIF from '@ohif/core'; import moment from 'moment'; import ConnectedHeader from './ConnectedHeader.js'; import ConnectedToolbarRow from './ConnectedToolbarRow.js'; -import ConnectedLabellingOverlay from './ConnectedLabellingOverlay'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedViewerMain from './ConnectedViewerMain.js'; import SidePanel from './../components/SidePanel.js'; @@ -317,7 +316,6 @@ class Viewer extends Component { )}
    - ); } diff --git a/platform/viewer/src/connectedComponents/ViewerMain.js b/platform/viewer/src/connectedComponents/ViewerMain.js index 250857fb1fe..34fa43d06f1 100644 --- a/platform/viewer/src/connectedComponents/ViewerMain.js +++ b/platform/viewer/src/connectedComponents/ViewerMain.js @@ -2,7 +2,6 @@ import './ViewerMain.css'; import { Component } from 'react'; import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js'; -import ConnectedToolContextMenu from './ConnectedToolContextMenu.js'; import PropTypes from 'prop-types'; import React from 'react'; @@ -153,7 +152,6 @@ class ViewerMain extends Component { setViewportData={this.setViewportData} > {/* Children to add to each viewport that support children */} - )}
    diff --git a/platform/viewer/src/index-umd.js b/platform/viewer/src/index-umd.js index 810097fce98..34a7a959092 100644 --- a/platform/viewer/src/index-umd.js +++ b/platform/viewer/src/index-umd.js @@ -7,7 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; -function installViewer(props, containerId = 'root', callback) { +function installViewer(config, containerId = 'root', callback) { const container = document.getElementById(containerId); if (!container) { @@ -16,7 +16,7 @@ function installViewer(props, containerId = 'root', callback) { ); } - return ReactDOM.render(, container, callback); + return ReactDOM.render(, container, callback); } export { App, installViewer }; diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js index 604a35de21a..3e66efd7373 100644 --- a/platform/viewer/src/index.js +++ b/platform/viewer/src/index.js @@ -18,35 +18,36 @@ import ReactDOM from 'react-dom'; * "baked in" to the published application. * * Depending on your use case/needs, you may want to consider not adding any extensions - * by default HERE, and instead provide them via the configuration specified at - * `window.config.extensions`, or by using the exported `App` component, and passing - * in your extensions as props. + * by default HERE, and instead provide them via the extensions configuration key or + * by using the exported `App` component, and passing in your extensions as props using + * the defaultExtensions property. */ import OHIFVTKExtension from '@ohif/extension-vtk'; import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html'; import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy'; import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf'; -// Default Settings +/* + * Default Settings + */ let config = {}; -const appDefaults = { - routerBasename: '/', -}; if (window) { config = window.config || {}; - config.extensions = [ +} + +const appProps = { + config, + defaultExtensions: [ OHIFVTKExtension, OHIFDicomHtmlExtension, OHIFDicomMicroscopyExtension, OHIFDicomPDFExtension, - ]; -} - -const appProps = Object.assign({}, appDefaults, config); + ], +}; -// Create App +/** Create App */ const app = React.createElement(App, appProps, null); -// Render +/** Render */ ReactDOM.render(app, document.getElementById('root')); diff --git a/platform/viewer/src/store/index.js b/platform/viewer/src/store/index.js index 53fb0e07c1d..ded8ed45199 100644 --- a/platform/viewer/src/store/index.js +++ b/platform/viewer/src/store/index.js @@ -6,18 +6,16 @@ import { } from 'redux/es/redux.js'; // import { createLogger } from 'redux-logger'; -import layoutReducers from './layout/reducers.js'; import { reducer as oidcReducer } from 'redux-oidc'; import { redux } from '@ohif/core'; import thunkMiddleware from 'redux-thunk'; -// Combine our @ohif/core, ui, and oidc reducers +// Combine our @ohif/core and oidc reducers // Set init data, using values found in localStorage const { reducers, localStorage, sessionStorage } = redux; const middleware = [thunkMiddleware]; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -reducers.ui = layoutReducers; reducers.oidc = oidcReducer; const rootReducer = combineReducers(reducers); diff --git a/platform/viewer/src/store/layout/reducers.js b/platform/viewer/src/store/layout/reducers.js deleted file mode 100644 index 8e75a117927..00000000000 --- a/platform/viewer/src/store/layout/reducers.js +++ /dev/null @@ -1,33 +0,0 @@ -const defaultState = { - labelling: {}, - contextMenu: {}, -}; - -const ui = (state = defaultState, action) => { - switch (action.type) { - case 'SET_LABELLING_FLOW_DATA': { - const labelling = Object.assign({}, action.labellingFlowData); - - return Object.assign({}, state, { labelling }); - } - case 'SET_TOOL_CONTEXT_MENU_DATA': { - const contextMenu = Object.assign({}, state.contextMenu); - - contextMenu[action.viewportIndex] = Object.assign( - {}, - action.toolContextMenuData - ); - - return Object.assign({}, state, { contextMenu }); - } - case 'RESET_LABELLING_AND_CONTEXT_MENU': - return Object.assign({}, state, { - labelling: defaultState.labelling, - contextMenu: defaultState.contextMenu, - }); - default: - return state; - } -}; - -export default ui; diff --git a/yarn.lock b/yarn.lock index 8b411db82b4..9478b3afffe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12246,7 +12246,7 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.4.0, lodash.merge@^4.6.1: +lodash.merge@^4.4.0, lodash.merge@^4.6.1, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== From c7008621e49b98fe66b582104888ca9e80184ee2 Mon Sep 17 00:00:00 2001 From: ohif-bot Date: Mon, 9 Dec 2019 17:49:51 +0000 Subject: [PATCH 2/2] chore(release): publish [skip ci] - @ohif/extension-cornerstone@2.0.0 - @ohif/extension-vtk@1.0.0 - @ohif/core@2.0.0 - @ohif/ui@1.0.0 - @ohif/viewer@3.0.0 --- extensions/cornerstone/CHANGELOG.md | 14 ++++++++++++++ extensions/cornerstone/package.json | 2 +- extensions/vtk/CHANGELOG.md | 14 ++++++++++++++ extensions/vtk/package.json | 6 +++--- platform/core/CHANGELOG.md | 14 ++++++++++++++ platform/core/package.json | 2 +- platform/ui/CHANGELOG.md | 14 ++++++++++++++ platform/ui/package.json | 2 +- platform/viewer/CHANGELOG.md | 14 ++++++++++++++ platform/viewer/package.json | 8 ++++---- 10 files changed, 80 insertions(+), 10 deletions(-) diff --git a/extensions/cornerstone/CHANGELOG.md b/extensions/cornerstone/CHANGELOG.md index 62b0a352320..67d6a3a00ac 100644 --- a/extensions/cornerstone/CHANGELOG.md +++ b/extensions/cornerstone/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + ## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02) **Note:** Version bump only for package @ohif/extension-cornerstone diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index cf08da0ad7d..388173b87f4 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone", - "version": "1.7.2", + "version": "2.0.0", "description": "OHIF extension for Cornerstone", "author": "OHIF", "license": "MIT", diff --git a/extensions/vtk/CHANGELOG.md b/extensions/vtk/CHANGELOG.md index 633f3698498..7a86ea98264 100644 --- a/extensions/vtk/CHANGELOG.md +++ b/extensions/vtk/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + ## [0.54.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.5...@ohif/extension-vtk@0.54.6) (2019-12-07) **Note:** Version bump only for package @ohif/extension-vtk diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 331b753dae5..23b11f5dcfe 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-vtk", - "version": "0.54.6", + "version": "1.0.0", "description": "OHIF extension for VTK.js", "author": "OHIF", "license": "MIT", @@ -52,8 +52,8 @@ "react-vtkjs-viewport": "^0.3.9" }, "devDependencies": { - "@ohif/core": "^1.13.3", - "@ohif/ui": "^0.65.4", + "@ohif/core": "^2.0.0", + "@ohif/ui": "^1.0.0", "cornerstone-tools": "^4.8.0", "cornerstone-wado-image-loader": "^3.0.0", "dcmjs": "^0.6.1", diff --git a/platform/core/CHANGELOG.md b/platform/core/CHANGELOG.md index 290fd2c03c2..8db72e44ecd 100644 --- a/platform/core/CHANGELOG.md +++ b/platform/core/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + ## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06) **Note:** Version bump only for package @ohif/core diff --git a/platform/core/package.json b/platform/core/package.json index f5c1da16805..b771861b93c 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/core", - "version": "1.13.3", + "version": "2.0.0", "description": "Generic business logic for web-based medical imaging applications", "author": "OHIF Core Team", "license": "MIT", diff --git a/platform/ui/CHANGELOG.md b/platform/ui/CHANGELOG.md index 650c9e4452b..dfdc926ab6d 100644 --- a/platform/ui/CHANGELOG.md +++ b/platform/ui/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + ## [0.65.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.3...@ohif/ui@0.65.4) (2019-12-07) **Note:** Version bump only for package @ohif/ui diff --git a/platform/ui/package.json b/platform/ui/package.json index 048218f7cae..4d269b013e8 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/ui", - "version": "0.65.4", + "version": "1.0.0", "description": "A set of React components for Medical Imaging Viewers", "author": "OHIF Contributors", "license": "MIT", diff --git a/platform/viewer/CHANGELOG.md b/platform/viewer/CHANGELOG.md index 7248137505a..1e76c0dba9c 100644 --- a/platform/viewer/CHANGELOG.md +++ b/platform/viewer/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + ## [2.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.7...@ohif/viewer@2.11.8) (2019-12-07) **Note:** Version bump only for package @ohif/viewer diff --git a/platform/viewer/package.json b/platform/viewer/package.json index fcd7b38cb19..8ec58b5c02e 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/viewer", - "version": "2.11.8", + "version": "3.0.0", "description": "OHIF Viewer", "author": "OHIF Contributors", "license": "MIT", @@ -45,14 +45,14 @@ }, "dependencies": { "@babel/runtime": "^7.5.5", - "@ohif/core": "^1.13.3", + "@ohif/core": "^2.0.0", "@ohif/extension-cornerstone": "^2.0.0", "@ohif/extension-dicom-html": "^1.0.2", "@ohif/extension-dicom-microscopy": "^0.50.6", "@ohif/extension-dicom-pdf": "^1.0.0", - "@ohif/extension-vtk": "^0.54.6", + "@ohif/extension-vtk": "^1.0.0", "@ohif/i18n": "^0.52.2", - "@ohif/ui": "^0.65.4", + "@ohif/ui": "^1.0.0", "@tanem/react-nprogress": "^1.1.25", "classnames": "^2.2.6", "core-js": "^3.2.1",