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', 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 = [ const definitions = [
{ {
id: 'StackScroll', id: 'StackScroll',
@ -102,7 +113,7 @@ const definitions = [
// //
type: TOOLBAR_BUTTON_TYPES.BUILT_IN, type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: { options: {
behavior: 'CINE', behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
}, },
}, },
{ {
@ -220,7 +231,8 @@ const definitions = [
// //
type: TOOLBAR_BUTTON_TYPES.BUILT_IN, type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: { 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 user from './user.js';
import utils from './utils/'; import utils from './utils/';
import { createUINotificationService, createUIModalService } from './services'; import {
createUINotificationService,
createUIModalService,
createUIDialogService,
} from './services';
const OHIF = { const OHIF = {
MODULE_TYPES, MODULE_TYPES,
@ -48,6 +52,7 @@ const OHIF = {
// //
createUINotificationService, createUINotificationService,
createUIModalService, createUIModalService,
createUIDialogService,
}; };
export { export {
@ -76,6 +81,7 @@ export {
// //
createUINotificationService, createUINotificationService,
createUIModalService, createUIModalService,
createUIDialogService,
}; };
export { OHIF }; export { OHIF };

View File

@ -12,6 +12,7 @@ describe('Top level exports', () => {
// //
'createUINotificationService', 'createUINotificationService',
'createUIModalService', 'createUIModalService',
'createUIDialogService',
// //
'utils', 'utils',
'studies', '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 * UI Modal
* *
* @typedef {Object} ModalProps * @typedef {Object} ModalProps
* @property {string} [header=null] - * @property {boolean} [shouldCloseOnEsc=false] -
* @property {string} [footer=null] - * @property {boolean} [isOpen=true] -
* @property {string} [backdrop=false] - * @property {boolean} [closeButton=true] -
* @property {string} [keyboard=false] -
* @property {number} [show=true] -
* @property {string} [closeButton=true] -
* @property {string} [title=null] - 'Modal Title' * @property {string} [title=null] - 'Modal Title'
* @property {boolean} [customClassName=null] - '.ModalClass' * @property {string} [customClassName=null] - '.ModalClass'
*/ */
const uiModalServicePublicAPI = { const uiModalServicePublicAPI = {
@ -38,16 +35,13 @@ function createUIModalService() {
* Show a new UI modal; * Show a new UI modal;
* *
* @param {Modal} component React component * @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( function show(
component, component,
props = { props = {
header: null, shouldCloseOnEsc: false,
footer: null, isOpen: true,
backdrop: false,
keyboard: false,
show: true,
closeButton: true, closeButton: true,
title: null, title: null,
customClassName: null, customClassName: null,

View File

@ -1,5 +1,11 @@
import ServicesManager from './ServicesManager.js'; import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService'; import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService'; 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": "9.4.0",
"react-dnd-html5-backend": "^9.4.0", "react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0", "react-dnd-touch-backend": "^9.4.0",
"react-draggable": "^4.1.0",
"react-i18next": "^10.11.0", "react-i18next": "^10.11.0",
"react-modal": "^3.11.1",
"react-with-direction": "1.3.0" "react-with-direction": "1.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,64 +1,69 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal'; import Modal from 'react-modal';
import classNames from 'classnames'; 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 = ({ const OHIFModal = ({
className, className,
closeButton, closeButton,
backdrop, shouldCloseOnEsc,
keyboard, isOpen,
show,
title, title,
onHide, onClose,
footer: Footer,
header: Header,
children, children,
}) => ( }) => {
<ReactBootstrapModal const renderHeader = () => {
className={classNames('modal fade themed in', className)} return (
backdrop={backdrop} title && (
keyboard={keyboard} <div className="OHIFModal__header">
show={show} <h4>{title}</h4>
large={true} {closeButton && (
title={title} <button data-cy="close-button" onClick={onClose}>
onHide={onHide} ×
> </button>
{(Header || title) && ( )}
<ReactBootstrapModal.Header closeButton={closeButton}> </div>
{title && ( )
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title> );
)} };
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header> return (
)} <Modal
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body> className={classNames('OHIFModal', className)}
{Footer && ( shouldCloseOnEsc={shouldCloseOnEsc}
<ReactBootstrapModal.Footer> isOpen={isOpen}
<Footer hide={onHide} /> title={title}
</ReactBootstrapModal.Footer> style={customStyle}
)} >
</ReactBootstrapModal> <>
); {renderHeader()}
<div className="OHIFModal__content">{children}</div>
</>
</Modal>
);
};
OHIFModal.propTypes = { OHIFModal.propTypes = {
className: PropTypes.string, className: PropTypes.string,
closeButton: PropTypes.bool, closeButton: PropTypes.bool,
backdrop: PropTypes.bool, shouldCloseOnEsc: PropTypes.bool,
keyboard: PropTypes.bool, isOpen: PropTypes.bool,
show: PropTypes.bool,
title: PropTypes.string, title: PropTypes.string,
onHide: PropTypes.func, onClose: PropTypes.func,
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([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
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 PropTypes from 'prop-types';
import { Overlay as BaseOverlay } from 'react-overlays'; import { Overlay as BaseOverlay } from 'react-overlays';
import elementType from 'prop-types-extra/lib/elementType'; import elementType from 'prop-types-extra/lib/elementType';
import { withTranslation } from '../../utils/LanguageProvider'; import { withTranslation } from '../../contextProviders';
import Fade from './Fade'; import Fade from './Fade';

View File

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

View File

@ -6,7 +6,7 @@ import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js'; import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js'; 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 React from 'react';
import { Icon } from './../../elements/Icon'; import { Icon } from './../../elements/Icon';
// TODO: useTranslation // TODO: useTranslation
import { withTranslation } from '../../utils/LanguageProvider'; import { withTranslation } from '../../contextProviders';
function StudyListLoadingText({ t: translate }) { function StudyListLoadingText({ t: translate }) {
return ( return (

View File

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

View File

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

View File

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

View File

@ -3,23 +3,27 @@
@import './../../design/styles/common/state.styl' @import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl' @import './../../design/styles/common/global.styl'
.modal-body .UserPreferences
overflow: hidden display: flex
flex-direction: column
.errorMessage &__selector
color: var(--state-error-text) border-bottom: 3px solid black
font-size: 10px
text-transform: uppercase;
.form-content .errorMessage
border-bottom: 3px solid var(--primary-background-color) color: var(--state-error-text)
margin-bottom: 20px font-size: 10px
margin-left: -22px text-transform: uppercase;
margin-right: -22px
max-height: 70vh
overflow-y: auto
padding: 22px
min-height: 500px
.popover .form-content
width: 300px 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 React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider'; import { withTranslation } from '../../contextProviders';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';

View File

@ -16,7 +16,6 @@
.footer .footer
display: flex display: flex
flex-direction: row flex-direction: row
padding-bottom: 20px
justify-content: space-between justify-content: space-between
div 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 ModalProvider = ({ children, modal: Modal, service }) => {
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
component: null /* The component instance inside the modal. */, component: null /* The component instance inside the modal. */,
header: null /* The content inside the modal header. */, shouldCloseOnEsc: false /* Modal is dismissible via the esc key. */,
footer: null /* The content inside the modal footer. */, isOpen: true /* Make the Modal visible or hidden. */,
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. */,
closeButton: true /* Should the modal body render the close button. */, closeButton: true /* Should the modal body render the close button. */,
title: null /* Should the modal render the title independently of the body content. */, 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); const [options, setOptions] = useState(DEFAULT_OPTIONS);
@ -69,14 +66,11 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
options.customClassName, options.customClassName,
options.component.className options.component.className
)} )}
backdrop={options.backdrop} shouldCloseOnEsc={options.keyboard}
keyboard={options.keyboard} isOpen={options.isOpen}
show={options.show}
title={options.title} title={options.title}
closeButton={options.closeButton} closeButton={options.closeButton}
footer={options.footer} onClose={hide}
header={options.header}
onHide={hide}
> >
<Component {...options} show={show} hide={hide} /> <Component {...options} show={show} hide={hide} />
</Modal> </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 Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js'; import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js'; import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
import SnackbarProvider, { import {
SnackbarProvider,
useSnackbarContext, useSnackbarContext,
withSnackbar, withSnackbar,
} from './utils/SnackbarProvider'; DialogProvider,
import ModalProvider, { useDialog,
withDialog,
ModalProvider,
ModalConsumer,
useModal, useModal,
withModal, withModal,
ModalConsumer, } from './contextProviders';
} from './utils/ModalProvider';
export { export {
// Elements // Elements
@ -111,6 +114,9 @@ export {
ModalConsumer, ModalConsumer,
withModal, withModal,
OHIFModal, OHIFModal,
DialogProvider,
withDialog,
useDialog,
// Hooks // Hooks
useDebounce, useDebounce,
useMedia, useMedia,

View File

@ -4,7 +4,7 @@ import { Icon } from './../elements/Icon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { withTranslation } from '../utils/LanguageProvider'; import { withTranslation } from '../contextProviders';
export function ToolbarButton(props) { export function ToolbarButton(props) {
const { isActive, icon, labelWhenActive, onClick, t } = 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"]') cy.get('[data-cy="about-item-menu"]')
.first() .first()
.click(); .click();
cy.get('.modal-content') cy.get('[data-cy="about-modal"]')
.as('aboutOverlay') .as('aboutOverlay')
.should('be.visible'); .should('be.visible');
@ -296,7 +296,7 @@ describe('OHIF Study Viewer Page', function() {
cy.percyCanvasSnapshot('About modal - Should display modal'); cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal //close modal
cy.get('.close').click(); cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.be.enabled'); 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'; import { hot } from 'react-hot-loader/root';
// TODO: This should not be here import {
import './config'; SnackbarProvider,
ModalProvider,
DialogProvider,
OHIFModal,
} from '@ohif/ui';
import { import {
CommandsManager, CommandsManager,
@ -10,44 +21,48 @@ import {
HotkeysManager, HotkeysManager,
createUINotificationService, createUINotificationService,
createUIModalService, createUIModalService,
createUIDialogService,
utils, utils,
} from '@ohif/core'; } from '@ohif/core';
import React, { Component } from 'react';
import i18n from '@ohif/i18n';
// TODO: This should not be here
import './config';
/** Utils */
import { import {
getUserManagerForOpenIdConnectClient, getUserManagerForOpenIdConnectClient,
initWebWorkers, initWebWorkers,
} from './utils/index.js'; } from './utils/index.js';
import { I18nextProvider } from 'react-i18next'; /** Extensions */
// ~~ EXTENSIONS
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions'; 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 WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext'; import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext'; import AppContext from './context/AppContext';
// ~~~~ APP SETUP /** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = { const commandsManagerConfig = {
getAppState: () => store.getState(), getAppState: () => store.getState(),
getActiveContexts: () => getActiveContexts(store.getState()), getActiveContexts: () => getActiveContexts(store.getState()),
}; };
// Services /** Services */
const UINotificationService = createUINotificationService(); const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService(); const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig); const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager); const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager(); const servicesManager = new ServicesManager();
@ -55,7 +70,7 @@ const extensionManager = new ExtensionManager({
commandsManager, commandsManager,
servicesManager, servicesManager,
}); });
// ~~~~ END APP SETUP /** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React // TODO[react] Use a provider when the whole tree is React
window.store = store; window.store = store;
@ -72,6 +87,7 @@ class App extends Component {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
}) })
), ),
hotkeys: PropTypes.array,
}; };
static defaultProps = { static defaultProps = {
@ -91,7 +107,7 @@ class App extends Component {
const { servers, extensions, hotkeys, oidc } = props; const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc); this.initUserManager(oidc);
_initServices([UINotificationService, UIModalService]); _initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys); _initExtensions(extensions, hotkeys);
_initServers(servers); _initServers(servers);
initWebWorkers(); initWebWorkers();
@ -114,12 +130,14 @@ class App extends Component {
<Router basename={routerBasename}> <Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}> <WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}> <SnackbarProvider service={UINotificationService}>
<ModalProvider <DialogProvider service={UIDialogService}>
modal={OHIFModal} <ModalProvider
service={UIModalService} modal={OHIFModal}
> service={UIModalService}
<OHIFStandaloneViewer userManager={userManager} /> >
</ModalProvider> <OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
</DialogProvider>
</SnackbarProvider> </SnackbarProvider>
</WhiteLabellingContext.Provider> </WhiteLabellingContext.Provider>
</Router> </Router>
@ -138,9 +156,11 @@ class App extends Component {
<Router basename={routerBasename}> <Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}> <WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}> <SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal} service={UIModalService}> <DialogProvider service={UIDialogService}>
<OHIFStandaloneViewer /> <ModalProvider modal={OHIFModal} service={UIModalService}>
</ModalProvider> <OHIFStandaloneViewer />
</ModalProvider>
</DialogProvider>
</SnackbarProvider> </SnackbarProvider>
</WhiteLabellingContext.Provider> </WhiteLabellingContext.Provider>
</Router> </Router>

View File

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

View File

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

View File

@ -1100,13 +1100,34 @@
pirates "^4.0.0" pirates "^4.0.0"
source-map-support "^0.5.9" 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" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies: dependencies:
regenerator-runtime "^0.13.2" 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": "@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" version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
@ -8074,6 +8095,11 @@ executable@4.1.1:
dependencies: dependencies:
pify "^2.2.0" 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: exif-parser@^0.1.12, exif-parser@^0.1.9:
version "0.1.12" version "0.1.12"
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" 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" prop-types "^15.6.2"
scheduler "^0.17.0" 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: react-dropzone@^10.1.7:
version "10.1.10" version "10.1.10"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.10.tgz#f340290dfc26ac09ad68abc020ab6232c23d6cf3" 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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa"
integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== 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" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@ -16101,6 +16135,16 @@ react-live@2.0.1:
react-simple-code-editor "^0.9.0" react-simple-code-editor "^0.9.0"
unescape "^0.2.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: react-moment-proptypes@^1.6.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.7.0.tgz#89881479840a76c13574a86e3bb214c4ba564e7a" 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" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== 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: regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
version "0.13.3" version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@ -20041,6 +20090,13 @@ warning@^3.0.0:
dependencies: dependencies:
loose-envify "^1.0.0" 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: watchpack@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"