From 5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Wed, 13 Nov 2019 17:02:14 -0300 Subject: [PATCH] feat: expose UiNotifications service (#1172) * playing around * Change extension init and preRegistration signature * Clear test code * Update core index test * Fix test * Tricking the bug * Renaming file back to trick some weird bug --- extensions/_example/src/index.js | 2 +- extensions/cornerstone/src/index.js | 4 +- extensions/cornerstone/src/init.js | 16 ++-- .../core/src/extensions/ExtensionManager.js | 8 +- platform/core/src/index.js | 9 ++ platform/core/src/index.test.js | 3 + platform/core/src/services/ServicesManager.js | 9 ++ .../services/UINotificationService/index.js | 84 +++++++++++++++++++ platform/core/src/services/index.js | 4 + platform/ui/src/utils/SnackbarProvider.js | 22 +++-- platform/viewer/src/App.js | 23 ++++- .../appExtensions/MeasurementsPanel/index.js | 4 +- .../appExtensions/MeasurementsPanel/init.js | 2 +- 13 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 platform/core/src/services/ServicesManager.js create mode 100644 platform/core/src/services/UINotificationService/index.js create mode 100644 platform/core/src/services/index.js diff --git a/extensions/_example/src/index.js b/extensions/_example/src/index.js index 0ed2644f8..6d1e82bac 100644 --- a/extensions/_example/src/index.js +++ b/extensions/_example/src/index.js @@ -11,7 +11,7 @@ export default { * LIFECYCLE HOOKS */ - preRegistration(extensionConfiguration) {}, + preRegistration({ serviceManager, configuration: extensionConfiguration }) {}, /** * MODULE GETTERS diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index a8b166a1d..5451f4041 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -24,8 +24,8 @@ export default { * @param {object} [configuration={}] * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` */ - preRegistration(configuration = {}) { - init(configuration); + preRegistration({ serviceManager, configuration = {} }) { + init({ serviceManager, configuration }); }, getViewportModule() { return OHIFCornerstoneViewport; diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index fbd2def8d..0795e2034 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -4,16 +4,18 @@ import csTools from 'cornerstone-tools'; import initCornerstoneTools from './initCornerstoneTools.js'; import queryString from 'query-string'; -function fallbackMetaDataProvider (type, imageId) { +function fallbackMetaDataProvider(type, imageId) { if (!imageId.includes('wado?requestType=WADO')) { - return + return; } - // If you call for an WADO-URI imageId and get no + // If you call for an WADO-URI imageId and get no // metadata, try reformatting to WADO-RS imageId const qs = queryString.parse(imageId); - const wadoRoot = window.store.getState().servers.servers[0].wadoRoot - const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${qs.seriesUID}/instances/${qs.objectUID}/frames/${qs.frame || 1}`; + const wadoRoot = window.store.getState().servers.servers[0].wadoRoot; + const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${ + qs.seriesUID + }/instances/${qs.objectUID}/frames/${qs.frame || 1}`; return cornerstone.metaData.get(type, wadoRsImageId); } @@ -21,13 +23,12 @@ function fallbackMetaDataProvider (type, imageId) { // Add this fallback provider with a low priority so it is handled last cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1); - /** * * @param {object} configuration * @param {Object|Array} configuration.csToolsConfig */ -export default function init(configuration = {}) { +export default function init({ serviceManager, configuration = {} }) { const { csToolsConfig } = configuration; const { StackManager } = OHIF.utils; const metadataProvider = new OHIF.cornerstone.MetadataProvider(); @@ -97,6 +98,7 @@ export default function init(configuration = {}) { ]; tools.forEach(tool => csTools.addTool(tool)); + csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 672160821..ddb702485 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -2,12 +2,13 @@ import MODULE_TYPES from './MODULE_TYPES.js'; import log from './../log.js'; export default class ExtensionManager { - constructor({ commandsManager }) { + constructor({ commandsManager, servicesManager }) { this.modules = {}; this.registeredExtensionIds = []; this.moduleTypeNames = Object.values(MODULE_TYPES); // this._commandsManager = commandsManager; + this._servicesManager = servicesManager; this.moduleTypeNames.forEach(moduleType => { this.modules[moduleType] = []; @@ -66,7 +67,10 @@ export default class ExtensionManager { // preRegistrationHook if (extension.preRegistration) { - extension.preRegistration(configuration); + extension.preRegistration({ + serviceManager: this._servicesManager, + configuration, + }); } // Register Modules diff --git a/platform/core/src/index.js b/platform/core/src/index.js index 0b01e4255..6cec02db8 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -1,6 +1,7 @@ import './lib'; import { ExtensionManager, MODULE_TYPES } from './extensions'; +import { ServicesManager } from './services'; import classes, { CommandsManager, HotkeysManager } from './classes/'; import DICOMWeb from './DICOMWeb'; @@ -18,12 +19,15 @@ import ui from './ui'; import user from './user.js'; import utils from './utils/'; +import { createUiNotificationService } from './services'; + const OHIF = { MODULE_TYPES, // CommandsManager, ExtensionManager, HotkeysManager, + ServicesManager, // utils, studies, @@ -41,6 +45,8 @@ const OHIF = { viewer: {}, measurements, hangingProtocols, + // + createUiNotificationService, }; export { @@ -49,6 +55,7 @@ export { CommandsManager, ExtensionManager, HotkeysManager, + ServicesManager, // utils, studies, @@ -65,6 +72,8 @@ export { DICOMWeb, measurements, hangingProtocols, + // + createUiNotificationService, }; export { OHIF }; diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index 43b1691a0..a017a8a39 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -8,6 +8,9 @@ describe('Top level exports', () => { 'CommandsManager', 'ExtensionManager', 'HotkeysManager', + 'ServicesManager', + // + 'createUiNotificationService', // 'utils', 'studies', diff --git a/platform/core/src/services/ServicesManager.js b/platform/core/src/services/ServicesManager.js new file mode 100644 index 000000000..0e38e5695 --- /dev/null +++ b/platform/core/src/services/ServicesManager.js @@ -0,0 +1,9 @@ +export default class ServicesManager { + constructor() { + this.services = {}; + } + + register(service) { + this.services[service.name] = service; + } +} diff --git a/platform/core/src/services/UINotificationService/index.js b/platform/core/src/services/UINotificationService/index.js new file mode 100644 index 000000000..97db61040 --- /dev/null +++ b/platform/core/src/services/UINotificationService/index.js @@ -0,0 +1,84 @@ +/** + * A UI Notification + * + * @typedef {Object} Notification + * @property {string} title - + * @property {string} message - + * @property {number} [duration=5000] - in ms + * @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight" + * @property {string} [type="info"] - "info" | "error" | "warning" | "success" + * @property {boolean} [autoClose=true] + */ + +const uiNotificationServicePublicApi = { + name: 'UINotificationService', + hide, + show, + setServiceImplementation, +}; + +const uiNotificationServiceImplementation = { + _hide: () => console.warn('hide() NOT IMPLEMENTED'), + _show: () => console.warn('show() NOT IMPLEMENTED'), +}; + +function createUiNotificationService() { + return uiNotificationServicePublicApi; +} + +/** + * Create and show a new UI notification; returns the + * ID of the created notification. + * + * @param {Notification} notification { title, message, duration, position, type, autoClose} + * @returns {number} id + */ +function show({ + title, + message, + duration = 5000, + position = 'bottomRight', + type = 'info', + autoClose = true, +}) { + return uiNotificationServiceImplementation._show({ + title, + message, + duration, + position, + type, + autoClose, + }); +} + +/** + * Hides/dismisses the notification, if currently shown + * + * @param {number} id - id of the notification to hide/dismiss + * @returns undefined + */ +function hide(id) { + return uiNotificationServiceImplementation._hide({ id }); +} + +/** + * + * + * @param {*} { + * hide: hideImplementation, + * show: showImplementation, + * } + */ +function setServiceImplementation({ + hide: hideImplementation, + show: showImplementation, +}) { + if (hideImplementation) { + uiNotificationServiceImplementation._hide = hideImplementation; + } + if (showImplementation) { + uiNotificationServiceImplementation._show = showImplementation; + } +} + +export default createUiNotificationService; diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js new file mode 100644 index 000000000..ded69e822 --- /dev/null +++ b/platform/core/src/services/index.js @@ -0,0 +1,4 @@ +import ServicesManager from './ServicesManager.js'; +import createUiNotificationService from './UINotificationService'; + +export { createUiNotificationService, ServicesManager }; diff --git a/platform/ui/src/utils/SnackbarProvider.js b/platform/ui/src/utils/SnackbarProvider.js index 929e768cb..4151aec53 100644 --- a/platform/ui/src/utils/SnackbarProvider.js +++ b/platform/ui/src/utils/SnackbarProvider.js @@ -1,4 +1,10 @@ -import React, { useState, createContext, useContext } from 'react'; +import React, { + useState, + createContext, + useContext, + useCallback, + useEffect, +} from 'react'; import SnackbarContainer from '../components/snackbar/SnackbarContainer'; import SnackbarTypes from '../components/snackbar/SnackbarTypes'; @@ -6,7 +12,7 @@ const SnackbarContext = createContext(null); export const useSnackbarContext = () => useContext(SnackbarContext); -const SnackbarProvider = ({ children }) => { +const SnackbarProvider = ({ children, service }) => { const DEFAULT_OPTIONS = { title: '', message: '', @@ -19,7 +25,11 @@ const SnackbarProvider = ({ children }) => { const [count, setCount] = useState(1); const [snackbarItems, setSnackbarItems] = useState([]); - const show = options => { + useEffect(() => { + service.setServiceImplementation({ hide, show }); + }, [service, hide, show]); + + const show = useCallback(options => { if (!options || (!options.title && !options.message)) { console.warn( 'Snackbar cannot be rendered without required parameters: title | message' @@ -37,9 +47,9 @@ const SnackbarProvider = ({ children }) => { setSnackbarItems(state => [...state, newItem]); setCount(count + 1); - }; + }); - const hide = id => { + const hide = useCallback(id => { const hideItem = items => { const newItems = items.map(item => { if (item.id === id) { @@ -57,7 +67,7 @@ const SnackbarProvider = ({ children }) => { setTimeout(() => { setSnackbarItems(state => [...state.filter(item => item.id !== id)]); }, 1000); - }; + }); const hideAll = () => { // reset count diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js index 1526009a9..862a2c2ba 100644 --- a/platform/viewer/src/App.js +++ b/platform/viewer/src/App.js @@ -6,7 +6,9 @@ import './config'; import { CommandsManager, ExtensionManager, + ServicesManager, HotkeysManager, + createUiNotificationService, utils, } from '@ohif/core'; import React, { Component } from 'react'; @@ -41,9 +43,16 @@ const commandsManagerConfig = { getActiveContexts: () => getActiveContexts(store.getState()), }; +// Services +const UINotificationService = createUiNotificationService(); + const commandsManager = new CommandsManager(commandsManagerConfig); const hotkeysManager = new HotkeysManager(commandsManager); -const extensionManager = new ExtensionManager({ commandsManager }); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, +}); // ~~~~ END APP SETUP // TODO[react] Use a provider when the whole tree is React @@ -76,9 +85,11 @@ class App extends Component { super(props); this._appConfig = props; + const { servers, extensions, hotkeys, oidc } = props; this.initUserManager(oidc); + _initServices([UINotificationService]); _initExtensions(extensions, hotkeys); _initServers(servers); initWebWorkers(); @@ -100,7 +111,7 @@ class App extends Component { - + @@ -121,7 +132,7 @@ class App extends Component { - + @@ -168,6 +179,10 @@ class App extends Component { } } +function _initServices(services) { + services.forEach(service => servicesManager.register(service)); +} + /** * @param */ @@ -214,4 +229,4 @@ function _makeAbsoluteIfNecessary(url, base_url) { const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App; export default ExportedApp; -export { commandsManager, extensionManager, hotkeysManager }; +export { commandsManager, extensionManager, hotkeysManager, servicesManager }; diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/index.js b/platform/viewer/src/appExtensions/MeasurementsPanel/index.js index 52a600027..52a89b961 100644 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/index.js +++ b/platform/viewer/src/appExtensions/MeasurementsPanel/index.js @@ -7,8 +7,8 @@ export default { */ id: 'measurements-table', - preRegistration(configuration = {}) { - init(configuration); + preRegistration({ serviceManager, configuration = {} }) { + init({ serviceManager, configuration }); }, getPanelModule() { return { diff --git a/platform/viewer/src/appExtensions/MeasurementsPanel/init.js b/platform/viewer/src/appExtensions/MeasurementsPanel/init.js index da1325297..e957c6d28 100644 --- a/platform/viewer/src/appExtensions/MeasurementsPanel/init.js +++ b/platform/viewer/src/appExtensions/MeasurementsPanel/init.js @@ -34,7 +34,7 @@ const MEASUREMENT_ACTION_MAP = { * @export * @param {*} configuration */ -export default function init(configuration) { +export default function init({ serviceManager, configuration = {} }) { // 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...