wire up UI Services
This commit is contained in:
1 parent
c9a4ab13ef
commit
db76eac047
14 files changed
+662
-28
No files matched your search
@@ -4,16 +4,22 @@ export { utils };
|
|||||||
|
|
||||||
/** CONTEXT/HOOKS */
|
/** CONTEXT/HOOKS */
|
||||||
export {
|
export {
|
||||||
|
DialogProvider,
|
||||||
|
useDialog,
|
||||||
|
withDialog,
|
||||||
DragAndDropProvider,
|
DragAndDropProvider,
|
||||||
ModalProvider,
|
ModalProvider,
|
||||||
ModalConsumer,
|
ModalConsumer,
|
||||||
useModal,
|
useModal,
|
||||||
withModal,
|
withModal,
|
||||||
ViewportDialogProvider,
|
|
||||||
useViewportDialog,
|
|
||||||
ImageViewerContext,
|
ImageViewerContext,
|
||||||
ImageViewerProvider,
|
ImageViewerProvider,
|
||||||
useImageViewer,
|
useImageViewer,
|
||||||
|
SnackbarProvider,
|
||||||
|
useSnackbar,
|
||||||
|
withSnackbar,
|
||||||
|
ViewportDialogProvider,
|
||||||
|
useViewportDialog,
|
||||||
ViewportGridContext,
|
ViewportGridContext,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
@@ -37,6 +43,7 @@ export {
|
|||||||
Label,
|
Label,
|
||||||
MeasurementsPanel,
|
MeasurementsPanel,
|
||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
|
Modal,
|
||||||
NavBar,
|
NavBar,
|
||||||
Notification,
|
Notification,
|
||||||
Select,
|
Select,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"react-dnd-html5-backend": "^10.0.2",
|
"react-dnd-html5-backend": "^10.0.2",
|
||||||
"react-dnd-touch-backend": "^10.0.2",
|
"react-dnd-touch-backend": "^10.0.2",
|
||||||
"react-dom": "16.11.0",
|
"react-dom": "16.11.0",
|
||||||
|
"react-modal": "^3.11.2",
|
||||||
"react-powerplug": "1.0.0",
|
"react-powerplug": "1.0.0",
|
||||||
"react-select": "^3.0.8",
|
"react-select": "^3.0.8",
|
||||||
"theme-ui": "^0.2.38"
|
"theme-ui": "^0.2.38"
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const customStyle = {
|
||||||
|
overlay: {
|
||||||
|
zIndex: 1071,
|
||||||
|
backgroundColor: 'rgb(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactModal.setAppElement(document.getElementById('root'));
|
||||||
|
|
||||||
|
const Modal = ({
|
||||||
|
className,
|
||||||
|
closeButton,
|
||||||
|
shouldCloseOnEsc,
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
title && (
|
||||||
|
<header>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
{closeButton && (
|
||||||
|
<button data-cy="close-button" onClick={onClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
className={classNames(className)}
|
||||||
|
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={title}
|
||||||
|
style={customStyle}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{renderHeader()}
|
||||||
|
<section>{children}</section>
|
||||||
|
</>
|
||||||
|
</ReactModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
closeButton: PropTypes.bool,
|
||||||
|
shouldCloseOnEsc: PropTypes.bool,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
title: PropTypes.string,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
/** The modal's content */
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import Modal from './Modal';
|
||||||
|
export default Modal;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SnackbarItem from './SnackbarItem';
|
||||||
|
import { useSnackbar } from '../../contextProviders';
|
||||||
|
|
||||||
|
const SnackbarContainer = () => {
|
||||||
|
const { snackbarItems, hide } = useSnackbar();
|
||||||
|
|
||||||
|
const renderItem = item => {
|
||||||
|
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!snackbarItems) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItems = () => {
|
||||||
|
const items = {
|
||||||
|
topLeft: [],
|
||||||
|
topCenter: [],
|
||||||
|
topRight: [],
|
||||||
|
bottomLeft: [],
|
||||||
|
bottomCenter: [],
|
||||||
|
bottomRight: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
snackbarItems.map(item => {
|
||||||
|
items[item.position].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{Object.keys(items).map(pos => {
|
||||||
|
if (!items[pos].length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={pos} className={`sb-container sb-${pos}`}>
|
||||||
|
{items[pos].map((item, index) => (
|
||||||
|
<div key={item.id + index}>{renderItem(item)}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{renderItems()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnackbarContainer;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const SnackbarItem = ({ options, onClose }) => {
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose(options.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (options.autoClose) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
}, options.duration);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span onClick={handleClose}>
|
||||||
|
<span>x</span>
|
||||||
|
</span>
|
||||||
|
{options.title && <div>{options.title}</div>}
|
||||||
|
{options.message && <div>{options.message}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnackbarItem;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
INFO: 'info',
|
||||||
|
WARNING: 'warning',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import InputText from './InputText';
|
|||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import MeasurementsPanel from './MeasurementsPanel';
|
import MeasurementsPanel from './MeasurementsPanel';
|
||||||
import MeasurementTable from './MeasurementTable';
|
import MeasurementTable from './MeasurementTable';
|
||||||
|
import Modal from './Modal';
|
||||||
import NavBar from './NavBar';
|
import NavBar from './NavBar';
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
@@ -62,6 +63,7 @@ export {
|
|||||||
Label,
|
Label,
|
||||||
MeasurementsPanel,
|
MeasurementsPanel,
|
||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
|
Modal,
|
||||||
NavBar,
|
NavBar,
|
||||||
Notification,
|
Notification,
|
||||||
Select,
|
Select,
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
const DialogContext = createContext(null);
|
||||||
|
|
||||||
|
export const useDialog = () => useContext(DialogContext);
|
||||||
|
|
||||||
|
const DialogProvider = ({ children, service }) => {
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dialogs, setDialogs] = useState([]);
|
||||||
|
const [lastDialogId, setLastDialogId] = useState(null);
|
||||||
|
const [lastDialogPosition, setLastDialogPosition] = useState(null);
|
||||||
|
const [centerPositions, setCenterPositions] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCenterPositions(
|
||||||
|
dialogs.map(dialog => ({
|
||||||
|
id: dialog.id,
|
||||||
|
...getCenterPosition(dialog.id),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}, [dialogs]);
|
||||||
|
|
||||||
|
const getCenterPosition = id => {
|
||||||
|
const root = document.querySelector('#root');
|
||||||
|
const centerX = root.offsetLeft + root.offsetWidth / 2;
|
||||||
|
const centerY = root.offsetTop + root.offsetHeight / 2;
|
||||||
|
const item = document.querySelector(`#draggableItem-${id}`);
|
||||||
|
const itemBounds = item.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: centerX - itemBounds.width / 2,
|
||||||
|
y: centerY - itemBounds.height / 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the implementation of a dialog service that can be used by extensions.
|
||||||
|
*
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (service) {
|
||||||
|
service.setServiceImplementation({ create, dismiss, dismissAll });
|
||||||
|
}
|
||||||
|
}, [create, dismiss, service]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI Dialog
|
||||||
|
*
|
||||||
|
* @typedef {Object} DialogProps
|
||||||
|
* @property {string} id The dialog id.
|
||||||
|
* @property {DialogContent} content The dialog content.
|
||||||
|
* @property {Object} contentProps The dialog content props.
|
||||||
|
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
||||||
|
* @property {boolean} showOverlay Controls dialog overlay.
|
||||||
|
* @property {boolean} centralize Center the dialog on the screen.
|
||||||
|
* @property {boolean} preservePosition Use last position instead of default.
|
||||||
|
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||||
|
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||||
|
* @property {Function} onStop Called when dragging stops.
|
||||||
|
* @property {Function} onDrag Called while dragging.
|
||||||
|
*/
|
||||||
|
|
||||||
|
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new dialog and return its id.
|
||||||
|
*
|
||||||
|
* @param {DialogProps} props The dialog props.
|
||||||
|
* @returns The new dialog id.
|
||||||
|
*/
|
||||||
|
const create = useCallback(props => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
let dialogId = id;
|
||||||
|
if (!dialogId) {
|
||||||
|
dialogId = utils.guid();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
||||||
|
setLastDialogId(dialogId);
|
||||||
|
|
||||||
|
return dialogId;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the dialog with a given id.
|
||||||
|
*
|
||||||
|
* @param {Object} props -
|
||||||
|
* @property {string} props.id The dialog id.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const dismiss = useCallback(
|
||||||
|
({ id }) =>
|
||||||
|
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses all dialogs.
|
||||||
|
*
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const dismissAll = () => {
|
||||||
|
setDialogs([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate if there are no dialogs present.
|
||||||
|
*
|
||||||
|
* @returns True if no dialogs are present.
|
||||||
|
*/
|
||||||
|
const isEmpty = () => dialogs && dialogs.length < 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the dialog to the foreground if clicked.
|
||||||
|
*
|
||||||
|
* @param {string} id The dialog id.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const _bringToFront = useCallback(id => {
|
||||||
|
setDialogs(dialogs => {
|
||||||
|
const topDialog = dialogs.find(dialog => dialog.id === id);
|
||||||
|
return topDialog
|
||||||
|
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
||||||
|
: dialogs;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderDialogs = () =>
|
||||||
|
dialogs.map(dialog => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
content: DialogContent,
|
||||||
|
contentProps,
|
||||||
|
defaultPosition,
|
||||||
|
centralize = false,
|
||||||
|
preservePosition = true,
|
||||||
|
isDraggable = true,
|
||||||
|
onStart,
|
||||||
|
onStop,
|
||||||
|
onDrag,
|
||||||
|
} = dialog;
|
||||||
|
|
||||||
|
let position =
|
||||||
|
(preservePosition && lastDialogPosition) || defaultPosition;
|
||||||
|
if (centralize) {
|
||||||
|
position = centerPositions.find(position => position.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
key={id}
|
||||||
|
disabled={!isDraggable}
|
||||||
|
position={position}
|
||||||
|
defaultPosition={position}
|
||||||
|
bounds="parent"
|
||||||
|
onStart={event => {
|
||||||
|
const e = event || window.event;
|
||||||
|
const target = e.target || e.srcElement;
|
||||||
|
const BLACKLIST = [
|
||||||
|
'SVG',
|
||||||
|
'BUTTON',
|
||||||
|
'PATH',
|
||||||
|
'INPUT',
|
||||||
|
'SPAN',
|
||||||
|
'LABEL',
|
||||||
|
];
|
||||||
|
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validCallback(onStart)) {
|
||||||
|
return onStart(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onStop={event => {
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
if (validCallback(onStop)) {
|
||||||
|
return onStop(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDrag={event => {
|
||||||
|
setIsDragging(true);
|
||||||
|
_bringToFront(id);
|
||||||
|
_updateLastDialogPosition(id);
|
||||||
|
|
||||||
|
if (validCallback(onDrag)) {
|
||||||
|
return onDrag(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={`draggableItem-${id}`}
|
||||||
|
className={classNames(
|
||||||
|
'DraggableItem',
|
||||||
|
isDragging && 'dragging',
|
||||||
|
isDraggable && 'draggable'
|
||||||
|
)}
|
||||||
|
style={{ zIndex: '999', position: 'absolute' }}
|
||||||
|
onClick={() => _bringToFront(id)}
|
||||||
|
>
|
||||||
|
<DialogContent {...dialog} {...contentProps} />
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last dialog position to be used as the new default position.
|
||||||
|
*
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const _updateLastDialogPosition = dialogId => {
|
||||||
|
const draggableItemBounds = document
|
||||||
|
.querySelector(`#draggableItem-${dialogId}`)
|
||||||
|
.getBoundingClientRect();
|
||||||
|
setLastDialogPosition({
|
||||||
|
x: draggableItemBounds.x,
|
||||||
|
y: draggableItemBounds.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validCallback = callback => callback && typeof callback === 'function';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
||||||
|
<div className="DraggableArea">
|
||||||
|
{dialogs.some(dialog => dialog.showOverlay) ? (
|
||||||
|
<div className="Overlay active">{renderDialogs()}</div>
|
||||||
|
) : (
|
||||||
|
renderDialogs()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</DialogContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* High Order Component to use the dialog methods through a Class Component
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const withDialog = Component => {
|
||||||
|
return function WrappedComponent(props) {
|
||||||
|
const { create, dismiss, dismissAll, isEmpty } = useDialog();
|
||||||
|
return (
|
||||||
|
<Component {...props} dialog={{ create, dismiss, dismissAll, isEmpty }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
DialogProvider.defaultProps = {
|
||||||
|
service: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
DialogProvider.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.func,
|
||||||
|
]).isRequired,
|
||||||
|
service: PropTypes.shape({
|
||||||
|
setServiceImplementation: PropTypes.func,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DialogProvider;
|
||||||
+11
-14
@@ -39,6 +39,17 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
|||||||
|
|
||||||
const [options, setOptions] = useState(DEFAULT_OPTIONS);
|
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.
|
* Show the modal and override its configuration props.
|
||||||
*
|
*
|
||||||
@@ -58,17 +69,6 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
|||||||
DEFAULT_OPTIONS,
|
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]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
content: ModalContent,
|
content: ModalContent,
|
||||||
contentProps,
|
contentProps,
|
||||||
@@ -115,18 +115,15 @@ ModalProvider.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ModalProvider.propTypes = {
|
ModalProvider.propTypes = {
|
||||||
/** Children that will be wrapped with Modal Context */
|
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
/** Modal component */
|
|
||||||
modal: PropTypes.oneOfType([
|
modal: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
PropTypes.func,
|
PropTypes.func,
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
/** service to be update once modal provider is instanciated */
|
|
||||||
service: PropTypes.shape({
|
service: PropTypes.shape({
|
||||||
setServiceImplementation: PropTypes.func,
|
setServiceImplementation: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import SnackbarContainer from '../components/Snackbar/SnackbarContainer';
|
||||||
|
import SnackbarTypes from '../components/Snackbar/SnackbarTypes';
|
||||||
|
|
||||||
|
const SnackbarContext = createContext(null);
|
||||||
|
|
||||||
|
export const useSnackbar = () => useContext(SnackbarContext);
|
||||||
|
|
||||||
|
const SnackbarProvider = ({ children, service }) => {
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
duration: 5000,
|
||||||
|
autoClose: true,
|
||||||
|
position: 'bottomRight',
|
||||||
|
type: SnackbarTypes.INFO,
|
||||||
|
};
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = {
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
...options,
|
||||||
|
id: count,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
setSnackbarItems(state => hideItem(state));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
[setSnackbarItems]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideAll = () => {
|
||||||
|
// reset count
|
||||||
|
setCount(1);
|
||||||
|
|
||||||
|
// remove all items from array
|
||||||
|
setSnackbarItems(() => []);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* expose snackbar methods to window for debug purposes
|
||||||
|
* TODO: Check if it's really necessary
|
||||||
|
*/
|
||||||
|
window.snackbar = {
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
hideAll,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SnackbarContext.Provider value={{ show, hide, hideAll, snackbarItems }}>
|
||||||
|
{!!snackbarItems && <SnackbarContainer />}
|
||||||
|
{children}
|
||||||
|
</SnackbarContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const withSnackbar = Component => {
|
||||||
|
return function WrappedComponent(props) {
|
||||||
|
const snackbarContext = {
|
||||||
|
...useSnackbarContext(),
|
||||||
|
};
|
||||||
|
return <Component {...props} snackbarContext={snackbarContext} />;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnackbarProvider;
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
export {
|
||||||
|
default as DialogProvider,
|
||||||
|
useDialog,
|
||||||
|
withDialog,
|
||||||
|
} from './DialogProvider'
|
||||||
|
|
||||||
|
export { default as DragAndDropProvider } from './DragAndDropProvider';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as ModalProvider,
|
default as ModalProvider,
|
||||||
useModal,
|
useModal,
|
||||||
@@ -5,21 +13,25 @@ export {
|
|||||||
ModalConsumer,
|
ModalConsumer,
|
||||||
} from './ModalProvider';
|
} from './ModalProvider';
|
||||||
|
|
||||||
export {
|
|
||||||
default as ViewportDialogProvider,
|
|
||||||
useViewportDialog,
|
|
||||||
} from './ViewportDialogProvider';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ImageViewerContext,
|
ImageViewerContext,
|
||||||
ImageViewerProvider,
|
ImageViewerProvider,
|
||||||
useImageViewer,
|
useImageViewer,
|
||||||
} from './ImageViewerProvider';
|
} from './ImageViewerProvider';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as SnackbarProvider,
|
||||||
|
useSnackbar,
|
||||||
|
withSnackbar,
|
||||||
|
} from './SnackbarProvider'
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as ViewportDialogProvider,
|
||||||
|
useViewportDialog,
|
||||||
|
} from './ViewportDialogProvider';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ViewportGridContext,
|
ViewportGridContext,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from './ViewportGridProvider';
|
} from './ViewportGridProvider';
|
||||||
|
|
||||||
export { default as DragAndDropProvider } from './DragAndDropProvider';
|
|
||||||
@@ -2,7 +2,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
||||||
import { ThemeWrapper } from '@ohif/ui';
|
import {
|
||||||
|
DialogProvider,
|
||||||
|
Modal,
|
||||||
|
ModalProvider,
|
||||||
|
SnackbarProvider,
|
||||||
|
ThemeWrapper,
|
||||||
|
} from '@ohif/ui';
|
||||||
// Viewer Project
|
// Viewer Project
|
||||||
// TODO: Should this influence study list?
|
// TODO: Should this influence study list?
|
||||||
import { appConfigContext } from '@state/appConfig.context';
|
import { appConfigContext } from '@state/appConfig.context';
|
||||||
@@ -10,7 +16,7 @@ import { useAppConfig } from '@hooks/useAppConfig';
|
|||||||
import createRoutes from './routes';
|
import createRoutes from './routes';
|
||||||
import appInit from './appInit.js';
|
import appInit from './appInit.js';
|
||||||
|
|
||||||
// Temporarily for testing
|
// TODO: Temporarily for testing
|
||||||
import '@ohif/mode-example';
|
import '@ohif/mode-example';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,11 +46,20 @@ function App({ config, defaultExtensions }) {
|
|||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager
|
servicesManager
|
||||||
);
|
);
|
||||||
|
const { UIDialogService, UIModalService, UINotificationService } = servicesManager.services;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<appConfigContext.Provider value={appConfigContextApi}>
|
<appConfigContext.Provider value={appConfigContextApi}>
|
||||||
<Router basename={routerBasename}>
|
<Router basename={routerBasename}>
|
||||||
<ThemeWrapper>{appRoutes}</ThemeWrapper>
|
<ThemeWrapper>
|
||||||
|
<SnackbarProvider service={UINotificationService}>
|
||||||
|
<DialogProvider service={UIDialogService}>
|
||||||
|
<ModalProvider modal={Modal} service={UIModalService}>
|
||||||
|
{appRoutes}
|
||||||
|
</ModalProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</ThemeWrapper>
|
||||||
</Router>
|
</Router>
|
||||||
</appConfigContext.Provider>
|
</appConfigContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -868,13 +868,27 @@
|
|||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.16"
|
source-map-support "^0.5.16"
|
||||||
|
|
||||||
"@babel/runtime@7.1.2", "@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.4":
|
"@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.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
|
||||||
version "7.7.6"
|
version "7.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
||||||
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.7.7", "@babel/runtime@^7.8.4":
|
||||||
|
version "7.9.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
|
||||||
|
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/standalone@^7.4.5":
|
"@babel/standalone@^7.4.5":
|
||||||
version "7.8.6"
|
version "7.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.8.6.tgz#1364534775c83bf7b7988e4ca98823bef56a0a53"
|
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.8.6.tgz#1364534775c83bf7b7988e4ca98823bef56a0a53"
|
||||||
@@ -16720,7 +16734,7 @@ react-live@^2.2.1:
|
|||||||
react-simple-code-editor "^0.10.0"
|
react-simple-code-editor "^0.10.0"
|
||||||
unescape "^1.0.1"
|
unescape "^1.0.1"
|
||||||
|
|
||||||
react-modal@^3.11.1:
|
react-modal@^3.11.1, react-modal@^3.11.2:
|
||||||
version "3.11.2"
|
version "3.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
|
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
|
||||||
integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==
|
integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==
|
||||||
@@ -17282,6 +17296,11 @@ regenerator-runtime@^0.11.0:
|
|||||||
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"
|
||||||
|
|||||||
Reference in new issue
Block a user