feat: New dialog service (#1202)

* Add dialog provider boilerplate and improve provider imports

* Add react-draggable dependency

* Add dialog service boilerplate

* Refactor modal to use react-modal and remove dependency on modal themed styles

* Update cinedialog to use new dialog provider

* Add multiple dialogs and refactor provider (dialogs)

* Set app root (modal)

* Add modal zindex bigger than tooltip

* Block drag on buttons/svgs/paths/input and improve jsdoc

* Use guid util to generate dialog ids

* Explicit props in dialog provider

* Improve jsdocs

* Fix tests broken

* Fix boundaries calculation

* Remember last dialog position

* Update providers location

* Add scroll to modal

* Add toggable button toolbar and fix css modal
This commit is contained in:
Igor Octaviano 2019-11-19 16:17:33 -03:00 committed by Danny Brown
parent cdd75bda2a
commit f65639c2b0
39 changed files with 741 additions and 193 deletions

View File

@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
BUILT_IN: 'builtIn',
};
const TOOLBAR_BUTTON_BEHAVIORS = {
CINE: 'CINE',
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
};
/* TODO: Export enums through a extension manager. */
const enums = {
TOOLBAR_BUTTON_TYPES,
TOOLBAR_BUTTON_BEHAVIORS,
};
const definitions = [
{
id: 'StackScroll',
@ -102,7 +113,7 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'CINE',
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
},
},
{
@ -220,7 +231,8 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'DOWNLOAD_SCREEN_SHOT',
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
togglable: true,
},
},
],

View File

@ -19,7 +19,11 @@ import ui from './ui';
import user from './user.js';
import utils from './utils/';
import { createUINotificationService, createUIModalService } from './services';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
} from './services';
const OHIF = {
MODULE_TYPES,
@ -48,6 +52,7 @@ const OHIF = {
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export {
@ -76,6 +81,7 @@ export {
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export { OHIF };

View File

@ -12,6 +12,7 @@ describe('Top level exports', () => {
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
//
'utils',
'studies',

View File

@ -0,0 +1,125 @@
/**
* A UI Element
*
* @typedef {ReactElement|HTMLElement} DialogContent
*/
/**
* A UI Position
*
* @typedef {Object} ElementPosition
* @property {number} top -
* @property {number} left -
* @property {number} right -
* @property {number} bottom -
*/
/**
* UI Dialog
*
* @typedef {Object} DialogProps
* @property {string} id -
* @property {DialogContent} content -
* @property {boolean} isDraggable -
* @property {ElementPosition} defaultPosition -
* @property {ElementPosition} position -
* @property {Function} onSubmit -
* @property {Function} onClose -
* @property {Function} onStart -
* @property {Function} onStop -
* @property {Function} onDrag -
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, onSubmit, onClose, onStart, onDrag, onStop, isDraggable, defaultPosition, position }
*/
function create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
});
}
/**
* Destroys all dialogs, if any
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
}
/**
* Destroy the dialog, if currently created
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
}
/**
*
*
* @param {*} {
* dismiss: dismissImplementation,
* dismissAll: dismissAllImplementation,
* create: createImplementation,
* }
*/
function setServiceImplementation({
dismiss: dismissImplementation,
dismissAll: dismissAllImplementation,
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
}
}
export default createUIDialogService;

View File

@ -8,14 +8,11 @@
* 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 {boolean} [shouldCloseOnEsc=false] -
* @property {boolean} [isOpen=true] -
* @property {boolean} [closeButton=true] -
* @property {string} [title=null] - 'Modal Title'
* @property {boolean} [customClassName=null] - '.ModalClass'
* @property {string} [customClassName=null] - '.ModalClass'
*/
const uiModalServicePublicAPI = {
@ -38,16 +35,13 @@ function createUIModalService() {
* Show a new UI modal;
*
* @param {Modal} component React component
* @param {ModalProps} props { header, footer, backdrop, keyboard, show, closeButton, title, customClassName }
* @param {ModalProps} props { shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show(
component,
props = {
header: null,
footer: null,
backdrop: false,
keyboard: false,
show: true,
shouldCloseOnEsc: false,
isOpen: true,
closeButton: true,
title: null,
customClassName: null,

View File

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

View File

@ -52,7 +52,9 @@
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0",
"react-draggable": "^4.1.0",
"react-i18next": "^10.11.0",
"react-modal": "^3.11.1",
"react-with-direction": "1.3.0"
},
"devDependencies": {

View File

@ -1,7 +1,7 @@
import './CineDialog.styl';
import React, { PureComponent } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import PropTypes from 'prop-types';

View File

@ -58,7 +58,7 @@ const AboutContent = () => {
);
return (
<div data-cy="about-modal">
<div className="AboutContent" data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"

View File

@ -189,7 +189,7 @@ const ViewportDownloadForm = ({
};
return (
<>
<div className="ViewportDownloadForm">
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
@ -284,10 +284,6 @@ const ViewportDownloadForm = ({
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
@ -303,7 +299,7 @@ const ViewportDownloadForm = ({
</button>
</div>
</div>
</>
</div>
);
};

View File

@ -3,15 +3,13 @@
@import '../../../design/styles/common/button.styl'
.ViewportDownloadForm
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
border-radius: 8px;
width: inherit;
padding: 15px;
background: transparent;
display: flex;
flex-direction: column;
z-index: 1080 !important;
input, select
max-height: 30px;
.title
margin: 0;
font-weight: bold;
@ -67,6 +65,7 @@
padding: 10px;
border-radius: 5px;
align-self: center;
margin-bottom: 20px;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
@ -90,7 +89,6 @@
.actions
display: flex;
height: 60px;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import i18n from '@ohif/i18n';
import './LanguageSwitcher.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
const LanguageSwitcher = () => {
const getCurrentLanguage = (language = i18n.language) =>

View File

@ -1,7 +1,7 @@
import './MeasurementTable.styl';
import React, { Component } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { MeasurementTableItem } from './MeasurementTableItem.js';

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { OverlayTrigger } from './../overlayTrigger';

View File

@ -1,64 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal';
import Modal from 'react-modal';
import classNames from 'classnames';
import './OHIFModal.styl';
const customStyle = {
overlay: {
zIndex: 1071,
backgroundColor: 'rgb(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
};
Modal.setAppElement(document.getElementById('root'));
const OHIFModal = ({
className,
closeButton,
backdrop,
keyboard,
show,
shouldCloseOnEsc,
isOpen,
title,
onHide,
footer: Footer,
header: Header,
onClose,
children,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
backdrop={backdrop}
keyboard={keyboard}
show={show}
large={true}
title={title}
onHide={onHide}
>
{(Header || title) && (
<ReactBootstrapModal.Header closeButton={closeButton}>
{title && (
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
)}
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
</ReactBootstrapModal>
);
}) => {
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
×
</button>
)}
</div>
)
);
};
return (
<Modal
className={classNames('OHIFModal', className)}
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
style={customStyle}
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
</>
</Modal>
);
};
OHIFModal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
backdrop: PropTypes.bool,
keyboard: PropTypes.bool,
show: PropTypes.bool,
shouldCloseOnEsc: PropTypes.bool,
isOpen: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
footer: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
header: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
onClose: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,

View File

@ -0,0 +1,67 @@
.OHIFModal
background-color: var(--ui-gray-darker)
border-color: var(--ui-border-color)
color: var(--text-secondary-color)
border-radius: 6px
border: 0
color: var(--text-primary-color)
position: relative
-webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5)
box-shadow: 0 3px 9px rgba(0,0,0,.5);
background-clip: padding-box
outline: 0
@media (min-width: 320px)
width: 78%
min-width: 300px
@media (min-width: 768px)
width: 600px
@media (min-width: 992px)
width: 900px
&__content
padding: 20px
max-height: 90vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar
display: none;
&__header
display: flex
justify-content: space-between
align-items: center
border-bottom-width: 3px
border-bottom-style: solid
border-bottom-color: #000000
padding: 20px
position: relative
h4
color: var(--text-secondary-color)
font-size: 20px
font-weight: 500
line-height: 24px
padding-right: 24px
margin: 0
button
cursor: pointer
margin: -10px 0 0 0
padding: 0
background-color: transparent
border: none
color: var(--text-secondary-color)
font-size: 25px
font-weight: lighter
&:active,
&:focus,
&:focus:active
background-image: none
outline: 0
box-shadow: none

View File

@ -3,7 +3,7 @@ import React, { cloneElement } from 'react';
import PropTypes from 'prop-types';
import { Overlay as BaseOverlay } from 'react-overlays';
import elementType from 'prop-types-extra/lib/elementType';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import Fade from './Fade';

View File

@ -1,7 +1,7 @@
import React from 'react';
import SnackbarItem from './SnackbarItem';
import './Snackbar.css';
import { useSnackbarContext } from '../../utils/SnackbarProvider';
import { useSnackbarContext } from '../../contextProviders';
const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbarContext();

View File

@ -6,7 +6,7 @@ import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
/**
*

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Icon } from './../../elements/Icon';
// TODO: useTranslation
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
function StudyListLoadingText({ t: translate }) {
return (

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import './PaginationArea.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
class TablePagination extends PureComponent {
static defaultProps = {

View File

@ -1,7 +1,5 @@
.HotKeysPreferences
display: flex;
margin-right: -15px;
margin-left: -15px;
.column
width: 50%;

View File

@ -90,8 +90,8 @@ export class UserPreferences extends Component {
render() {
return (
<div>
<div className="dialog-separator-after">
<div className="UserPreferences">
<div className="UserPreferences__selector">
<ul className="nav nav-tabs">
<li
onClick={() => {

View File

@ -3,23 +3,27 @@
@import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl'
.modal-body
overflow: hidden
.UserPreferences
display: flex
flex-direction: column
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
&__selector
border-bottom: 3px solid black
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -22px
margin-right: -22px
max-height: 70vh
overflow-y: auto
padding: 22px
min-height: 500px
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
.popover
width: 300px
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -20px
margin-right: -20px
max-height: 70vh
overflow-y: auto
padding: 20px
min-height: 500px
.popover
width: 300px

View File

@ -2,7 +2,7 @@ import './UserPreferencesForm.styl';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import cloneDeep from 'lodash.clonedeep';
import isEqual from 'lodash.isequal';

View File

@ -16,7 +16,6 @@
.footer
display: flex
flex-direction: row
padding-bottom: 20px
justify-content: space-between
div

View File

@ -0,0 +1,208 @@
import React, {
useState,
createContext,
useContext,
useCallback,
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import Draggable from 'react-draggable';
import classNames from 'classnames';
import { utils } from '@ohif/core';
import './DialogProvider.styl';
const DialogContext = createContext(null);
export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
/**
* Sets the implementation of a dialog service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ create, dismiss, dismissAll });
}
}, [create, dismiss, service]);
/**
* Creates a dialog and return its id.
*
* @returns id
*/
const create = useCallback(
({
id,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) => {
let dialogId = id;
if (!dialogId) {
dialogId = utils.guid();
}
const newDialog = {
id: dialogId,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
};
setDialogs(dialogs => [...dialogs, newDialog]);
return dialogId;
},
[]
);
/**
* Dismisses the dialog with a given id.
*
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
/**
* Dismisses all dialogs.
*
* @returns void
*/
const dismissAll = () => {
setDialogs([]);
};
/**
* Moves the dialog to the foreground if clicked.
*
* @returns void
*/
const _reorder = id => {
setDialogs(dialogs => [
...dialogs.filter(dialog => dialog.id !== id),
dialogs.find(dialog => dialog.id === id),
]);
};
const _updateLastDialogPosition = dialogId => {
const draggableItemBounds = document
.querySelector(`#draggableItem-${dialogId}`)
.getBoundingClientRect();
setLastDialogPosition({
x: draggableItemBounds.x,
y: draggableItemBounds.y,
});
};
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, dialogs }}>
<div className="DraggableArea">
{dialogs.map(dialog => {
const {
id,
content: Dialog,
position /* Position of the dialog. {{x: 0, y: 0}} */,
defaultPosition,
isDraggable = true,
onStart = () => {},
onStop = () => {},
onDrag = () => {},
} = dialog;
return (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
bounds="parent"
onStart={event => {
const e = event || window.event;
const target = e.target || e.srcElement;
const BLACKLIST = ['SVG', 'BUTTON', 'PATH', 'INPUT'];
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
return false;
}
onStart(event);
}}
onStop={event => {
onStop(event);
setIsDragging(false);
return;
}}
onDrag={event => {
setIsDragging(true);
_reorder(id);
_updateLastDialogPosition(id);
onDrag(event);
}}
>
<div
id={`draggableItem-${id}`}
className={classNames(
'DraggableItem',
isDragging && 'dragging'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _reorder(id)}
>
<Dialog {...dialog} />
</div>
</Draggable>
);
})}
</div>
{children}
</DialogContext.Provider>
);
};
DialogProvider.defaultProps = {
service: null,
};
DialogProvider.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 dialog methods through a Class Component
*
*/
export const withDialog = Component => {
return function WrappedComponent(props) {
const { create, dismiss, dismissAll } = useDialog();
return <Component {...props} dialog={{ create, dismiss, dismissAll }} />;
};
};
export default DialogProvider;

View File

@ -0,0 +1,12 @@
.DraggableItem
div
cursor: grab !important
.DraggableItem.dragging
div
cursor: grabbing !important
.DraggableArea
width: 100vw
height: 100vh
position: absolute

View File

@ -16,14 +16,11 @@ export const useModal = () => useContext(ModalContext);
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. */,
shouldCloseOnEsc: false /* Modal is dismissible via the esc key. */,
isOpen: true /* Make the Modal visible or hidden. */,
closeButton: true /* Should the modal body render the close button. */,
title: null /* Should the modal render the title independently of the body content. */,
customClassName: null /* The custom class to style the modal. */,
customClassName: '' /* The custom class to style the modal. */,
};
const [options, setOptions] = useState(DEFAULT_OPTIONS);
@ -69,14 +66,11 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
options.customClassName,
options.component.className
)}
backdrop={options.backdrop}
keyboard={options.keyboard}
show={options.show}
shouldCloseOnEsc={options.keyboard}
isOpen={options.isOpen}
title={options.title}
closeButton={options.closeButton}
footer={options.footer}
header={options.header}
onHide={hide}
onClose={hide}
>
<Component {...options} show={show} hide={hide} />
</Modal>

View File

@ -0,0 +1,20 @@
export {
default as ModalProvider,
useModal,
withModal,
ModalConsumer,
} from './ModalProvider.js';
export {
default as SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './SnackbarProvider.js';
export {
default as LanguageProvider,
withTranslation,
} from './LanguageProvider.js';
export {
default as DialogProvider,
withDialog,
useDialog,
} from './DialogProvider.js';

View File

@ -49,15 +49,18 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
import SnackbarProvider, {
import {
SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './utils/SnackbarProvider';
import ModalProvider, {
DialogProvider,
useDialog,
withDialog,
ModalProvider,
ModalConsumer,
useModal,
withModal,
ModalConsumer,
} from './utils/ModalProvider';
} from './contextProviders';
export {
// Elements
@ -111,6 +114,9 @@ export {
ModalConsumer,
withModal,
OHIFModal,
DialogProvider,
withDialog,
useDialog,
// Hooks
useDebounce,
useMedia,

View File

@ -4,7 +4,7 @@ import { Icon } from './../elements/Icon';
import PropTypes from 'prop-types';
import React from 'react';
import classnames from 'classnames';
import { withTranslation } from '../utils/LanguageProvider';
import { withTranslation } from '../contextProviders';
export function ToolbarButton(props) {
const { isActive, icon, labelWhenActive, onClick, t } = props;

View File

@ -273,7 +273,7 @@ describe('OHIF Study Viewer Page', function() {
cy.get('[data-cy="about-item-menu"]')
.first()
.click();
cy.get('.modal-content')
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
@ -296,7 +296,7 @@ describe('OHIF Study Viewer Page', function() {
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('.close').click();
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.be.enabled');
});
});

View File

@ -1,7 +1,18 @@
import React, { Component } from 'react';
import { OidcProvider } from 'redux-oidc';
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';
// TODO: This should not be here
import './config';
import {
SnackbarProvider,
ModalProvider,
DialogProvider,
OHIFModal,
} from '@ohif/ui';
import {
CommandsManager,
@ -10,44 +21,48 @@ import {
HotkeysManager,
createUINotificationService,
createUIModalService,
createUIDialogService,
utils,
} from '@ohif/core';
import React, { Component } from 'react';
import i18n from '@ohif/i18n';
// TODO: This should not be here
import './config';
/** Utils */
import {
getUserManagerForOpenIdConnectClient,
initWebWorkers,
} from './utils/index.js';
import { I18nextProvider } from 'react-i18next';
// ~~ EXTENSIONS
/** Extensions */
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
import { OidcProvider } from 'redux-oidc';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { getActiveContexts } from './store/layout/selectors.js';
import i18n from '@ohif/i18n';
import store from './store';
import { SnackbarProvider, ModalProvider, OHIFModal } from '@ohif/ui';
// Contexts
/** Viewer */
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
/** Store */
import { getActiveContexts } from './store/layout/selectors.js';
import store from './store';
/** Contexts */
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
// ~~~~ APP SETUP
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
getAppState: () => store.getState(),
getActiveContexts: () => getActiveContexts(store.getState()),
};
// Services
/** Services */
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
@ -55,7 +70,7 @@ const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// ~~~~ END APP SETUP
/** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React
window.store = store;
@ -72,6 +87,7 @@ class App extends Component {
id: PropTypes.string.isRequired,
})
),
hotkeys: PropTypes.array,
};
static defaultProps = {
@ -91,7 +107,7 @@ class App extends Component {
const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc);
_initServices([UINotificationService, UIModalService]);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys);
_initServers(servers);
initWebWorkers();
@ -114,12 +130,14 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
@ -138,9 +156,11 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>

View File

@ -49,7 +49,6 @@ class Header extends Component {
onClick: () =>
show(AboutContent, {
title: t('OHIF Viewer - About'),
customClassName: 'AboutContent',
}),
},
{

View File

@ -7,6 +7,7 @@ import {
RoundedButtonGroup,
ToolbarButton,
withModal,
withDialog,
} from '@ohif/ui';
import './ToolbarRow.css';
@ -45,7 +46,6 @@ class ToolbarRow extends Component {
this.state = {
toolbarButtons: toolbarButtonDefinitions,
activeButtons: [],
isCineDialogOpen: false,
};
this._handleBuiltIn = _handleBuiltIn.bind(this);
@ -106,13 +106,6 @@ class ToolbarRow extends Component {
this.state.activeButtons
);
const cineDialogContainerStyle = {
display: this.state.isCineDialogOpen ? 'block' : 'none',
position: 'absolute',
top: '82px',
zIndex: 999,
};
const onPress = (side, value) => {
this.props.handleSidePanelChange(side, value);
};
@ -145,9 +138,6 @@ class ToolbarRow extends Component {
)}
</div>
</div>
<div className="CineDialogContainer" style={cineDialogContainerStyle}>
<ConnectedCineDialog />
</div>
</>
);
}
@ -160,7 +150,8 @@ function _getCustomButtonComponent(button, activeButtons) {
// Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented.
if (isValidComponent) {
const parentContext = this;
const isActive = activeButtons.includes(button.id);
const activeButtonsIds = activeButtons.map(button => button.id);
const isActive = activeButtonsIds.includes(button.id);
return (
<CustomComponent
@ -168,7 +159,7 @@ function _getCustomButtonComponent(button, activeButtons) {
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
button={button}
key={button.id}
activeButtons={activeButtons}
activeButtons={activeButtonsIds}
isActive={isActive}
/>
);
@ -181,7 +172,7 @@ function _getExpandableButtonComponent(button, activeButtons) {
const childButtons = button.buttons.map(childButton => {
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
if (activeButtons.indexOf(childButton.id) > -1) {
if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) {
activeCommand = childButton.id;
}
@ -206,7 +197,7 @@ function _getDefaultButtonComponent(button, activeButtons) {
label={button.label}
icon={button.icon}
onClick={_handleToolbarButtonClick.bind(this, button)}
isActive={activeButtons.includes(button.id)}
isActive={activeButtons.map(button => button.id).includes(button.id)}
/>
);
}
@ -241,6 +232,8 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
* @param {*} props
*/
function _handleToolbarButtonClick(button, evt, props) {
const { activeButtons } = this.state;
if (button.commandName) {
const options = Object.assign({ evt }, button.commandOptions);
commandsManager.runCommand(button.commandName, options);
@ -250,11 +243,12 @@ function _handleToolbarButtonClick(button, evt, props) {
// TODO: We can update this to be a `getter` on the extension to query
// For the active tools after we apply our updates?
if (button.type === 'setToolActive') {
this.setState({
activeButtons: [button.id],
});
const toggables = activeButtons.filter(
({ options }) => options && !options.togglable
);
this.setState({ activeButtons: [...toggables, button] });
} else if (button.type === 'builtIn') {
this._handleBuiltIn(button.options);
this._handleBuiltIn(button);
}
}
@ -279,21 +273,47 @@ function _getVisibleToolbarButtons() {
return toolbarButtonDefinitions;
}
function _handleBuiltIn({ behavior } = {}) {
if (behavior === 'CINE') {
this.setState({
isCineDialogOpen: !this.state.isCineDialogOpen,
});
function _handleBuiltIn(button) {
/* TODO: Keep cine button active until its unselected. */
const { dialog, modal, t } = this.props;
const { dialogId } = this.state;
const { id, options } = button;
if (options.behavior === 'CINE') {
if (dialogId) {
dialog.dismiss({ id: dialogId });
this.setState(state => ({
dialogId: null,
activeButtons: [
...state.activeButtons.filter(button => button.id !== id),
],
}));
} else {
const spacing = 20;
const { x, y } = document
.querySelector(`.ViewerMain`)
.getBoundingClientRect();
const newDialogId = dialog.create({
content: ConnectedCineDialog,
defaultPosition: {
x: x + spacing || 0,
y: y + spacing || 0,
},
});
this.setState(state => ({
dialogId: newDialogId,
activeButtons: [...state.activeButtons, button],
}));
}
}
if (behavior === 'DOWNLOAD_SCREEN_SHOT') {
this.props.modal.show(ConnectedViewportDownloadForm, {
title: this.props.t('Download High Quality Image'),
customClassName: 'ViewportDownloadForm',
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
modal.show(ConnectedViewportDownloadForm, {
title: t('Download High Quality Image'),
});
}
}
export default withTranslation(['Common', 'ViewportDownloadForm'])(
withModal(ToolbarRow)
withModal(withDialog(ToolbarRow))
);

View File

@ -1100,13 +1100,34 @@
pirates "^4.0.0"
source-map-support "^0.5.9"
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
"@babel/runtime@7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
version "7.7.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
@ -8074,6 +8095,11 @@ executable@4.1.1:
dependencies:
pify "^2.2.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exif-parser@^0.1.12, exif-parser@^0.1.9:
version "0.1.12"
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
@ -16019,6 +16045,14 @@ react-dom@^16.8.6:
prop-types "^15.6.2"
scheduler "^0.17.0"
react-draggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.1.0.tgz#e1c5b774001e32f0bff397254e1e9d5448ac92a4"
integrity sha512-Or/qe70cfymshqoC8Lsp0ukTzijJObehb7Vfl7tb5JRxoV+b6PDkOGoqYaWBzZ59k9dH/bwraLGsnlW78/3vrA==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-dropzone@^10.1.7:
version "10.1.10"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.10.tgz#f340290dfc26ac09ad68abc020ab6232c23d6cf3"
@ -16082,7 +16116,7 @@ react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa"
integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==
react-lifecycles-compat@^3.0.4:
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@ -16101,6 +16135,16 @@ react-live@2.0.1:
react-simple-code-editor "^0.9.0"
unescape "^0.2.0"
react-modal@^3.11.1:
version "3.11.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.1.tgz#2a0d6877c9e98f123939ea92d2bb4ad7fa5a17f9"
integrity sha512-8uN744Yq0X2lbfSLxsEEc2UV3RjSRb4yDVxRQ1aGzPo86QjNOwhQSukDb8U8kR+636TRTvfMren10fgOjAy9eA==
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-moment-proptypes@^1.6.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.7.0.tgz#89881479840a76c13574a86e3bb214c4ba564e7a"
@ -16613,6 +16657,11 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@ -20041,6 +20090,13 @@ warning@^3.0.0:
dependencies:
loose-envify "^1.0.0"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"