get ViewportDialog to display as a part of machine transition

This commit is contained in:
dannyrb 2020-06-05 15:19:09 -04:00
parent a4fc5e21b2
commit 00f8d56a4e
14 changed files with 318 additions and 212 deletions

View File

@ -0,0 +1,81 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
import {
machineConfiguration,
defaultOptions,
} from './measurementTrackingMachine';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider(
UIViewportDialogService,
{ children }
) {
function promptUser() {
// TODO: ... ActiveViewport? Or Study + Series --> Viewport?
// Let's just use zero for meow?
return new Promise(function(resolve, reject) {
const handleSubmit = result => {
UIViewportDialogService.hide();
resolve(result);
};
UIViewportDialogService.show({
viewportIndex: 0,
type: 'info',
message: 'Would you like to track?',
actions: [
{ type: 'cancel', text: 'No', value: 0 },
{ type: 'secondary', text: 'No, do not ask again', value: -1 },
{ type: 'primary', text: 'Yes', value: 1 },
],
onSubmit: handleSubmit,
});
});
}
const machOptions = Object.assign({}, defaultOptions);
// Merge services
machOptions.services = Object.assign({}, machOptions.services, {
shouldTrackPrompt: promptUser,
});
console.log(
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
machOptions
);
const measurementTrackingMachine = Machine(machineConfiguration, machOptions);
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
};
export {
TrackedMeasurementsContext,
TrackedMeasurementsContextProvider,
useTrackedMeasurements,
};

View File

@ -1,45 +1,5 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useMachine } from '@xstate/react';
import {
measurementTrackingMachine,
defaultOptions,
} from './measurementTrackingMachine';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider({ children }) {
// TODO: Configure `UIViewportNotificationService` for appropriate actions
const measurementTrackingMachineOptions = Object.assign({}, defaultOptions);
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine, measurementTrackingMachineOptions);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
};
export { export {
TrackedMeasurementsContext, TrackedMeasurementsContext,
TrackedMeasurementsContextProvider, TrackedMeasurementsContextProvider,
useTrackedMeasurements, useTrackedMeasurements,
}; } from './TrackedMeasurementsContext.jsx';

View File

@ -27,7 +27,7 @@ const machineConfiguration = {
prompt: { prompt: {
invoke: { invoke: {
id: 'shouldTrackPrompt', id: 'shouldTrackPrompt',
src: () => confirmDialog('Should we start tracking?'), src: 'shouldTrackPrompt',
onDone: { onDone: {
target: 'validateResponse', target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }), actions: assign({ promptResponse: (ctx, evt) => evt.data }),
@ -135,6 +135,11 @@ function confirmDialog(msg) {
} }
const defaultOptions = { const defaultOptions = {
services: {
shouldTrackPrompt: () => {
return confirmDialog('Should we start tracking?');
},
},
actions: { actions: {
clearContext: assign({ clearContext: assign({
prevTrackedStudy: '', prevTrackedStudy: '',
@ -186,11 +191,13 @@ const defaultOptions = {
}, },
}; };
const measurementTrackingMachine = Machine(machineConfiguration, defaultOptions); // const measurementTrackingMachine = Machine(
// machineConfiguration,
// defaultOptions
// );
// .transition(state, eventArgument).value // .transition(state, eventArgument).value
// const service = interpret(measurementTrackingMachine).start(); // const service = interpret(measurementTrackingMachine).start();
// .send(event): nextState // .send(event): nextState
// .state (getter) // .state (getter)
// .onTransition(state => { state.vale }) // .onTransition(state => { state.vale })
export { defaultOptions, machineConfiguration, measurementTrackingMachine }; export { defaultOptions, machineConfiguration };
export default measurementTrackingMachine;

View File

@ -4,12 +4,18 @@ import {
useTrackedMeasurements, useTrackedMeasurements,
} from './contexts'; } from './contexts';
function getContextModule() { function getContextModule({ servicesManager }) {
const { UIViewportDialogService } = servicesManager.services;
const BoundTrackedMeasurementsContextProvider = TrackedMeasurementsContextProvider.bind(
null,
UIViewportDialogService
);
return [ return [
{ {
name: 'TrackedMeasurementsContext', name: 'TrackedMeasurementsContext',
context: TrackedMeasurementsContext, context: TrackedMeasurementsContext,
provider: TrackedMeasurementsContextProvider, provider: BoundTrackedMeasurementsContextProvider,
}, },
]; ];
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import('./viewports/OHIFCornerstoneViewport'); return import('./viewports/TrackedCornerstoneViewport');
}); });
const OHIFCornerstoneViewport = props => { const OHIFCornerstoneViewport = props => {

View File

@ -3,12 +3,16 @@ import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import CornerstoneViewport from 'react-cornerstone-viewport'; import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import { ViewportActionBar, useViewportGrid } from '@ohif/ui'; import {
Notification,
ViewportActionBar,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle'; import throttle from 'lodash.throttle';
import { useTrackedMeasurements } from './../getContextModule'; import { useTrackedMeasurements } from './../getContextModule';
// const cine = viewportSpecificData.cine; // const cine = viewportSpecificData.cine;
// isPlaying = cine.isPlaying === true; // isPlaying = cine.isPlaying === true;
@ -16,7 +20,7 @@ import { useTrackedMeasurements } from './../getContextModule';
const { StackManager } = OHIF.utils; const { StackManager } = OHIF.utils;
function OHIFCornerstoneViewport({ function TrackedCornerstoneViewport({
children, children,
dataSource, dataSource,
displaySet, displaySet,
@ -24,6 +28,10 @@ function OHIFCornerstoneViewport({
}) { }) {
const [trackedMeasurements] = useTrackedMeasurements(); const [trackedMeasurements] = useTrackedMeasurements();
const [{ viewports }, dispatchViewportGrid] = useViewportGrid(); const [{ viewports }, dispatchViewportGrid] = useViewportGrid();
// viewportIndex, onSubmit
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
console.warn('VIEWPORT DIALOG', viewportDialogState, viewportDialogApi);
const [viewportData, setViewportData] = useState(null); const [viewportData, setViewportData] = useState(null);
// TODO: Still needed? Better way than import `OHIF` and destructure? // TODO: Still needed? Better way than import `OHIF` and destructure?
// Why is this managed by `core`? // Why is this managed by `core`?
@ -98,7 +106,7 @@ function OHIFCornerstoneViewport({
// ); // );
// TODO: This display contains the meta for all instances. // TODO: This display contains the meta for all instances.
// That can't be right... // That can't be right...
console.log('DISPLAYSET', displaySet) console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, ''); // const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta); // console.log(seriesMeta);
@ -145,22 +153,35 @@ function OHIFCornerstoneViewport({
}, },
}} }}
/> />
<CornerstoneViewport {/* TODO: Viewport interface to accept stack or layers of content like this? */}
viewportIndex={viewportIndex} <div className="relative flex flex-row w-full h-full">
imageIds={imageIds} <CornerstoneViewport
imageIdIndex={currentImageIdIndex} viewportIndex={viewportIndex}
// TODO: ViewportGrid Context? imageIds={imageIds}
isActive={true} // todo imageIdIndex={currentImageIdIndex}
isStackPrefetchEnabled={true} // todo // TODO: ViewportGrid Context?
isPlaying={false} isActive={true} // todo
frameRate={24} isStackPrefetchEnabled={true} // todo
/> isPlaying={false}
{childrenWithProps} frameRate={24}
/>
<div style={{ position: 'absolute' }}>
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
/>
)}
</div>
{childrenWithProps}
</div>
</> </>
); );
} }
OHIFCornerstoneViewport.propTypes = { TrackedCornerstoneViewport.propTypes = {
displaySet: PropTypes.object.isRequired, displaySet: PropTypes.object.isRequired,
viewportIndex: PropTypes.number.isRequired, viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object, dataSource: PropTypes.object,
@ -168,7 +189,7 @@ OHIFCornerstoneViewport.propTypes = {
customProps: PropTypes.object, customProps: PropTypes.object,
}; };
OHIFCornerstoneViewport.defaultProps = { TrackedCornerstoneViewport.defaultProps = {
customProps: {}, customProps: {},
}; };
@ -207,6 +228,6 @@ async function _getViewportData(dataSource, displaySet) {
}; };
return viewportData; return viewportData;
}; }
export default OHIFCornerstoneViewport; export default TrackedCornerstoneViewport;

View File

@ -17,7 +17,8 @@ const publicAPI = {
}; };
const serviceImplementation = { const serviceImplementation = {
_viewports: [], _hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
}; };
/** /**
@ -25,39 +26,21 @@ const serviceImplementation = {
* *
* @param {ViewportDialogProps} props { content, contentProps, viewportIndex } * @param {ViewportDialogProps} props { content, contentProps, viewportIndex }
*/ */
function _show({ content = null, contentProps = null, viewportIndex }) { function _show({ viewportIndex, type, message, actions, onSubmit }) {
const viewportIndexImplementation = return serviceImplementation._show({
(viewportIndex !== undefined &&
serviceImplementation._viewports[viewportIndex]) ||
{};
if (!viewportIndexImplementation._show) {
console.warn('show() NOT IMPLEMENTED');
return;
}
return viewportIndexImplementation._show({
content,
contentProps,
viewportIndex, viewportIndex,
type,
message,
actions,
onSubmit,
}); });
} }
/** /**
* Hides/dismisses the viewport dialog, if currently shown * Hides/dismisses the viewport dialog, if currently shown
*
* @param {*} { viewportIndex }
*/ */
function _hide({ viewportIndex }) { function _hide() {
const viewportIndexImplementation = return serviceImplementation._hide();
(viewportIndex && serviceImplementation._viewports[viewportIndex]) || {};
if (!viewportIndexImplementation._hide) {
console.warn('hide() NOT IMPLEMENTED');
return;
}
return viewportIndexImplementation._hide();
} }
/** /**
@ -72,22 +55,12 @@ function _hide({ viewportIndex }) {
function setServiceImplementation({ function setServiceImplementation({
hide: hideImplementation, hide: hideImplementation,
show: showImplementation, show: showImplementation,
viewportIndex,
}) { }) {
if (viewportIndex !== undefined) { if (hideImplementation) {
const newImplementations = {}; serviceImplementation._hide = hideImplementation;
if (hideImplementation) { }
newImplementations._hide = hideImplementation; if (showImplementation) {
} serviceImplementation._show = showImplementation;
if (showImplementation) {
newImplementations._show = showImplementation;
}
serviceImplementation._viewports[viewportIndex] = Object.assign(
{},
serviceImplementation._viewports[viewportIndex],
newImplementations
);
} }
} }

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Icon } from '@ohif/ui'; import { Button, Icon } from '@ohif/ui';
const Notification = ({ type, text, actionButtons }) => { const Notification = ({ type, message, actions, onSubmit }) => {
const iconsByType = { const iconsByType = {
error: { error: {
icon: 'info', icon: 'info',
@ -35,12 +35,30 @@ const Notification = ({ type, text, actionButtons }) => {
const { icon, color } = getIconData(); const { icon, color } = getIconData();
return ( return (
<div className="mx-2 mt-2 p-2 flex flex-col bg-common-bright rounded"> <div className="flex flex-col p-2 mx-2 mt-2 rounded bg-common-bright">
<div className="flex flex-grow"> <div className="flex flex-grow">
<Icon name={icon} className={classnames('w-5', color)} /> <Icon name={icon} className={classnames('w-5', color)} />
<span className="text-base text-black ml-2">{text}</span> <span className="ml-2 text-base text-black">{message}</span>
</div>
<div className="flex justify-end mt-2">
{actions.map((action, index) => {
const isFirst = index === 0;
const isPrimary = action.type === 'primary';
return (
<Button
key={index}
className={classnames({ 'ml-2': !isFirst })}
color={isPrimary ? 'primary' : undefined}
onClick={() => {
onSubmit(action.value);
}}
>
{action.text}
</Button>
);
})}
</div> </div>
<div className="flex justify-end mt-2">{actionButtons}</div>
</div> </div>
); );
}; };
@ -51,8 +69,15 @@ Notification.defaultProps = {
Notification.propTypes = { Notification.propTypes = {
type: PropTypes.string, type: PropTypes.string,
text: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
actionButtons: PropTypes.node.isRequired, actions: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
type: PropTypes.oneOf(['primary', 'secondary', 'cancel']).isRequired,
})
).isRequired,
onSubmit: PropTypes.func.isRequired,
}; };
export default Notification; export default Notification;

View File

@ -5,7 +5,7 @@ route: components/notification
--- ---
import { Playground, Props } from 'docz'; import { Playground, Props } from 'docz';
import { Notification, Button } from '@ohif/ui'; import { Notification } from '@ohif/ui';
# Notification # Notification
@ -22,23 +22,31 @@ import { Notification } from '@ohif/ui';
<Playground> <Playground>
{() => { {() => {
const actionButtons = () => {
return (
<div>
<Button>No</Button>
<Button className="ml-2">No, do not ask again</Button>
<Button className="ml-2" color="primary">
Yes
</Button>
</div>
);
};
return ( return (
<div className="p-4 w-full lg:w-2/3"> <div className="p-4 w-full lg:w-2/3">
<Notification <Notification
text="Track all measurement for this series?" message="Track all measurement for this series?"
type="info" type="info"
actionButtons={actionButtons()} actions={[
{
type: 'cancel',
text: 'No',
value: 0,
},
{
type: 'secondary',
text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => {
window.alert(value);
}}
/> />
</div> </div>
); );

View File

@ -4,7 +4,7 @@ import { ViewportActionBar, Notification, Button } from '@ohif/ui';
const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => { const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
return ( return (
<div className="flex flex-col h-full relative"> <div className="relative flex flex-col h-full">
<div className="absolute top-0 left-0 w-full"> <div className="absolute top-0 left-0 w-full">
<ViewportActionBar <ViewportActionBar
onSeriesChange={onSeriesChange} onSeriesChange={onSeriesChange}
@ -13,17 +13,28 @@ const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
{/* TODO: NOTIFICATION API DEFINITION - OHIF-112 */} {/* TODO: NOTIFICATION API DEFINITION - OHIF-112 */}
<Notification <Notification
text="Track all measurement for this series?" message="Track all measurement for this series?"
type="info" type="info"
actionButtons={ actions={[
<div> {
<Button>No</Button> type: 'cancel',
<Button className="ml-2">No, do not ask again</Button> text: 'No',
<Button className="ml-2" color="primary"> value: 0,
Yes },
</Button> {
</div> type: 'secondary',
} text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => {
window.alert(value);
}}
/> />
</div> </div>

View File

@ -7,10 +7,20 @@ import React, {
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const DEFAULT_OPTIONS = { const DEFAULT_STATE = {
content: null, viewportIndex: null,
contentProps: null, message: undefined,
customClassName: null, type: 'info', // "error" | "warning" | "info" | "success"
actions: undefined, // array of { type, text, value }
// dismissable?
// blockInteraction (single viewport? allViewports? everything?)
// TODO: Buttons --> type/color? text? value?
onSubmit: () => {
console.log('btn value?');
},
onDismiss: () => {
console.log('dismiss? -1');
},
}; };
const ViewportDialogContext = createContext(null); const ViewportDialogContext = createContext(null);
@ -18,42 +28,26 @@ const { Provider } = ViewportDialogContext;
export const useViewportDialog = () => useContext(ViewportDialogContext); export const useViewportDialog = () => useContext(ViewportDialogContext);
const ViewportDialogProvider = ({ const ViewportDialogProvider = ({ children, service }) => {
children, const [viewportDialogState, setViewportDialogState] = useState(DEFAULT_STATE);
dialog: Dialog, const show = useCallback(
service, params => setViewportDialogState({ ...viewportDialogState, ...params }),
viewportIndex, [viewportDialogState]
}) => { );
const [options, setOptions] = useState(DEFAULT_OPTIONS); const hide = useCallback(() => setViewportDialogState(DEFAULT_STATE), []);
const show = useCallback((props) => setOptions({ ...options, ...props }), [
options,
]);
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), []);
useEffect(() => { useEffect(() => {
if (service) { if (service) {
service.setServiceImplementation({ hide, show, viewportIndex }); service.setServiceImplementation({
hide,
show,
});
} }
}, [hide, service, show, viewportIndex]); }, [hide, service, show]);
const {
content: ViewportDialogContent,
contentProps,
customClassName,
} = options;
return ( return (
<Provider value={{ show, hide }}> <Provider value={[viewportDialogState, { show, hide }]}>
<div className="relative w-full h-full"> {children}
{ViewportDialogContent && (
<Dialog className={customClassName}>
<ViewportDialogContent {...contentProps} show={show} hide={hide} />
</Dialog>
)}
{children}
</div>
</Provider> </Provider>
); );
}; };
@ -64,16 +58,9 @@ ViewportDialogProvider.propTypes = {
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node, PropTypes.node,
]).isRequired, ]).isRequired,
/** dialog component */
dialog: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
service: PropTypes.shape({ service: PropTypes.shape({
setServiceImplementation: PropTypes.func, setServiceImplementation: PropTypes.func,
}), }),
viewportIndex: PropTypes.number,
}; };
export default ViewportDialogProvider; export default ViewportDialogProvider;

View File

@ -28,19 +28,26 @@ component across all application.
const ViewportNotification = ({ hide }) => { const ViewportNotification = ({ hide }) => {
return ( return (
<Notification <Notification
text="Track all measurement for this series?" message="Track all measurement for this series?"
type="info" type="info"
actionButtons={ actions={[
<div> {
<Button onClick={hide}>No</Button> type: 'cancel',
<Button onClick={hide} className="ml-2"> text: 'No',
No, do not ask again value: 0,
</Button> },
<Button onClick={hide} className="ml-2" color="primary"> {
Yes type: 'secondary',
</Button> text: 'No, do not ask again',
</div> value: -1,
} },
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => { window.alert(value); }}
/> />
); );
}; };
@ -88,17 +95,26 @@ component across all application.
const ViewportNotification = ({ hide }) => { const ViewportNotification = ({ hide }) => {
return ( return (
<Notification <Notification
text="Track all measurement for this series?" message="Track all measurement for this series?"
type="info" type="info"
actionButtons={ actions={[
<div> {
<Button onClick={hide}>No</Button> type: 'cancel',
<Button className="ml-2">No, do not ask again</Button> text: 'No',
<Button className="ml-2" color="primary"> value: 0,
Yes },
</Button> {
</div> type: 'secondary',
} text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => { window.alert(value); }}
/> />
); );
}; };

View File

@ -6,8 +6,10 @@ import {
DialogProvider, DialogProvider,
Modal, Modal,
ModalProvider, ModalProvider,
Notification,
SnackbarProvider, SnackbarProvider,
ThemeWrapper, ThemeWrapper,
ViewportDialogProvider,
ViewportGridProvider, ViewportGridProvider,
} from '@ohif/ui'; } from '@ohif/ui';
// Viewer Project // Viewer Project
@ -48,7 +50,12 @@ function App({ config, defaultExtensions }) {
extensionManager, extensionManager,
servicesManager servicesManager
); );
const { UIDialogService, UIModalService, UINotificationService } = servicesManager.services; const {
UIDialogService,
UIModalService,
UINotificationService,
UIViewportDialogService,
} = servicesManager.services;
// A UI Service may need to use the ViewportGrid context // A UI Service may need to use the ViewportGrid context
const viewportGridReducer = (state, action) => { const viewportGridReducer = (state, action) => {
@ -82,13 +89,15 @@ function App({ config, defaultExtensions }) {
}} }}
reducer={viewportGridReducer} reducer={viewportGridReducer}
> >
<SnackbarProvider service={UINotificationService}> <ViewportDialogProvider service={UIViewportDialogService}>
<DialogProvider service={UIDialogService}> <SnackbarProvider service={UINotificationService}>
<ModalProvider modal={Modal} service={UIModalService}> <DialogProvider service={UIDialogService}>
{appRoutes} <ModalProvider modal={Modal} service={UIModalService}>
</ModalProvider> {appRoutes}
</DialogProvider> </ModalProvider>
</SnackbarProvider> </DialogProvider>
</SnackbarProvider>
</ViewportDialogProvider>
</ViewportGridProvider> </ViewportGridProvider>
</ThemeWrapper> </ThemeWrapper>
</Router> </Router>

View File

@ -6,6 +6,7 @@ import {
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIViewportDialogService,
MeasurementService, MeasurementService,
DisplaySetService, DisplaySetService,
ToolBarSerivce, ToolBarSerivce,
@ -44,6 +45,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
UINotificationService, UINotificationService,
UIModalService, UIModalService,
UIDialogService, UIDialogService,
UIViewportDialogService,
MeasurementService, MeasurementService,
DisplaySetService, DisplaySetService,
ToolBarSerivce, ToolBarSerivce,