feat: Notification Service (#1011)
* WIP - Notification Service * add snackbar provider into the main App component * feat: Notification Service * revert unmodified file * Update DicomHtmlViewport.js * update css classnames * revert toolbarRow * fix revert toolbarRow * styles adjustments and hide method * remove unecessary default options * minor fixes * fix long truncated messages * move SnackbarContext to ui project, rename, export
This commit is contained in:
parent
f07dca5fa4
commit
92c8996ed4
196
platform/ui/src/components/snackbar/Snackbar.css
Normal file
196
platform/ui/src/components/snackbar/Snackbar.css
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
.sb-container {
|
||||||
|
position: fixed;
|
||||||
|
width: var(--snackbar-size);
|
||||||
|
padding: 20px;
|
||||||
|
z-index: var(--snackbar-zIndex);
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-topLeft {
|
||||||
|
top: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-topCenter {
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
bottom: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-topRight {
|
||||||
|
top: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-bottomLeft {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-bottomCenter {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
top: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-bottomRight {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0px;
|
||||||
|
left: auto;
|
||||||
|
right: 0px;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-topLeft .sb-item,
|
||||||
|
.sb-topCenter .sb-item,
|
||||||
|
.sb-topRight .sb-item {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-bottomLeft .sb-item,
|
||||||
|
.sb-bottomCenter .sb-item,
|
||||||
|
.sb-bottomRight .sb-item {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeBtn {
|
||||||
|
height: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: none;
|
||||||
|
width: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeBtn:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeIcon {
|
||||||
|
display: block;
|
||||||
|
font-size: 0;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeIcon:after,
|
||||||
|
.sb-closeIcon:before {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 12px;
|
||||||
|
background-color: #222;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeIcon:before {
|
||||||
|
left: 4px;
|
||||||
|
top: 3px;
|
||||||
|
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: 0px 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-closeIcon:after {
|
||||||
|
right: 3px;
|
||||||
|
top: 5px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transform-origin: calc(100% - 3px) 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-message {
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-item {
|
||||||
|
position: relative;
|
||||||
|
transition: height 300ms ease;
|
||||||
|
animation: fadein 1s;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
|
||||||
|
0 3px 5px -1px rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-item a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-container .sb-hidden {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-success {
|
||||||
|
background-color: var(--snackbar-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-error {
|
||||||
|
background-color: var(--snackbar-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-warning {
|
||||||
|
background-color: var(--snackbar-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-info {
|
||||||
|
background-color: var(--snackbar-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Internet Explorer */
|
||||||
|
@-ms-keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
platform/ui/src/components/snackbar/SnackbarContainer.js
Normal file
53
platform/ui/src/components/snackbar/SnackbarContainer.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SnackbarItem from './SnackbarItem';
|
||||||
|
import './Snackbar.css';
|
||||||
|
import { useSnackbarContext } from '../../utils/SnackbarProvider';
|
||||||
|
|
||||||
|
const SnackbarContainer = () => {
|
||||||
|
const { snackbarItems, hide } = useSnackbarContext();
|
||||||
|
|
||||||
|
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 => (
|
||||||
|
<div key={item.id}>{renderItem(item)}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{renderItems()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnackbarContainer;
|
||||||
31
platform/ui/src/components/snackbar/SnackbarItem.js
Normal file
31
platform/ui/src/components/snackbar/SnackbarItem.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const SnackbarItem = ({ options, onClose }) => {
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose(options.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (options.autoClose) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
}, options.duration);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${options.visible ? '' : 'sb-hidden'} sb-${
|
||||||
|
options.type
|
||||||
|
} sb-item`}
|
||||||
|
>
|
||||||
|
<span className="sb-closeBtn" onClick={handleClose}>
|
||||||
|
<span className="sb-closeIcon">x</span>
|
||||||
|
</span>
|
||||||
|
{options.title && <div className="sb-title">{options.title}</div>}
|
||||||
|
{options.message && <div className="sb-message">{options.message}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnackbarItem;
|
||||||
6
platform/ui/src/components/snackbar/SnackbarTypes.js
Normal file
6
platform/ui/src/components/snackbar/SnackbarTypes.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
INFO: 'info',
|
||||||
|
WARNING: 'warning',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
@ -32,6 +32,10 @@ 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, {
|
||||||
|
useSnackbarContext,
|
||||||
|
withSnackbar,
|
||||||
|
} from './utils/SnackbarProvider';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ICONS,
|
ICONS,
|
||||||
@ -68,4 +72,7 @@ export {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
ViewerbaseDragDropContext,
|
ViewerbaseDragDropContext,
|
||||||
|
SnackbarProvider,
|
||||||
|
useSnackbarContext,
|
||||||
|
withSnackbar,
|
||||||
};
|
};
|
||||||
|
|||||||
102
platform/ui/src/utils/SnackbarProvider.js
Normal file
102
platform/ui/src/utils/SnackbarProvider.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState, createContext, useContext } from 'react';
|
||||||
|
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
|
||||||
|
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
|
||||||
|
|
||||||
|
const SnackbarContext = createContext(null);
|
||||||
|
|
||||||
|
export const useSnackbarContext = () => useContext(SnackbarContext);
|
||||||
|
|
||||||
|
const SnackbarProvider = ({ children }) => {
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
duration: 5000,
|
||||||
|
autoClose: true,
|
||||||
|
position: 'bottomRight',
|
||||||
|
type: SnackbarTypes.INFO,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [count, setCount] = useState(1);
|
||||||
|
const [snackbarItems, setSnackbarItems] = useState([]);
|
||||||
|
|
||||||
|
const show = 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
@ -27,6 +27,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
|||||||
import { getActiveContexts } from './store/layout/selectors.js';
|
import { getActiveContexts } from './store/layout/selectors.js';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import { SnackbarProvider } from '@ohif/ui';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||||
@ -99,7 +100,9 @@ class App extends Component {
|
|||||||
<WhiteLabellingContext.Provider
|
<WhiteLabellingContext.Provider
|
||||||
value={this.props.whiteLabelling}
|
value={this.props.whiteLabelling}
|
||||||
>
|
>
|
||||||
<OHIFStandaloneViewer userManager={userManager} />
|
<SnackbarProvider>
|
||||||
|
<OHIFStandaloneViewer userManager={userManager} />
|
||||||
|
</SnackbarProvider>
|
||||||
</WhiteLabellingContext.Provider>
|
</WhiteLabellingContext.Provider>
|
||||||
</Router>
|
</Router>
|
||||||
</UserManagerContext.Provider>
|
</UserManagerContext.Provider>
|
||||||
@ -116,7 +119,9 @@ class App extends Component {
|
|||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Router basename={this.props.routerBasename}>
|
<Router basename={this.props.routerBasename}>
|
||||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||||
<OHIFStandaloneViewer />
|
<SnackbarProvider>
|
||||||
|
<OHIFStandaloneViewer />
|
||||||
|
</SnackbarProvider>
|
||||||
</WhiteLabellingContext.Provider>
|
</WhiteLabellingContext.Provider>
|
||||||
</Router>
|
</Router>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* Sizes */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
/* Sizes */
|
||||||
|
|
||||||
--top-bar-height: 40px;
|
--top-bar-height: 40px;
|
||||||
--top-bar-expanded-height: 160px;
|
--top-bar-expanded-height: 160px;
|
||||||
--toolbar-height: 78px;
|
--toolbar-height: 78px;
|
||||||
@ -9,19 +9,26 @@
|
|||||||
--right-sidepanel-menu-width: 323px;
|
--right-sidepanel-menu-width: 323px;
|
||||||
--study-list-padding: 8%;
|
--study-list-padding: 8%;
|
||||||
--study-list-padding-medium-screen: 10px;
|
--study-list-padding-medium-screen: 10px;
|
||||||
}
|
--snackbar-size: 344px;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
|
||||||
:root {
|
|
||||||
--transition-duration: 0.3s;
|
--transition-duration: 0.3s;
|
||||||
--transition-effect: ease;
|
--transition-effect: ease;
|
||||||
--sidepanel-transition: all 0.3s ease;
|
--sidepanel-transition: all 0.3s ease;
|
||||||
}
|
|
||||||
|
|
||||||
/* Thicknesses */
|
/* Thicknesses */
|
||||||
|
|
||||||
:root {
|
|
||||||
--viewport-border-thickness: 1px;
|
--viewport-border-thickness: 1px;
|
||||||
--ui-border-thickness: 1px;
|
--ui-border-thickness: 1px;
|
||||||
|
|
||||||
|
/** Colors **/
|
||||||
|
|
||||||
|
--snackbar-success: rgba(94, 164, 0, 0.9);
|
||||||
|
--snackbar-error: rgba(236, 61, 61, 0.9);
|
||||||
|
--snackbar-warning: rgba(235, 173, 23, 0.9);
|
||||||
|
--snackbar-info: rgba(54, 156, 199, 0.9);
|
||||||
|
|
||||||
|
/** z-Index **/
|
||||||
|
--snackbar-zIndex: 10;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user