feat: Add new modal service

[UI Services to Extensions]: Add new modal service
This commit is contained in:
Danny Brown 2019-11-15 13:42:20 -05:00 committed by GitHub
commit 6174109d5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 273 additions and 96 deletions

View File

@ -11,7 +11,10 @@ export default {
* LIFECYCLE HOOKS
*/
preRegistration({ serviceManager, configuration: extensionConfiguration }) {},
preRegistration({
servicesManager,
configuration: extensionConfiguration,
}) {},
/**
* MODULE GETTERS

View File

@ -24,8 +24,8 @@ export default {
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ serviceManager, configuration = {} }) {
init({ serviceManager, configuration });
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getViewportModule() {
return OHIFCornerstoneViewport;

View File

@ -28,7 +28,7 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ serviceManager, configuration = {} }) {
export default function init({ servicesManager, configuration = {} }) {
const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider();

View File

@ -68,7 +68,7 @@ export default class ExtensionManager {
// preRegistrationHook
if (extension.preRegistration) {
extension.preRegistration({
serviceManager: this._servicesManager,
servicesManager: this._servicesManager,
configuration,
});
}

View File

@ -19,7 +19,7 @@ import ui from './ui';
import user from './user.js';
import utils from './utils/';
import { createUiNotificationService } from './services';
import { createUINotificationService, createUIModalService } from './services';
const OHIF = {
MODULE_TYPES,
@ -46,7 +46,8 @@ const OHIF = {
measurements,
hangingProtocols,
//
createUiNotificationService,
createUINotificationService,
createUIModalService,
};
export {
@ -73,7 +74,8 @@ export {
measurements,
hangingProtocols,
//
createUiNotificationService,
createUINotificationService,
createUIModalService,
};
export { OHIF };

View File

@ -10,7 +10,8 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
'createUiNotificationService',
'createUINotificationService',
'createUIModalService',
//
'utils',
'studies',

View File

@ -0,0 +1,88 @@
/**
* A UI Element
*
* @typedef {ReactElement|HTMLElement} Modal
*/
/**
* UI Modal
*
* @typedef {Object} ModalProps
* @property {string} [header=null] -
* @property {string} [footer=null] -
* @property {string} [backdrop=false] -
* @property {string} [keyboard=false] -
* @property {number} [show=true] -
* @property {string} [closeButton=true] -
* @property {string} [title=null] - 'Modal Title'
* @property {boolean} [customClassName=null] - '.ModalClass'
*/
const uiModalServicePublicAPI = {
name: 'UIModalService',
hide,
show,
setServiceImplementation,
};
const uiModalServiceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUIModalService() {
return uiModalServicePublicAPI;
}
/**
* Show a new UI modal;
*
* @param {Modal} component React component
* @param {ModalProps} props { header, footer, backdrop, keyboard, show, closeButton, title, customClassName }
*/
function show(
component,
props = {
header: null,
footer: null,
backdrop: false,
keyboard: false,
show: true,
closeButton: true,
title: null,
customClassName: null,
}
) {
return uiModalServiceImplementation._show(component, props);
}
/**
* Hides/dismisses the modal, if currently shown
*
* @returns void
*/
function hide() {
return uiModalServiceImplementation._hide();
}
/**
*
*
* @param {*} {
* hide: hideImplementation,
* show: showImplementation,
* }
*/
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
}) {
if (hideImplementation) {
uiModalServiceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiModalServiceImplementation._show = showImplementation;
}
}
export default createUIModalService;

View File

@ -10,7 +10,7 @@
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicApi = {
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
@ -22,8 +22,8 @@ const uiNotificationServiceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUiNotificationService() {
return uiNotificationServicePublicApi;
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
@ -81,4 +81,4 @@ function setServiceImplementation({
}
}
export default createUiNotificationService;
export default createUINotificationService;

View File

@ -1,4 +1,5 @@
import ServicesManager from './ServicesManager.js';
import createUiNotificationService from './UINotificationService';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
export { createUiNotificationService, ServicesManager };
export { createUINotificationService, createUIModalService, ServicesManager };

View File

@ -13,7 +13,7 @@ const OHIFModal = ({
onHide,
footer: Footer,
header: Header,
children: Component,
children,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
@ -32,12 +32,9 @@ const OHIFModal = ({
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>
{Component && <Component hide={onHide} />}
</ReactBootstrapModal.Body>
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
{' '}
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
@ -52,9 +49,20 @@ OHIFModal.propTypes = {
show: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
footer: PropTypes.node,
header: PropTypes.node,
children: PropTypes.node,
footer: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
header: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export default OHIFModal;

View File

@ -1,15 +1,23 @@
import React, { useState, createContext, useContext } from 'react';
import React, {
useState,
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const ModalContext = createContext(null);
const { Provider, Consumer } = ModalContext;
const { Provider } = ModalContext;
export const useModal = () => useContext(ModalContext);
const ModalProvider = ({ children, modal: Modal }) => {
const ModalProvider = ({ children, modal: Modal, service }) => {
const DEFAULT_OPTIONS = {
component: null /* The component instance inside the modal. */,
header: null /* The content inside the modal header. */,
footer: null /* The content inside the modal footer. */,
backdrop: false /* Should the modal render a backdrop overlay. */,
keyboard: false /* Modal is dismissible via the esc key. */,
show: true /* Make the Modal visible or hidden. */,
@ -20,51 +28,81 @@ const ModalProvider = ({ children, modal: Modal }) => {
const [options, setOptions] = useState(DEFAULT_OPTIONS);
/**
* Sets the implementation of a modal service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [hide, service, show]);
/**
* Show the modal and override its configuration props.
*
* @returns void
*/
const show = (component, props = {}) =>
setOptions(Object.assign({}, options, props, { component }));
const show = useCallback(
(component, props = {}) =>
setOptions(Object.assign({}, options, props, { component })),
[options]
);
/**
* Hide the modal and set its properties to default.
*
* @returns void
*/
const hide = () => setOptions(DEFAULT_OPTIONS);
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [
DEFAULT_OPTIONS,
]);
const { component: Component } = options;
return (
<Provider value={{ ...options, show, hide }}>
<Consumer>
{props => {
const { component, footer, header, customClassName } = props;
return component ? (
<Modal
className={classNames(customClassName, component.className)}
backdrop={options.backdrop}
keyboard={options.keyboard}
show={options.show}
title={options.title}
closeButton={options.closeButton}
onHide={hide}
footer={footer}
header={header}
>
{component}
</Modal>
) : null;
}}
</Consumer>
<Provider value={{ show, hide }}>
{options.component && (
<Modal
className={classNames(
options.customClassName,
options.component.className
)}
backdrop={options.backdrop}
keyboard={options.keyboard}
show={options.show}
title={options.title}
closeButton={options.closeButton}
footer={options.footer}
header={options.header}
onHide={hide}
>
<Component {...options} show={show} hide={hide} />
</Modal>
)}
{children}
</Provider>
);
};
ModalProvider.defaultProps = {
service: null,
};
ModalProvider.propTypes = {
children: PropTypes.node,
modal: PropTypes.node,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
modal: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
};
/**
@ -74,7 +112,8 @@ ModalProvider.propTypes = {
*/
export const withModal = Component => {
return function WrappedComponent(props) {
return <Component {...props} modalContext={{ ...useModal() }} />;
const { show, hide } = useModal();
return <Component {...props} modal={{ show, hide }} />;
};
};

View File

@ -5,6 +5,8 @@ import React, {
useCallback,
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
@ -25,49 +27,62 @@ const SnackbarProvider = ({ children, service }) => {
const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]);
/**
* Sets the implementation of a notification service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
service.setServiceImplementation({ hide, show });
if (service) {
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'
);
const show = useCallback(
options => {
if (!options || (!options.title && !options.message)) {
console.warn(
'Snackbar cannot be rendered without required parameters: title | message'
);
return null;
}
return null;
}
const newItem = {
...DEFAULT_OPTIONS,
...options,
id: count,
visible: true,
};
const newItem = {
...DEFAULT_OPTIONS,
...options,
id: count,
visible: true,
};
setSnackbarItems(state => [...state, newItem]);
setCount(count + 1);
});
setSnackbarItems(state => [...state, newItem]);
setCount(count + 1);
},
[count, DEFAULT_OPTIONS]
);
const hide = useCallback(id => {
const hideItem = items => {
const newItems = items.map(item => {
if (item.id === id) {
item.visible = false;
}
const hide = useCallback(
id => {
const hideItem = items => {
const newItems = items.map(item => {
if (item.id === id) {
item.visible = false;
}
return item;
});
return item;
});
return newItems;
};
return newItems;
};
setSnackbarItems(state => hideItem(state));
setSnackbarItems(state => hideItem(state));
setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000);
});
setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000);
},
[setSnackbarItems]
);
const hideAll = () => {
// reset count
@ -95,6 +110,21 @@ const SnackbarProvider = ({ children, service }) => {
);
};
SnackbarProvider.defaultProps = {
service: null,
};
SnackbarProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
};
/**
*
* High Order Component to use the snackbar methods through a Class Component

View File

@ -8,7 +8,8 @@ import {
ExtensionManager,
ServicesManager,
HotkeysManager,
createUiNotificationService,
createUINotificationService,
createUIModalService,
utils,
} from '@ohif/core';
import React, { Component } from 'react';
@ -44,7 +45,8 @@ const commandsManagerConfig = {
};
// Services
const UINotificationService = createUiNotificationService();
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
@ -89,7 +91,7 @@ class App extends Component {
const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc);
_initServices([UINotificationService]);
_initServices([UINotificationService, UIModalService]);
_initExtensions(extensions, hotkeys);
_initServers(servers);
initWebWorkers();
@ -112,7 +114,10 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
</SnackbarProvider>
@ -133,7 +138,7 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
</SnackbarProvider>

View File

@ -7,8 +7,8 @@ export default {
*/
id: 'measurements-table',
preRegistration({ serviceManager, configuration = {} }) {
init({ serviceManager, configuration });
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getPanelModule() {
return {

View File

@ -34,7 +34,7 @@ const MEASUREMENT_ACTION_MAP = {
* @export
* @param {*} configuration
*/
export default function init({ serviceManager, configuration = {} }) {
export default function init({ servicesManager, 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...

View File

@ -20,7 +20,7 @@ class Header extends Component {
t: PropTypes.func.isRequired,
userManager: PropTypes.object,
user: PropTypes.object,
modalContext: PropTypes.object,
modal: PropTypes.object,
};
static defaultProps = {
@ -40,7 +40,7 @@ class Header extends Component {
t,
user,
userManager,
modalContext: { show },
modal: { show },
} = this.props;
this.options = [
{

View File

@ -287,7 +287,7 @@ function _handleBuiltIn({ behavior } = {}) {
}
if (behavior === 'DOWNLOAD_SCREEN_SHOT') {
this.props.modalContext.show(ConnectedViewportDownloadForm, {
this.props.modal.show(ConnectedViewportDownloadForm, {
title: this.props.t('Download High Quality Image'),
customClassName: 'ViewportDownloadForm',
});