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:
-
Create a JS Object to hold the OHIF Viewer's configuration. Here are some
+
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).
+
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/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/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..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",
@@ -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/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/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/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/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/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/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 }) => {
>