* Added docs with new screenshots * Added doc to architecture * Added documentations to various extension modules * Added more documentation to modes * Added docs to managers * Added docs for services * Fixed deployment docs * Added white labelling documentation * Added i18n docs and measurement export
316 lines
8.9 KiB
Markdown
316 lines
8.9 KiB
Markdown
# UI Services
|
||
|
||
|
||
## Overview
|
||
|
||
|
||
<!-- - Consistent look & feel
|
||
- Available in and “out” of React
|
||
- Supports ad hoc containers
|
||
- Exposes “setImplementation” method passed to Context Provider
|
||
- May set a default UI component
|
||
- API exposes get/set of Context Provider’s state -->
|
||
|
||
|
||
|
||
A typical web application will have components and state for common UI like
|
||
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
||
these components from an extension.
|
||
|
||
We maintain the following UI Services:
|
||
|
||
- [UI Notification Service](ui-notification-service.md)
|
||
- [UI Modal Service](ui-modal-service.md)
|
||
- [UI Dialog Service](ui-dialog-service.md)
|
||
- [UI Viewport Dialog Service](ui-viewport-dialog-service.md)
|
||
- [Cine Service](cine.md)
|
||
- [Viewport Grid Service](ViewportGridService.md)
|
||
|
||
|
||
|
||
<div style="text-align: center;">
|
||
<a href="/assets/img/ui-services.png">
|
||
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||
</a>
|
||
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
||
</div>
|
||
|
||
<!-- In `@ohif/core`, we have a collection of service factories. We select one we
|
||
would like our application to support, create an instance of it, and pass that
|
||
instance to our `ServicesManager` AND to a React component (in this example,
|
||
`ModalContext`'s provider).
|
||
|
||
The `ModalContext`'s provider:
|
||
|
||
- Exposes context values
|
||
- Exposes methods that leverage `useCallback` hooks
|
||
- Sets the service's implementation in a `useEffect` hook
|
||
|
||
The `ServicesManager` is:
|
||
|
||
- Passed to the `ExtensionManager`
|
||
- The `ExtensionManager` makes the `ServicesManager` available to:
|
||
- All of it's lifecycle hooks (`preRegistration`)
|
||
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.) -->
|
||
|
||
|
||
## Providers for UI services
|
||
<mark>There are several context providers that wraps the application routes. This
|
||
makes the context values exposed in the app, and service's `setImplementation` can get run to override the implementation of the service.
|
||
|
||
```js
|
||
// platform/viewer/src/App.jsx
|
||
|
||
function App({ config, defaultExtensions }) {
|
||
/**...**/
|
||
/**...**/
|
||
return (
|
||
/**...**/
|
||
<ViewportGridProvider service={ViewportGridService}>
|
||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||
<CineProvider service={CineService}>
|
||
<SnackbarProvider service={UINotificationService}>
|
||
<DialogProvider service={UIDialogService}>
|
||
<ModalProvider modal={Modal} service={UIModalService}>
|
||
{appRoutes}
|
||
</ModalProvider>
|
||
</DialogProvider>
|
||
</SnackbarProvider>
|
||
</CineProvider>
|
||
</ViewportDialogProvider>
|
||
</ViewportGridProvider>
|
||
/**...**/
|
||
)
|
||
}
|
||
```
|
||
|
||
## Example
|
||
|
||
For instance `UIModalService` has the following Public API:
|
||
|
||
```js
|
||
// platform/core/src/services/UIModalService/index.js
|
||
|
||
const publicAPI = {
|
||
name,
|
||
hide: _hide,
|
||
show: _show,
|
||
setServiceImplementation,
|
||
}
|
||
|
||
function setServiceImplementation({
|
||
hide: hideImplementation,
|
||
show: showImplementation,
|
||
}) {
|
||
/** ... **/
|
||
serviceImplementation._hide = hideImplementation
|
||
serviceImplementation._show = showImplementation
|
||
/** ... **/
|
||
}
|
||
|
||
export default {
|
||
name: 'UIModalService',
|
||
create: ({ configuration = {} }) => {
|
||
return publicAPI
|
||
},
|
||
}
|
||
```
|
||
|
||
`UIModalService` implementation can be set (override) in its context provider. For instance in `ModalProvider` we have:
|
||
|
||
```js
|
||
// platform/ui/src/contextProviders/ModalProvider.jsx
|
||
|
||
import { Modal } from '@ohif/ui'
|
||
|
||
const ModalContext = createContext(null)
|
||
const { Provider } = ModalContext
|
||
|
||
export const useModal = () => useContext(ModalContext)
|
||
|
||
const ModalProvider = ({ children, modal: Modal, service }) => {
|
||
const DEFAULT_OPTIONS = {
|
||
content: null,
|
||
contentProps: null,
|
||
shouldCloseOnEsc: true,
|
||
isOpen: true,
|
||
closeButton: true,
|
||
title: null,
|
||
customClassName: '',
|
||
}
|
||
|
||
const show = useCallback(
|
||
(props) => setOptions({ ...options, ...props }),
|
||
[options]
|
||
)
|
||
|
||
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [DEFAULT_OPTIONS])
|
||
|
||
useEffect(() => {
|
||
if (service) {
|
||
service.setServiceImplementation({ hide, show })
|
||
}
|
||
}, [hide, service, show])
|
||
|
||
const {
|
||
content: ModalContent,
|
||
contentProps,
|
||
isOpen,
|
||
title,
|
||
customClassName,
|
||
shouldCloseOnEsc,
|
||
closeButton,
|
||
} = options
|
||
|
||
return (
|
||
<Provider value={{ show, hide }}>
|
||
{ModalContent && (
|
||
<Modal
|
||
className={classNames(customClassName, ModalContent.className)}
|
||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||
isOpen={isOpen}
|
||
title={title}
|
||
closeButton={closeButton}
|
||
onClose={hide}
|
||
>
|
||
<ModalContent {...contentProps} show={show} hide={hide} />
|
||
</Modal>
|
||
)}
|
||
{children}
|
||
</Provider>
|
||
)
|
||
}
|
||
|
||
export default ModalProvider
|
||
|
||
export const ModalConsumer = ModalContext.Consumer
|
||
```
|
||
|
||
Therefore, anywhere in the app that we have access to react context we can use it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are utilizing the modal for the preference window which shows the hotkeys after clicking on the gear button on the right side of the header.
|
||
|
||
A `simplified` code for our worklist is:
|
||
|
||
|
||
```js
|
||
// platform/viewer/src/routes/WorkList/WorkList.jsx
|
||
|
||
import { useModal, Header } from '@ohif/ui'
|
||
|
||
function WorkList({
|
||
history,
|
||
data: studies,
|
||
dataTotal: studiesTotal,
|
||
isLoadingData,
|
||
dataSource,
|
||
hotkeysManager,
|
||
}) {
|
||
const { show, hide } = useModal()
|
||
|
||
/** ... **/
|
||
|
||
const menuOptions = [
|
||
{
|
||
title: t('Header:About'),
|
||
icon: 'info',
|
||
onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }),
|
||
},
|
||
{
|
||
title: t('Header:Preferences'),
|
||
icon: 'settings',
|
||
onClick: () =>
|
||
show({
|
||
title: t('UserPreferencesModal:User Preferences'),
|
||
content: UserPreferences,
|
||
contentProps: {
|
||
hotkeyDefaults:
|
||
hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
||
hotkeyDefinitions,
|
||
onCancel: hide,
|
||
currentLanguage: currentLanguage(),
|
||
availableLanguages,
|
||
defaultLanguage,
|
||
onSubmit: (state) => {
|
||
i18n.changeLanguage(state.language.value)
|
||
hotkeysManager.setHotkeys(state.hotkeyDefinitions)
|
||
hide()
|
||
},
|
||
onReset: () => hotkeysManager.restoreDefaultBindings(),
|
||
},
|
||
}),
|
||
},
|
||
]
|
||
/** ... **/
|
||
return (
|
||
<div>
|
||
/** ... **/
|
||
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
|
||
/** ... **/
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
|
||
|
||
<!-- ## Example -->
|
||
|
||
<!-- That's all fine and good, but it's still a little too abstract. What does this
|
||
translate to in practice?
|
||
|
||
```js
|
||
// In the application
|
||
import UINotificationService from '@ohif/core';
|
||
const servicesManager = new ServicesManager();
|
||
|
||
servicesManager.registerService(UINotificationService);
|
||
|
||
// UI Service Provider
|
||
useEffect(() => {
|
||
if (service) {
|
||
service.setServiceImplementation({ hide, show });
|
||
}
|
||
}, [service, hide, show]);
|
||
|
||
// In an extension
|
||
const { UINotificationService } = servicesManager.services;
|
||
|
||
if (UINotificationService) {
|
||
UINotificationService.show('Hello from the other side 👋');
|
||
}
|
||
``` -->
|
||
|
||
<!-- <div style="text-align: center;">
|
||
<a href="/assets/img/notification-example.gif">
|
||
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||
</a>
|
||
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
||
</div> -->
|
||
|
||
## Tips & Tricks
|
||
|
||
It's important to remember that all we're doing is making it possible to control
|
||
bits of the application's UI from an extension. Here are a few non-obvious
|
||
takeaways worth mentioning:
|
||
|
||
- Your application code should continue to use React context
|
||
(consumers/providers) as it normally would
|
||
- You can substitute our "out of the box" UI implementations with your own
|
||
- You can create and register your own UI services
|
||
- You can choose not to register a service or provide a service implementation
|
||
- In extensions, you can provide fallback/alternative behavior if an expected
|
||
service is not registered
|
||
- No `UIModalService`? Use the `UINotificationService` to notify users.
|
||
- You can technically register a service in an extension and expose it to the core application
|
||
|
||
> Note: These are recommended patterns, not hard and fast rules. Following them
|
||
> will help reduce confusion and interoperability with the larger OHIF
|
||
> community, but they're not silver bullets. Please speak up, create an issue,
|
||
> if you would like to discuss new services or improvements to this pattern.
|
||
|
||
<!-- ## Maintained Services
|
||
|
||
Our `@ohif/viewer` project is an example of how to glue together the different
|
||
parts and pieces of the OHIF Platform to create a polished and powerful product.
|
||
To accomplish that, we maintain several UI Services that you can use in your own
|
||
project, or provide alternative implementations for: -->
|