From b59b9ffca21aa4ee32e619ab1c8baf6f86848701 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Mon, 30 Dec 2024 16:35:40 +0530 Subject: [PATCH 01/40] intergrate customizationservice with uidilaogservice and implment support for custom annotation labelling component --- platform/app/src/App.tsx | 2 +- .../components/Labelling/LabellingFlow.tsx | 20 +++++++++++++++++-- .../src/contextProviders/DialogProvider.tsx | 17 +++++++++------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index a92a970a5b7..03aecd70672 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -120,7 +120,7 @@ function App({ [CineProvider, { service: cineService }], [NotificationProvider, { service: uiNotificationService }], [TooltipProvider], - [DialogProvider, { service: uiDialogService }], + [DialogProvider, { services: { uiDialogService, customizationService } }], [ModalProvider, { service: uiModalService, modal: Modal }], [ShepherdJourneyProvider], ]; diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 8d1dfe75f6f..41619696712 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -9,6 +9,7 @@ interface PropType { labelData: any; exclusive: boolean; componentClassName: any; + customizationService: any; } interface StateType { @@ -87,13 +88,28 @@ class LabellingFlow extends Component { }; labellingStateFragment = () => { - return ( + const annotationLabelComponent = this.props.customizationService?.get( + 'customAnnotationLabelComponent' + ); + + const CustomAnnotationLabelComponent = annotationLabelComponent?.component; + + return CustomAnnotationLabelComponent ? ( + + ) : ( diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index 647816220f5..aa30859b9c4 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -18,14 +18,15 @@ import classNames from 'classnames'; * we import to instantiate cornerstone */ import guid from './../../../core/src/utils/guid'; - +import { CustomizationService } from '@ohif/core'; import './DialogProvider.css'; const DialogContext = createContext(null); export const useDialog = () => useContext(DialogContext); -const DialogProvider = ({ children, service = null }) => { +const DialogProvider = ({ children, services = null }) => { + const { uiDialogService, customizationService } = services; const [isDragging, setIsDragging] = useState(false); const [dialogs, setDialogs] = useState([]); const [lastDialogId, setLastDialogId] = useState(null); @@ -137,10 +138,10 @@ const DialogProvider = ({ children, service = null }) => { * @returns void */ useEffect(() => { - if (service) { - service.setServiceImplementation({ create, dismiss, dismissAll }); + if (uiDialogService) { + uiDialogService.setServiceImplementation({ create, dismiss, dismissAll }); } - }, [create, dismiss, service]); + }, [create, dismiss, uiDialogService]); useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]); @@ -215,6 +216,7 @@ const DialogProvider = ({ children, service = null }) => { @@ -316,8 +318,9 @@ export const withDialog = Component => { DialogProvider.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node, PropTypes.func]) .isRequired, - service: PropTypes.shape({ - setServiceImplementation: PropTypes.func, + services: PropTypes.shape({ + uiDialogService: PropTypes.shape({ setServiceImplementation: PropTypes.func }), + customizationService: PropTypes.instanceOf(CustomizationService), }), }; From 1f1d705d35be5ba2c593ca4a03dad8b076f868e4 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Wed, 1 Jan 2025 14:21:32 +0530 Subject: [PATCH 02/40] updated the customization name and added documentation --- .../services/ui/customization-service.md | 517 ++++++++++++++++++ .../components/Labelling/LabellingFlow.tsx | 2 +- 2 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 platform/docs/docs/platform/services/ui/customization-service.md diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md new file mode 100644 index 00000000000..4028b88ea46 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -0,0 +1,517 @@ +--- +sidebar_position: 7 +sidebar_label: Customization Service +--- +# Customization Service + +There are a lot of places where users may want to configure certain elements +differently between different modes or for different deployments. A mode +example might be the use of a custom overlay showing mode related DICOM header +information such as radiation dose or patient age. + +The use of this service enables these to be defined in a typed fashion by +providing an easy way to set default values for this, but to allow a +non-default value to be specified by the configuration or mode. + +This service is a UI service in that part of the registration allows for registering +UI components and types to deal with, but it does not directly provide an UI +displayable elements unless customized to do so. + +Note: Customization Service itself doesn't implement the actual customization, +but rather just provide mechanism to register reusable prototypes, to configure +those prototypes with actual configurations, and to use the configured objects +(components, data, whatever). +Actual implementation of the customization is totally up to the component that +supports customization. (for example, `CustomizableViewportOverlay` component uses +`CustomizationService` to implement viewport overlay that is easily customizable +from configuration.) + +## Global, Default and Mode customizations +There are various customization sets that define the lifetime/setup of the +customization. The global customizations are those used for overriding +customizations defined elsewhere, and allow replacing a customization. + +Mode customizations are only registered for the lifetime of the mode, allowing +the mode definition to update/modify the underlying behaviour. This is related +to default customizations, which provide a fallback if the mode or global customization +isn't defined. Default customizations may only be defined once, otherwise throwing +an exception. + +## Append and Merge Customizations +In addition to the replace a customization, there is the ability to merge or append +a customization. The merge customization simply applies the lodash merge functionality +to the existing customization, with the new one, while the append customization +modifies the customization by appending to the value. + +### Append Behaviour +When a list is found in the destination object, the append source object is +examined to see how to handle the change. If the source is simply a list, +then the list object is appended, and no additional changes are performed. +However, if the source is an object other than a list, then the iterable +attributes of the object are examined to match child objects to the destination list, +according to the following table: + +* Natural or zero number value - match the given index location and merge at the point +* Fractional number value - insert at a new point in the list, starting from the end or beginning +* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above. + +#### Example Append + +```javascript +const destination = [ + 1, + {id: 'two', value: 2}, + {id: 'three', value: 3} +] + +const source = { + two: { value: 'updated2' }, + 1: { extraValue: 2 }, + 1.0001: { id: 'inserted', value: 1.0001 }, + -1: { value: -3 }, +} +``` + +Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one +does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }` + +Then, it inserts the id 'inserted' after position 1. + +Finally, position -1 (the end position) is updated from value 3 to value -3. + +The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts. + +## Registering customizable modules (or defining customization prototypes) + +Extensions and Modes can register customization templates they support. +It is done by adding `getCustomizationModule()` in the extension or mode definition. + +Below is the protocol of the `getCustomizationModule()`, if defined in Typescript. + +```typescript + getCustomizationModule() : { name: string, value: any }[] +``` + +If the name is 'default', it is the a default customization, while if it +is 'global', then it is a priority/over-riding customization. + +In the `value` of each customizations, you will define customization prototype(s). +These customization prototype(s) can be considered like "Prototype" in Javascript. +These can be used to extend the customization definitions from configurations. +Default customizations will be often used to define all the customization prototypes, +Default customizations will be often used to define all the customization prototypes, +as they will be loaded automatically along with the defining extension or mode. + + +For example, the `@ohif/extension-default` extension defines, + +```js + getCustomizationModule: () => [ + //... + + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + if (this.condition && !this.condition(props)) return null; + + const { instance } = props; + const value = + instance && this.attribute + ? instance[this.attribute] + : this.contentF && typeof this.contentF === 'function' + ? this.contentF(props) + : null; + if (!value) return null; + + return ( + + {this.label && ( + {this.label} + )} + {value} + + ); + }, + }, + ], + }, + + //... + ], +``` + +And this `ohif.overlayItem` object will be used as a prototype (and template) to define items +to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in +the example below. + +## Configuring customizations + +There are several ways to register customizations. The +`APP_CONFIG.customizationService` +field is used as a per-configuration entry. This object can list single +configurations by id, or it can list sets of customizations by referring to +the `customizationModule` in an extension. + +NOTE that these definitions from APP_CONFIG will be loaded by default, just like +extension/modes default customization. + +Below is the example configuration for `CustomizableViewportOverlay` component +customization, using the customization prototype `ohif.overlayItem` defined in +`ohif/extension-defaul` extension.: + +```js +window.config = { + //... + + // in the APP_CONFIG file set the top right area to show the patient name + // using PN: as a prefix when the study has a non-empty patient name. + customizationService: { + cornerstoneOverlayTopRight: { + id: 'cornerstoneOverlayTopRight', + items: [ + { + id: 'PatientNameOverlay', + // Note below that here we are using the customization prototype of + // `ohif.overlayItem` which was registered to the customization module in + // `ohif/extension-default` extension. + customizationType: 'ohif.overlayItem', + // the following props are passed to the `ohif.overlayItem` prototype + // which is used to render the overlay item based on the label, color, + // conditions, etc. + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => + instance && + instance.PatientName && + instance.PatientName.Alphabetic, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName.Alphabetic) + + ' ' + + (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), + }, + ], + }, + }, + + //... +} +``` + +In the customization configuration, you can use `customizationType` fields to +define the prototype that customization object should inherit from. +The `customizationType` field is simply the id of another customization object. + + +## Implementing customization using CustomizationService + +### Mode Customizations + +Mode-specific customizations are no different from the global ones, +except that the mode customizations are specific to one mode and +are not globally applied. Mode-specific customizations are also cleared +before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter` + +Following on our example above to customize the overlay, we can now add a mode customization +with a bottom-right overlay. + +```js +// Import the type from the extension itself +import OverlayUICustomization from "@ohif/cornerstone-extension"; + +// In the mode itself, customizations can be registered: +onModeEnter: { + // Note how the object can be strongly typed + const bottomRight: OverlayUICustomization = { + id: 'cornerstoneOverlayBottomRight', + // Note the type is the previously registered ohif.cornerstoneOverlay + customizationType: 'ohif.cornerstoneOverlay', + // The cornerstoneOverlay definition requires an items list here. + items: [ + // Custom definitions for the context menu here. + ], + }; + customizationService.addModeCustomizations(bottomRight); +} +``` + +The mode customizations are retrieved via the `getModeCustomization` function, +providing an id, and optionally a default value. The retrieval will return, +in order: + +1. Global customization with the given id. +2. Mode customization with the id. +3. The default value specified. + +The return value then inherits the `customizationType` instance, so that the +value can be typed and have default values and functionality provided. The object +can then be used in a way defined by the extension provided that customization +point. + +```ts +const cornerstoneOverlay = customizationService.getModeCustomization( + "cornerstoneOverlay", + { customizationType: "ohif.cornerstoneOverlay" }, +); + +const { component: overlayComponent, props } = + customizationService.getComponent(cornerstoneOverlay); + +return ( + +); +``` + +This example shows fetching the default component to render this object. The +returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This +object can be a React component or other object such as a commands list, for +example (this example comes from the context menu customizations as that one +uses commands lists): + +```ts +cornerstoneContextMenu = customizationService.get( + "cornerstoneContextMenu", + defaultMenu, +); +commandsManager.run(cornerstoneContextMenu, extraProps); +``` + +### Global Customizations + +Global customizations are retrieved in the same was as mode customizations, except +that the `getGlobalCustomization` is called instead of the mode call. + +### Types + +Some types for the customization service are provided by the `@ohif/ui` types +export. Additionally, extensions can provide a Types export with custom +typing, allowing for better typing for the extension specific capabilities. +This allows for having strong typing when declaring customizations, for example: + +```ts +import { Types } from '@ohif/ui'; + +const customContextMenu: Types.ContextMenu.Menu = + { + id: 'cornerstoneContextMenu', + customizationType: 'ohif.contextMenu', + // items will be type checked to be in accordance with UIContextMenu.items + items: [ ... ] + }, +``` + +### Inheritance + +JavaScript property inheritance can be supplied by defining customizations +with id corresponding to the customizationType value. For example: + +```js +getCustomizationModule = () => ([ + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + return (

{this.label} {props.instance[this.attribute]}

) + }, + }, + ], + } +]) +``` + +defines an overlay item which has a React content object as the render value. +This can then be used by specifying a `customizationType` of `ohif.overlayItem`, for example: + +```js +const overlayItem: Types.UIOverlayItem = { + id: 'anOverlayItem', + customizationType: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'PN:', +}; +``` + +# Customizations + +This section can be used to specify various customization capabilities. + +## Text color for StudyBrowser tabs + +This is the recommended pattern for deep customization of class attributes, +making it fine grained, and have it apply a set of attributes, mostly from +tailwind. In this case it is a double indirection, as the buttons class +uses it's own internal class names. + +* Name: 'class:StudyBrowser' +* Attributes: +** `true` for the is active true text color +** `false` for the is active false text color. +** Values are button colors, from the Button class, eg default, white, black + +## customRoutes + +* Name: `customRoutes` global +* Attributes: +** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add. +** Should any element of routes match an existing baked in element, the baked in one will be replaced. +** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes) + +### Example + +```js +{ + id: 'customRoutes', + routes: [ + { + path: '/myroute', + children: MyRouteReactFunction, + } + ], +} +``` + +There is a usage of this example commented out in config/default.js that +looks like the code below. This example is provided by the default extension, +again with commented out code. Uncomment the getCustomizationModule customRoutes +code in the default module to activate this, and then go to: `http://localhost:3000/custom` +to see the custom route. + +Note the name of this is the customization module name, which usually won't match +the id, and in fact there can be multiple customization objects defined for a single +customization module, to allow for customizing sets of related values. + +```js +customizationService: [ + // Shows a custom route -access via http://localhost:3000/custom + '@ohif/extension-default.customizationModule.helloPage', +], +``` + +## Customizable Viewport Overlay + +Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay. + +There are working examples that can be run with: +``` +set APP_CONFIG=config/customization.js +yarn dev +``` + +```javascript +// this is part of customization.js, an example customization dataset +window.config = { + + // This shows how to append to the customization data + customizationService: [ + { + id: '@ohif/cornerstoneOverlay', + // Append recursively, rather than replacing + merge: 'Append', + topRightItems: { + id: 'cornerstoneOverlayTopRight', + items: [ + { + id: 'PatientNameOverlay', + // Note below that here we are using the customization prototype of + // `ohif.overlayItem` which was registered to the customization module in + // `ohif/extension-default` extension. + customizationType: 'ohif.overlayItem', + // the following props are passed to the `ohif.overlayItem` prototype + // which is used to render the overlay item based on the label, color, + // conditions, etc. + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => instance?.PatientName, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName) + + (instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''), + }, + ], + }, + + topLeftItems: { + items: { + // Note the -10000 means -10000 + length of existing list, which is + // much before the start of hte list, so put the new value at the start. + '-10000': + { + id: 'Species', + customizationType: 'ohif.overlayItem', + label: 'Species:', + color: 'red', + background: 'green', + condition: ({ instance }) => + instance?.PatientSpeciesDescription, + contentF: ({ instance }) => + instance.PatientSpeciesDescription + + '/' + + instance.PatientBreedDescription, + }, + }, + }, + }, +... +``` + + + +## Context Menus + +Context menus can be created by defining the menu structure and click +interaction, as defined in the `ContextMenu/types`. There are examples +below specific to the cornerstone context, because the actual click +handler and attributes used to decide when and how to display the menu +are specific to the context used for where the menu is displayed. + +## Cornerstone Context Menu + +The default cornerstone context menu can be customized by setting the +`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. + +## Customizable Cornerstone Viewport Click Behaviour + +The behaviour on clicking on the cornerstone viewport can be customized +by setting the `cornerstoneViewportClickCommands`. This is intended to +support both the cornerstone 3D internal commands as well as things like +context menus. Currently it supports buttons 1-3, as well as modifier keys +by associating a commands list with the button to click. See `initContextMenu` +for more details. + +## Customizable Annotation Labelling component. + +The Annotation Labelling Component can be customized using the ID `measurement.labellingComponent`. This allows users to replace the default `SelectTree` component with a custom component of their choice. Below is a sample for customization implementation: + +``` +customizationService.setGlobalCustomization('measurement.labellingComponent', { + component: AnnotationLabel, +}); + + +``` + + +## Please add additional customizations above this section +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js +[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js +[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 41619696712..5d9224c7c96 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -89,7 +89,7 @@ class LabellingFlow extends Component { labellingStateFragment = () => { const annotationLabelComponent = this.props.customizationService?.get( - 'customAnnotationLabelComponent' + 'measurement.labellingComponent' ); const CustomAnnotationLabelComponent = annotationLabelComponent?.component; From 62e63c5af9757678a7b328d84ed94e6d48b67ce4 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Wed, 22 Jan 2025 17:58:41 +0530 Subject: [PATCH 03/40] implmentation of customized render component --- platform/app/src/App.tsx | 2 ++ .../LoadingIndicatorProgress.tsx | 19 ++++++++++++++++-- .../LoadingIndicatorTotalPercent.tsx | 20 ++++++++++++++++++- .../ProgressLoadingBar/ProgressLoadingBar.tsx | 20 +++++++++++++------ .../src/contextProviders/ServicesProvider.tsx | 18 +++++++++++++++++ platform/ui/src/contextProviders/index.js | 2 ++ platform/ui/src/index.js | 2 ++ .../src/utils/CustomizableRenderComponent.tsx | 16 +++++++++++++++ 8 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 platform/ui/src/contextProviders/ServicesProvider.tsx create mode 100644 platform/ui/src/utils/CustomizableRenderComponent.tsx diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 03aecd70672..a3b9ab684d6 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -21,6 +21,7 @@ import { ViewportDialogProvider, CineProvider, UserAuthenticationProvider, + ServicesProvider, } from '@ohif/ui'; import { ThemeWrapper as ThemeWrapperNext, @@ -114,6 +115,7 @@ function App({ [I18nextProvider, { i18n }], [ThemeWrapperNext], [ThemeWrapper], + [ServicesProvider, { services: servicesManager.services }], [ToolboxProvider], [ViewportGridProvider, { service: viewportGridService }], [ViewportDialogProvider, { service: uiViewportDialogService }], diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index b81c78dca9a..59a190ba382 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,15 +1,27 @@ import React from 'react'; import classNames from 'classnames'; +import Icon from '../Icon'; import ProgressLoadingBar from '../ProgressLoadingBar'; -import { Icons } from '@ohif/ui-next'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; + /** * A React component that renders a loading indicator. * if progress is not provided, it will render an infinite loading indicator * if progress is provided, it will render a progress bar * Optionally a textBlock can be provided to display a message */ + function LoadingIndicatorProgress({ className, textBlock, progress }) { + return CustomizableRenderComponent({ + customizationId: 'ui.LoadingIndicatorProgress', + FallbackComponent: FallbackLoadingIndicatorProgress, + className, + textBlock, + progress, + }); +} +function FallbackLoadingIndicatorProgress({ className, textBlock, progress }) { return (
- +
diff --git a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx index 199fb8ab6b5..a5e2572a7af 100644 --- a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -1,5 +1,5 @@ import React from 'react'; - +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; import LoadingIndicatorProgress from '../LoadingIndicatorProgress'; interface Props { @@ -15,6 +15,24 @@ interface Props { * and percentComplete to display a more detailed message. */ function LoadingIndicatorTotalPercent({ + className, + totalNumbers, + percentComplete, + loadingText, + targetText, +}: Props) { + return CustomizableRenderComponent({ + customizationId: 'ui.LoadingIndicatorTotalPercent', + FallbackComponent: FallbackLoadingIndicatorTotalPercent, + className, + totalNumbers, + percentComplete, + loadingText, + targetText, + }); +} + +function FallbackLoadingIndicatorTotalPercent({ className, totalNumbers, percentComplete, diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx index 62034dc46d3..894fefa6fa4 100644 --- a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -1,5 +1,5 @@ import React, { ReactElement } from 'react'; - +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; import './ProgressLoadingBar.css'; export type ProgressLoadingBarProps = { @@ -11,17 +11,25 @@ export type ProgressLoadingBarProps = { * If progress is provided, it will render a progress bar * The progress text can be optionally displayed to the left of the bar. */ -function ProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { + +function ProgressLoadingBar({ progress }) { + return CustomizableRenderComponent({ + customizationId: 'ui.ProgressLoadingBar', + FallbackComponent: FallbackProgressLoadingBar, + progress, + }); +} +function FallbackProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { return ( -
+
{progress === undefined || progress === null ? ( -
+
) : (
)} diff --git a/platform/ui/src/contextProviders/ServicesProvider.tsx b/platform/ui/src/contextProviders/ServicesProvider.tsx new file mode 100644 index 00000000000..5f3fa86ec0e --- /dev/null +++ b/platform/ui/src/contextProviders/ServicesProvider.tsx @@ -0,0 +1,18 @@ +import React, { createContext, useContext } from 'react'; +import PropTypes from 'prop-types'; + +const servicesManagerContext = createContext(null); +const { Provider } = servicesManagerContext; + +export const useServices = () => useContext(servicesManagerContext); + +export function ServicesProvider({ children, services }) { + return {children}; +} + +ServicesProvider.propTypes = { + children: PropTypes.any, + services: PropTypes.any, +}; + +export default ServicesProvider; diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index 95a72df0801..8288530a0a3 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -16,6 +16,8 @@ export { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './Vi export { useToolbox, ToolboxProvider } from './Toolbox/ToolboxContext'; +export { useServices, ServicesProvider } from './ServicesProvider'; + export { UserAuthenticationContext, UserAuthenticationProvider, diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 2efd0594d3a..9dcb53995aa 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -10,6 +10,7 @@ export { ModalProvider, ModalConsumer, useModal, + useServices, withModal, ImageViewerContext, ImageViewerProvider, @@ -27,6 +28,7 @@ export { useUserAuthentication, useToolbox, ToolboxProvider, + ServicesProvider, } from './contextProviders'; /** COMPONENTS */ diff --git a/platform/ui/src/utils/CustomizableRenderComponent.tsx b/platform/ui/src/utils/CustomizableRenderComponent.tsx new file mode 100644 index 00000000000..b5e47fe25db --- /dev/null +++ b/platform/ui/src/utils/CustomizableRenderComponent.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useServices } from '@ohif/ui'; + +interface ICustomizableRenderComponent { + customizationId: string; + FallbackComponent: React.ElementType; + [key: string]: any; +} + +export default function CustomizableRenderComponent(props: ICustomizableRenderComponent) { + const { customizationId, FallbackComponent, ...rest } = props; + const { services } = useServices(); + const CustomizedComponent = + services.customizationService.getCustomization(customizationId)?.component; + return CustomizedComponent ? : ; +} From 7bc94893fd695aca419be18101347a30e46d685a Mon Sep 17 00:00:00 2001 From: ashikcn Date: Thu, 23 Jan 2025 10:41:31 +0530 Subject: [PATCH 04/40] used customization service to customize WindowLevelActionMenu --- .../platform/services/ui/customization-service.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index 4028b88ea46..8183bb4985d 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -501,6 +501,17 @@ customizationService.setGlobalCustomization('measurement.labellingComponent', { ``` +## Customizable WindowLevelActionMenu component. + +The WindowLevelActionMenu Component can be customized using the ID `cornerstone.windowLevelActionMenu`. This allows users to replace the default `WindowLevelActionMenu` component with a custom component of their choice. Below is a sample for customization implementation: + +``` +customizationService.setGlobalCustomization('cornerstone.windowLevelActionMenu', { + content: AnnotationLabel, +}); + + +``` ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. From 91bc31af9c3fea191a83eada8a7d500ec4260e6f Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 12:52:36 +0530 Subject: [PATCH 05/40] modifying Labwllinflow using customizable rendering component method --- .../components/Labelling/LabellingFlow.tsx | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 5d9224c7c96..0a9b3e06ebe 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -2,6 +2,7 @@ import SelectTree from '../SelectTree'; import React, { Component } from 'react'; import LabellingTransition from './LabellingTransition'; import cloneDeep from 'lodash.clonedeep'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; interface PropType { labellingDoneCallback: (label: string) => void; @@ -23,6 +24,7 @@ interface StateType { export interface LabelInfo { label: string; value: string; + searchItem: boolean; } class LabellingFlow extends Component { @@ -82,36 +84,23 @@ class LabellingFlow extends Component { }; selectTreeSelectCalback = (event, itemSelected) => { - const label = itemSelected.value; + const label = itemSelected.label || itemSelected.value; this.closePopup(); return this.props.labellingDoneCallback(label); }; labellingStateFragment = () => { - const annotationLabelComponent = this.props.customizationService?.get( - 'measurement.labellingComponent' - ); - - const CustomAnnotationLabelComponent = annotationLabelComponent?.component; - - return CustomAnnotationLabelComponent ? ( - - ) : ( - ); }; From b24ab3fb15fa0f03b6822477eb212ce3e7d4ad9e Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 13:05:52 +0530 Subject: [PATCH 06/40] document update of customization render component --- .../platform/services/ui/customization-service.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index 8183bb4985d..839d6655a34 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -498,7 +498,6 @@ customizationService.setGlobalCustomization('measurement.labellingComponent', { component: AnnotationLabel, }); - ``` ## Customizable WindowLevelActionMenu component. @@ -513,6 +512,19 @@ customizationService.setGlobalCustomization('cornerstone.windowLevelActionMenu', ``` +## Customizable Render component. + +The CustomizableRenderComponent dynamically renders a custom component based on a customizationId. If a component for the given ID is found, it is rendered with the provided props; otherwise, a fallback component is rendered. To set a custom component for a specific customizationId, you must register it using the customizationService, where the custom component is added within an object under the component key. If no component is found for the specified customizationId, the FallbackComponent will be rendered instead. + +``` +customizationService.setGlobalCustomization('customizationId', { + component: CustomizedComponent, +}); + + +``` + + ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. From a508b0c99c0cdf4e2f1bf061bdb98fadb052693b Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 14:23:54 +0530 Subject: [PATCH 07/40] modify usage of customizationservice in labellinglfow --- platform/app/src/App.tsx | 2 +- .../ui/src/components/Labelling/LabellingFlow.tsx | 3 +-- .../ui/src/contextProviders/DialogProvider.tsx | 15 +++++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index a3b9ab684d6..093ea5f26e3 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -122,7 +122,7 @@ function App({ [CineProvider, { service: cineService }], [NotificationProvider, { service: uiNotificationService }], [TooltipProvider], - [DialogProvider, { services: { uiDialogService, customizationService } }], + [DialogProvider, { service: uiDialogService }], [ModalProvider, { service: uiModalService, modal: Modal }], [ShepherdJourneyProvider], ]; diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 0a9b3e06ebe..8b866d99c50 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -10,7 +10,6 @@ interface PropType { labelData: any; exclusive: boolean; componentClassName: any; - customizationService: any; } interface StateType { @@ -100,7 +99,7 @@ class LabellingFlow extends Component { measurementData={this.props.measurementData} items={this.currentItems} exclusive={this.props.exclusive} - selectTreeFirstTitle={'Select Label'} + selectTreeFirstTitle={'Annotation'} /> ); }; diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index aa30859b9c4..90f408559db 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -25,8 +25,7 @@ const DialogContext = createContext(null); export const useDialog = () => useContext(DialogContext); -const DialogProvider = ({ children, services = null }) => { - const { uiDialogService, customizationService } = services; +const DialogProvider = ({ children, service }) => { const [isDragging, setIsDragging] = useState(false); const [dialogs, setDialogs] = useState([]); const [lastDialogId, setLastDialogId] = useState(null); @@ -138,10 +137,10 @@ const DialogProvider = ({ children, services = null }) => { * @returns void */ useEffect(() => { - if (uiDialogService) { - uiDialogService.setServiceImplementation({ create, dismiss, dismissAll }); + if (service) { + service.setServiceImplementation({ create, dismiss, dismissAll }); } - }, [create, dismiss, uiDialogService]); + }, [create, dismiss, service]); useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]); @@ -216,7 +215,6 @@ const DialogProvider = ({ children, services = null }) => {
@@ -318,10 +316,7 @@ export const withDialog = Component => { DialogProvider.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node, PropTypes.func]) .isRequired, - services: PropTypes.shape({ - uiDialogService: PropTypes.shape({ setServiceImplementation: PropTypes.func }), - customizationService: PropTypes.instanceOf(CustomizationService), - }), + service: PropTypes.shape({ setServiceImplementation: PropTypes.func }), }; export default DialogProvider; From 03604fb907082bdcf8c90f6fd20cf2b27ac5dc73 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 14:27:22 +0530 Subject: [PATCH 08/40] updated customization service doc --- .../platform/services/ui/customization-service.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index 839d6655a34..6e7a13be8aa 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -498,18 +498,6 @@ customizationService.setGlobalCustomization('measurement.labellingComponent', { component: AnnotationLabel, }); -``` - -## Customizable WindowLevelActionMenu component. - -The WindowLevelActionMenu Component can be customized using the ID `cornerstone.windowLevelActionMenu`. This allows users to replace the default `WindowLevelActionMenu` component with a custom component of their choice. Below is a sample for customization implementation: - -``` -customizationService.setGlobalCustomization('cornerstone.windowLevelActionMenu', { - content: AnnotationLabel, -}); - - ``` ## Customizable Render component. From ed50765ab2e95960a363dd3f76badba119b71ef9 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 14:33:12 +0530 Subject: [PATCH 09/40] reverted modifications from progressloadingbar --- .../components/ProgressLoadingBar/ProgressLoadingBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx index 894fefa6fa4..80f67e93500 100644 --- a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -21,15 +21,15 @@ function ProgressLoadingBar({ progress }) { } function FallbackProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { return ( -
+
{progress === undefined || progress === null ? ( -
+
) : (
)} From 4d9cf82dadfb535453d88660e53763c29446bb8f Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 14:35:39 +0530 Subject: [PATCH 10/40] updated format of customization service doc --- .../docs/docs/platform/services/ui/customization-service.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index 6e7a13be8aa..ed0fdfed03f 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -509,10 +509,8 @@ customizationService.setGlobalCustomization('customizationId', { component: CustomizedComponent, }); - ``` - ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. From c1ac8ee473a170a04fd9154cffbc6b7e6c41fa8d Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 23 Jan 2025 14:37:14 +0530 Subject: [PATCH 11/40] updated labellingflow component --- platform/ui/src/components/Labelling/LabellingFlow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 8b866d99c50..53343f31884 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -23,7 +23,6 @@ interface StateType { export interface LabelInfo { label: string; value: string; - searchItem: boolean; } class LabellingFlow extends Component { From c2b8f4cbcc0b3cbe8e57bb662e59a5646b3b3cd3 Mon Sep 17 00:00:00 2001 From: Devu Jayalekshmi Date: Thu, 23 Jan 2025 15:46:09 +0530 Subject: [PATCH 12/40] ViewportActionCorner customization --- .../ViewportActionCorners/ViewportActionCorners.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx index 78d5dd4dc2d..48d872299e9 100644 --- a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx +++ b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import React from 'react'; import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export enum ViewportActionCornersLocations { topLeft, @@ -45,6 +46,14 @@ const classes = { * rendered from left to right in the order that they appear in the array. */ function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { + return CustomizableRenderComponent({ + customizationId: 'ui.ViewportActionCorner', + FallbackComponent: FallbackViewportActionCorners, + cornerComponents, + }); +} + +function FallbackViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { if (!cornerComponents) { return null; } From de3a12a26b451968e5ca5fc9d484f4046c111427 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 24 Jan 2025 14:30:47 +0530 Subject: [PATCH 13/40] modifications as part of latest customization-service-api --- .../services/ui/customization-service.md | 526 ------------------ .../services/ui/customization-service.md | 22 + .../src/utils/CustomizableRenderComponent.tsx | 3 +- 3 files changed, 23 insertions(+), 528 deletions(-) delete mode 100644 platform/docs/docs/platform/services/ui/customization-service.md diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md deleted file mode 100644 index ed0fdfed03f..00000000000 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ /dev/null @@ -1,526 +0,0 @@ ---- -sidebar_position: 7 -sidebar_label: Customization Service ---- -# Customization Service - -There are a lot of places where users may want to configure certain elements -differently between different modes or for different deployments. A mode -example might be the use of a custom overlay showing mode related DICOM header -information such as radiation dose or patient age. - -The use of this service enables these to be defined in a typed fashion by -providing an easy way to set default values for this, but to allow a -non-default value to be specified by the configuration or mode. - -This service is a UI service in that part of the registration allows for registering -UI components and types to deal with, but it does not directly provide an UI -displayable elements unless customized to do so. - -Note: Customization Service itself doesn't implement the actual customization, -but rather just provide mechanism to register reusable prototypes, to configure -those prototypes with actual configurations, and to use the configured objects -(components, data, whatever). -Actual implementation of the customization is totally up to the component that -supports customization. (for example, `CustomizableViewportOverlay` component uses -`CustomizationService` to implement viewport overlay that is easily customizable -from configuration.) - -## Global, Default and Mode customizations -There are various customization sets that define the lifetime/setup of the -customization. The global customizations are those used for overriding -customizations defined elsewhere, and allow replacing a customization. - -Mode customizations are only registered for the lifetime of the mode, allowing -the mode definition to update/modify the underlying behaviour. This is related -to default customizations, which provide a fallback if the mode or global customization -isn't defined. Default customizations may only be defined once, otherwise throwing -an exception. - -## Append and Merge Customizations -In addition to the replace a customization, there is the ability to merge or append -a customization. The merge customization simply applies the lodash merge functionality -to the existing customization, with the new one, while the append customization -modifies the customization by appending to the value. - -### Append Behaviour -When a list is found in the destination object, the append source object is -examined to see how to handle the change. If the source is simply a list, -then the list object is appended, and no additional changes are performed. -However, if the source is an object other than a list, then the iterable -attributes of the object are examined to match child objects to the destination list, -according to the following table: - -* Natural or zero number value - match the given index location and merge at the point -* Fractional number value - insert at a new point in the list, starting from the end or beginning -* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above. - -#### Example Append - -```javascript -const destination = [ - 1, - {id: 'two', value: 2}, - {id: 'three', value: 3} -] - -const source = { - two: { value: 'updated2' }, - 1: { extraValue: 2 }, - 1.0001: { id: 'inserted', value: 1.0001 }, - -1: { value: -3 }, -} -``` - -Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one -does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }` - -Then, it inserts the id 'inserted' after position 1. - -Finally, position -1 (the end position) is updated from value 3 to value -3. - -The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts. - -## Registering customizable modules (or defining customization prototypes) - -Extensions and Modes can register customization templates they support. -It is done by adding `getCustomizationModule()` in the extension or mode definition. - -Below is the protocol of the `getCustomizationModule()`, if defined in Typescript. - -```typescript - getCustomizationModule() : { name: string, value: any }[] -``` - -If the name is 'default', it is the a default customization, while if it -is 'global', then it is a priority/over-riding customization. - -In the `value` of each customizations, you will define customization prototype(s). -These customization prototype(s) can be considered like "Prototype" in Javascript. -These can be used to extend the customization definitions from configurations. -Default customizations will be often used to define all the customization prototypes, -Default customizations will be often used to define all the customization prototypes, -as they will be loaded automatically along with the defining extension or mode. - - -For example, the `@ohif/extension-default` extension defines, - -```js - getCustomizationModule: () => [ - //... - - { - name: 'default', - value: [ - { - id: 'ohif.overlayItem', - content: function (props) { - if (this.condition && !this.condition(props)) return null; - - const { instance } = props; - const value = - instance && this.attribute - ? instance[this.attribute] - : this.contentF && typeof this.contentF === 'function' - ? this.contentF(props) - : null; - if (!value) return null; - - return ( - - {this.label && ( - {this.label} - )} - {value} - - ); - }, - }, - ], - }, - - //... - ], -``` - -And this `ohif.overlayItem` object will be used as a prototype (and template) to define items -to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in -the example below. - -## Configuring customizations - -There are several ways to register customizations. The -`APP_CONFIG.customizationService` -field is used as a per-configuration entry. This object can list single -configurations by id, or it can list sets of customizations by referring to -the `customizationModule` in an extension. - -NOTE that these definitions from APP_CONFIG will be loaded by default, just like -extension/modes default customization. - -Below is the example configuration for `CustomizableViewportOverlay` component -customization, using the customization prototype `ohif.overlayItem` defined in -`ohif/extension-defaul` extension.: - -```js -window.config = { - //... - - // in the APP_CONFIG file set the top right area to show the patient name - // using PN: as a prefix when the study has a non-empty patient name. - customizationService: { - cornerstoneOverlayTopRight: { - id: 'cornerstoneOverlayTopRight', - items: [ - { - id: 'PatientNameOverlay', - // Note below that here we are using the customization prototype of - // `ohif.overlayItem` which was registered to the customization module in - // `ohif/extension-default` extension. - customizationType: 'ohif.overlayItem', - // the following props are passed to the `ohif.overlayItem` prototype - // which is used to render the overlay item based on the label, color, - // conditions, etc. - attribute: 'PatientName', - label: 'PN:', - title: 'Patient Name', - color: 'yellow', - condition: ({ instance }) => - instance && - instance.PatientName && - instance.PatientName.Alphabetic, - contentF: ({ instance, formatters: { formatPN } }) => - formatPN(instance.PatientName.Alphabetic) + - ' ' + - (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), - }, - ], - }, - }, - - //... -} -``` - -In the customization configuration, you can use `customizationType` fields to -define the prototype that customization object should inherit from. -The `customizationType` field is simply the id of another customization object. - - -## Implementing customization using CustomizationService - -### Mode Customizations - -Mode-specific customizations are no different from the global ones, -except that the mode customizations are specific to one mode and -are not globally applied. Mode-specific customizations are also cleared -before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter` - -Following on our example above to customize the overlay, we can now add a mode customization -with a bottom-right overlay. - -```js -// Import the type from the extension itself -import OverlayUICustomization from "@ohif/cornerstone-extension"; - -// In the mode itself, customizations can be registered: -onModeEnter: { - // Note how the object can be strongly typed - const bottomRight: OverlayUICustomization = { - id: 'cornerstoneOverlayBottomRight', - // Note the type is the previously registered ohif.cornerstoneOverlay - customizationType: 'ohif.cornerstoneOverlay', - // The cornerstoneOverlay definition requires an items list here. - items: [ - // Custom definitions for the context menu here. - ], - }; - customizationService.addModeCustomizations(bottomRight); -} -``` - -The mode customizations are retrieved via the `getModeCustomization` function, -providing an id, and optionally a default value. The retrieval will return, -in order: - -1. Global customization with the given id. -2. Mode customization with the id. -3. The default value specified. - -The return value then inherits the `customizationType` instance, so that the -value can be typed and have default values and functionality provided. The object -can then be used in a way defined by the extension provided that customization -point. - -```ts -const cornerstoneOverlay = customizationService.getModeCustomization( - "cornerstoneOverlay", - { customizationType: "ohif.cornerstoneOverlay" }, -); - -const { component: overlayComponent, props } = - customizationService.getComponent(cornerstoneOverlay); - -return ( - -); -``` - -This example shows fetching the default component to render this object. The -returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This -object can be a React component or other object such as a commands list, for -example (this example comes from the context menu customizations as that one -uses commands lists): - -```ts -cornerstoneContextMenu = customizationService.get( - "cornerstoneContextMenu", - defaultMenu, -); -commandsManager.run(cornerstoneContextMenu, extraProps); -``` - -### Global Customizations - -Global customizations are retrieved in the same was as mode customizations, except -that the `getGlobalCustomization` is called instead of the mode call. - -### Types - -Some types for the customization service are provided by the `@ohif/ui` types -export. Additionally, extensions can provide a Types export with custom -typing, allowing for better typing for the extension specific capabilities. -This allows for having strong typing when declaring customizations, for example: - -```ts -import { Types } from '@ohif/ui'; - -const customContextMenu: Types.ContextMenu.Menu = - { - id: 'cornerstoneContextMenu', - customizationType: 'ohif.contextMenu', - // items will be type checked to be in accordance with UIContextMenu.items - items: [ ... ] - }, -``` - -### Inheritance - -JavaScript property inheritance can be supplied by defining customizations -with id corresponding to the customizationType value. For example: - -```js -getCustomizationModule = () => ([ - { - name: 'default', - value: [ - { - id: 'ohif.overlayItem', - content: function (props) { - return (

{this.label} {props.instance[this.attribute]}

) - }, - }, - ], - } -]) -``` - -defines an overlay item which has a React content object as the render value. -This can then be used by specifying a `customizationType` of `ohif.overlayItem`, for example: - -```js -const overlayItem: Types.UIOverlayItem = { - id: 'anOverlayItem', - customizationType: 'ohif.overlayItem', - attribute: 'PatientName', - label: 'PN:', -}; -``` - -# Customizations - -This section can be used to specify various customization capabilities. - -## Text color for StudyBrowser tabs - -This is the recommended pattern for deep customization of class attributes, -making it fine grained, and have it apply a set of attributes, mostly from -tailwind. In this case it is a double indirection, as the buttons class -uses it's own internal class names. - -* Name: 'class:StudyBrowser' -* Attributes: -** `true` for the is active true text color -** `false` for the is active false text color. -** Values are button colors, from the Button class, eg default, white, black - -## customRoutes - -* Name: `customRoutes` global -* Attributes: -** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add. -** Should any element of routes match an existing baked in element, the baked in one will be replaced. -** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes) - -### Example - -```js -{ - id: 'customRoutes', - routes: [ - { - path: '/myroute', - children: MyRouteReactFunction, - } - ], -} -``` - -There is a usage of this example commented out in config/default.js that -looks like the code below. This example is provided by the default extension, -again with commented out code. Uncomment the getCustomizationModule customRoutes -code in the default module to activate this, and then go to: `http://localhost:3000/custom` -to see the custom route. - -Note the name of this is the customization module name, which usually won't match -the id, and in fact there can be multiple customization objects defined for a single -customization module, to allow for customizing sets of related values. - -```js -customizationService: [ - // Shows a custom route -access via http://localhost:3000/custom - '@ohif/extension-default.customizationModule.helloPage', -], -``` - -## Customizable Viewport Overlay - -Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay. - -There are working examples that can be run with: -``` -set APP_CONFIG=config/customization.js -yarn dev -``` - -```javascript -// this is part of customization.js, an example customization dataset -window.config = { - - // This shows how to append to the customization data - customizationService: [ - { - id: '@ohif/cornerstoneOverlay', - // Append recursively, rather than replacing - merge: 'Append', - topRightItems: { - id: 'cornerstoneOverlayTopRight', - items: [ - { - id: 'PatientNameOverlay', - // Note below that here we are using the customization prototype of - // `ohif.overlayItem` which was registered to the customization module in - // `ohif/extension-default` extension. - customizationType: 'ohif.overlayItem', - // the following props are passed to the `ohif.overlayItem` prototype - // which is used to render the overlay item based on the label, color, - // conditions, etc. - attribute: 'PatientName', - label: 'PN:', - title: 'Patient Name', - color: 'yellow', - condition: ({ instance }) => instance?.PatientName, - contentF: ({ instance, formatters: { formatPN } }) => - formatPN(instance.PatientName) + - (instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''), - }, - ], - }, - - topLeftItems: { - items: { - // Note the -10000 means -10000 + length of existing list, which is - // much before the start of hte list, so put the new value at the start. - '-10000': - { - id: 'Species', - customizationType: 'ohif.overlayItem', - label: 'Species:', - color: 'red', - background: 'green', - condition: ({ instance }) => - instance?.PatientSpeciesDescription, - contentF: ({ instance }) => - instance.PatientSpeciesDescription + - '/' + - instance.PatientBreedDescription, - }, - }, - }, - }, -... -``` - - - -## Context Menus - -Context menus can be created by defining the menu structure and click -interaction, as defined in the `ContextMenu/types`. There are examples -below specific to the cornerstone context, because the actual click -handler and attributes used to decide when and how to display the menu -are specific to the context used for where the menu is displayed. - -## Cornerstone Context Menu - -The default cornerstone context menu can be customized by setting the -`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. - -## Customizable Cornerstone Viewport Click Behaviour - -The behaviour on clicking on the cornerstone viewport can be customized -by setting the `cornerstoneViewportClickCommands`. This is intended to -support both the cornerstone 3D internal commands as well as things like -context menus. Currently it supports buttons 1-3, as well as modifier keys -by associating a commands list with the button to click. See `initContextMenu` -for more details. - -## Customizable Annotation Labelling component. - -The Annotation Labelling Component can be customized using the ID `measurement.labellingComponent`. This allows users to replace the default `SelectTree` component with a custom component of their choice. Below is a sample for customization implementation: - -``` -customizationService.setGlobalCustomization('measurement.labellingComponent', { - component: AnnotationLabel, -}); - -``` - -## Customizable Render component. - -The CustomizableRenderComponent dynamically renders a custom component based on a customizationId. If a component for the given ID is found, it is rendered with the provided props; otherwise, a fallback component is rendered. To set a custom component for a specific customizationId, you must register it using the customizationService, where the custom component is added within an object under the component key. If no component is found for the specified customizationId, the FallbackComponent will be rendered instead. - -``` -customizationService.setGlobalCustomization('customizationId', { - component: CustomizedComponent, -}); - -``` - -## Please add additional customizations above this section -> 3rd Party implementers may be added to this table via pull requests. - - - - -[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js -[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js -[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal -[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c - diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md index a6baf90f447..ed0fdfed03f 100644 --- a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -489,6 +489,28 @@ context menus. Currently it supports buttons 1-3, as well as modifier keys by associating a commands list with the button to click. See `initContextMenu` for more details. +## Customizable Annotation Labelling component. + +The Annotation Labelling Component can be customized using the ID `measurement.labellingComponent`. This allows users to replace the default `SelectTree` component with a custom component of their choice. Below is a sample for customization implementation: + +``` +customizationService.setGlobalCustomization('measurement.labellingComponent', { + component: AnnotationLabel, +}); + +``` + +## Customizable Render component. + +The CustomizableRenderComponent dynamically renders a custom component based on a customizationId. If a component for the given ID is found, it is rendered with the provided props; otherwise, a fallback component is rendered. To set a custom component for a specific customizationId, you must register it using the customizationService, where the custom component is added within an object under the component key. If no component is found for the specified customizationId, the FallbackComponent will be rendered instead. + +``` +customizationService.setGlobalCustomization('customizationId', { + component: CustomizedComponent, +}); + +``` + ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. diff --git a/platform/ui/src/utils/CustomizableRenderComponent.tsx b/platform/ui/src/utils/CustomizableRenderComponent.tsx index b5e47fe25db..a550c608695 100644 --- a/platform/ui/src/utils/CustomizableRenderComponent.tsx +++ b/platform/ui/src/utils/CustomizableRenderComponent.tsx @@ -10,7 +10,6 @@ interface ICustomizableRenderComponent { export default function CustomizableRenderComponent(props: ICustomizableRenderComponent) { const { customizationId, FallbackComponent, ...rest } = props; const { services } = useServices(); - const CustomizedComponent = - services.customizationService.getCustomization(customizationId)?.component; + const CustomizedComponent = services.customizationService.getCustomization(customizationId); return CustomizedComponent ? : ; } From f1629c5d3604c24905c70b9560b47d1f8ee8a12e Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 24 Jan 2025 15:33:57 +0530 Subject: [PATCH 14/40] updagted the documentation of customization-service --- .../services/ui/customization-service.md | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md index ed0fdfed03f..8e52689bca6 100644 --- a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -489,25 +489,16 @@ context menus. Currently it supports buttons 1-3, as well as modifier keys by associating a commands list with the button to click. See `initContextMenu` for more details. -## Customizable Annotation Labelling component. - -The Annotation Labelling Component can be customized using the ID `measurement.labellingComponent`. This allows users to replace the default `SelectTree` component with a custom component of their choice. Below is a sample for customization implementation: - -``` -customizationService.setGlobalCustomization('measurement.labellingComponent', { - component: AnnotationLabel, -}); - -``` - ## Customizable Render component. The CustomizableRenderComponent dynamically renders a custom component based on a customizationId. If a component for the given ID is found, it is rendered with the provided props; otherwise, a fallback component is rendered. To set a custom component for a specific customizationId, you must register it using the customizationService, where the custom component is added within an object under the component key. If no component is found for the specified customizationId, the FallbackComponent will be rendered instead. ``` -customizationService.setGlobalCustomization('customizationId', { - component: CustomizedComponent, -}); + customizationService.setCustomizations({ + 'customiztionId': { + $set: CustomizedComponent, + }, + }); ``` From 91bffee838dc38063cc1654e181b1c2800c52500 Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Mon, 27 Jan 2025 10:08:11 +0530 Subject: [PATCH 15/40] added customization for context menu item --- .../ContextMenuController.tsx | 8 +++ .../src/CustomizableContextMenu/types.ts | 1 + .../components/ContextMenu/ContextMenu.tsx | 59 +++++++++++++------ 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx index 9b7c1078b09..dd07810bb59 100644 --- a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx @@ -72,6 +72,13 @@ export default class ContextMenuController { menuId ); + const menu = ContextMenuItemsBuilder.findMenu( + menus, + { selectorProps: selectorProps || contextMenuProps, event }, + menuId + ); + const customClassName = menu?.customClassName || ''; + this.services.uiDialogService.dismiss({ id: 'context-menu' }); this.services.uiDialogService.create({ id: 'context-menu', @@ -96,6 +103,7 @@ export default class ContextMenuController { menus, event, subMenu, + customClassName, eventData: event?.detail || event, onClose: () => { diff --git a/extensions/default/src/CustomizableContextMenu/types.ts b/extensions/default/src/CustomizableContextMenu/types.ts index e86d1a82483..813e0833c06 100644 --- a/extensions/default/src/CustomizableContextMenu/types.ts +++ b/extensions/default/src/CustomizableContextMenu/types.ts @@ -98,6 +98,7 @@ export interface Menu { selector?: Types.Predicate; items: MenuItem[]; + customClassName: string; } export type Point = { diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index c2bd83889a4..78a39d34262 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import Typography from '../Typography'; import { Icons } from '@ohif/ui-next'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; const ContextMenu = ({ items, ...props }) => { const contextMenuRef = useRef(null); @@ -29,26 +30,19 @@ const ContextMenu = ({ items, ...props }) => {
e.preventDefault()} > - {items.map((item, index) => ( -
item.action(item, props)} - style={{ justifyContent: 'space-between' }} - className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" - > - {item.label} - {item.iconRight && ( - - )} -
- ))} + {items.map((item, index) => { + const itemProps = { item, index, ...props }; + return CustomizableRenderComponent({ + customizationId: 'ui.ContextMenuItem', + FallbackComponent: FallbackContextMenuItem, + ...itemProps, + }); + })}
); }; @@ -66,4 +60,33 @@ ContextMenu.propTypes = { ), }; +const FallbackContextMenuItem = ({ index, item, ...props }) => { + return ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ); +}; + +FallbackContextMenuItem.propTypes = { + index: PropTypes.number.isRequired, + item: PropTypes.shape({ + label: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + iconRight: PropTypes.string, + }), +}; + export default ContextMenu; From ef0df2eeb2fdd37d77f32bccaec5d77599582ad1 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Mon, 27 Jan 2025 12:07:18 +0530 Subject: [PATCH 16/40] update the examples of sample customizations --- .../docs/assets/img/Loading-Indicator.png | Bin 0 -> 3207 bytes .../docs/docs/assets/img/labelling-flow.png | Bin 0 -> 66131 bytes .../sampleCustomizations.tsx | 73 ++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 platform/docs/docs/assets/img/Loading-Indicator.png create mode 100644 platform/docs/docs/assets/img/labelling-flow.png diff --git a/platform/docs/docs/assets/img/Loading-Indicator.png b/platform/docs/docs/assets/img/Loading-Indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..d559db4021aaba94eebab4ca0f1c0cb3e20d937b GIT binary patch literal 3207 zcmeHKYgEz+7XRxsYie0jZEIyXJI!YKh~rp3^1;;FSZe5siGo7qs-qYvh>9rWSX-S; zXV=%@Bbsq4gV4+nl~KkubrsPvK|uknfC^C5Kn0Pd+0XlN&)E-sxaa=vi_`2)-$I=V*tQ58USp9H~-F>d3ruE(dujnF<~cx=24fo)?icGiL)mF zptam_{lcf#*gorY0s#Pa3a$@{-rFz=)zP;U4tSu?v*=r^1%ImCQSuHa6PhT#e9+B~HQeujKsJscYpNn;= z|785n==#7YV`=|)CgBREdBfLcu4^DaOh(1&SgXvdRE6ir|*fvV^MlzwTa79f;$&jWx<)Qi@TZc07 z10VX~w0o3p@vBoyYmR>yUISaQ*CN%4ge%!pi;`ZsV9}!e$XSn9-Gcm6>Bje}S})X5 z-r6;4bcmVH8`oq{;izjKg5CdNdm2$8*bT0QQ}mO=Z-;yYpljxnOkrm4(1AMuaMC(2 zL60VX=-Ad{0V@W{kW7QW#%`OdFEVRs^CACKh*5{GvktJV)=wEjBGwfe>jaLtVyu)eu|T`zf|s)1@$;f*7-r3Y}-;rr-j-r)TZ zv0UCu_nz92tqeof#c)2En#T=ra4Ux-1HkqrUKarT(dJWN`wr{f`k#Uz$TBm9z(H5n zN>vv%ds_6q5$AB=vN%dOA_bex&dgk>mP)0M%}wa8Wgh|JaD@5!qT4-);rARRe_Zm7 z7}R{JCd`c8!*%ihanml{dL*>^LBYYg-^k>@Y{Vs?P)8O@<0$Tkn;gEt?;c;YB*ZI* ztCmF4A$;~4G@&~PL4Qc^&ZCk#4EeG|kXQTeVafSR1@~uqP_DeL#QC!*PyUMUa5y=~ zAq#(QzJPDX}T;omR$f)u-+jwKKKstF6hb z3`#3f+7To{i<_KSu8~}B!nyJ|W*2*)ERTUIT1}sHPKJ)Ske)dwhin8LxP$0p7ZBM< zA{Qf6Vx@b#`Hk#-YJnu2{rEBBQYKuO$_YjwHE^!5ks)Wcs9Es3Y5i5xnDY#Q%tB1b zLG_)j2sy8;DNIkQlQM$iFcxbL(Y(BX@i&sS>p-m#I$xU z3OCS#+3K!Hw_$Tp(r-sS2uxz`tx-%lHil@JL`3K-%AJ~GQWJ**Q{R0)4Z)TMkY4RSH0wN4-3AL(^h$BZOl*#8B@5pk zIVPNDIV#5M{P;>;zm_r6nUk9XEs{?Bs0PWXJk8N*OP>ry;)y-w{{Fb!42CAC0|}BV z=1LwfPt+COFVvRz#D`c2J5vlV^>}q}Y-dLy=b^Bw9jBTXe&y}Gm>*&aTo8pVY+DEj z5bte>4GauMrlzS$`WTXVoGJLr?3X*DfAyxh!&J3`VnZzL;ZBtqSBck(1bAWr(-Ykp ze@~qikcVLgq|ZJhTI|UB$fBR?o`jp&o#W{g%zU7{fLo}@8>^{XapP<5wtjbfoWqy0 z8t_;{ftZS3*Mb|ssxdm4V}~AU1+R` zoaUyklyo6w8%hqp&sfSiq8S{WQy))J+;Swwr1a#PC}LW7j0pQAf+;-W*v(Y@6NX(f z>X8Zq3roXLzMrT?y0<_l=?i94<0oUfc_fElLTIT$GX28I`R3WEFCNwy57dzJzTtUH z)uP?I=h=hkF*dfvQ&0{vyxyy8Q~rxO?PJwRa9`dXCyWf9;T6j)@f&1HGxBtFErmU? zCutH9zrQE82MN~9W8vnFjlk$ewdkGnIngTIje%Zr+%_tD?_c$gF{H8JAGa}P~Ki7ZA_-KnNTkAeXY0hsUWn?MmLn*2?t4ki8stk53jTv zh9@-r_{EtxQkwCmBFfcQ1q7Ak-MJifo3|R%sWS#DWU|gVT>sfiQg>Y9_NZUORBj*6 wE~MBN*ld3YxcvR)kKp4H{<{*cS1!rF5GTX%#jl{&=K(l#3L4sc^89!I2I76}TmS$7 literal 0 HcmV?d00001 diff --git a/platform/docs/docs/assets/img/labelling-flow.png b/platform/docs/docs/assets/img/labelling-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..d16f46149e444d6936690eb659c9e5144d0cb161 GIT binary patch literal 66131 zcmW)neLU0q|Nlvn9H~^3%g(1$>L}xAuIHkToUYK7Ya2OChQ(aOY(i2o&N-cq>#O8? zu3|K7BQ=#>5ZjomL>rrtW*eLF^ZottzHRUQwcG9adcNO}=l$_`KJ{>O)!e7MPf1Bh z^YS0R-&9gkZrHh!_wL#GCHmKYWji0r$eXSgl$z-Ji#s>FVb0f`m6Td@H8z7(ckb2i z|KWpFQaTj-f0y!fsL=x@rR}QAzdPT402x*IG)H;;SDo27Vr75sSoVh}AF5v?Kbrnq z|M!)Sz^hK|M@RLv2+xOgGnI?KosGIt~`3{2gJXqD-m+Zo( z)6DVO%@867LP}2jk_7<%_lues7|<1Kdm&Ii@wijU#*c3&#VT+;6JfWl@ZsP7UQPZs z+2A4`qYU9*YH#y>ybYEK`ejdvjS{-@@2NHpKNY4bw&B2na$Z!gGOsnl+G_x`bE9vJ zF43<`INpOvIq&{(Hig@C7)1=6<{JJUmm`_c;YxlYu>2h_rBdXoh5g{hA2&x(eW<;6 zy{yENa!St-i~{=Im=+WfWZLrC{Wh?kIWw)(+6t8np)G*EBQ}+C=}+y=GUYZc!l!Rx zkgrMA+%#D$l$a!TYL@>w_sT)!w4yqOIn_>s&;#S$~u%SS6^qa*OYY977@x z3xhzJ%*I+WC>3>(gRRVw{tG(BjNMH+!j0X{|MZmKSL%jB0dB1%ag;5EqGWsmut5>) zpw2CQ_TrKs@=04vqANi#|dIq5?RMyGMk->9YLwKbf!)dH%i?ZEKX2h}5) z-tJ}czhzvapdZnIFR&L4pcVfy4(EQF^y20qVKWZ_;MY(94#~O9e&t z82!dk-gss7813Y>TQcWwW|dGuv;L!LBS>kf4fs+rlQo)mcTCL zc2$Uc%rc`Xha3C!vGGBMJbf?eeiuKRmz8gEJBjB;~5zEU-~GRw10`GvNv#`;o@*lUxATbFq~i{T<(LVEPwRZ<}fxvfmYUpw^!OeH%0VP zJoN(I^uglvAp$7K$UBm5km}G{Uo(?zG3}5Wklj=R)!dxe%Lt6gc#$KDNqN5BPw{6#m6rhnN!T^J-zyr50K7q+fubQ zZat}eMnbhlDsizT`5bkTTOh*dv+g?`o`q>BPH^XJK0Fp{PQS5`e#`ELF#Oz7rQ#uf z$_xIRRDXTx1W9xxLv+u9Ev;HtU6 zoDGc(ei%KZ@xNAD-t#U>UqNRJi33%`sY~g!8>5gCR3_)+M@P||%#5xxWFucD*s!gF zmg*CHpuC5}zR&sfagg#O)J32CLFAc6a}>1C9tk0exc}?KP06xdKqf~6nv;;bIf8V_ z&$x@^NedBSkA;?5)Xf?F5-Y7ym?B45!Bay^uw^#h0G4V=I>joU>*2K>evaSFXlmSriDZ=ZNyRb+)Qgm|9XLqEtjxkvfs`ks0_4RAzIdPwTD-kpEc_JPLu znGMt4ojFKXu|3n* zyw`;l zdv6*_TRv$?mKr9P6F~ooOET&T6l1qUsDP&>yD5oN)C%ae0P)T9QmgmWlcA1I!d8br z8G#r4@My#AbH2@SGwYx)`;HbI{-qu}e9GUQcz^nFZ*ksyp^;KU6374Qux_uo-D=NR z=3)9V-gRR=FEr1DYKRH&F3D3|iF^+sj}fA_hs%-N!6yXCSHZ*3#avo=Q}q zE$qu^X4=k~Lx4C6YBbK>E$rfo7VNWs1`(<0UsSoljD7SrG4g#q6zLRW2|P@p3;q{w zQpDICiT^1Rs)DgeQE9jv()#17!o_NTS1HnmS`->5qNe{F_~KTP?vPEs+E3Mcx^*1_ zufX@RM=mjcd#GdLU`k`(+AIIiVDbZ z=`supu17v%4e!sFLZm*&-JK(vkx09{UhEC?T9tV-qx;jv!anOjoIC+CV^{=dr7E&- zdo7W0zXPx6oo#<{x5;a4WUZwub=r)$zzy2(pXZ|WE>U0&9sdxb#qDU)1wI5++UO6a zu+13SVMPv%XYg+-f{TJDHk$7CS=)yO7t3X4UAT{)5-Z5mAl+o;CdF|1ZS0uBYl%kU?(WrJ*KE|2M5n zwLCyqL{mYVh^tHspm!a}FDh&(($bmGr%H!5XAUEq$yZ`m8f{QJ!o-6dwzT>2%JO1r zVpU+%e{CzEgV@Re$W`GjH6r~b;n?7!3OE1!_M>Q2ZOV)OI4MaD&rNN!8emNcZk#i7 ztwU=uua7o0$?l|x%T(hi=l^SaG}-ph;>T7hChG_0^K{!;{7T=}F)#TcS&acMyfM7K zAwI{WacF8W(ksd~k!7Qlu;DK3f9OEM7C;6HaWmi5Yb0oTyZKgET*zfI0L zY4&(sgvqzwC(6UC!*|7ak8DsimY8F7RXFP3`9oNI8OKRLPdsC5oSC=}WFWPcA2c~v zY+ZfNjVt3?2bErq`NQ$g0wfn>=mha>VRx`YxOc3ElVVL?y+~o;vP7#X=1#bSp@^-{s$0Kgp9*hIsVXsxM~p;1Nkr_@YufYIV6sIs{EK&f1EKDu?_hBWqpOW{e_A)2 z({NrUlxixW|vpS{Ix1G|di z#Ue1;y`h9QXNMPFE?IHce0Kx{O{{r309op{mBy>v?HK`sV{>p57bS z^eOAZk@s_7625$@M!&Vsnf}A@W{?~2VNY9*Am>o~4#WRJ2NY4=qZZ#mRKH)62!ZRgKGP`%O*+wrb$3eFyg}-`z7gGLDc;j9EFJUD& z%UVv|&zVVLg`GpcB&Ofl;`IJ{$#-z$k@VSE!&LE*$UKD?E(s*F_{kJP)KBdQ0p7AQ zurdA+v~K!flf(iPz`%TZoBtA=niC@J6}8GjRh0{$UPTbatl6R?Eql%CGDw=)CsTND zdn`j~m(hV-i%cZCywyUh)*!!fblDot=INVRIRXMPPvXy3BdRt!m1SW-Ve=^08)eQQ zA&26S=WAdlMYlBB4eSV2v;Vy8+d)61ywFW3fMLLm8&h})w;#vtZC_9>4 z(4sY_a}P6D5q36zi{P+F)XXmkW+8VoL2i~Q15deXC0RZR_+|7s9%aXJ4faPUcFklWVQlTEfDPiE{srE+yC_aYoW93Cnb&0WCRe|P_4Fok`=%GZm zO!b(z%(%l$s+#*uJ*A#rI!%yVm{|L}CK$vOXH*lxL*!oG6OxBx5SE)4n4>*b6fEg_ z)^CZbgZ$$P9eB$0QGzeMuaL?tBtF;ySNt!T(w# z+7cjAmnZBG-Ou*0hD9Wg!^xR^9q>S+qY~Z(;dD^gV zwYd)%AKRj1Az1UTpG)i#_5&MUv}J@|jC@^LoLQNYLEeIHi6!e>b3zHKlic33hxi9u z8}cURF83bPa4IAA<9iL4{IJo}SFylwi{=e#K)0-5P=}D<=p^S8>r=ESanNO<4(?tu8?`{H z=uk(V=d~Z9jkoa=37%g5T`1`?8>4B>?)_@A@1Jf67A1mi_5nMLSM0|PQ&t~zix(Ty ziFyTC)B?n8duk7RJvF*2ei-73DZdmD1hGbyI4EddM{zXdPZZiZRBfr-U%49=G1th;d9kg8w9P z*=D&f|4=?4)}i$W%^uc@m(u@Ox>umGR-6%r5hMRi%YVT_9k%q0-Jd;?rMkQ_=CJc; zbhshIaia5pT5KzN7nkUczOkBluHEKnX5NvHp7}KV{VV`}M++WGUY8q1O~{S4KDh!r zi`=840MMQdw&JSOPhcn0eBiH+wMz^gt111rhEIkwNggL$kmklwhGs~%|4%%Yz58<8 z&{zR4uP^Zj75qd3wL=E$Ts7)VG^omNrGQA$lz9JfqUXt8N0iOc6U5xmYpHHf_`^E3 zS`8~H1a+?%7P`OJn-~_+>U%M!%d}AYnqxkmY0V-R_Lp9*^9+;Pzp|t-ZPMzHyK|Vq z&%=0%u4h2c=q4noVt(06%G z|6faf>lbDfyKACsX1d9~`$v_S*7NSNd`Y#~D8^zs-N)eZ{eU8GiK425pg7Zq#|%xw zwk6Xxu?&;gGJf_(NZ_VVB98Dm(el7;!E~2&xjR_+Tr5@l?`EVRev0%^;kKYQ<;8o+ z-a4B$ei43z8E5p+e{_C&4x#$j)@q>qUNWPRmxIrRa~ z*rec!I03Yklm`Ds{LQBIhkF2<4l`=Y<7(sd)Izf>Ii>x{rTTj8gZXluq4Ut-r4X*C zl@HZH65AlYqV}fgue@t0IQ5n8T!}zU46aYL$ zg}E9)FQX(>m{FD5`@NoV8tfbk&wO%(R(NlHY0?Kn?1 z1ET4uoE#;n{4I=rC#tVAagO_tOj_)u9Z+YDGs*bKnE2s(7ml#u8RbW=_FW&6}BD#GxG8;X%0%@;|J{OR+4EMfg;{?o*KOVrOa^?m2zl z6Ay=Z!TzLDE3|A^{z&FIDV}LBCrH|#y#W1#( z#I9`G=CUjl;?xA0e{Gf%-OM|tHPL1n)Kq#)QKAtkL`k$f3Bn*r+cRxWZiNhy#wrCr ztsD2#NLn>+n6Bl+_Ro?|dW=|KLXXf-Q-h7@8KB?s%mn&r?D$30nU+qeWQMTH4-g~DqRgsQbTQTO z_?t4W3AKN$)H8xyj^+$N&V6lP1bQ8>Zpl4Tm}$85d>G-i^|Qt}@i+1s-EKu6;%WQL z(x*&BP}d8;MMXKu-VJ5}-JzaVDHvq8WQmhk8Yy2}N$CpM`uZb&b=+&!@J$5iPVW0r z-I-xEC%xY_P!F6Oi}QKodjC!yI!}(C$2BDhj*ZyvOlIWIrAlyD8#YpFxu9qSsT4k& zEhLqAwH0=owyMj++$b*vbtR)}#oeVXxsqVU5Jo=RA^Yq9NRrx`Q%bS)QKDDa3LT~? zKgh`Pk}tphQ8lkiYSmE@o}t>rt^WJz>3QwhmQ zhYQT=kH{MAv~X|-S5dp=I|SJuxPANp(nm9SuINR?-xrK3NjWBTl|xNhx5R2x{@PvL)m1VN9Xl zJ~sbLa<48F7xB>HRQq_d6pN%B8|XT_)JP0RV1hQ6#uG)&;qm(mRc<@tW#w{df2pB? z-VA(p!-Bt(Xx`O)DBraR?o@m>JMOR~iZP|qZan@#&@Vw|^QkcOQ%&n#YrXj&)y1sX zX+!+M8BSA!)P=d-ai_Ur!*q(AvW?AS?&CxzgKU8<<9`;t`10-BbA%^3;RlW1_J3dA z5CT%RWbB7uzKXIY{Iq&kNnpZ9)T~-J+3TL8oWd_pZUPaoA4kwcU6+E_JbvaXqtKBeJy0Ug;#1S;Qdrn zSD?`?lQF3DSG@D==326z?*E4fo_5)l-lm6LJnM&E>GDhLQjT9YvYDOU)_hy*w5#AxsiNUU)yek#tHiUCh!0R2{#Yh{@x~} zxLS2QA6&cCQ%=5TqC2PG1!S*T0o&hcJ#jV`i{K;N-w? z`7m7a5OYMs8$JT{W6NmY2)&fgBlZBZIe+aFTnx3Y^oHJfSEREOTK*kZLH_T4Vxb3W?8;!v zJ@%V%S2Ldghfh6(TU!yoX4lbLMP6Re5)y8w{G?=`x=QZs|9ZOG74v0Ve|B#-FC&ag z)2sEA7YH7dKah1QB>>vM8{6NV-3I7d3QS_Zb$FW*QlcEb`D&wYDJY!kO6H|T8x#RL zq&ZBxgyPniDmC%F1mH4(ebEe2+!`2ikwK8(9S zh4Ep&rzHivPmstMb>tcNt>BP8&fURCB{&1+dbEvlg{Lp9hob3y{#iTNQ2uSYOiJeD zylh3!p3eo1@^4KT6{ae>Gl|0xeS8*S54P3l9#o>qRQlmu*2WZ_@;2IHiGl1I)gU(g zoG0I}(K!4TQbyy?ZzIrata!yY@hzxTf_Rm=qu-(;9J{yMgwyG$8od|27lY1c-nMXk z2wmH=+!HvZ^MxZ*s2s^=7WwTv|L%|lBZ;ZzRO308fOJ2f5FAhhkc6gV)3JhNO6%ir z+g)>~CQ&}!*6xT>O|1-IO^tgQMi>GzDlV;ylb*J9)DRNRXA)qFaIe}K(W1?f`CtZ9 znG3NB^p_W)7opgUq!?RcuOQ$*cz(J)X2;P#y{S#ueJ7?W^!gzkN`cmxvH9f(g znrtZ$Q5^3G+oU?HEghk5DI={>XGU>msK*i;pT-RxH0u*n5i5KB+f#M?#}|m42Agtk zw(R4^S5B7j`HW5CEp$(0rz43NfJzjOS|y4%n8M}C?un1C10QSXMZlwXlV)|*x)v#i zX#T2`?WxpzPlDdgaVd}c!?M2Gfm!R4SNm!wFSs4Tf?M(+}i0^S|!Kdl4)KlwDj1~l!-QYg+YA4Xy{bI07)74bzG`?=c_0KU!QoCbp0BtX69}3+VbQk{}?`% zd#kdEx1%+#ia!gg89J!nrVY7!!~Hgt;YiMjFrdU^tjJ-Y&ccz%aXkA(_y_eF>u;7H zO?d}@jE3s-XuOBO<3jEs{(APR%Qv3d_p>3buN%ul!zai@p9x-oTO+lOro7 zbECV?H{sPI8Xsa%S`lO8aVba?G{wbg-zS;ny|O1OK~x0 zrzOn?wp#|Sk=7uOs6|=%#MZRIm*QW?@$}xdI3jOYT`S)8Ks@M6Z)tipDn|Dw%fb+C0Uz}6HZsw5&pjMOpe@fW| zc%=4H1?nheA1M0(l*c;#VMTm|Bx5m0fPi60^0{?SM%~KNzpzmvl04tx= z@|M4nC=`Af>1N4R9vd1}w#gEtgvLpqcTb3)Z`Ua|7&f6}gx}SEbLbKe#|!!zM`#Ob zjllc{i|SUT>%o#(r;(l^MI&@K5BJN@YY;STTBU#Dx@)SVg3Se@*g9S$=VY z*qVclc0?~f@ToK3AH(HF7@qc&sRzHOM*lRgN)t>DC!6ew{m0N)NZ#a-VJENgPy5EY z>?Z?gaf{eJ(8=Uh^pF1MR?JLeU;|^Bvy8mbnnq-pA#vvG1f&(ttoJs&KM+e zP;pGls2%uJx4Oyg*H92aC&&YmvZ8y)99AMa`v6%n_8b`oEJ>h+T{CCwY9#ulY;6%0 z?U&Sjvk0JGe~Xp$FNHUbT5Ne`5Y*W8sb&~g7b+JRW|vG(XgLHe^N)e8%8fdV%muY^ zrb8d!_AGarmq#}|KxHV*aIDf-9=E{AhjdJ6N)8Q}k5YL4a@3bmDA`Zd_yF7DfAa}L z^x_l!1~>7__#vYEJ}zyT7!=${w%VPI5XUrs#&_B}Iz=lr8 z6jXQo4>ARW;dC@iNWROpI`yEF-g5G8V#4lQozF5QXQbz|m$PijO93yP=G3AYY1VgN z0W`g@5?ceVLT_&UthJ~S$ZyRX`vAWlrqzqN8dsv?Iz&|Gdo-gVWQ+ofnXE|+68tfg zC?oUSPd;90=$c0;8M;jD(Zw>nMRn~jd=0{Q0++SV9 zH_PG6FF7^|k05@IhZTXlx#zcW!}7NWYfsVzL1hgPWqPH`Un8135ji5I z#>3o*D33^aUa#Tr?Bxc6U;oA;`EbPNoO7e|vWSS7Z)I>GRiw95@0GHhR<}*`K}Y|1 z-SwS0Sf3Pma(kIsL*7`Ftga;YOr>mZ)w#8=jHe72w}46*lg!_e+UhDwwNz8yUb9Pz z#q3~eCq}d96X`DB0o@_MzIW`r{lGI1<|x=(evWI*yzu;n)X{3drMl+ABdL|Bpp;!k zAR9Fl*M*sPMB%O0&c$i!FOI`=Xo=21i7GaFD{>gKPbl;3x)X4Ixd=W73eI7Woag)o zT49}OzD}mB?Bb_kq-hgj4UGT9y%8yX-UbnS)|uPsG1P)#A;V&KI8)Ce*lUO$#>j3H?9Y45CBLrDXyC3(3RSWS!GGS&!=dag2$$lHd%dIqetg`$FqF8GogXn zOaRW0QOZ0vr+HO#439zM>~>-^6(C`MYvHl(v@XhJ#gM&2@@q>8H|s;*Xa6F=kcCx? z)@AAcc-J8YW>5v!#H)4YaOwMftQk>rAc;u`&*bZlB&u@_rLj(pmZ(`@^B7?dELHxf z*!Xz7ftQ)jpcZh5XZ{EcPCUFg6AiTf(100_N|nk z!m=p&D%h?UM(t4Pkj0Kq?*C7-e8SIY>*4#Kjy`$!2^fxqpcORr(y62lv`nuoYC>&7 zLa*FG$tIS9$1ZxAQF|t3o2>Ya@o)W(-TV|44KqY(+fb>nLh%8-;0p8>m9GVvtJ#u# zs9JE-BxHfRUYL6c%0Xd9@!YjzC6Hbct`6DQ%KpPVBy=|MFoMip$hg{!r6O~KQz3ue zp!G{D;DoqN9R9iWwWZ|w<0lbf{+lq6Zefu4ajcqJ?ZG* z*f8h?#h*gT+q>LDW76al?G~-@2aRChLZZ}g!CIX<4*q&VjZkdya@OBEB^Aq=tPy$9 zWW-b15N)|tg8WQ|uhclT6-TRzr?_7}bc(^gnZd*>3#8$ODgqRtjEr8}dM;_k#W zdTlvg{FVv41MPe4br?_@Fy^o>bsRG!iWk0@=_(`E0ciDDG3o=#91l9GE7+^U!wwN_ zjZU>Y1YQUbD}kNh#Xy$Q6K+6cUWCrRA6Yycr0-|0O`fYPrl8<&}5kMpI zWRn9SiQlOvP~lS2!arRg#)BZWO*L3d7W4i0X4&73Y8uI(9~e?LUh=)~J{P*sbva7J zHv>m9z*e5r$x+}^ox&WLT}1GNRW()mH%hf&97v2VvA_5bFe7^}NV&ZJHS2l5L$pO_N9M2JpUN7PxqD;{ z0uJ%TSW$Yvv}!hELD#SWW{Su1GI%|>$BE-fId5$_i+$*5g{++5wbP#&oL4Bbqi?J> zQQ&%?joonXsaBBv9%+l3BHUJzU7eyTR+_437jz=)G}E}NhDu?|wkb5We%c#~g<9As z5~>7OMwfISg>BH7(TCmu$54>%-{-)7bwtI3QX%3+WyjkG)=*Dw1a4KRAp3zZYO9qwf0kJm|v36gXT zHSz8FHqE|wjF>Em`GS3p_-#ig0?Ke&Q{wEEy|!w&on#(L!TQqseyM~2aFqpq@m{d<5|pS0Kch@sT~t`CocJ>R z`&^gRo3vJ18vwd*uAKKjfeM8X?_bf1lD$aDm3f`0b;C(G2ftVywWPpV0=1b3J}(Rl zE5kl>N}AjMy1T=b+@l;575@i1#0uWbj45=T-wgOw7qQa4#XWKhQr=7PMnRjZ;wBqfd$+DQ`wtV3@T<3aR-3#e9M3IQsaRqrI?VML zK?W~#L?%yWH+AefJO9zoemFcSD?ov%@3WpcM)c_~0N(j9@cEV$+0&w0*7Q;vao5+7 zVQ;)PY6l;mXYUjCwWW?g8b#`gkl^>DAMSFbmn6(YR}Zw$Ik*(3`9LcV_JOP7(0&X%!4hxL4+svoU$*f1Mxr#o!J?Ch|-P*qX2ME z&(KIF=D|<02B277t}GnxQX+zXI9c$^0Iqt$JWHB#e#dl>d@D^JOk;FLS6JnMF;8#&L+iCd$Z>+#OBn)>r0cTB!$tW3kV%= zVsZ@cqacsT*kL^-$!#$z*o=9Ln&X*z3Bt`~R6?h*SE>G%o%gPD^Uj4~(;_GI00Bne zYb^kf`=_h`Ek4Y|yf3Y!CQ&#l!gS(qJ zZ)Sc{IlXm@U&7YXCBJl#ho^|`%JK_uUYl5Rzcw=oa}A;sC&#ku=yHlK+Q!+@IKONw z?y8Gk(6rt)YL-KAHAz!f#c&4IgI+BGb_k6pB+qsRNz#+g3p0eJFrAk{?qh0X# z8*tzy+}B5>92xDT@>T(Zvy?Yn6i?}exQncJam9HuulOJu_}ccb16F-jPrVwZk-~z@ zj6?OUQu@bliHYtPg3ir$B{mLx@I6W~zcOr7TYY#Gzis0sa45FIyww5Sf@1=S9Nn3& z40Dr4*=2M1)4G}9pe7D}yi#-|yJ*8st%PCmvz0WnEPC2*+A+Ik1}D+Id+waWnbGSz z-dXMprU$%EPw%&CFd}Wv#xH*L+73LLqPMJxaO~YQ`C-zVq97~S6OB?tq*p0v13LK* zdOih>gwpNY{>eNzmgS++(pe)3F=Pa;OxzB_IUtwr)fv}Um9An>P;_7FD#G=PG&ouq z=}+QYDRCpaf{z1#-QIYD*Dr#;{_nzd;k%;&MG0Com3&4r%KE>A3?B`TK?~8Q{ng}? zW+XVH5>|sqvtB9iVZ-+fHGXZqm#|-`Hfk!oY)~1iIqG&aT-E$-(#yDV)cH&qG0DPk zW|U#nTi%j$BEOK*$329F_H7j>9Ohaih?+f0P$!LHdx`E<@yk_KBp$t@k>WqD0&fTiKQj4=gmQC>cRI3WI*565#J3y*2^#;`+ zyiEjO57=pGEqa6Y%x$eF1<|6(_Qt>n#h8Cl3d_4sa1kfYDrf-FF9f3)3nrl}3l+eJ zi6s*-9%b;z(A6N!0L4qt12(4vj)xEgElg$svG^E=Ne9x^ID!?>|oA25Lt(I znOn#*tg=GNRf%NkxyciaNY?c}dK^jTdm+>c$tUPxAx zG$#F+adw3(mp1F`hjwv9ZQ?{Cx7V1tkdTMF*s|R&@Mf(y9XG7LX0+U+WFj0V4N0Q= z6PTXJ6?+VCEhGeOHt^P8Y5R)G2h<|}qvrh|oM&mg zTgdvP6@qUl=BkE03$NFf{LdxWKKBJ04{;`I1@TODRTc-4s}Y;9`0x*E3hi`G>MF7f z8)qs8AQgjOC%A2{@}tXH(cxVKN0LnG-AB3JtzUYK7y7iglqKe^bW%iPcyV5X_juP3 z>Q$nbN3gyJytq9yC_5i~W$x$v#9pfE(DiOX4yu*sf~QuX^R!2q4yZ4>-;?D&r+vyu zuX(4+0nN_OnOBE5CVTI-0m!5Fj`!Bydy(5B+5)*Ves%zsa-LDZ^w#|LcjJl6tdZ$d z#oDO??{WGCxst@Y%XLxhNAbnr-lZBici=Q!sgzOO9l0Jlf($VUIzt$<2j0jrx*S6w=9b4E{f{Fo?U0sW)Rkuqkeg z0Wf!ov$1gjM^R#D{NOfD@oRzgf8JpTWXRl(_*f`$osb|PDQ&-X6dp1DAlQpS?)$pD zwXiJVrK~VI6EHG*Uv(%qfT={Ys%V~D>bGs!B-+^J_3lJx<9zdOxRsp^&$RFy8(304 zPm0dTFb_O(qJ3N=aQ}x^TOd9g6Iho=DVUi=Mm2FTcUtJJy2480 zV|3cOkDj9#=TnRL$)lUEou5E&v>@aKPum6Zdq-m-?fYLz%-da;Z;k5@ltUZE{&@o} zpKYK*yzFD!X3Sfo;?3FWx!DjGZO!7#5r-vj-40j@^^38zo-e}_xo%KTso}I8ZAv%0 zttmK26WD_cTzX#v=$u%YnKtxCw*;WJm?JUe`YLC8cVUFk6a=bwEYmcLh*5zO1-{(444wcDc>DE5_6AWNFs{rri zAZMMLgb(L8ecLy(To_%dogY`?rM09?jMc19!2HDgxKW|yBDi9P=hS%0f1jJ!HmrxQ z;5B#+JJuuPZT52%D&St#R+@M+PZz%a@<+jA>P9>!dXMg;joGpDmm^Jdpi}dMS;kgB z@}&4g$qRJJT`CMb$|D4EP+P?7j>PUmZGqz>XSYh(l zvt`B7eTR`oe7!l0*I6oD-zIs{+N#W|jjX%ol{QApB9RZB&&@}-htB=t5SBvT?_DGI zCC5u<6_zXA7xW7G``Ap@o?2nSgD6SxPiyJ5(I)%iOm175i!Kk_jE$(v@(*Ku-Z%fa zj{D*Mj1{wa%74~gw}r2EzM90xHv};2-K`yWG*IUFF|$>}41vQ3ak|4{Et34pccbe) z-=AwtOl2mJZ!c96LZ?|K;9vS=8)DuBbQ`gi)GalZxSo zNWZg6lwuy)Xy@8a482PqzHJp;Jh*JMV>=gT5iIFFEkx_Wt1;MYZLBu^Y4r3YX8lSk zX|7q9wPLt50KT|#y`srKRR?Xct)CI++iV6CeSC{gtfq*c?I<1Z4GwZn*ry9LF<+agnFw$^tGSeZh;Z14Z?|7_oX*-GF*m(5cG=dmtPiP6)nExc1+aAvC z>~LXXG%pk9GUz*P!3^TBsNe9tDyt_@(z$q5+!wB3G#F8Y>$w@u_!!jWup> zEY%|1)TxWt8UDmI6=&kuh+bvMu2aUHYSvw=ONocqk4J!QHeJ*D1caajQj~o%Z{eM=`uvxYusY{OOls3^^S0(k`-mm#N*Ox z+;!To+dgL{66GQzzN6r$Q_jJm5qQ>NQt=gt|Nl8oE7RajWsT>c#Yd{PK=@2QPQ0L& z+W|``HGp__lHZ_P(%WKU+p7V9XFgiBnO@KU@b|A7&8Gi$wGB-MHmTYWR;5vx%?qf= z-f2&JEl=~2Qxkr2MfERv`Jg|I4m1ZS4&=8X;X1+ghba#v<%x>GTFvrr&iAOrR>rm4 zXKN!j6K651HlNh-*`M`Ik}y=M8Tg8`CnkS{%?^3{10 zJVVE$+m=W9?FZu`!N`E8P!}@Rj~MHopg$|9jiT73iAMi%0Qb^oh zAod%eF^KVm4SiRr}q-%_y0|K9!B?>i5r$tCyHR6^*b09-ngs^ z(q{S@-Fy!3^X@j+zU4)%^uE_Ssw@W@Q|Ej>7LZrl{uAVI7r=|)dW7nd2{aNVzwiHT zOyt!GD;r{?bu_>x3rJ%EbiT^3GinE~{dUmoV6U3Lz{G`(UzuIiGDI*Cjx(lP^Is(_ z0Ky=`Vt^V$*Lq=&yRN$qJ-9a` zG^z*S4Jr$nd$~68raPOBp!s5OUbro=aO9w-jd5#(bds>@GbdG1l2w!&mP>28CATSWf}#z>Cvf^}u^qsosg_0%FiB3Jw|G*xeku269^r57RPtRuLoba$Rp7y(J&Kdq?G3H~fRcE12bqm*eSxi*r*dCyWdY zoCW8b9UjK>2f+s_cd9pH+I~?Q1Qjcz(w1wA!!}=Sea)u~6UR<0#f9N&8+VpKx4;W! z;vmK&e6UUAUAiD)Z4!O?WB(wZg4IK84vSCS+vPDaN+s0NXn9&GbF-2qN}KR2!!$}- zI%!8sZw?M}%$8+qH^Ef`7`mG$J9}#Vjj}?PRTfXf^uP3G0Cty07N4!JV{O!L=^Hn^ z(3b4=EF|m%_&uj}V&(#2NFJ4z3(@24E*pqpcHjMr{0P-BC z2F3&Bqtltit;N<{WNvwvq)L-&5e+F3?eM+|ctt5(skp!2sO8Phe)5lIzkk~Dhum)k zT%7G8B_1TL_W^eNd}xZ#WvhUpTk_A2aoqgcDF>yF+!WrNaBfENpj)(6a*JlKvP5~& zpmzBD^mJ8HaOm0IOIzVpypPE_up1@!9pnK)AqdkihSC@+#{?YzNAuLq>&##`ZMbJB zv%;prXtY7=Y*+1XN^qB8r& zpFLa4@uwQJ$3EKOJ;)~_fRf6`n-XU9O|TilM)59~32zd*9^Y+BwXyBurCG;FXPK0-H{8N7v|{XvNYJ?_%#9nq zsDU*yc+!9{z(zI_-y475x!uvO`-sax9-VmZw-V9&jUig6DJ;yn{GwrX;wy z{lDHlqxEVz?{r{>53a48_L4pqy|e^j&*k=Q>DXlLED}2JY>1<^C7(&5x$io2FxHGc zwH8g5H?}9;ogpjQmlAiB{NxBTGC95)7XUO zZUI@pa>~-0%3aZ%#Ka{f7eGKWvvO<3aWpqHE6oKpMRNf(x6B1sP*E%wP!Kl+6a@a? zd7ihva^t?wea`2)&SNM~sFq0|(#X90&4kChLS$N4dBRQb)B5D!HFe71mU6 zhFs~T6Bt2VMA5`%d)kQFacjQ-(YN&0kR2XNHmae@8qTl84t|WWq^8z9{EhCYvzbY* zSvBN~)$a=x$m5zUio;$%$m~wUtXEcF=5*8 zQCyRbM75j7XPDV2l1z|-hp1fU>A10bWbXbRgG?4Kv9c7TTN}?@I5IN$miB`A+Kg=n z9APr9zW8X%0xrcr;kBOXyMwxU-?0wQ5m+R(sbi;O+)q|8MROrrWM_a_;hYB^Z<}HZ z!-|Mw?O$KW7AnJeUW;nC7$9njl4HxBZb+#$1iM>d)%};^--*U_9jHFYqV|x#oxMFr z^uD@kkLj6+OETjG!%ZJ%Vo73EYNv)wN`gNc)ySBdga&-RH3??6$EztAJ|PL6Bn;Op#GF?`y;AJC@{LpCn0A!eP*x2cX1U$3-G4ovkN9^ySc!AO#N6P_>#h?_?atJf2->)dDAExF)^Gwy7? znJ6dZp0Tc{b*^^)kT3TTAB1PREkA(P&Ik;*qh+C_*2tSc=25ofa(Lu_P$OpYtxx2= z-I`@|kpp45^ZSD~ixL1#&E*^m)XkH=IjmW166`YMNV38fm@kRrS+xle#F8Em!N1|d zb0wR0$Dwg?P|?2Z1GqiIH(l>omcK{wBz?4|;@Y$t^U*Mrn(V0vwmnHBr{K2ciwMqz z_KJgs(BmTwHN(&O#lb*oZTqmg{I6U)A4k9B1G?u?Y>R2*B1m(0AYGf!Fxdye>6Z>u zmI%M-1JR)J1Eo;!z`RM$C&#~a9OXMqkm#~i`@FxNjL(%#M=@WY>Fdoz&Sx{i`URl? z(@JFc?`QzVv_V(9xAxrjBPksSqEz9GF|@IC@j`=$qV7GOP_|<~{kfn&L&gb;hFOdi^LDpTW70pY-%h0z56 zZjf7pqRP=H(Jyg-cmKdrWo}G}pYK`T`dcgSnBH9c?TwEET4Q#+I9D$Z-le?-x+6-* z$>MJ=8Ppo`#VS6U>3@c|CDEg;BzU~_x)WZ(%&&oz7&zocDIU!345Vc}9QXhMD&sKI zwfUu}dTOLtMsk@TVC5){t&S`HM+EA3Ce8kYFjsFKp=hqLh@n6*mxPa8KQ2kvrz#s} z`qG;=x(g;g$rd{6d`@%CwsiN{1}Xdu5=*Y?^?AO0SmH2ZE9H(jD~6YD8wKlQd7K4@ ze6Cd2=pf(A`-qXWw7DK8Ggg%%C)+Q6r}h0S8@dy*6x+JVJ!hX2l32Dkal81YNBD?m^Ubn-!tdI6)rKjVUL`pFE=z@jeFMSbbhA4~IHHL~S;dNM-veh1C zrc>#C>nW1-h4>=F=HT3rH90TnT|d_|Cah=)^uy+hi9sUcdZot6K+gXRN1n&UWQlw$ zV(Pv-5aolwgurkKVTE^5n?`0&@2*Or=4MOp1MFdLZl@4VrQ&6hLbh89XUx8_ux|KZ zZlP(qXdlpXLo#|l{Y*Phs-Eqj7r>B=ZGUqPA%lrFV8*1H%nc)+->}1VZ;H*F!{cdXxL>A<;n8mA6gk@_#HXU#j-9@ZwG@ka-wJW{^jLiWO=^?kl${l6HD;_~040U9E2 z%xZBo52;mm&@+7y?qv)rX$|_r%cN;5TITM1G3Kl% z2R&jk9Ej3E;c$WzdsYewuFY-Sn5;Sndc4B>(HL_f1*GJG16Z(z z8V~HMn5a^tSGJ$8?U3wnZ77|n`X~E?$Un}E`%AsAmT;w%5K|)?(j_|TOHU2=-oD#K zun{?;0?|LH4|TbycUua2MB!(BJ;OW=Sxlb=(fgx$Y0Pu+760z{bC1a0U-p1tOIgsk zAzZjt8UO&ICE^8xVbg!n!;6F?$VP^@O)0CL#5vppa@xp=mD%~<(^xw&%r;oy^(^S! ziYO1q?74H9R>uQ=dEk>U93+8!-hflZ8(UZ3*7#VZRKWS{sE=2g&$xC!Q(JkpsqA_+ z8&Bl52#2ej*kXkHB!{*v(F;rI-z>xE-w=+21ossDAQ}-ZUxBMu!g;n4H6=QpsvdRa z&27rSZG9#u32`oLnnW@PJ%1_K=tFsd-%ajEdA4rHA6b4md|&%^iA&KPO=}&0M`-!l z8=4DU;7JsC2`-fcjA+qPl<>_~Xjz8SI_c%XnI4BH( z#R}U-a@`Q_UTN`l7X|UhARqpAe}Zjqk>nFoa7CoQXnCZTQ0AF>r+l*PaP5=2Lr5)P z2m{RuAeE^XmvDH|sOhTkNEfPnv4J?_eNF32$SJp&t%27gNAw9mFj0ICu4-n~rZW&h z7VyS8DtaYnu)f0&%zWTOZeDP~;TE#9w+KtO_Y^!MPV$v>8l4)Uq6#N;mh>e+XhW>d z>v|b^k;*g$`w>&~DBEF>VoV#S5Hn5^2c@JW))?YR8DSUr?j!+NzmPt4&{iPI%7rnOIN|8)|dQ!jm zAv>>qA%vi1Y{1NxbW^sB_~sJc7scQphEZ><7Hpn890V}i^9HqRRRfG<$=>IcaOrMG z%wd$y1SzBL6lKZ#olXhX>zx>jy@_xCbT*wPr7i&=1J*Kh_+o+yBnCUx*?_%frJ9c* zV%L4!h?~n3T}y;AnkjgHzVFBMOSgK8JGhT&x;<8SZLuAn^1i5N zJkk9!+|m-MuS#gXGs^{dh;caqsy$FDlo^!ELJyD25q zo8OM-(vHkWNCLn!Gl}mQIt0J*Bf^+|odd*yZk;q%nc^RA{F|CFsgc=Ys=T`rQu3Mw z-S0mX=7E<^-mc~pn`fYIx$(-5$7EVzL7_*jd9J{#Rdj1lfZ7vl9lsaCn3my$7q{tS zV7h2pI6IggpFSK>9wjw(LXUyB2|Ico2a?F|%=;S`M-{aiS8=%#y3Bw`nUJ?pg;OOg ztD=@>*)hwGMHiO>{O8dEpWAfIm($3-VbPY1OOp3RbPAGuz>V1EQ~3tf{<;cvHO%nP zoo+?dBJ2t(k`9F09D~oS>EA{{S^T#(FJxa>xQc3m6}~4JF2Y(6Bn4!d=sv~58-}Wh zU;Imuckn1$COvt)+vbMg%`E1BONjg9;ET(#%zq@OiheT~GQQ4wV#%D5=nKZNiTtwJ z#=7B#@NMnGPQun|J&-jbA0u5|iW%pmcsiAjzFQ)yg_n*+I0*a3ML$129`tO*nKAa1>Q>NqY9fm|XV4+GW!1u! z9mGbeJeUK07;t{=yTe2L&Th>Y1>k1)v64q*he&#0-#V*j~ zVA1e7(wu~bi8~7oABF}cu% zjqZMDMoZP|rk7d!7XRN-uBhJpCT2+cc_1)QKC{q&7fDK_ls{f{@K^UwiXB?heE-1-iDspIx#c*P%c7rf{oLK2m&dg!%6l;*5x>{hpe6$zu}*Qp{4A>39PS z{TVd?-AU@C6?x$*aw*=TJofPlIPM?A-{zFo7P&tv8j&^v%lJ30xAtZ$U*kX~GbBG~GA-TLNKxO8P=P{Sq@l!NkxBkT+-$1%*N_<@vMa3pv2gClr)P_WL!L=!WBCWiUcqQCo_QfS z8eE2r0X#(STc0uN+d~6gYLS!yir*45J}-pZ%A+#nFK+Lx?k(1TOgNbQ(3g{J_$htY z{kM;Ty1Pp_QHcxpLVL96JBH@xwy${|iMdA2gZ>E`OBkxtx$#OJVD}XwpyjF5k=9Or zTG1SXL(OX#X-R5D0h9~6!uJkE>FSPK!o z`SM@I*BEz;tF*F>|H%81Qf@Sz*v5e&;mm*=9oM|v(f-x z?z_h#ZjbsTR#0Z4JhqpxgkOC%h*}ia9^B9LCw?Tb^2|rGdn>c5Hf(UGV|1Ixj$Fzr zx+=k1+26PDtOg1ForXXcP|thZandc;c?i+e+?L!Dv{SFSmb(o*RDY9U-|` zt?J)CP`5>(Hf1ygbGXNtVc)F}41E@U1r_~^{hT{uvN_h}zx%DZ=DE-eQS?vS-{}Md z)%PemC*Y$s8tiaYvqC7hpc9MJ{gVdY)A5(R=*g*^aefT#@H9+e_DXk3ap3cJ14^NR zz>ZNPRdveQJ@7h=?AT81_KAeYN$n;j^dW`|i$fh+#Ms=LXvIV&b8Cv~jTw>Gt|9KyN*j5d5$iFtT?m5^p^KaC2oeW; z8wIyage99Zj~&`SAsIJx3$FU1w2DA;J^?pJEyH@R$bXO}x%6M?Lt+-oq%lp4)akHu zz1Sq5>ST;tAv&zWsyZf3KS}DgbQ-1Z^QX5yMVrEKuJ~wHxYA={ll%cyT0unF3x{x? ziGb6Bs9UisS}^weP{Mq(@W?;C0n_f=VQ8kJXDmB#;DLHM+Uo-Ni0(qO$@+#P&HfUljXp%;qs6+Fb8X}hT z0~$+Dg!H~J0Zo@KaWd>!i|EC`trHit17|`X!_al+zP(9tl0xD~*AJkD;T`^`-tUlP zpFr7kM`_^?vfW}F!f!&wWfO#9*UL}Z*(H@pS&IB=+ILTH(}M@O(PYLYoJJ{+;CTGe z68~s_>Dq9ZXOA&;8*{mFJ^zUy3UC^Q+`W)Lae2)PKSD7MPwg44T+14600Egps&dih z&LM-uf@Ej$l5S0K@ZU1M6f$h5rKwga$QHaWG}@3s4zK`oE_8pGe(lT->-ZZxQkW9y z68JE?)jYqfpz^4l^ptb@B6v4?RIp%+7v%jlY^lNZGKCcIK3W?r-kXPZ*>zT;X=W>- z^#I=y(~SucKZr~I3jw;nl!<{q9=F2{@;n{okN5j)GP+lNEjliN9yz#+up|>H0=K5uV)f=pPy5ZnmY;vn9fgb^v)Acf3ndYf!+W)`7iCr zDiM6xm_BbXS!(a;7*!?fr7Rs;+d?WLv{#+J8g@z5c-nF0$WC57&7c7*NoH`6{CYsjb({uK?hp&4XCeAn0uYYy$klb z!4tL}YWMF(A^hE1tVNB-q(zX^opx6~N=~+x%NHjaB4i(@vJOB_c;26DoXldUA3Sc( zZ!l=mA4{cPmN4=4PPSgTyBN(1H#B#MkjSls3$LK=6_BA3@UcRDqshVQe~)kD^SZ8& zk+5jj1WY~xNY9$k#XS##Pf8BA>>Npv#*zvi1(Om#qP$3n!aB(tE5`!N_~n?Z2iuE+ zd-)}=0T3!Xjb04JyBuvqTxj@Y7Zjg<9`Vn`9OI<0 zAkJc^b;-&FQ|WFB+rV#WOt9o~&!?<2jd?o{{j`KyIu|C%LE;Z^mS%yA@7G^=-$S2sO!cb^bUHNuyq$Hr1QW8-CPCJ!7M4HCIMYjk zlXc4BE`RPm$+m?-&m47mzHRWcYtkq7dT3k*rL(8G4Cb7OZd~Cc3EN_I*wd&KK)4gE zzo0D~9q*0Xh*6B_S>?;;2-f6<2lz5$?}@bF#VgpWy~t9*>)J}MUV}t-Sg{&lnkq4 z65vow%uGOw;$~egMGZu~vcA>e71%5J;MptRmuJ@NT$D=~D)9m1Z(s9iEZRy3739{hg`UroOCpE7<6pE21K+y0RL0gGly3 ztSFqE1e{vEtX0eUm=}_9PG`SfBb-`_MdBR;QJ84vWsRgX3;oHFpbHHlGO+(YCP+WzNiGmVmr0 z3b!52iS14C=Bjnv$LVh=JLAt{SsCdRAUh&uA3v+%ZvUBzF@69#bRYG--SIe(1D&@h zF_>g?>Xf~#`%e|oX%l*vcB94sK>vJZaM~Td4%d{pvdIw%`=$DUEO-OK*&@PLR2DjS zho@VVzF3m^dE8&GB$yde`o4()%kKJ6=CJ53drzyQI92H;g_P>+&D>aD{?@i?p$1F* zGVdalUF_`vjg0zO<%jzh=im$t#!}Ei^kbTa8Loc5yjN*y07tw9^JRzSANekswwYP3 z9xa|=ys#4abHeL_W6Vgig?xyi)O=dXR|}K`xKBHo8;NdC3^B}#k}y7Yfq>pyDj21% zeEef5B<`TJkJnRx=nWGx&cfKrO$jNOjYQB&0eWYcuAxW_(!v&8>Q|U*4RvJ4zP6yQ zeJj19WhsR=6Z2N4>8cgUTyKG}48*Y>=?-&*<$;SW!apAw{%2&^>UzHYllFUt@9m4) zIE^I)V1`mYmdt(1HLE@D{*Y((%$>+1Jm38IE%P8id#Bd}M581q%53=D;=`xtutdp} zE9q9%M-*Cl^OrFLBs2I(PKlalIjk0tmGF{b$VV`AQ`IB7Fs*;&TDh6%a6ALrot_5k zvra;IGa+18Q&Og6!o8AdTnDSMd zPC9+WvoWgD1CJG(PX1bGLADwiu?=juymY_kW+K+77`<)%y7<;1L;g~f0}_l=L<8fW zLXYlos_#hLivO@U^E|Im3^bdM{CqqAEj-}DAH-Y9A8Q)3G;MR zCn2y(*@G~^Yhr=eURx>1Za;^S(qo{ z1388i+9U);oS%?7{C|&-=Q4Gq{DTf<)V>#-K_sH7M{LPCZto~TH(7l>4?KI@n z-FQ+yH;&8)a~BVsUxrCX(=H}$oT{|@rsRG4*)K{I^PyuA`5toG*5~l>QuF3#adqsN zqt!~wu#4r(x`Ts@o|@9TPpG=a|>o(SUU-8EX8{7e1g^n?u7bm>z(fd`H?P) z4YzXQ-t|}>JaYQM_H2_ey+@1B^7Q@|pI_=(_^Dg(=+O#WUh*sampWBl2bnZMtbx)+ zQ4OohCB)=rm#_Zpi2e*dR9n)d#6RyjK9tbD_3C|H2mQ{%GTYjlO5p%zKS;9iknbx<$HsVKCQV!NJI4hvj5d-N7DKqhW?m z7`XzI#}l~A{XKM@_RfjeR=1`_Q&oJ5Svqw<&VikZ!T&DW6_z;ifdM_?f+POuc1W`u zV(=dC;51juJ51N5@4Zl-Tjob_DI<9k-Xs32==3H_kJ(H-ZRN&Ez8x+M+Nbz+5~#0$ zhWBs+3u1WtXoUqYsOa$>!tZuLp0>gMkH-X6?WLfM1`v1)@#$n<5YOcqMlx_#9G08_Bb_ zc{qVyvp_NUf6G7_s+)}!(U?Xg#%0v4ABJ3NfU-eUo*m6(W#?gr?`k;7TbP1B zJ~1s6R|F0-v)m{I#(=l9dF<@qZqgbH+Z?~ux z!l)YpIhwg5ndt1z2hCmcR4M2Dax4fo0^68L6*k9eu%|mxKrnvjsU~U|k=y{8N*raF1?IaY~*dydEVjK>l&s@eYt-odCJMnD@jidHq09XxwsJ3G;U zS$9!g>OWQ@@HyiML*wl8VA5v&&4YsgkIJ%|NqivJSRA%aW&WL;K*>>^`^FxUFVTsS zw-pOW_<6hwex*$ZcU)@8nlCUgQ%2OIJ7ucefxY84dG>3t571=!*KYeUY}j zrm#T6Qknn4Tfh;2L=;mMs)ni$u6ttq*%`A(<}@~X=QcvoQ}uP%kYFk7x03mBGrz#- zl&C`KjjVsPO^kJe+`)-ebC(A-_{-kQQzaUts2}9b(ph);TVU$uJK*|`#(mYKzWR#W z0(?M&&+;fQa>1WAzV`@iRr4t|8WH;0%yg3uW1^IO$ZIlH_<;LtU2@+MD5BKS)~w0K zOG1of7Nl;7ooEafe%PQIV9RBOR!av2|4tqx5tSQ~1jOcj`CI{jOTrav%TKWD%iq5{ z#o(mAjAHqdMa>-K-AwhZJj0u}@!rrney51UvZ>cW@AnYI^EKuFmM9MP@LWxULgT7J z53G-)t%hX==GkbnP_n`UCy4z7- ztiwIardzr|wR9RlFG;@F;8h#a+AJNQVh=q~&_e;AiTbj>)ZVRgqF!>V0&XikjrnxS zSBvrcTyXgt^siKy1fc=dZ9Hqx)-k3mLvN#tX3H%&@2O>8Pw*Yg> z&_xd*VtgSH@+~E~^#5L}yf?Yh8M{B&71p4H-vGtu7% zqH4Si_dDth`n!><-F_b>R5ril{=hv=Nsv%3QM}&eV32zRvgBd6{EbAj!fZLB&FB(& z7i;Z5ckSf*lFREN(J8Z-Ycg+sSHAh^Ar|N?e1s4xH7Er11qP}?R>rWao>|>BjLXJn zzC*_kIr}Fdb1-Ui0bt^ioZq$~%LiIlIP6lmBcs z!=RdbjVK?9~f6Sq~v7xWHVY?v@_ z63*Siq`>>F`Va)S?hC>y@V0$!j65_e@kw@WnRI{;Ta|s!YU%*}zIi<*{YUzg=ww^D zf}j+Uz}ykMOFoG;K&wY601_T}vn0F*d>_stJH}nv&#%O0_RcItZ9Lz}>e#Uw)0Dc* zJBI|n=vMt)A;Cya4xvx+Fg7os2()_Gpwc3yF8u&7AQ&UbJ%lCyjpOEl%#yW;6|FtS zOB&S+mz{b+j1sR%s&H_2O$t!^LxI3n*orLwVK5AS!V7M60(^#0@Pt-f{L_19@%{2e z&E24Z+-;fdi~UiHEf@AJLw2b)h#NfigzEd^cUH_uL%e^d8H1(aU9KaoD&FgeQ{($i z#-Z>Ynu7DiCWRXwvF)EUMk$XL{r28g4ON&M6u{M_Ec5In-aUYFzZT91FU0TWK`n<-HqWVjxFS zvlN;eW-RKa%$a@HMbw8(w|{@|U{!yrI3shQ1^EvUQAfiYC691Ri#WEVxwDJkCapyo zsH(h!n>ZuA?;wVVKM2Q~fDPtZvCVYeSNGv$XI z>#LZCrKVgaGB`O;#P2%r+>ml}&GnH7WTFuxTK@R6ar}A6|MJ11-`?qXrzDxFVo{(P z=@N|y6wS+Rpjwm;4eQd(F%5KGRWm~TAsf1E({s-OUG8B)%aH%rdl*$~u(KOiOeA_YL`Pz0^=;LG9^~k6}2W*@1s`hLu1B{;7*M!Oj2vH*9}WM3C^B&Dq}C>>6fZGR$dTmg{{g0cOAq$ZcmbFC_PBPQO4ULVlf7JUyBb zdkA1yy&V8E@k)uiwjn*_(Y*-CE$U;^lMKwX{)(>8#WSj*F)n&{r!7xE4-R}t#JTeLjv3-i%?$du zAO}qJo8R+4n_GD@sKA9 z5d>8S1K+H#+ajTCR0ivzp(tgdt*W3fDB;<7kLJqADB{P1p3wDZc;``2U%+_0fK{jlD^YxHmf7-04O-j?=#2JqvdjZ#AZFWY)@Z;Yf>e zTZR*HmvwL5jJst?CwE464IM3+9H|3^_TcWPwV>WUI|MUS#BlTz(*cnfp|q;duy-H> zT^{eVA{*}*cd+vI2)WTgS(dGKAIWWcd6W5e9XdzLYy823#n>LY_Vcw+TK(FCB%NTw zt9woj_yIStXJAvLT--vfPUoJf1|P2gfrjd;;W+j7-Dp!!y!5;0Ata~=wD!U6$3rN^ zM}<=f&G?Sn4S8|OLs4eAU-v=_2Gx_X3x%Qa)iC2=h^GF*PR-}($&e5GvL@dSS^YaA z8{q4|r5Fgi6#50Y9|sX&=bU?CNwGkLP+WGAz`(av@zr@;>u0hg!6#eqqyuebtXYK%W6DZT^LF0XU2CpK@_0;{8D6G72jg z?A5R2j;bk`wYI`0-LmOMO0?O<>CHY-Y&pg;du413zAlBSE$JDt>|YOkVCV}*@2$R> z^QhNvue49T17nbR!l2q?R7fs$oZBt2Fx^!OQ&vLq88C=u}oDr$l;ZX9%7+3#?z>8y`xi zSRs-+WLBEoA7-AEsX-lh+p%N~R;mC8wExm+gWNsbXvG$3NdSnh1JX6DzKpJIN3#Bt zAZ)6n{Ob?R17xL_iRGlDGfZPs* z4_w8mcJpbMG(R(`8B6nk`t9ZYVELri#>|8)*k@To>IKA-ZU<);30QCi5}XYPVKl$i zf%95-twst@n*R>*>l!6Fi#>CWh{|oZ*zv6(81ArnV@3aO{!ukFI-&jXZ>19#I@a<8eCuACV+Q65`tW)5 zH#ZNRoqf2QpWa`%qZ77u+rY(rA$-pow86nywFZ^YUS!*3bS|0 zd!;BrEXG8dcs-f{GPCtjvzA9dOdO<6Nfi7qN1YVc`Y0iNAQo&Hhxt~lm7&m7`A-<+}4*ah0q&*PF1$vIn1;-vcnCms$2B0 zx7q&_<)lfS^^K;bYZQm1Fh|MoFRl&p*E~nC<)-eV-8;_#L)(wna?10r5-pwyfvFm! zMkpmKIrA2VVlQ%Au6;PD*Q4t*C(lB~_l~|Ix`C+Vt1Zly`o59zb)C*1Yp1Sz!{9>? z@`?N{)1~TsxTXn5>;f!Va7?--(05W~xse0LUaZ7v;SXEMJ{RUfewpTps$?-=tS@Ep z@&J`+(gR~@Am|lkM+YiAgTdp<8tH#Zq(OaItpT?5BZU z;`^TEy-0iB@J_Qnj(54CF>sui3jrn4>gh5#0hJKtoA< z9EN*g-?oJuwGo$OyirPi_C0GU?X&W-*IJtAu(;+sdeJd?iZ7o>?zsyN^Il_fcwM_K zpK#N(XPBHynU(zwoU~3yy;!@$zYM#*!~aI!JqOsADlsM4k!sYNE6S zqIjl|Mxqg(lkrL!1TGxS(P92bE~LtTTcCidhMrA^e2&~C+X#fi?)k2OB;w|jx&-NY zRnhK~CHMZPJ=$vPrkAXH8nDci(%$A-MYGt*=P*iLkm8LiFEPpcvC!SVFI{f&EL-BM z)sMeX;kggO8hi9GG{}1TIuLLxRN6TW9 z6;OF^m2g@$H{FsMl}>p4utBmyX}R1FGKdscm9Wb|!AB!Egk9`Gy8Lm&W_o(9u+U(s z477MuS<+OJ#T@^Lx}5`9oy~a@s;F0m^h)90Id$3BvBd8^@4`k1n0<6%4E zeyf4U3tJL*X!SJ&!{1_iW{1kSrzW0B|yq4I2jKsw}Uy zDqSb!+p^^gSIRq#vBYlF&Nx9D;N=$n_2Y<>k9)*^bnq6p(Tv^?oufi(Q698uIKCi& zSSlc4xk&sMwSYhNX3NzRx#skt9WQja>XM%XhVKIn zTddDh%JqPYRF=NEz6XO6pbspVs`GX5YA1*8uPW#B>NDy^QyKqye!EcqIg&Dr#*q|2ruLu?0Wv^Z{YIryrVVwJ07Q9CK?1ROae&xzbd z#h>yS;%aBOSjVY09ZXt_m=(8dt`L`3!Utw03mpa;YpZXyh}yDF)rc)vOZhsExI0l? zK$rXBLIg8~4T#1u@&)S03Hbu&Cld5r{I_{UVSJ{OQeYTQ^zG1`PgRW?u!SnVB>cMy z^T$$WiLgMmULZ_Z!mnV#>7GF;#OalAA-%39P&Jtf>Q7bSml07F9?JPnO(EU7bEVuq zlqP?STOv^-WZu7SM5mQPH5u_CAHx}IjgLw1V7G*SDxzh)7-ME=_9KTr^rP}O1V%ZF z3OO@f!~&fnd_u#P?BuZ=y`A94-n@I!Ya_xHbmZO> z8%dP%#g@oE{ih*|h+beMl~@@r$U=R-P!dHyGgc^e!e@6d-I3yA$r*Ar6*2dHsxf9P zLrwJl7cm3$KRoxqOl^CYCW3G@im;m6*ID~3r05u-O~T$BV@!$k7`0%P4ji^=qp4iq ztdf!fgP|By!^%{<x}Vd&86EZho?11 zDy@9=ca5VNFE_U&A?>uLFStEyG}S+J#~(7DzJIPX7}9fGGDSb_n;Uvxvc<7JE%1Jt zAnQJ}a%ng$lpq5mvNM`t`#C30JtEVsH+v>-_U=W{)`v;k_Lo6~B^Vpl@IUPcY~(|E zk_xB~Xv2-08`n6*<u>lp-A$iaauLP#J^#u*eUky3}h@o9t9uM+W8gMk3p zShQYffWtf5cM^mA*mpAR)ne(Z${u<}uMe7xjOUVQo>wL@Byl^(1g^w{4AKIcH2 z0AfhWPH}4at#Z~J1WJ8e(LZSuasPNP6s19iYV?l9@0P@FEkbF13-iD60dbQx_MTSZ zi#qR^&|@Nr!g*v~J}7MCd`rCD;h7JvpcT1CKw3xD3o_)x!AtMtt4bH)?Frz;Gc}*^ z)qNx%QbSEo!$qh>d$0$``b)h^Ww~w-kB*Ama}J=4aDQ$yAs4`Y84o^P?9t{jslQNw z@jWt!l{xxp@pv!f5Bkts(0hCN-m}4~zfsd+d^5HJvfZNZ*oek^bMQ6=^}CG)FKVnl zNO29%U5xbnFKZ(gN{Ki*RV9(u*rh-M!J8Znc0675|B>*1kbWRU8IG8?0jtXDhw>hvh8hLi4ZPqEhRxds1i2ffT z^Q!`)k-Xg{YNLJT%H!|zPO3c`-_Q`$2c@Q3Y&RM5#%66OQ`4?1N8 z@epqsH{97PM*Oig6LyuBkHNUYtY}P z1JVH@<@c7US^l2B zNhC$(Z!~u%2+GQrG#X{Soeud1lmD(u(6b4K8hR9}xBQXh%^mQ|bciQ$JyZ2(VZ3UM z+H}^zeR5}^RqEy4*jPeQH)tP^pKOza^y)pJ=yO5^F_6Pm*+xtmI;i_s&~a;Q9$1QU z=;1jR2ObgmZwVXEnjtLMb?97YaRjtm&()dAQx&I9AW&avt-c;3qF7)X&l}@hO-00h zzoILFg3Hq}R?!k4cd=GX-SwFFI32%Fy#pvvV&$`ts;i>1zDNq%BO@a2UbbjVfbdQu zmUE2{`7p@B?SQrNZ_hoL1#Y&r6-9ewUHzaz;8A7q$uMpx*+W;eZ2sn+BsKJ)lal{c z!k;P;@>H8V_HWcZ#s6eowx&F1P3@mW#hKFHEwU$}Pnl}M+y1Hir_iz@@F6%_-BjW> z#(KK3(seHYRZCv;l*A3EKZ9r@4pm6)aTUE7FAsDr8B#3q@<3f?42aYvCOpI6ZXF-h zxQIN;D8yN$Y}lY~WR|qTjOC+Upk+)C4y1R7YWxP1;A8wo;$tnJEV*aO_!WuHHkfqD ztXdsaXU(sHoY$s=w>`A8uROlry8-_8fTGi2YsP81;3rbss=q~G2d7t0D2d~o8UfG{ zR&B%z%T*8}eqHYjSrx$o5zj3tW95jFke3iQ;)c{dK*dV!oFr0V-%iYy=ovgQFu+-1 zZm$?#y%_iRj^ZK{RLk66C9+$`AZj6tF_JCi|Byb?DLa>uC$ zxIg&&+9PhfL#ijah%H$^Sni%WdR}Kv8k&zPPnykOhhcsZ|6rrkq-Q`sTg0a&lPoq=X7 zbb9^GN3i#*((1g-~TW-8qV%VEb;_5sIA*(j>nWqNR6e32r zY#=@hr#5=1rev%j_nlubJjBE(Z5^1dnh_1#;~-~&HDhZs$+!Mk;nxB~b{phukdtb7 zShyiCr+ZJVC+M-;;M7l0BIa*ZRTsO>R;SjG81ewOk&vqIpo|btr_x~OD-{Wb>_MJG zE%2VhVjpcJDeRiV@-5&;j^KK-OR`FX7sFD3+;8g$nnkuZ=U@{X@76OJmi_1-lFZ1J z!iSd{bfw=2vY*&GAQ~AJ&)bf7ZP?6u!ZTvD=~j`q`5#V_CQ=RF5-v6(4WK&c1g3kX zA3Rmk=H9Kp5Zl;3N{5ep(m`J{VtGj{ZwoO-qNQf&g3YAM#KG<{i0C zzQ5K!=M(rD0F9vxV(JZEP-FSo{jtaXw9tN>Yq`M^uy}Z)e{liVU>qLR5GDe zhf^LN^NU z$9%u6T72F+D`2Y!pOCw87Sk4nK;Q64MbiGwXT$s#Mlmq>cBibW2zWkh{)fKCXVxff zpGhODZ5r}oPEe_tIAwydWm$Ncyy6D~ux)uK$s2_C(xaU1?>oOH#QOwrIK$lXN*(tC zzM%eVsy0BK^-6iD?BWoa7O}{b3i1liA0H4_iMNq*so08p2k zgIShVRr+@tigF8!amLhdbYiy~$NzvQY0*BEPu)aBrlM-S3t20Y(w4d&@u_0Qt-NUg zUDi^=cgk=R0(D1MK8641sV2h2^Yl5@af^m~SusQv#uxB4q~CI%;|W7sZQ9dmB|)oX z*ytktI6M2a!{1hDyv4oToga}pet#N?3aa2jbd`@9lzYC9nctKLYW&JF>nOgb6WoEn zxDxSMmlRPTc%oz$yL?k98q*ZVK49x1)>j&X)_77e*(_)q6LB<57>RB7GtyduQiBD~ zcvJQBnUH%A0fXgrNIS2hWX!XR?f{pkS;g*DRct+0Y}@?bde(bc`D5Vuq5PP$Q&0qq zx3SYXHdB_`Ai3-`jN`4f6eVB&K%gU}i9`FsvqdIdu<<7~o%!gmpZ#1X_QVq(BY`noLv2Syv z`dy6bT=G=vq~;n7Zu?#(d7?vV7M;L4v9xjC9e=S%bt7e0--w|=A{rA;-@N*gNt4TJ z5%Hd%*y*i+jD1GAR80UiqgZVvxjW!Ih*xSyan|T@ZDcMTF3jBr4yjsJz>CTFdh4F^ z>1m&IL^vJ>ZqFM6eJv}E|Hr|YuwBMYSEal@;XB#naHZ-GTak89$K*40-{?nBg=GKj zw$?}?_MCvopyHVgy2Je!nuLyCSKB&tm93iL@t!1*%c6xcKNab8_RKZit#r}ooo=nc z624s4qRm~WKieKlK)3GY#T$Xp>3%=qzE-R3s$=N0zs%$x`>R?$7u6T;~O zXVm6u_!D&M+rL5%W}l>8XFUh^D(1vIx|<)Bq79+A5oCrc{Cn^;)^^jeBD|OgQYg;0 z*NZO7zIrXs!Z!#QwLJ4P>c!0eCwH_)E)HmTJrsEB&COKj@p^z_VCc-dajWl*gnHm& zy#^>by`tM8W7)`a{G~x(b*!L(N1~Z~D^lCN00r&Z8j@XV)AmA-q~fTmaopw%OF8A< z2Xas}(bm9FoGR*FW05mlyXza{LEUkwI@m5gUQ2hbdQ^`3`zdS^j9{X~1XM-sJ#UIM z&O;t1k^O+_1sHcA*>krKyazV8+N(A|J!CQ`ZD7 z3i5T4Ti;3#HO7DA6vz%r8u=1icGd;y9Lp+Wa71nM?)HvClN{Dn4r_pQr6upj2DEDZ z($<2X^pe`g7;odnSuL9c%W&dlXO%|}yNBHtE zlV0)O>0!k_`e1g20OR{gOF*ChGT#~olT{tVtFDxf7gD}bZam8cdqBCy^I*iU+9A*r zCZZFu+?@c@#A}qmD3v+HiXr3Mz6ESKrf8n>)S^Lj0S~(5kO9zy`_S?n`of>OCKaLx z3(#`E`^7>-A&#!p(QaW|TD(|b?^MJ@RQz$Z*3e0vKMEzXvP5>$Pr_9F_g|w~Z)!}y zzsaAfX&m(;zWUACQZ21?2& z(Y5R?67Om_JoH3+kJs)O%=HB(;s=}!wXZV2E2|b4&wm8H*Qe=H9 zDz+@v3ht0#qS*p>f|^+CA%U;j_vL1(RvsX*I+rg{%5}V#9qZt#`!xV3jE5uokk`Ub zovfkObx!K|9q>d*t?P+pwqR~YFmGH`R?BS2xLlpUv>Ozkh=B^6RL8|tyhEhoN1*I7 ztS%{`ce?3nmFcnsWPehmX{g1^Cc~-P3bM;6XUp4?qn!EEC@RY}*IAn8RHccDv<^xk zAlB86_x2xdp^VhN8jdt&kIB>l5OJeygV9*ZAbPNpbCP*t(BpvecN-n4`6{9#QQ@cK zJvk2(PG0a)SkG*j6*I}zH^-aWimfxL>|A^LG;dJsK(P9H%p>FH6Inv2RYQp4&d(RJ zQQ-kCn=56ziY|=)Qin%~%cF z|D937X6Dz)_A47b-URnq} z5vlze#Jo`f1I;cU-*dw%hcU7M@7X`Z@M;lvRjj*Ff=Ur>-g}T?FV|^@4LUoR`2MTs zi7Me)1@=Of&_(hSBnwipQp^qJF{LpzT2kJ{$` z$5;->y!&r1i>`C7qhQ6e;fA4OB@?^>Kl`7bkzZql& zhd^yotkp#WMrzE0_!*zw&hQwHj@QyHMjoiHV(k|p{{6kt$4K^p@Ux@t+x9X<7#(L5 z8MA2Xsb)7MqMaJh$3j!`f(h|ng|8W5_c%t!J>XGUedrUs)_D`sWU{r2erF&$ifJ6P zbHVP!$s&yl$j^RskS!wd5u6M%&(q#57cLduDy!X@)ia}4Eu z{pP{nEEzH_Qy}1d@T`X%cHZa5-V+^OzSwF%nV8)s=tIV1mPADTsi?xX1+^e+=~Xo0A<3{?{HKDmcdmjax(4@clh}1|6dr%V zt|0(92+y6JzxFpQ1Npn#=YnWR~8-Y{MUZj!QO5)`y5%QL9&5Pl0;E@)3{Nm7ut-Q^|u&+tZvQ-w4 zhge-QqkDh->!#%BAE;8e1?;$Fb8b@+RZKf7sIRqbl9)(-&Xm}T(8II5a*M0NDzr&? zeYj2uUqO%J-|~At5d1!Y*PSB>TiuinpaNH6S5TtAn)Nm=So&I3=q)CA-$wjN)q4UI z;bjLk2Rk35)`)G=gk5ru_E5H&F*Uq0sRT}c=3-KA*jJ@_gnFqEAt)E)<>8p~TT6FW zX-NM=1=OkPV7bjPMhgwJjKLzMDs{jE5WVn=9zeu$8A3mC0;uMW+nXymbj0=RuZ1mr zWA}k9=_S!F-tB!?f@xF;Evhh1_ z9*yUDNpBUoztY>@ejlL{iN21ysMHhlVWXdOcG1$VFXBrW_B{VLb77zeKK@x7k3jsA z;3)%|?)Yq6XEAE}+%5U%u~>{aEO^rKjXt#P0=I zp|NMKfD#dXC5LOtP526GB@SPK3EjwxH0v{-eHkgIJh;@64`(EaV?K!!`DohCYw2DS zdhSs@WUb1U{->y*Rh6SvjEnQFD5h$$s0vd6{-D~mS@<7*zN++3$)l>zaWXqXlN0i9 zF>b2GssLdsb#rM*e97K!Zjf*jfQ?sFpeg0^?WUA^K%|>Goi7Dm(U0hhEblU|Pq;Z) z+c%}Xd~H5`0ROKGKCTq*%-YT&@veJk;UHojY~%r^FX!J#C^#H9^%-qo3rg5h6aX`> zM8DQ%*mE7Du|ym@hJ0U-?F^~9c)Bv|`PSYnA7Y8qoI_1@?2(5iun*GhT*Vg1h;}_X z16FM-nIT+iJ*B_R`5C0oA9#L7w?j3m#NEtwm6aFZMtxQ0{8^kKE}`o-ACve?+$Bm@ ziY8R?*EIY=m0c7&set$VN0_ghfLint!DJgQO}AdwWZmW(j{r=fuKZQ-jk23hEHSe5 zqb(+#_o(uyfI;#pW!$L*XZ+ffm<`4joI^VxuQ$_CqnP}u1{u)#Y*H92d)gDbh}Ce4 zaW*t5_lU@^lJ8Tp0UNv+Efva1EYTSK=?oumzPGCp!=Y_w_}+JHM$m`?s|w0nB-C;(b7;VJ?0qg|#Lu`XY()Z*XGmDwN(+<{%A&fjUOTcjtq->nSZuyD zUFN!U|4{0kx?C18Cth~hYU)C)hrsHbRSchNKzl~chuzK6I1RZCj?zK7$SG%Dt2B0z zS9afB^17dkIqm6;`EIvZ2HC_I6x70J1o-=%R?gGqzJ7r6SohXKM9{K zpQU0xMSWX2045PW{VK-1IahWEh24@Yadj3)T`2-iWHd(A}GB0E6r~n|SO^=6SvK2g(5Ec&wz5!(&V76Wk zjO1hibwPk=gQcpNG3gANbpq8cA4Lz*_|ZC9hV`ID^2)?O4hr=e_J)q^qKa}{z@g9& z=&_d`3kFM05X=xSCe>vA%90keV_aI`I#ygQ(IZ$tQ?CY*F)|fnTlaO`Up=}@6Q~L> zBLAy-YS=NEG zk0F_~F_$VX+KKcn7)G2-AQ*-W0;tA{0!^vqF^oY`gJ6nHpfDk>4wo#))y<#GfJ>6< z!HrAUz5Ms`2m!zZ47Il`5JunOIs%bg(K9pvh&89p5-fF1{2v^c;^)^|-!o9uXSo~k zS(=rj1pU1*Nk(Vrv6Q*X0Ak_9K+h<`CyCXx`vUQ&h)Dm5aln@Lwz1-@RQ7$0A464&I3vmdE4 z`i%|6wJTSLqCY7J%;C+uVN&Q<1_A?_lQtagvKTENL)=i8-%YPYe1b^EhQ#f*z?*SC7>UPwfFb(MO_dl0(4fsVLDVyV|JS4ujh| zNmLhRlR-#Da{pxn+^S;p6l+MymDv*wdWn&rk~4Sl_1+ut&A}I`nHs4t-;iMYfZ-*o z<>8gENLB1XjXhuwKdtJY*7!DbQbk_Pw$B4yQk%clRi+9-T3ZxoPs&sM)LkN-@3&@u z#$WJ+r=&}(?$&vUGk@bor)u;&kE^VklFHXxQvzOrr_d<$2^Yp@Gg`j7)D{~i4=-2N zjNR;dA6am@(@S_1W>~LR`O0blY-$oW)U)mK)KHQP>G$XY?)juGKM+n#euS45Hszl7 zMlAmcB7Hq=o>{0|t?S~K$`W^Sb4WDMjqn`LT_t=1Ry?bK@Mn|6E2DQ71*|6Ub5{E! z2xFlLOMg^JcJ5djg5;!!|Hd@?(}+2Gm*o1s+kZwS$eexsN_MJdwGvraJa7Z2>xbee zJvW%Q^y2^S4K;^5!0o=@lqw0#+%)?Q`|iq7dgO z-7whZPzD7w-&E{0w>OD1w8Nnyt)R}&EfPlS8i3!&c^|A@Jb*0pI!evf6DZ9FDYbB~ z=Bk*Rd|&O|_;js61GAt(#keytLA&Tz^87O>bA-@hQ!eV7Pt;O+&B{2xZD^aIuM5m` z=hZ^y?SB#IP@VeKyc%!Ue6s#cw6|@*_hAXPpa>c=chM&7C1+^z_-YH>x0?G~&^uT! zDIi`r+ZUUros=A`OsOeW$`f)l@3ozuP6qUh1c*lTWxcV^(&`a1mZ*E2-mge@ zs&UcyW8^}}0Mt2`^g1HSztYLMxT`JUP>#P3psWT_7YKL&O#zR#@ZNksoOeb=x%hRc z3-L$97j_S2JPCCZqtS<~01lFUd})NNLWCxTX$D*$VT}Y%hC=go1+X#!yUqU1jbmk4 z?JA;@+^TknTJeH2qZhMYD>e#P*HdFPsLfhUMeH%Lg5Pxo*2+@WeE{t(8gW&t1Q~*^ zpo#ZX^)Y`(SHfML;JGSa4cw;voxgFGdzq6eEB|X8{36}95Jkob?P-bKYk$WYTnxw< zko?}8XTkey?84NQ!Yn|%PUA*o`v}A6+Yx>8i+7&eG{(9e6)vrHFTr}$UM?DH&AgvH z^Vu`EcK5dgOTTh`fEtwM&2hmaauE{|R)>x{8%T!X1B$X9bj%TyqoALZim=6x}M5r~fn*}~e z26>*O`+`D_a=YY{mdx~y(j9vNoj3>TN_-DcO!Q?^=R=dEw+pi+ZL6FG>AB2UujhcH z<;%lnE1Iz7-UlrPt)_K{;ZAT(PrsT{z-WV3vhb|-ZjvmSP4z<)lb`xn7HFAKk{5;} z5cosoIta&nEvp{XaTSa(i=xi06jk%Cs`u~pTuwJ*U)fb$ef%H;j#j>|($YXMrEDPa zzsXsiMs+ySZIg#J4JzM@ojGQY))h$Q6nW|4=2(tZxcMx%{IiQ6U~w>kGk}wu_*ht{ z{v=nRzU&!={rk;nL37elpicO@eIaY0RKqXb4Xm5ey(Za-E(BdOi z*HD0&sMLewP;E2Us{i_uoACB+o5#`sN;TT+Nj;Ia0bZhM+cks&giKXyp}iR>3f zUvyzFN}?+cmJ2A8TLey3eV zcLdQX3t3jsItv#;6H0+A^@T+2J+odiEn93{#aDt|WmC9-nMDFA>RO;N3< zUMCT>(~;ck)`o&Vs@t?yuE@ks2DbTl?PZUAKesPA#f0tW*-I1HXfPI<-;33W<*m*M zZ&HjZow4D6F)+$K+Kz?#7tUsz!##zdF%M)WcrOL9OLeYc&)ybiTYwoO?8}1O6Ih7P zj5GrkAhZxIQ8$;-yNt(mXvgV{sqCaO)a84FD5YMly3R*cm27r++Z`1>HZbhw#nL_d zK}zeb)^@gqy-m21eh;wxy4jGdw8-qb5Ig#1d9Rri^fniIkcZEnZ@-CVRZY# zSYEdH%i1HgWhoI!)P-|YhiAFpgGerzO6+WFiyu*tBK2-}#&=#hFzXWzNFe)N%sZAT zUI&=*KnIEbYX3NNrAi_9OsoKP z7rQ^)`V~s!g&wK|f7z?G$vdR(Z1U=hMLck>Tg)m^R@&tFn)GKAvrKV8n%;oABw3kP zHHg!@oa?E9oVm`g_IT*6QPUtxP9d7o1bSL@^U_E!20^$*L|b3<@rz8ldT^L7sP}lT zJ#D-ik&$Sxz-dj2F8>0$TshoY5Y}}xX^b^SQRK!=dAw`#!E+8IMOT_~YZKmAL@HIT zSv-Wp5hmKXkzs(_KHe`^Q=~CLeo{VZiX0oBRzjt%vO$|S;QPiSpMIPzw5GHs_EQL9 zuTKRfV&gL<;Vo=~P$krTRpGKm`=a?mabaV(FX7Z58%>rJB21WVcu&FOq0NIHeU~cY37TVY zZlHGp=TGR;t3O6R8gKQ7^&6Kz^((2I*-z*Pay4Mo-hoN#h~-obT^spw^tu2@QD);= zRT*qbEP*mWFOZng$J~9}X{X0k1hbhB5Q zHM=+tNAwW)dgP+Xky^LO+Ft{tk;cq2Mb7?~%C7-K7!Q}bq1T9BiZXq+gN#LMy1y_fzYBpNOH`P)y7$s5!gM}cF-er9_Kl3-v?iSFJ9>mdKsY(X%v{5 z1T>*X-}gU&W(!LCN(_A8VdSGzWo0x3D3(-np)FkTw#}SSp7LwiAy+7b?0eB1=~_on zRG_<>!c6(p#eIXJViU_3RIX4m3}U^H)0Od{+pZ_<%Og~Kou@hm_n3!^EV&YzQ z?SQj8Z7qBU?m;Ggj^C+~6%bsJtMc*Q`9X?js>umI3<^CcEVVYRDUo2FS5yyMz$kLF z)MgE-B*CQ2g9ta*%GcyIY(1>xqLSJ-n%dbx*`&gzn0=H7aYp z;>-7ztI#W%5;QqieQ++Srxg-NPmBkECa6}YMgdynRJy5{%rW5{NsZUC#j>S>d&a?Q z&kz222(g);klSpP2({KyaZXlXy&5pS>44=>S`P`YK0`vSmsW)#cv8d-n)U80w@3 zq+Bt)BDtQG%MVj0WQ{pgoOh(?!n6X#1O-cFm#YP*8nrq&XL>v`j&N*9r--wa%AVQb zY{7AlcABE5aew!>KTfp!I~q0}E;z>oTSZy7@U7$Pe*=2P5Dow;gT13jM zT8*}@Eb@INTekfJHNAa(lXV%1t`Jg;2a7#J@}G$e5E}n{Vj1xVmlb=)kpa@lLW_(J z42@1R$n6IJjmOph*D0HNU_M`9RC9Z7(CnPyTt^k54oUr*w&)Nq047}J_lPSD=+|+C zBHIotJ4iRm!mcpW1y(Z@t1l5O3>sR@9n|YLZm}iBTMz54CwU_dI@rpoD2NYI$W5B} z93)3;1vgOE7SP*4eH+xrbLZUQj0ZUZy;H5<3=FHHsIbbwswh@)cJ`oS5J?IFE)Ij4K$O3 zEAdvsgSTpmys(d)uB18-1db|<<0v;J0EM)(jwDpqaVU=9?=*-(%Q#Vt z^GVr#?3V_ocpoNNm4?gOcDf(81SsFZ%43{x5Oo+Q4NtE5~zirXfPExk} zcHinLS4P{XDj;Ex1$>-r+8w*GeG+~EQ!XyQtk1GlHltn5Dm35(zGnwXjog9!>eK!K zUs?(xPZrv3KH-~@ywF)9>kfZvDZemKL_kHT!S6}EUX8lxTz>f#aOWQ!!&*UEvxrVO ztXvKl{n_)tGbtNXs1I~1b+8a#UjM~=tTylP6QOr4W{ zaE%yd89H$s6aOfAtIaV6K9B`W z&FM>>w~Mf`Jk#3hsHKODF^`Y!zKO3h@T^|GrM@Ko=^+Dbg89b(w3#xEf4;+ zwg+3zba@q<|1=5Eb3qzi_rZ#~SQ$xE>le9^m_mBC?Dc1G~kAWqR(iLyP0+x_xkO#QZ zj2nh3qmA2W*mWfN%H}-97Jp;i!;$YA*!DtnV5jL!$f2D>|6=|MS?SCGpF}(>{yvp{ z@~z#oUomzmogL1|VcATy&m$S<~2c$Zrbya=p6d+ z5o6he>B8d8_DG;vovc;}-+0xeXO!Fc0wOy{T9!to`dj89j9yo+cI47W&%M<%kW5|J zbd}|w?xgV5rdPj1HG{Qo`&l!RFQn371u*AADq#Eu6IETcdBfy)RO)89Y=Fk7b}Ic4 z%XQu8ms#P!QN-E5CXA+PDGW{GXM|ZVX<=9Eo~iX6XmBv$(^=WwXO64%lwL& z+0%2JFB@(C#rja9+(tDKsC4E(>$ve_E31%DiE(j-h9F=(0u;WAK>qU@RO)?5SsR$w zstJmK{D)&W4qs~a?a9HM1M}o4XCjhH+nR|3ZGrzzoi~uRl3%i7CYAB{r zz4^bx4-TboSmzlWeE0`sa2bV8Er3bQmRW~N1;}7Pee(2#_)zt@AjW?}M{RgIQV=tN_hbRTA98IE^r8hAS{v^2l(gV(J_s@Oy64_j{l>QPo%0rXFnE`5rFo zgj`E(2jP{D6?N-XO1f>zF(*jO)9B+H+OkHEha88TOZcSGDhEtylx%;-wU?wu}VqXkIZ`OASNdSY6A;e7r-+0sZ2STqKX%hQz?ZN|b7i)sfHdUXs z8z>GPzSZ7gOt@Qh?f_jtzStRO{v;-sS{4rx-(0oT~sXF6y6H7>`% zIMr9=;73p5~_W z?i_|B@*L?&aaCSQzR!9;X;psjj64oI^mE3me6v%3r*+(xBqrgrPB#;v`7#~9J^B4T zRYxf0R; z_L+A^8sT^+@j#MwpxsX*_xGK_z@L_B8fI?}8=i`~zI30-jXQaE)e&X>jyBs)SFdK- z{ro#Jcv_CX^XbNTC)5SPni-8e9c8}5Xw<`Zng$y7eL`I{(qK6KM^zt&CSA{!d*D*o zpQ~|XV}q~h0UXyOKuzu=(R)q)dA_>>8@_Zopu+t@p}Q>WgM|fXg)Qet#|6lMqQoCF zp);gO=%T$cKPjBt97G?DYAbO6Y(RPOJZ=A>f3oKEy(ZM8_Cese$s!Y&T$*EkkO;cyCDv_eq-;C00e`8;euS$akCa8V4;91wA2*cr zW0qF0C(S?|r8A|W!~3*sT9tH^&S}N&BaPmm-ZTg`MpZrBt3EDEFr|SR?NKcS!o8*0zwSgXZ7otbryV;xm;zhL#NLMK|OaAie-(Bs3t$jTcuML9UjozvC-mT?Sm;n;h z&+WP2mW-Gducfj=HItRMTs4fui`*sqr+kN=4?6M@Pw>E7#z%T3-kP|?8j0;~7^@9& z4Rawap6&N}DENj*pE2hyiw4H|2al1GuNU1OizC?&*PKrpIN!+kwUKLit`}46plRCX z4yk!3liVf!%+TDrTlpVSsk1*b@KNpk^tD~7WAOV+G(i7R)P2fe2DG|c43gvK;Aa$1 zkH@wv`qaqQgzfWbT*wrm-PAdk+v)xYW(V3t8QB_yIoGav2&OgZ)K~}ut{da*T-@Z0 zY>QM{cb|JL^F*WvhCFS<+h-Asev>G82|*Gg{>msoe=f_@%G&#(jw<<|J&M_6xEfJ{ z<=XdVJxV+MJ0suS>_iggaB+#cw`#rb3EvEk@0EmV!)g6}mRIFp&Bt6p$Xi=16~dOs zZ^8m5`2US-a2fT(orlL$o6xSGzToe?KYi zg~JHYdE>MvfFa^(4>yX>C&a~BnngmGV1SJUnRjwNtN*E%gI0X5@0yGp)v!C%Lz8n$ zvXdSTIm(vD(L4tK70BiG2OvMrP_oaAN!2S6An76ymsOn*Y$rkFGC5os`k>0IuoJgy6T8L0Z2wZlyhOT)`cN z{f9Xw;axod~!}Wckr)iFFjD`4u zm)fdn%GIt(T4|?m5#sHnqvh#)sR4Lj8;st;!^vk7!qgVmf}%LpX5olwrf8TVH*w>* zALZ!JHuVdm65G}=TQ%$$?#2 zwU-K*EcdwoaQA9#-JC2EBF$}WV}6oWv-NWiuM9Nn0e1w2o#Rpxo4?4prUxr(Woft4 zPS4yepZSmc%TDibP=iUZ@68n1wYVy%tn@P(g_d(G1{?C5L7N8>&(x`Rh?jW$P43D) zh>$sTkMik%9~B8YW-!=S8Cg_9!T(4J+344dhdTHt-G;b3%#Pw4-3N( zhlZPTsH=xm&}K>U0-8xG(?c`dhdX3^@Xr01Kj)qleJwPf+l^F3Ri5c++!sZ?V*J06 z+%A6~v*O_VU>9@J@A`J%Xj{KY*D3Rg!`D|TIs(S0X3pcby_0YA!V&ionT8>LWnnBT zn>JY;XB!g)Z1G7yeM?GK8X^1sw`e!gcH*wOWB;gAUuK7oHVC!I?-d_B zZB(7Q&wEGRr+FnqiL$D%WGk?a7j2);7oShwzxukP^SRTr9Kj0}_}O4ucDFHmD-H0W zsWW@{ahT?)A$w}q7lv#+K3JRwa{^^KT(l_rF}d&1hN2yUcP^IRIQjnEAY(Nip;8Gg z6E8YD3*_BeEyTy1p$dLppnlWbHT+}F(&QuEowb(k?4zA*8*U+p`P;lcR-vk?$LXG9 zVo=m*)PFDg&1@6tKVMpYR{aq7n}_bhO+E8D=eLX)`IEg))`{BL{^rv5B6!v!;p~~& zBE9Mj(F6b9#<)xfR>HM(7YbPKBM{4a#f+HIz``7UdMiAttq&myS0wuY^qc`uzI(D( zm+u(t_>sQO%CmEVvksZ_O6o{?i+6h@!7DyCON`rLS-(?z9;5=daTq`|UM@wup9!%1 zfjG2kblS~N@T_99>RtIGl3nHTBmCE9g?~ohdOy61>4SZJ2(v3J-kqwwZyH>)6zs(7m8rCV7{a~Mt=(?q`T^pHP`l&4bM2Wd5rg`{L z3&uG;<1bWw^1pvsFJ;~0|63}^yYn};5x;VJT)na+y0KdLpMlovpCIn3x3UPxweP6) z_cDf5d6LO^@$=km4GoRX>_~G%W10P1f6^`0EhCp(y3dm~Hz&L1ji~rtQ`JzHG0rz8 z$FQs3a7hi|Gu8jtBJDGQ*aAPN7d1chbZL$Kl=uzPyB#ummSgoDRwR>;3P0hS^TJR(il~ zNlT$CfD|4y61IG?A_Yla25f)$`}QQ6qQAdtD9Md1=nvxFe+JqQml1IX)|`##4N!;` z`Cg%$*XO3HZ_?g84xAZK+#N5kZ z)alUKs2%3FwQ)q(qpB!q)8F^_M@w7OhAvxYJ`y`|pIV{+)o2TYhXhYXMGf>BKUU%- zk}e`Xs-5RTLL!^Cdg14}PDo!^QA6H?T%P2dVbqq#(}@|NZHfWC{F?`b82B^R!~w^v z451So3}u>3P5E)#^Ay|fE~(r~5nIy2r{yA?af#-fj$TYYxGXu9I(UAJ_XNb)u!>LC$ zkO*u0`T%It94l7?;Pc7bSMV}C)HS9<-hEL)+kMCl_HJT!?>Y)%TZjRt^+sLuX0EYQ zupV5kOkE#ej$AJsB@_|-J3rzbk5xQ|kiknePr2lZxg~g1QF6$S0kngS1mJhm;_G{B7B;%8QQ^(CvTDrdmLD!<#Au)`ZfkkH4D`-7{0`gg}Ne z@nVQ`*wdz>RSkoj}`r5sMGH2QN10^s-`mf#0xC18O-}SXt|@QzOcKwg`G%Daa)+ z796FOiAha)_fyycS2$^zrg(7WX#y2sz1uB=tcR;}qn3f)39KlbT2%WS(-9V%HFsi; zME`on3X0t+b+u4;ZVmOUUHf6tb~cYBirc8Y;gh*9Z6Z&uec?r9vXbqeF&P9g)n6T8 zMp>P)nnD=e5OG`{K|aYGTD3&ox8Hr)`4PC&JM|8U-;KFs0Qa-sZC826%v!p@T~AnL zQ2&RQ%60CVy}tRU3{|wf0R?I_=p6K3`X*?iQ`S)9?yfX{+uTwZEx=LVrmC6jw3~dW z+Ma}U*U%c!qI^@e&N3z|ok z_~WkBENTsZsik|>grz-4wTxSOy!^bKt2atI-;(1Cx1! z*}757=}$XEZ&bQoeEe;uux9{&1?p@rTyslP^yBz(Oxg-t;_)`tWn> zWc4ZIO=Lte14%r>ow4|xbqUsQx!3Ius(1Tl%}&~#(T3NCRomgnNS-Zjv^9SGG1-`E zefzUhOhv=^sLod)r}B0at!3&WGV{fhQTgQU^8g)-e?+S9zWQ;0=$ME*+vkIyQB#iz zXE?Ci!}xLiL@=`DtRL^+$IOPp$72m&)d#;K(a&*bnA zw4IBnI9uF7I}lxwn%%9S48LzZ**K2BbC0>HsQv*Rc-uaFjX?GFMm7D_+lBIm*f(%% z@1JsB2$mf}GL!UN3dFKe8bGbr!@Q?%B<1O;M6T_taGDe4BW!AmLFU{vjVvGP zk{`A%`|`EYQe){q`GfnF62Acp-n#$$lgaTswtB$rRg zKc7_V$E?+xm$cx{21~a-zVi*4d}O?-s-X@*HYi7>Nltv>}-7d;&t5#q5%mbf9f6Us}JlQ+p*=6Lx* z*EA3utY%j-kBbK6>^$GvePQP_2 z#dn+^p9=N*g!g_lAfs`PhFd@L)RLuG#cRRCnH(K>IZ|nOXc%0eX2PF-DoFDNlDIgC zyG8~?*D=@&S&^zmirVb%+MXNO9T3|x7)ui^#%%>m77GTjmj07ol<3fHH+HRH zwEU3hNBTY(jwU*uB>`&>G&6a~N)60Xf81u@44lk#E4(^h%L};Z7C}5T97vpYpO{gy zJxe8CY(On^W8W|QZ*Q!d8SON#KLI#ni{JE}BcwvC?`<`CyzAcCikw@8Zv&YMe;!pK zBb#`O@Os^?;QXDXcR2ao&kfiwd_zl0)Q0k0N)@Uidz;bSU2D9>ojVWT%@y3^^gg9C zL+XtEnUI}FJnixuSSq)x1J8r3|3w<|j-QD=eG7Fdl)sMp0}3vwsD+WsNAA=-z1ESn zw-Ux_)u)dF{1q}^PX?ckbjpd`_B`;nW=`OGH~F4|zwwB7+h|R0GB57;Ja>j(9-!{Q zJHA(eF-HHLK8h(kjy-YFK|tod|C9U}&a4p2^w5ECCVZPgw;(0I^w>AosrgK1uUDAy z=`v0|u*t??zfTvCS$cKq=3S?5r73r<(m53S#m{+ugv%EU!((taPA+JO!KZMMklMvf zu4FmhOJD2U5norozUf~Iac?RZG9N#`>^u`@dUx%g8e{uFa`~;lIQb@x_#j!%k)0%r z(HVb;j#Tf3TWOzPXTG<(_^{%M%%Ej__#!65{fzHez3&#jc}e-dV!am*`R*wd^84z0 zCY%omw-n38=oJ@le2hApj92~kjbgOEv z=H6CZ@+=6)wly`=_I089nI$PVspFksXGTj!P!5-9p=}yua<8* zM~KbF*_E9aoY0Ru8jhqE`m|DQvmRAk>2&$&b2wgzqq8rfrMLq+EB2-mb0Y$zNa~FQ z?O!u>`lV#{x}$hg5Je{`3|>tx{TVlU``{Ool|O%3;vm&dz@gz^HSVg(Y5OQwBBB8> zl!YTmB|df6PIg>zb>v$68S#%lKm8SONM!;9A#qy3ONR>`4h&G*<=eSdC%9jSbX6@h z-@OF>bpiOz7~`ZinxhVGyQ$sMTY%f>s52%0(bX7{GPBdlX$OOQ`0~}AxuC=R|K$*l z5B;!91d$g5AMWt&Dkksw4{RD5!>q2gFL6e zmGtd$xkQhSP5H31J3e)2L5N=2K58E$Pp==eT`Zyc)+Q+U<@GDPTRvYbl~>IprG5u+ zo`6l=o>d&q4a&E(?g!M}kW$|k!ELnI$!-k(+pK*Xmm?Szqm%B-bikyC-`@Si84B88 zNt$Sz>#!4Faqk#=Pma5DfYBy>JzyeB6k$5KPi!OrB7k$gK=kFA&mn&2zW={e&eDO> zSI!8tib`)|W^clC`I&5(=sKaDl=4O|LH_is9P^T6i3Amixhrcry~B!e%bpXyj)z!l za>F&oH#d-z{|l1iyiXk{ZE@Cy!WXzg0#*FtRi0L!-Um7Tv&YldU}@>w%a(^Nbzi)m zfrsjpZS1*1SBKj%Bm<{+bUZ8@bH*C&ppYg(=JhmhbxQ$uF7i7aPpJR;XOGbye!u`iRtF!o*cbu43e-oC%*dFCJgjQ8C8zUQ8E&+EL->t-rJ;l|}} z>YIDFn8Ux)oLRm$jPKMxyBNuJ561lZ=SAP9X!HRMO&(M1;0%pXKRQM!5%-ZE9Tm>6VIi(dSf$~NEsou7I-}L6vp&A|2a-_G+u1C1s z=GyDf^c+8LnXw|kYKZurYX`$?Rd^=|T&#)RCY4g->*c5>ZHN6_zH~Jzro_>s|1i>G z)DXAu{-<|xJ9+7rBfLG0qUOu!B-!c#>aM}N%BOJxNzaS23o64JZhvpCc~xJ@4$-C! zToN$&P@nsTp^ZKP>hcjS?kK?V-|khx`j+j=d0Gl$81eul+_5Lc+i|D%VjHx>1P}hK z<%S)wWlcW5SD^yj2A48)db~&KkC463YI`gy*Pd$NqRFy6JR-=!uNW>{ax|J z-Ha6aGj4U4elkPnyw5XhAJoMQkHB}EO59$B7qyMT90B?pGkU4M<7|gqs zFZnw|x}%=stE_0|+y)r3n-eBsp3XlaFt8jonqz(Ax6i^%!1?u0==8O6fzBs=>k^N3L2DZB|N0}&{hDRPxMMrFr9ju$Z&KE53iG%v zCaok%3NQa}1=d77M)6$3#;>3Evof=oL^$cPK)NFpXnPi;pSpY%m`>_qUTiX8;kj;e zC1nNY|Abph-Y57^WjL)RO#1E^uu-DKtj;YlR72*_Qk^xxcW5LE2@ZDI8(n{{%6jky zE*T}B{3&uShTc_~DGH|9eriH)J~tQW<#@O-RZ3S+HF& zbH@@nIDF~K!TV}iF|~(X%+YroID#MVf`$i#sRq2lc>XD%LEFfTR~kD}Wg7}ofSHZa zSC6cTBTCvoBr`=v)Lyati!};+`GK-Mb!n#m4&WIGR#;Hp3V$)p@!gnYy18e-iL?r6 zOY&TY8Z+XwMa1hnPs=0ab-3HemKRq0yjZ;BtIFkXM4 zi{*Ujc;^w-ii^ayLTJ_i?St#bC-SL_JMzH@=$?qPT-stbvChkFVfI)VOA33z)owHZ zyeaM5(27-|>IC0HSa`Cs55Xa?nGh+?8I&b9#Jm4#I_9<_A)S8b`_>T z@?VwXSa!27Ipk;$W>{y($wXJBDkijA~Ei9DQXOEZXKzb9;$c90L%7 zMh|@~5^!(w6W+y2`Z2Fm21g{U4I~7I&9*3$dlMR--T$68(6IkU=ddSM^|^a$ax%2q zV{N3upG3rz#fxlnyc{~N;iKgj65&Z^gzF3B%Ol0XXrlzN#wJ*OKh_oCM#jQ3?SK=PZZj4 zdyJ=fQcZnJiC6DsOhbvu?wEnXC=K ztS2>;*QNkNu}?B}w@$LytE*W4NT2Aomv;i%SIvB-R@5Dw{3V&C|EaRwh*a86)W0t( zGqfLS??zjtejCka==RBq|K0uoU+;`owtTDxU7UYUA}tm)Aw|88q_uV+B)Tn7&LnT73#he79Nwv(7r~N4#@;aH$3R4rXL`vpf`}?daRD!Be>3LMCy#Civ z2L|lkr#+~hs25s{bMYDCMdi!3x#*rPiU4@onPXEox3n1UZX^7}bnDK=PyHQ9{bF}m zLdRX&I%EK$fL725od*3krC(J#x7PmEHbg>2S~Nh1k500lO+cNiC1y3qG^kz^{1<0P z;mXKKdei@uK@0a2uyW(zUGC_PfPt(EQVi~(C}i=FgZ_$NBz4 z1nQD(+pk8(M2PoCXtGASa^{OJO=U1hX8&Y%cb^m#wo=ExdeX0HpFT1C`E*_P;4x2&bPa5toW**`48Nk_0 zN55m63>;!i_oRT$9=Dm>&ds0oN^&cj)Q~8CB_yd<^!yfU`)DhDQ<+wIUU}~T=lgU~ zc?*P;`|AdmCLU{=jlVhjLhUUNC58cvSVM(ds^{>3KB%U3=BTVKOVZwPqz((&FRQr9UGrcp(0d^&*OJL3U+nk644w?@E6iz{lh=2@@^h^ zE#|-iR4}0zq4@Ud12_8@;u&X9NAT5sU8nr*U1LzZtk`Ad?85i-FZzYk{>8S)-V1M1 z`ly-d4-$ZzO5fFX4HH{fyCZbCB5wl>D^BUFS@e2A?a<2~=L+9xK!PCQvw_&NI}d0h zgDez9t`@VpLJq6VUN|%3vThx7vdD^_=2Gr_0@}m3Yenan+nhkx+jsnI(iYzP10$-S zU*$)r`RAtn5o@xm8fM|Y@7p#9DZaUHD=60$Y=aB)qX$oWl}k{=FH3W9=zMkwK=bif zZ-3ONj8nQwL?u{%%ATen9zCI_6wP?gzWy1uvFG(uEbaY{V`Jr9mYeNpJcdNgg5a7mye9&D>WaVm5 zhB)|M1lGb*CL<+_`oUq!y8)17xca?~-o&MaJDA?Xuv@G)Itx4Twgaj4?*=C77vWQf zX}bj`O;x4zm~#mv+ho(iyOy!l*9LTvY!c3$YJJ~iOYfOg)%j0Jy}9`5(}fHn=;Ii? z2}eTC0rmZYTZ8#c+S<}w5#<>S2nHmF0WHLfC$0^@k!sKBKN|Tb&F%X?@4X$;>k?&D zT05>xAk$k(EE>zsy_WgWt!b@9cQG>k+#OOoB$ z+P%uTe;f~uWPR8jR-e5eZrkpEr2MASoYhl79cLqiTgnW=B%eo^oH-x&)Rw>xjtem+ zX{1y315cgfQ#+NHs6*h!-O~~HDfD>uu*HPi^(o!a?OOOEP zp4Of(e_7OMlSjZP;*j%#tE68<|h zJt57AVr9gy`Y47u<&+mG(02czyYX5?DJf zlx;|J>Gw-v|H(s8dWQVJd2o(3*EENj&Dwj*Ai2aQd6ANVj1@#$I`t7lo=_~fT$QAd zBJm=h+JpS zhUUJNFE*PGH>VsGd@Em7dRqF7E1orxgF2Asz-@JP^Up>0*w;y=d|M72okL6g8-5Hl z?BaJ@w&7O#Q|GgKUvPelz}2LX)2-geTXV1nf%~JGdi$P+PFW#F;a*tGH)PzceYq=VtYGU=cvfrY?l2s*A!?Esv z0l$bpTN2<|{3$hCkG1{ok<9ANttBNZ6FoncX!RoPbOm9qywvoUP@A2!&DnBXuylXJ zVKJa(5#94W(ZXozB=>w!X^(u)h666<^IA3@h@+l;$?{)7ps?_N&7Dg2ZH4_A>9OB|_^aF@BAzWgho z_(lIy{{9I>h?$BO1v4{=qZ2e;S8&g=rim=~JQWF;5Rh?<@AmZ`TB3V?X+(~ri7IEG z=)c@n^!iLAfqF&?;C_9uTD>@i^Kn)T=rNQK%S$~e+l?;7cyq}3A_iIvR9r%7LO;9Iowh3BX3ZfE=emVA_tQT9e0cTr74P#fT; zrqjP*$X^s!t`%XE05lPE5helj<_7=g1KLsjK1(|I{HRHg>Zwr|{O zId72tQrLoe=d;y#rdK~V&!Ym|AFl28cBLR5l%F!RlmE&t&1cE#`a@kEPPsq4?hM%r zYI3UCvJ@)L@%D5~x`9_kUBz9m*mY9{@KonredLvE4lD83rhU4+2i$sbTlFzTd{^5Z zA6RoY-d#psvkNjm0H|IcKO5a@$$O?mqolB>*?FT|E1>)w%W0-o|S$6>}V2NLOGW@)1)rHJp#FEv;{82oSkg z;cfNeD32HJ!_T2|&DW~Im2*xXc<=rS%X)RsPU{bYzP{k(l;qRB z2%Zgq{QaqRT3RHP7xhq`~Bm4`-G{orb}4FXkzoi849x; zqa>Wn8r=gXX20s@mQNGD?3k^0k*;SdJW)1A%mGJNjc>UFBdfNY>Xd1bo~_pvFF|2( zZDtmENdWb(-a8OoKJTNFn0^{Q&rr!m6~L#sm@7GO2|4!|Vv-04jxi6(=QF^yqNd4T zIg%Hm9UsvpzgBuOO3Os06H)x=dA6Iu%p_*5s7?sXkkcg3 zi9(tF#5&J_t>4Zj@h*@L5_W%bnhY+TQT3)FTmJsP$Le<*J147`ho5qMkrgW)CqoNp z*atX1jZjkgWR_?np?6D6CzNy5>7L9)g54(cj?9J!ffsj?Rn)JoP629}C;1BN z)o~opaKH0hrev#RVeJvDP9P$0Q^b$DO5ufP{{=Zu|@L?g|Ew#U}p%SPbFj30|x!25r5bJl zX~22cGC(^?>ECVziHJ*7Hk4hLdIDt6*k|_$`a(dz>^kNw_}$3;^pU463AeA&E537H zD>TbZ-XsSyzRe~Sz~8R%6l}xo*C1spvSJZMnsDPZfp)GRE;?2jAYp2br$d-KP4sj4 zQ^7?(dm-t&?XLyWNWX#ef#20tOttNIgIM}<(22idnk*{rD*;n*+pgz~L@0eFh7&`w zmkfIX=E-L_GBVyX)rcv11+l`}UI(BMyuXod?ufRuSRzM)H0*eH5y=|$NT*yYsLd#+ z`L$F-i)IdB{ST!nR(2O{1zPJPTBDCGVjDS_qay++Vr>^uz7KlRZ8h55!X)1C|Ks@F zI86n!UW*yt@aA>d#Mn}*0NXvLh=6ksi13PPmYdOxp10;5So17F-r!idkSI+s-qPW}H##1F(t>wSOmqb<|l zLlYEwf)KgMrUQyE*Qqovu^0=e+%3tA$&>H;TFV(w_~*$i@p8#0b&-jtxLR1fLAs&w3V|r9G%xmUxGSV7eYCm3@WC z6n!P&!?(G7?H}^(wu`Re!h=NuBi0thKFn)zZHT|#yyxMuK;n}fI9ddKQJgbSz*Z>9 z$MHp)&&ESGJblgnzRX<-0#mzXgr{>aW;-`mz5sNunA)02{V18m#sw^H?Iz@X4;Siv z^nR;Tt_z_9*ga7#`nzX`5B5;lsOxy;#=qoW5=MT_20r|kB?ii$kU*h^>!PP^9PfKS z={f4BwP2GnZuU&3MCi=$w@*yaDXAbJz*BC?8c|pl$(JW6Y$~c{TMu>GCL3q)EFro~9k=v4vOl3WI~Iou=A zEW9Q3?#%dMUf19J(~YOdheiy|+CG(ks;RC9QG`D}q5itf?+U*r-YPFGwtE0%V7qm3 zEw?%lWz4Cw$8UtY>C4)v{*QI|yfiunZ~$xpx|=O|I9k!j8V-WHwmbk>$axo3J3+#_lI0F6Ib#U)zfRW%iHeg3^NasemJ$UmM`J$k`C~n*NA6@ww?cns@*B zwf>?no()N7{Ko~48O3(Iue-G^H~UyEbCE7%Dq~A8HIH<*Zc2AA)J16?iSn!jCWPnzieP z0!|=hvHbdhlj~his;stK;B#5Vtr`*2H6+sHGHppUkp&fc#I%bt8JQRDB z8XFL-J%>===)8>HEE{1j0P1hCv+k;bwlB+#P++g29=(64Qhim0>h*()>ZO@3#SC(fn#BGW>?Ffmo zSu6Vn6l8+t7f$1bqsPY;ju6tr{}qD@f>r(9FM(1MIReTDts3>z4zo5xl-ay4t8cs> z1ZQ~hhF%}@mEt{n0JU?~yk9eoo;Hb^$1kYgaI8xgSBnBR#KQr7W*AdJ`jjxh`|$sk zA!0a@q{CuCzwTZf%rg*(b@%x*<0{{YucL0aqr*no4V@-Ycyd#KYrW1nP(Uq2`(cxv zw^KBz#>rS5Zv5hg6-^MWr}KObHcxbwBi-z8xNB=l21$ER3pVOSjzvo?wjs9V!~^|5 zaOneEnKwbg++aYCTvP!)NY3T%-z?^C#u7{m%ZFZ@$IT-j0+n6ZG$PDZ@Qjk~(1QmZ*|HFec5B1qZ1<(Oz!cH}uW@rzK!bvo z6wCyDmS~R$!mD`8>2hxfyOqUrg?|ffOSkM25d)~lV;Fz)v8sOqX~TMH*bxqduSCXDE{PW@fYZUms@w7;6}<98GC^#{VR0US@#U~hp&169ejd_jqY4@ zxvr2cAXoa)kz5>s*kSDp|F>J|e)ZU;1*J$M6MXQKDLv%F>1l%VXj!?^?2g`DcFX;*6{ro@ zcTgx|>|yoyROF`5;x8I;@$nk?#`vn!#*SWHPLZkWv-*u1bHWLN_-Z*pC++C7+ot^T zR-`}PQrBsGl7Rk|=~wxvAU&ua4><@Un1AVTfEy<_UdeXh3{`J_^^Yfh*aBJ6X_8Ii zwN#IisM>Pt>L$OP-yw5#a9|D6rYsugvLW2~s$v#vdspl<`3^ zJfPBVA)Du}?BxDD@nkNN+g#lLL@H&evf>$tcH;xH-aVXmM`bT{XS%89=BPmoyrXhJ zuC|&rFWuF4%w?{l!UJ0HLz%gF%Z7)9&E>}3M|y5Ka8BVYiABVdC-c=~%Q1MJ)iRjd z&RqKiw_f$HDHrAbW!=`L`w_JTqxoTa&#bZ5de%&3p|^EENeQBvHYT2k4VSJmfHmV3ku+savNe9!T+rRj4px$mJJF}31fiLUO(n5B@g zQ14uOWY0*k;bMfCidD(+h%Di1t?Rt#J5r3(A_3L{d9HF?^)R6I)NRrOF9UL@u<$<9 z(Crsp6oZi6FnAe#S27YUb)Av$j;o^Hs17f`irS=?sA9H=2M8FN3 z4Y|(ij+}U-y@aZr1;ZYnWvk!P$%ce;u0P+ROOuBSY)rlYk~Lod%*+<$G=tpnJlFAI&UK~HEmtNwCZCEPbRV7m|Hn5ijV?BFcjdiU@v|74L-yn{YQ zNViK(KSwg-KV}9W#CC2gCRXD1pGE;mN<=h9yeqt%#w}8QPRp?yp4#Mn4b>7P*HBs_ zB;qGN=Z{?z&}RCrCe+&?>;L6)_F8If zkAED3gn>Sj0eD~2njrSbA688aG#H-IcYJ<1BS1K>-o|&9xtU3ltGV%zw`K=D{kOgbM!2B z26yWLyye)c(lh5@K{#gMiEcEsh4u-fmrVp{cWI&B+$BRcxI~r>oO;s|v8}8|C#f|e zN2LT_T43GyFv3vI@Y+0b(ev2pr@$?G&9kJJf1eClj1;{x87T&r+%qjJBY)N78UEp5 z=`(CXXn`MWhkhGu;{4*aeB0|YYiH3tcF6t;5w_(u;e#YePEpXW-|aQwGJLM|MxEJ4 zjbyCBpG=T>DU;^@RDQFLnP8@5w>h!RV{bNC{TXFrW zaCOqu?4L9%@ss|DATg1nxMWpFBPPlu>*AX)l&&ZlEM0@b(h`4j%RgDjHNY?Y^F}SK zxyqwK;Nbe@E8N+Uj>bUHk0KO#u1^f2Uiw?^EmiprCll>no-`bf6r1-hw`?cXF#ZGM zFPb^re(i7);5X8$-q_Udbjp5y*PBoq{V`pB1IjVd&8g=E8^o8KKCM5TLJ~ZVr~Prc z6>r+Q7l#aaMr2gTO>3)gtCuC1leOZ05ia*iv+`0lu@+OkPecKzbh@ubzZsy;XeHj1 zL2=3BJ;Y{NrBASZZ@~gIm((C+J7H+^r?4XDTq_e|M1J(U&iyK;haUw`=L+v6m*v5T zcNQL@y6}D&kIq%10&wm~*&9eZPWKpoKwf+eO1!ODa|krBq+8xMt1Ej3igrBQr1CA4 zvUxLKnp_7=0%)G_BPT2RR#MEr$y+~9R+3;nv(7XCB@&9xW3tlPG0WxI=jla%JZ#M2 zA}ml=3yB4eB*U+;u9jR5NxBe}zTY=Jl?DTlK=L0E`HGUkjjnPrlWCq|nG50_Gyx-L z05f_-yL#4tSUVq>C0j$`U=(^CDrik>gk-E9Eqw($`Li!1=%b$sfIRO&9IF7r4nIGLJ-==L=E;o<2C$j>C7{w`ab@L~ zjdlc2_?ZQai^Uu{5#?rpDDWR*pO2*}1NSr^UBjyDoBuD-YGNCK-T~F=4Eg*Zqa{uP zdeY=q@1Al07PNMo%D+0M{cE02&+K)tY1{mt*lZ|P%HcN?GI*{6=r($|Ikd{>F|<~+ zuofrPiu1LOe?-{3v*E2jjn-&NxCb)`G8LHiU>>Dd3-V6WGE%}V+WpO!wzFhNhx6`I zs9VFH{g01R&OtRio_q%=nR6yg!|g1VXP0V(_eR&2=ys`g}Gz^b!)p+5WxD>sxlUSp}~m z$0dlKU5-4qGQ4|i%F!aVy&l88ShiH7iUBegfZ5{laO9=%$QB2EL9QHML`i92>9D{P zrr~YQFj+np9r@om7SdvFEZ9LG=^r8+$4R635&j~NF>egOw4qY;iaDDOKXI7RpFrcg z%!=2uZ1Dk=Oi?W6@XOb}lB_J4BW|UcjI`yq;b&pqm&q%(@VN3EDY=B@5l zjz*Wp$6F&HFC0xLYM0G|J$q~hHftF>lc;|=`6Tx zs1#2EWUj;2UKVcs5E(Yh=sQqRsYs=R(iVw2ZdA1;_iV^tN9n{&%4x|OV zJOhu?sLBO^Wx}-{=9HjW%MsHTv-w`g?OUAV=}Q}JUrY{l6kX{f6KMSUb8Uq$h3Z(1 zDoavZ7jd98qx!ifMq24F)ocPstXBCKj>#n#{~f{+-mqG-?Yq|X97YcC_%_HH(~A!& z-Mh->^nq^o?aQBJ0yFU|Hlplf@wPWMir$*#)G>eYKLvgn;iI+2+Zoy5$|43ANYrp4`aTg|=4{@v?W9E~ruw z__v}X=ACP>$FRQ`+EH=S>@XLGPz7#^X|o%7OZ;K|K7IVvM@L7&t@zSc2&j+y*!5y0 z;?YU6;NJg6pZpYZX|+K1AygD!PJB*UPEcAY`+0?Vv{UxZLYa_x;<#IySkQgcrjB#9 zpJlMixj2ubz47VVf%M(t|1sUFCDy`C6`O54K2nl-21LSSW3I(qnI!}dO6xoIvvPhB zkP9764ZZ_{;7WxYL((PQsB@u1n}6mOzS92R@MXn*7%Mg|fJ~&Dt&i9RTwJtTt#pG- zu|lW~u25y-WXzE?gL4;-6~jguQE1^En`T4n_6w(_ErJ1W|8D{WxhFvct~GfkW|md6 z@)CJ_2{!kQe+;b4;>a9C)so$V2Ku>s)SLrxUttTHmyn6-Du|`Ne|}dPi)}*b@kc9M zi=W}ffCEsp(gAt-G=En?HhUhVd{NW}3AbX#e+x9dd0qN-=f=RLT6O}W(Y{d1-qmM+ml}Q7>SZV;?0eX|eJP0uBqHHVmAK6g= z#Lr?q{T8e50$kn!@pg+F*Lf;(qQN8F!CsL$JEQ&#Ov6ZF4k)rvaVCET^AEU@M)T!> z_HTi)P-q@{=xJ$oxr5Jrr`4@9^!b%b*XHmsJnLtd&3SBYoVsJgIuO(1K4}xuvTMj0 z#CkS*yUE^EQWm|1_zWz{y4~l7m4o>5^TeQ63>YCFMz-=n?_gK3=*e|IAHG^yiaA;p)lrsBzAL&N!gw6u{SN0Chte4J_}X9lk~(i(v=Jlxc$fq_0XY$?A# zSp~{jyQ;1O2N|!6-fT(!B!8O6lqX?99L6TubAEG|@CNI%TRU00MiXiAZqSxkn zi;pO(Pj5!=M#qI^e(+uKfM8F0{Pg`dWW{7)nnCJ}b%4|ny&3WqA z^Jv*egC+tHLDGUF_EZB4xteMVH1sZNJRj0za_kcOF7HpHIn_2 zS&%oC(bUm#a#!eW6h(HF+H=%@cTDe#sFZY-`A_EPMCTAgZZO(tFLaZ64I=x)Mk7UP z{=TC^R=fhxsE>WHWY&XGPDX2bc`L&tz287ZNW+f)=HiXJ1n1rf8?0%@SLs;5g!C{w zOXN&mB5-EH3W$4Km|*cI^&4Y@F&ERpHr3OgQL$LxCnk4Ktd-)%ZHI?<8g5p}-j%$n zd#3<~S1aM_wK7y#{)b#fI$9l`&MbyKhlfmb3+7V3{!e@QS_9 zzu(4w6Xh)2q+OO*K5}Yp(M|Sm0rs9xHbwDXhO`?uQ(rfqiV@>7bG%M(QMi0F&{rLo z9nadE__-R!b_lzdongFtALh^*bKCz|Cj_rQ2sdWDuCN| zOdVYQ={xavW#^V4>Z;Ij!+^W!$7RM4+FzTk^KZw?b~%H9LcUX9T4jE!_pg%*m)Rx< zka6~E1C$r^rL+*1W6<8xyIQqlZ`|-&GDc($_BV1@Wr96$z3knj7IE_VgVN&023~px z{mMIZ^ojCoPllFUofJsPK;rZ#n6qA=`x8~?kJ907@9dLnhlPt**4xWFGQI|%W~@c2 zx!wlansIkJB5SfwB9fH$#Fdr;J0elRX@6TY5Sr1-tIb>`7PI=DJc3it(hS)x&s_aS zOZ`n!SC;Yd@yze0nz|nbOs(6dPIlaBBAHO0k_KjECLA zO9DQpU1f-dm54?8?Nn723$2v80kaJPKC`C7#~^s%%92uT@_W-TP~bQ8rdS!P3cK?- zC{)e$zn0|lOo6HM3=-2$gQrBB->I3(QG4aQNA%6!L96?o{Uwq1obwJ?aL1rk|MbQL z-f1$ieoI}+ur3|_v)YdltF&(y3l1GJD!DbBbF=~{5jUK&|K!7_^51)>)efSs9`VBDQM)f_fT&K5hh$YcH_9>38~+_G$Gp+@wj@p#%i| zNK&k1yJz})9e>VKI7Er$kXOydg>*g&@^9t9-i#R$nEcwNz{FiEQ_6a)B|$Ed(e>(# zMAxO^^-xB{f7bodRv$)Hn{Kjmu1~^0m+QEu!@`hh?6A|;60*X`I)8;4iuTCIqSJ|< z1wF`$+jA*Hqp}x3@uVMPLBxqtvBaA;`p^9QuvXbP|Mfdd(tX@zh=9XrSB<{z&gAB> znjm=yP(X8sTin`-juKbo;^SBrtz zP({rCX?1S1_=5@V7XczH*NEw1rN8@Ja)}_f4162&}$-IT9B@QxqlMd!(D4=2+QtmM(s1(HsgHKZ0|+z2#z2cd04tD3kkSkE_;PaB=QyBcI9^l1PO7 zHBgqaH7*>VCY8nd6i}-x+eBi7+QDLd{@)k@9AXFx^V2+{ zPS?Z!jG>CW^lGn7kXI-I;E9BxT;ckDBTyvv$2U>;Vq|8Zq?b83MQYTGdSlH8i?`Xn zi4mXkxHA35(^5oh64E!uSCcO2DlvhkQL^241&4x&FwZoJo!je{MV+p(?-57-n|9gu zIMctpb7M1@Z0rw%NHJTc$jZhc4JS0{&_i)?t>w(#NGSnF>8fOVUj+u@)Lf}pyR#%v z)#2*|@wOOn)qujXdacUX1-m2*t|EsFni;kSc@97)ZZUsl3crS}D44o!&i+1e8TG(5J zKPYc=VMQ>{oua)LRuPcGc6SoTDAM_tjm}w7!*;Wprz$25(G+9XpjYJq^<=^VN?T-g zT|8pW@4Wd}Y4@id;a~NGxe0$WUcIw~*N9rgPw0Fef85|VnU|X!ZP*xUzZHQcdvk9$9r@2(u4Q)xGvRhV z$-B3=)+})@N4e6D#-GZUc|Bk#Wb53Du3uN8>Idfh`@zIVpMKo@@k$Md^R+~Y*2fJ+ z?ot`AKpg+z0Hy0~`Cycu!jK`f;Ji*l@;5J_`4qxMzeFXMeKaSv;`=*!`v{ebV%BP~ zmfN9!|5HV!EBV8++KtZnFmZF@smRA;KkwB3Ld(e6G}K9XQ| ze?X-Cip(R#m80Xd&dvna->=@MljfJtcFczSs0<<(cRqB6BWGlS2X2ET@)FR6AEzUY zNP8fXLQgvl^KvPHF8hif+IS{{T2?*NF@cbA%Ua52H zN2gvPmAv5!L+2lB2NMMtx8gnWH*};w8KC-8lZ(TMqr)t&kkec3B4^p};rFk)FaFl( zW1gF6=fEoG)yRmHCO6Mu7+P>|^q&Pnu=uZ?219<|N}6B{g~UG1=JO{LT2^_nZ9WSx z6kBQrs<$MUaXISZz9|ttGoe>xswWEJ#xaT+W*Nz)E-7+UBca+ZAJd0DYVzA%7PNK{ z6T(xaIxZNRtm2z**N!mbB;V-dVx!;#`(>ZbrVpKa^)qJtI5r1bMs~n_JLLRcy^Dq4 zhij2e2@0vVC@Z`Imq1|c2alTAHiJ#=qL4Am%>Q;7xhr$kWU7@Rk%*ff_g^$>omyb5 zQX)%CXMp0{ujL55U?6-bf5=+FY|nSGr3G~2gTj>3w>eG9K8+2IocJ(KmrY((xa7}G zp`-fArT!aBJkubReDEnH-g1o_M@}Mv`0c1q0b0vt$%z&rG_%e7Eo~NSiC+<{T8Yeu z4QZKeY2)n{T~yTT9v{<#OOwNUZRC!$-&Lrg8^c+gxW|pUpj@{>I^hq1}}V#N3$(_U2EYVCx25U1ti* zJ%jD#ImAp+@*IZu>Ol~p_29utf6q5qpujA3><7chkkYWTCw1whhO*!~;V>!;B!vVt z)Ga-35%~!|Qw#rt`aBZ&)pA*GFt>NWRk7nMd;MjVxN~3XFMPit9$O&Bla2_2 zIc^Gh6ouqR(v1ej5x*8&Dh#`SHt$mBip(Y;beQ-d;2xB*cI|FZREgT-@i)Zxw~u3~ z@Qf{XnyvosATdof)KNt8Hy87O5nfV0M-hI#VXl3dPp$6<&EYLdYsMDMzIP14ZWPw8 zxinCie(yVdRQ2c#q6H^c8c5s1o>VEW)|dHlytAGr>>tzU;}-EZyksH!;&##B?aAgw+>L9lA&bb2Pws6PHd zm7X2sN=5k4rup_W2H1mp&e2InQE|vCNI%$F(Q)&85})wN(E6wCU+u9if5U8zL&FJoNC^YU@A#BW zFSc=kqwN`hhD%+)7NS92G>sn}Xxx5zZA$6(+2t}U*rKqU7$6fmcpmB)C~sr(Xiz-n z5u$|+i)e718%x!*_#u`?^R?1yk8;DKom+Zt`HZ6)9UX?Es|f?~Zswokt{`V!8)E%W zb(asB+7((fNoBcxFdVfhVSVNbvA<0{HZ<1`nht(L?33@+ zBEpE4k2#kn=y%Es{e$itb;3Z-<2A+SX#B}EgcZw)4L%!ki{%*wQPD-nzQ128{^Diq zfB#*U)qbpIYHm}IaqbmS zLMi3-=FwPtI2bhy-1)ft0gC^%%&-^X8hCKc({=q3$slH#ccO%dRIfR$&mgvgqW?Sy z(*_KB{ly02=T86K#P``?T$n4vUrYn{4rnPY3{a5}mfu#^w*V515y35N z#M-v%+hzL4fHYp?avcyeH?o=|o2;L&OQIR3Ju-&;8V5q&*fmJhoX$QjLUIih{~)e) zn6^1(nR;1sM{V*?41{~~k_gKDV2X3X0WVAEe5El(O7caL8}`QaQcBW<#L&QuLIL-b`YjLQ4@A21BEqdFxSRKjSIVHYx<@4O$+|@ogFn@i&&2M&$^LRJ( z{<%);%?<;GuQO3?r=y3U8Ft+x6&T}y)-F5hDu%=V_Jn5HvJ5u`6mdMn@rh_TWA|FD z%+*}7D2aO7OA*aF@7aE3Y@XTQ|J$^;3L2lx&xqLQ;Xm=Y2PmCzhwp?l`nzw`V0fGW zpqbXsJ~*EH041;r(pMlISd!5?mc@;5gUZsJv%(!;m_MzSNcW QAMnxEc>cKL(d(f92e_FG&Hw-a literal 0 HcmV?d00001 diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index 81214fb5f96..c59db93b4ed 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -10,6 +10,9 @@ import segmentationTableModeImage2 from '../../../assets/img/segmentationTableMo import segmentationShowAddSegmentImage from '../../../assets/img/segmentationShowAddSegmentImage.png'; import layoutSelectorCommonPresetsImage from '../../../assets/img/layoutSelectorCommonPresetsImage.png'; import layoutSelectorAdvancedPresetGeneratorImage from '../../../assets/img/layoutSelectorAdvancedPresetGeneratorImage.png'; +import labellingFLow from '../../../assets/img/labelling-flow.png'; +import loadingIndicator from '../../../assets/img/Loading-Indicator.png'; + import segDisplayEditingTrue from '../../../assets/img/segDisplayEditingTrue.png'; import segDisplayEditingFalse from '../../../assets/img/segDisplayEditingFalse.png'; import thumbnailMenuItemsImage from '../../../assets/img/thumbnailMenuItemsImage.png'; @@ -610,6 +613,58 @@ window.config = { }; `, }, + { + id: 'ui.LoadingIndicatorTotalPercent', + description: 'Customizes the LoadingIndicatorTotalPercent component.', + image: loadingIndicator, + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.LoadingIndicatorTotalPercent': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.LoadingIndicatorProgress', + description: 'Customizes the LoadingIndicatorProgress component.', + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.LoadingIndicatorProgress': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.ProgressLoadingBar', + description: 'Customizes the ProgressLoadingBar component.', + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.ProgressLoadingBar': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, ]; export const segmentationCustomizations = [ @@ -968,6 +1023,24 @@ window.config = { }; `, }, + { + id: 'measurement.labellingComponent', + description: 'Customizes the labelling flow component.', + image: labellingFLow, + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'measurement.labellingComponent': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, ]; export const studyBrowserCustomizations = [ From de0912c82edc397e0e178e09c60779e8d28bb4b0 Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Mon, 27 Jan 2025 12:22:53 +0530 Subject: [PATCH 17/40] Added documentation for context menu item customization --- .../sampleCustomizations.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index c59db93b4ed..15c942722da 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -665,6 +665,23 @@ window.config = { }; `, }, + { + id: 'ui.ContextMenuItem', + description: 'Customizes the Context menu item component.', + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.ContextMenuItem': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, ]; export const segmentationCustomizations = [ From 43f9991a898ce1fb39cc74afab494b4ea1f38385 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Mon, 27 Jan 2025 12:38:40 +0530 Subject: [PATCH 18/40] reverted OSS changes in progressLoading bar --- .../LoadingIndicatorProgress/LoadingIndicatorProgress.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 59a190ba382..1cb3dab95b8 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classNames from 'classnames'; -import Icon from '../Icon'; +import { Icons } from '@ohif/ui-next'; import ProgressLoadingBar from '../ProgressLoadingBar'; import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; @@ -29,10 +29,7 @@ function FallbackLoadingIndicatorProgress({ className, textBlock, progress }) { className )} > - +
From 4ef14b468332f5b9b2f0b6b90bf15058fa8f95ec Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Mon, 27 Jan 2025 16:51:58 +0530 Subject: [PATCH 19/40] created sample components for customization provided --- .../src/customizations/labellingFlowCustomization.tsx | 9 +++++++++ .../loadingIndicatorProgressCustomization.tsx | 9 +++++++++ .../loadingIndicatorTotalPercentCustomization.tsx | 9 +++++++++ .../customizations/progressLoadingBarCustomization.tsx | 9 +++++++++ 4 files changed, 36 insertions(+) create mode 100644 extensions/default/src/customizations/labellingFlowCustomization.tsx create mode 100644 extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx create mode 100644 extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx create mode 100644 extensions/default/src/customizations/progressLoadingBarCustomization.tsx diff --git a/extensions/default/src/customizations/labellingFlowCustomization.tsx b/extensions/default/src/customizations/labellingFlowCustomization.tsx new file mode 100644 index 00000000000..89d231d0715 --- /dev/null +++ b/extensions/default/src/customizations/labellingFlowCustomization.tsx @@ -0,0 +1,9 @@ +export default { + 'measurement.labellingComponent': { + $set: CustomLabellingFlow, + }, +}; + +function CustomLabellingFlow() { + return null; +} diff --git a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx new file mode 100644 index 00000000000..37392372aea --- /dev/null +++ b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx @@ -0,0 +1,9 @@ +export default { + 'ui.LoadingIndicatorProgress': { + $set: CustomLoadingIndicatorProgress, + }, +}; + +function CustomLoadingIndicatorProgress() { + return null; +} diff --git a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx new file mode 100644 index 00000000000..20a50b28bc7 --- /dev/null +++ b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx @@ -0,0 +1,9 @@ +export default { + 'ui.LoadingIndicatorTotalPercent': { + $set: CustomLoadingIndicatorTotalPercent, + }, +}; + +function CustomLoadingIndicatorTotalPercent() { + return null; +} diff --git a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx new file mode 100644 index 00000000000..7c899c266de --- /dev/null +++ b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx @@ -0,0 +1,9 @@ +export default { + 'ui.ProgressLoadingBar': { + $set: CustomProgressLoadingBar, + }, +}; + +function CustomProgressLoadingBar() { + return null; +} From 37120d7cf125b0e273f42ab2b9aec20d70245f57 Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Mon, 27 Jan 2025 17:10:22 +0530 Subject: [PATCH 20/40] added default customization for context menu item --- .../src/customizations/contextMenuItemCustomization.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 extensions/default/src/customizations/contextMenuItemCustomization.ts diff --git a/extensions/default/src/customizations/contextMenuItemCustomization.ts b/extensions/default/src/customizations/contextMenuItemCustomization.ts new file mode 100644 index 00000000000..c5c0e92559b --- /dev/null +++ b/extensions/default/src/customizations/contextMenuItemCustomization.ts @@ -0,0 +1,9 @@ +export default { + 'ui.ContextMenuItem': { + $set: CustomContextMenuItem, + }, +}; + +function CustomContextMenuItem() { + return null; +} From 7a2a885c02f6f79143e766d3ecc4c89bf52a17b2 Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Tue, 28 Jan 2025 14:49:39 +0530 Subject: [PATCH 21/40] rename customClassName as className in context menu customization --- .../src/CustomizableContextMenu/ContextMenuController.tsx | 4 ++-- extensions/default/src/CustomizableContextMenu/types.ts | 2 +- platform/ui/src/components/ContextMenu/ContextMenu.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx index dd07810bb59..c79045d110c 100644 --- a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx @@ -77,7 +77,7 @@ export default class ContextMenuController { { selectorProps: selectorProps || contextMenuProps, event }, menuId ); - const customClassName = menu?.customClassName || ''; + const className = menu?.className || ''; this.services.uiDialogService.dismiss({ id: 'context-menu' }); this.services.uiDialogService.create({ @@ -103,7 +103,7 @@ export default class ContextMenuController { menus, event, subMenu, - customClassName, + className, eventData: event?.detail || event, onClose: () => { diff --git a/extensions/default/src/CustomizableContextMenu/types.ts b/extensions/default/src/CustomizableContextMenu/types.ts index 813e0833c06..512c3fef5f6 100644 --- a/extensions/default/src/CustomizableContextMenu/types.ts +++ b/extensions/default/src/CustomizableContextMenu/types.ts @@ -98,7 +98,7 @@ export interface Menu { selector?: Types.Predicate; items: MenuItem[]; - customClassName: string; + className: string; } export type Point = { diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index 78a39d34262..94d83552724 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -31,7 +31,7 @@ const ContextMenu = ({ items, ...props }) => { ref={contextMenuRef} data-cy="context-menu" className={ - 'bg-secondary-dark relative z-50 block w-48 rounded ' + props?.contentProps?.customClassName + 'bg-secondary-dark relative z-50 block w-48 rounded ' + props?.contentProps?.className } onContextMenu={e => e.preventDefault()} > From 0af70c9da1c82c4c9116a195cd03f0d50969d061 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Tue, 28 Jan 2025 15:04:42 +0530 Subject: [PATCH 22/40] Reverted the customizable render component dependency and implemented based on latest customization service --- .../LoadingIndicatorProgress.tsx | 31 ++++++++++++ .../LoadingIndicatorProgress/index.js | 2 + .../LoadingIndicatorTotalPercent.tsx | 48 +++++++++++++++++++ .../LoadingIndicatorTotalPercent/index.js | 2 + .../ProgressLoadingBar/ProgressLoadingBar.css | 26 ++++++++++ .../ProgressLoadingBar/ProgressLoadingBar.tsx | 31 ++++++++++++ .../Components/ProgressLoadingBar/index.js | 2 + .../loadingIndicatorProgressCustomization.tsx | 10 ++-- ...dingIndicatorTotalPercentCustomization.tsx | 10 ++-- .../progressLoadingBarCustomization.tsx | 10 ++-- .../default/src/getCustomizationModule.tsx | 6 +++ .../LoadingIndicatorProgress.tsx | 30 ++---------- .../LoadingIndicatorTotalPercent.tsx | 44 +++-------------- .../ProgressLoadingBar/ProgressLoadingBar.tsx | 27 ++--------- 14 files changed, 171 insertions(+), 108 deletions(-) create mode 100644 extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx create mode 100644 extensions/default/src/Components/LoadingIndicatorProgress/index.js create mode 100644 extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx create mode 100644 extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js create mode 100644 extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css create mode 100644 extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx create mode 100644 extensions/default/src/Components/ProgressLoadingBar/index.js diff --git a/extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx new file mode 100644 index 00000000000..e4ef76a048a --- /dev/null +++ b/extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { Icons } from '@ohif/ui-next'; +import ProgressLoadingBar from '../ProgressLoadingBar'; + +/** + * A React component that renders a loading indicator. + * if progress is not provided, it will render an infinite loading indicator + * if progress is provided, it will render a progress bar + * Optionally a textBlock can be provided to display a message + */ + +function LoadingIndicatorProgress({ className, textBlock, progress }) { + return ( +
+ +
+ +
+ {textBlock} +
+ ); +} + +export default LoadingIndicatorProgress; diff --git a/extensions/default/src/Components/LoadingIndicatorProgress/index.js b/extensions/default/src/Components/LoadingIndicatorProgress/index.js new file mode 100644 index 00000000000..f8a0fbe79c5 --- /dev/null +++ b/extensions/default/src/Components/LoadingIndicatorProgress/index.js @@ -0,0 +1,2 @@ +import LoadingIndicatorProgress from './LoadingIndicatorProgress'; +export default LoadingIndicatorProgress; diff --git a/extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx new file mode 100644 index 00000000000..56f186b4ab7 --- /dev/null +++ b/extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import LoadingIndicatorProgress from '../LoadingIndicatorProgress'; + +interface Props { + className?: string; + totalNumbers: number | null; + percentComplete: number | null; + loadingText?: string; + targetText?: string; +} + +/** + * A React component that renders a loading indicator but accepts a totalNumbers + * and percentComplete to display a more detailed message. + */ +function LoadingIndicatorTotalPercent({ + className, + totalNumbers, + percentComplete, + loadingText = 'Loading...', + targetText = 'segments', +}: Props): JSX.Element { + const progress = percentComplete; + const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; + const numTargetsLoadedText = + percentComplete !== null ? Math.floor((percentComplete * totalNumbers) / 100) : ''; + + const textBlock = + !totalNumbers && percentComplete === null ? ( +
{loadingText}
+ ) : !totalNumbers && percentComplete !== null ? ( +
Loaded {percentComplete}%
+ ) : ( +
+ Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText} +
+ ); + + return ( + + ); +} + +export default LoadingIndicatorTotalPercent; diff --git a/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js b/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js new file mode 100644 index 00000000000..fc4e1fa109a --- /dev/null +++ b/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js @@ -0,0 +1,2 @@ +import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent'; +export default LoadingIndicatorTotalPercent; diff --git a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css new file mode 100644 index 00000000000..f9addbe9e91 --- /dev/null +++ b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css @@ -0,0 +1,26 @@ +.loading { + background-color: #091731; + height: 8px; + border-radius: 4px; + overflow: hidden; + position: relative; + width: 100%; +} + +.infinite-loading-bar { + animation: side2side 2s ease-in-out infinite; + height: 100%; + position: absolute; + border-radius: 4px; + width: 50%; +} + +@keyframes side2side { + 0%, + 100% { + transform: translateX(-50%); + } + 50% { + transform: translateX(150%); + } +} diff --git a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx new file mode 100644 index 00000000000..0624bdf112f --- /dev/null +++ b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -0,0 +1,31 @@ +import React, { ReactElement } from 'react'; +import './ProgressLoadingBar.css'; + +export type ProgressLoadingBarProps = { + progress?: number; +}; +/** + * A React component that renders a loading progress bar. + * If progress is not provided, it will render an infinite loading bar + * If progress is provided, it will render a progress bar + * The progress text can be optionally displayed to the left of the bar. + */ +function ProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { + return ( +
+ {progress === undefined || progress === null ? ( +
+ ) : ( +
+ )} +
+ ); +} + +export default ProgressLoadingBar; diff --git a/extensions/default/src/Components/ProgressLoadingBar/index.js b/extensions/default/src/Components/ProgressLoadingBar/index.js new file mode 100644 index 00000000000..ab8869ffcc4 --- /dev/null +++ b/extensions/default/src/Components/ProgressLoadingBar/index.js @@ -0,0 +1,2 @@ +import ProgressLoadingBar from './ProgressLoadingBar'; +export default ProgressLoadingBar; diff --git a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx index 37392372aea..a47e337e36d 100644 --- a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx @@ -1,9 +1,5 @@ +import LoadingIndicatorProgress from '../Components/LoadingIndicatorProgress'; + export default { - 'ui.LoadingIndicatorProgress': { - $set: CustomLoadingIndicatorProgress, - }, + 'ui.LoadingIndicatorProgress': LoadingIndicatorProgress, }; - -function CustomLoadingIndicatorProgress() { - return null; -} diff --git a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx index 20a50b28bc7..5a49c5a7973 100644 --- a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx @@ -1,9 +1,5 @@ +import LoadingIndicatorTotalPercent from '../Components/LoadingIndicatorTotalPercent'; + export default { - 'ui.LoadingIndicatorTotalPercent': { - $set: CustomLoadingIndicatorTotalPercent, - }, + 'ui.LoadingIndicatorTotalPercent': LoadingIndicatorTotalPercent, }; - -function CustomLoadingIndicatorTotalPercent() { - return null; -} diff --git a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx index 7c899c266de..278524f70af 100644 --- a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx +++ b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx @@ -1,9 +1,5 @@ +import ProgressLoadingBar from '../Components/ProgressLoadingBar'; + export default { - 'ui.ProgressLoadingBar': { - $set: CustomProgressLoadingBar, - }, + 'ui.ProgressLoadingBar': ProgressLoadingBar, }; - -function CustomProgressLoadingBar() { - return null; -} diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 47b7852511b..106a8d68124 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -11,6 +11,9 @@ import getDataSourceConfigurationCustomization from './customizations/dataSource import progressDropdownCustomization from './customizations/progressDropdownCustomization'; import sortingCriteriaCustomization from './customizations/sortingCriteriaCustomization'; import onDropHandlerCustomization from './customizations/onDropHandlerCustomization'; +import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization'; +import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; +import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; /** * @@ -48,6 +51,9 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...sortingCriteriaCustomization, ...defaultContextMenuCustomization, ...onDropHandlerCustomization, + ...loadingIndicatorProgressCustomization, + ...loadingIndicatorTotalPercentCustomization, + ...progressLoadingBarCustomization, }, }, ]; diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 1cb3dab95b8..3edbdcd4240 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,9 +1,4 @@ -import React from 'react'; -import classNames from 'classnames'; - -import { Icons } from '@ohif/ui-next'; -import ProgressLoadingBar from '../ProgressLoadingBar'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import { useServices } from '@ohif/ui'; /** * A React component that renders a loading indicator. @@ -13,29 +8,12 @@ import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent */ function LoadingIndicatorProgress({ className, textBlock, progress }) { - return CustomizableRenderComponent({ - customizationId: 'ui.LoadingIndicatorProgress', - FallbackComponent: FallbackLoadingIndicatorProgress, + const { services } = useServices(); + const Component = services.customizationService.getCustomization('ui.LoadingIndicatorProgress'); + return Component({ className, textBlock, progress, }); } -function FallbackLoadingIndicatorProgress({ className, textBlock, progress }) { - return ( -
- -
- -
- {textBlock} -
- ); -} - export default LoadingIndicatorProgress; diff --git a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx index a5e2572a7af..63c823c3088 100644 --- a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -1,6 +1,4 @@ -import React from 'react'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; -import LoadingIndicatorProgress from '../LoadingIndicatorProgress'; +import { useServices } from '../../contextProviders'; interface Props { className?: string; @@ -21,9 +19,11 @@ function LoadingIndicatorTotalPercent({ loadingText, targetText, }: Props) { - return CustomizableRenderComponent({ - customizationId: 'ui.LoadingIndicatorTotalPercent', - FallbackComponent: FallbackLoadingIndicatorTotalPercent, + const { services } = useServices(); + const Component = services.customizationService.getCustomization( + 'ui.LoadingIndicatorTotalPercent' + ); + return Component({ className, totalNumbers, percentComplete, @@ -32,36 +32,4 @@ function LoadingIndicatorTotalPercent({ }); } -function FallbackLoadingIndicatorTotalPercent({ - className, - totalNumbers, - percentComplete, - loadingText = 'Loading...', - targetText = 'segments', -}: Props): JSX.Element { - const progress = percentComplete; - const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; - const numTargetsLoadedText = - percentComplete !== null ? Math.floor((percentComplete * totalNumbers) / 100) : ''; - - const textBlock = - !totalNumbers && percentComplete === null ? ( -
{loadingText}
- ) : !totalNumbers && percentComplete !== null ? ( -
Loaded {percentComplete}%
- ) : ( -
- Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText} -
- ); - - return ( - - ); -} - export default LoadingIndicatorTotalPercent; diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx index 80f67e93500..ba32b10d12c 100644 --- a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -1,6 +1,4 @@ -import React, { ReactElement } from 'react'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; -import './ProgressLoadingBar.css'; +import { useServices } from '@ohif/ui'; export type ProgressLoadingBarProps = { progress?: number; @@ -13,28 +11,11 @@ export type ProgressLoadingBarProps = { */ function ProgressLoadingBar({ progress }) { - return CustomizableRenderComponent({ - customizationId: 'ui.ProgressLoadingBar', - FallbackComponent: FallbackProgressLoadingBar, + const { services } = useServices(); + const Component = services.customizationService.getCustomization('ui.ProgressLoadingBar'); + return Component({ progress, }); } -function FallbackProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { - return ( -
- {progress === undefined || progress === null ? ( -
- ) : ( -
- )} -
- ); -} export default ProgressLoadingBar; From cadd248b8d1c08e7ab1d16fe83282adbece3fd8e Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Tue, 28 Jan 2025 15:37:27 +0530 Subject: [PATCH 23/40] fix review comment for context menu item ui customization --- .../Components/ContextMenuItemComponent.tsx | 28 ++++++++++++ .../contextMenuItemCustomization.ts | 10 ++--- .../default/src/getCustomizationModule.tsx | 2 + .../components/ContextMenu/ContextMenu.tsx | 45 +++---------------- 4 files changed, 40 insertions(+), 45 deletions(-) create mode 100644 extensions/default/src/Components/ContextMenuItemComponent.tsx diff --git a/extensions/default/src/Components/ContextMenuItemComponent.tsx b/extensions/default/src/Components/ContextMenuItemComponent.tsx new file mode 100644 index 00000000000..12c59a3ad58 --- /dev/null +++ b/extensions/default/src/Components/ContextMenuItemComponent.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Typography } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; + +/** + * A React component that renders a context menu item. + */ +const ContextMenuItemComponent = ({ index, item, ...props }) => { + return ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ); +}; + +export default ContextMenuItemComponent; diff --git a/extensions/default/src/customizations/contextMenuItemCustomization.ts b/extensions/default/src/customizations/contextMenuItemCustomization.ts index c5c0e92559b..cb03600b475 100644 --- a/extensions/default/src/customizations/contextMenuItemCustomization.ts +++ b/extensions/default/src/customizations/contextMenuItemCustomization.ts @@ -1,9 +1,5 @@ +import ContextMenuItemComponent from '../Components/ContextMenuItemComponent'; + export default { - 'ui.ContextMenuItem': { - $set: CustomContextMenuItem, - }, + 'ui.ContextMenuItem': ContextMenuItemComponent, }; - -function CustomContextMenuItem() { - return null; -} diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 106a8d68124..03831e6764e 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -14,6 +14,7 @@ import onDropHandlerCustomization from './customizations/onDropHandlerCustomizat import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization'; import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; +import contextMenuItemCustomization from './customizations/contextMenuItemCustomization'; /** * @@ -54,6 +55,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...loadingIndicatorProgressCustomization, ...loadingIndicatorTotalPercentCustomization, ...progressLoadingBarCustomization, + ...contextMenuItemCustomization, }, }, ]; diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index 94d83552724..903aab861c2 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,13 +1,12 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import Typography from '../Typography'; -import { Icons } from '@ohif/ui-next'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import { useServices } from '@ohif/ui'; const ContextMenu = ({ items, ...props }) => { + const { services } = useServices(); const contextMenuRef = useRef(null); useEffect(() => { - if(!contextMenuRef?.current) { + if (!contextMenuRef?.current) { return; } @@ -36,11 +35,10 @@ const ContextMenu = ({ items, ...props }) => { onContextMenu={e => e.preventDefault()} > {items.map((item, index) => { - const itemProps = { item, index, ...props }; - return CustomizableRenderComponent({ - customizationId: 'ui.ContextMenuItem', - FallbackComponent: FallbackContextMenuItem, - ...itemProps, + return services.customizationService.getCustomization('ui.ContextMenuItem')({ + item, + index, + ...props, }); })}
@@ -60,33 +58,4 @@ ContextMenu.propTypes = { ), }; -const FallbackContextMenuItem = ({ index, item, ...props }) => { - return ( -
item.action(item, props)} - style={{ justifyContent: 'space-between' }} - className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" - > - {item.label} - {item.iconRight && ( - - )} -
- ); -}; - -FallbackContextMenuItem.propTypes = { - index: PropTypes.number.isRequired, - item: PropTypes.shape({ - label: PropTypes.string.isRequired, - action: PropTypes.func.isRequired, - iconRight: PropTypes.string, - }), -}; - export default ContextMenu; From fb4c0ab93e5a079e6cd4caecbb11b9aa670975cf Mon Sep 17 00:00:00 2001 From: Devu Jayalekshmi Date: Tue, 28 Jan 2025 15:56:04 +0530 Subject: [PATCH 24/40] Fix-review-comment-Viewport action corners customization --- .../ViewportActionCorners.tsx | 76 +++++++++++++++++++ .../ViewportActionCorners/index.tsx | 4 + .../viewportActionCornersCustomization.ts | 5 ++ .../default/src/getCustomizationModule.tsx | 2 + .../ViewportActionCorners.tsx | 59 ++------------ 5 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx create mode 100644 extensions/default/src/Components/ViewportActionCorners/index.tsx create mode 100644 extensions/default/src/customizations/viewportActionCornersCustomization.ts diff --git a/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx b/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx new file mode 100644 index 00000000000..b8a2534b174 --- /dev/null +++ b/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx @@ -0,0 +1,76 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Types } from '@ohif/ui'; + +export enum ViewportActionCornersLocations { + topLeft, + topRight, + bottomLeft, + bottomRight, +} + +export type ViewportActionCornersProps = { + cornerComponents: Record< + ViewportActionCornersLocations, + Array + >; +}; + +const commonClasses = 'pointer-events-auto flex items-center gap-1'; +const classes = { + [ViewportActionCornersLocations.topLeft]: classNames( + commonClasses, + 'absolute top-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.topRight]: classNames( + commonClasses, + 'absolute top-[4px] right-[4px] right-viewport-scrollbar' + ), + [ViewportActionCornersLocations.bottomLeft]: classNames( + commonClasses, + 'absolute bottom-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.bottomRight]: classNames( + commonClasses, + 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' + ), +}; + +/** + * A component that renders various action items/components to each corner of a viewport. + * The position of each corner's components is such that a single row of components are + * rendered absolutely without intersecting the ViewportOverlay component. + * Note that corner components are passed as an object mapping each corner location + * to an array of components for that location. The components in each array are + * rendered from left to right in the order that they appear in the array. + */ +function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { + if (!cornerComponents) { + return null; + } + + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + }} + > + {Object.entries(cornerComponents).map(([location, locationComponents]) => { + return ( +
+ {locationComponents.map(componentInfo => { + return
{componentInfo.component}
; + })} +
+ ); + })} +
+ ); +} + +export default ViewportActionCorners; diff --git a/extensions/default/src/Components/ViewportActionCorners/index.tsx b/extensions/default/src/Components/ViewportActionCorners/index.tsx new file mode 100644 index 00000000000..9136db97de7 --- /dev/null +++ b/extensions/default/src/Components/ViewportActionCorners/index.tsx @@ -0,0 +1,4 @@ +import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners'; + +export { ViewportActionCornersLocations }; +export default ViewportActionCorners; diff --git a/extensions/default/src/customizations/viewportActionCornersCustomization.ts b/extensions/default/src/customizations/viewportActionCornersCustomization.ts new file mode 100644 index 00000000000..6273a05198e --- /dev/null +++ b/extensions/default/src/customizations/viewportActionCornersCustomization.ts @@ -0,0 +1,5 @@ +import ViewportActionCorners from '../Components/ViewportActionCorners'; + +export default { + 'ui.ViewportActionCorner': ViewportActionCorners, +}; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 03831e6764e..6773c01f9aa 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -15,6 +15,7 @@ import loadingIndicatorProgressCustomization from './customizations/loadingIndic import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; import contextMenuItemCustomization from './customizations/contextMenuItemCustomization'; +import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; /** * @@ -56,6 +57,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...loadingIndicatorTotalPercentCustomization, ...progressLoadingBarCustomization, ...contextMenuItemCustomization, + ...viewportActionCornersCustomization, }, }, ]; diff --git a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx index 48d872299e9..c3e52930160 100644 --- a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx +++ b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx @@ -1,7 +1,5 @@ -import classNames from 'classnames'; -import React from 'react'; +import { useServices } from '@ohif/ui'; import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export enum ViewportActionCornersLocations { topLeft, @@ -17,26 +15,6 @@ export type ViewportActionCornersProps = { >; }; -const commonClasses = 'pointer-events-auto flex items-center gap-1'; -const classes = { - [ViewportActionCornersLocations.topLeft]: classNames( - commonClasses, - 'absolute top-[4px] left-[0px] pl-[4px]' - ), - [ViewportActionCornersLocations.topRight]: classNames( - commonClasses, - 'absolute top-[4px] right-[4px] right-viewport-scrollbar' - ), - [ViewportActionCornersLocations.bottomLeft]: classNames( - commonClasses, - 'absolute bottom-[4px] left-[0px] pl-[4px]' - ), - [ViewportActionCornersLocations.bottomRight]: classNames( - commonClasses, - 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' - ), -}; - /** * A component that renders various action items/components to each corner of a viewport. * The position of each corner's components is such that a single row of components are @@ -46,40 +24,15 @@ const classes = { * rendered from left to right in the order that they appear in the array. */ function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { - return CustomizableRenderComponent({ - customizationId: 'ui.ViewportActionCorner', - FallbackComponent: FallbackViewportActionCorners, - cornerComponents, - }); -} - -function FallbackViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { + const { services } = useServices(); if (!cornerComponents) { return null; } - return ( -
{ - event.preventDefault(); - event.stopPropagation(); - }} - > - {Object.entries(cornerComponents).map(([location, locationComponents]) => { - return ( -
- {locationComponents.map(componentInfo => { - return
{componentInfo.component}
; - })} -
- ); - })} -
- ); + const Component = services.customizationService.getCustomization('ui.ViewportActionCorner'); + return Component({ + cornerComponents, + }); } export default ViewportActionCorners; From 5a3086b608119b713f41db8913de284bd874bcad Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Tue, 28 Jan 2025 16:27:26 +0530 Subject: [PATCH 25/40] changed set customization methods as per review comments --- .../default/src/Components/LabellingFlow.tsx | 42 +++++++++++++++++++ .../LoadingIndicatorProgress.tsx | 2 +- .../LoadingIndicatorProgress/index.js | 2 - .../LoadingIndicatorTotalPercent.tsx | 17 +++----- .../LoadingIndicatorTotalPercent/index.js | 2 - .../ProgressLoadingBar/ProgressLoadingBar.tsx | 4 +- .../labellingFlowCustomization.tsx | 10 ++--- .../default/src/getCustomizationModule.tsx | 2 + .../services/ui/customization-service.md | 13 ------ .../components/Labelling/LabellingFlow.tsx | 20 +++++---- .../LoadingIndicatorTotalPercent.tsx | 4 +- .../src/utils/CustomizableRenderComponent.tsx | 15 ------- 12 files changed, 67 insertions(+), 66 deletions(-) create mode 100644 extensions/default/src/Components/LabellingFlow.tsx rename extensions/default/src/Components/{LoadingIndicatorProgress => }/LoadingIndicatorProgress.tsx (93%) delete mode 100644 extensions/default/src/Components/LoadingIndicatorProgress/index.js rename extensions/default/src/Components/{LoadingIndicatorTotalPercent => }/LoadingIndicatorTotalPercent.tsx (78%) delete mode 100644 extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js delete mode 100644 platform/ui/src/utils/CustomizableRenderComponent.tsx diff --git a/extensions/default/src/Components/LabellingFlow.tsx b/extensions/default/src/Components/LabellingFlow.tsx new file mode 100644 index 00000000000..23671a0aea1 --- /dev/null +++ b/extensions/default/src/Components/LabellingFlow.tsx @@ -0,0 +1,42 @@ +import React, { ReactElement } from 'react'; +import SelectTree from '@ohif/ui/src/components/SelectTree'; + +export type LabellingFlowProps = { + columns: number; + onSelected: () => void; + closePopup: () => void; + label: string; + measurementData: any; + items: any[]; + exclusive: boolean; + selectTreeFirstTitle: string; +}; +/** + * A React component that renders a loading progress bar. + * If progress is not provided, it will render an infinite loading bar + * If progress is provided, it will render a progress bar + * The progress text can be optionally displayed to the left of the bar. + */ +function LabellingFlow({ + items, + columns, + onSelected, + closePopup, + selectTreeFirstTitle, + exclusive, + label, +}: LabellingFlowProps): ReactElement { + return ( + + ); +} + +export default LabellingFlow; diff --git a/extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/extensions/default/src/Components/LoadingIndicatorProgress.tsx similarity index 93% rename from extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx rename to extensions/default/src/Components/LoadingIndicatorProgress.tsx index e4ef76a048a..898c77ae0ef 100644 --- a/extensions/default/src/Components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/extensions/default/src/Components/LoadingIndicatorProgress.tsx @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import { Icons } from '@ohif/ui-next'; -import ProgressLoadingBar from '../ProgressLoadingBar'; +import ProgressLoadingBar from './ProgressLoadingBar'; /** * A React component that renders a loading indicator. diff --git a/extensions/default/src/Components/LoadingIndicatorProgress/index.js b/extensions/default/src/Components/LoadingIndicatorProgress/index.js deleted file mode 100644 index f8a0fbe79c5..00000000000 --- a/extensions/default/src/Components/LoadingIndicatorProgress/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import LoadingIndicatorProgress from './LoadingIndicatorProgress'; -export default LoadingIndicatorProgress; diff --git a/extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx similarity index 78% rename from extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx rename to extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx index 56f186b4ab7..60518cf5cf7 100644 --- a/extensions/default/src/Components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx @@ -1,13 +1,6 @@ import React from 'react'; -import LoadingIndicatorProgress from '../LoadingIndicatorProgress'; - -interface Props { - className?: string; - totalNumbers: number | null; - percentComplete: number | null; - loadingText?: string; - targetText?: string; -} +import { LoadingIndicatorTotalPercentProps } from '@ohif/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent'; +import LoadingIndicatorProgress from './LoadingIndicatorProgress'; /** * A React component that renders a loading indicator but accepts a totalNumbers @@ -17,9 +10,9 @@ function LoadingIndicatorTotalPercent({ className, totalNumbers, percentComplete, - loadingText = 'Loading...', - targetText = 'segments', -}: Props): JSX.Element { + loadingText, + targetText, +}: LoadingIndicatorTotalPercentProps): JSX.Element { const progress = percentComplete; const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; const numTargetsLoadedText = diff --git a/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js b/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js deleted file mode 100644 index fc4e1fa109a..00000000000 --- a/extensions/default/src/Components/LoadingIndicatorTotalPercent/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent'; -export default LoadingIndicatorTotalPercent; diff --git a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx index 0624bdf112f..cd0674e96e2 100644 --- a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -1,9 +1,7 @@ import React, { ReactElement } from 'react'; +import { ProgressLoadingBarProps } from '@ohif/ui/src/components/ProgressLoadingBar/ProgressLoadingBar'; import './ProgressLoadingBar.css'; -export type ProgressLoadingBarProps = { - progress?: number; -}; /** * A React component that renders a loading progress bar. * If progress is not provided, it will render an infinite loading bar diff --git a/extensions/default/src/customizations/labellingFlowCustomization.tsx b/extensions/default/src/customizations/labellingFlowCustomization.tsx index 89d231d0715..ff03a04af57 100644 --- a/extensions/default/src/customizations/labellingFlowCustomization.tsx +++ b/extensions/default/src/customizations/labellingFlowCustomization.tsx @@ -1,9 +1,5 @@ +import LabellingFlow from '../Components/LabellingFlow'; + export default { - 'measurement.labellingComponent': { - $set: CustomLabellingFlow, - }, + 'measurement.labellingComponent': LabellingFlow, }; - -function CustomLabellingFlow() { - return null; -} diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 6773c01f9aa..cbd586f142a 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -16,6 +16,7 @@ import loadingIndicatorTotalPercentCustomization from './customizations/loadingI import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; import contextMenuItemCustomization from './customizations/contextMenuItemCustomization'; import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; +import labellingFlowCustomization from './customizations/labellingFlowCustomization'; /** * @@ -58,6 +59,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...progressLoadingBarCustomization, ...contextMenuItemCustomization, ...viewportActionCornersCustomization, + ...labellingFlowCustomization, }, }, ]; diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md index 8e52689bca6..a6baf90f447 100644 --- a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -489,19 +489,6 @@ context menus. Currently it supports buttons 1-3, as well as modifier keys by associating a commands list with the button to click. See `initContextMenu` for more details. -## Customizable Render component. - -The CustomizableRenderComponent dynamically renders a custom component based on a customizationId. If a component for the given ID is found, it is rendered with the provided props; otherwise, a fallback component is rendered. To set a custom component for a specific customizationId, you must register it using the customizationService, where the custom component is added within an object under the component key. If no component is found for the specified customizationId, the FallbackComponent will be rendered instead. - -``` - customizationService.setCustomizations({ - 'customiztionId': { - $set: CustomizedComponent, - }, - }); - -``` - ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 53343f31884..52055025c79 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -1,8 +1,7 @@ -import SelectTree from '../SelectTree'; +import { useServices } from '@ohif/ui'; import React, { Component } from 'react'; import LabellingTransition from './LabellingTransition'; import cloneDeep from 'lodash.clonedeep'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; interface PropType { labellingDoneCallback: (label: string) => void; @@ -88,17 +87,20 @@ class LabellingFlow extends Component { }; labellingStateFragment = () => { + const { services } = useServices(); + const Component = services.customizationService.getCustomization( + 'measurement.labellingComponent' + ); return ( - ); }; diff --git a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx index 63c823c3088..7ae25970320 100644 --- a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -1,6 +1,6 @@ import { useServices } from '../../contextProviders'; -interface Props { +export interface LoadingIndicatorTotalPercentProps { className?: string; totalNumbers: number | null; percentComplete: number | null; @@ -18,7 +18,7 @@ function LoadingIndicatorTotalPercent({ percentComplete, loadingText, targetText, -}: Props) { +}: LoadingIndicatorTotalPercentProps) { const { services } = useServices(); const Component = services.customizationService.getCustomization( 'ui.LoadingIndicatorTotalPercent' diff --git a/platform/ui/src/utils/CustomizableRenderComponent.tsx b/platform/ui/src/utils/CustomizableRenderComponent.tsx deleted file mode 100644 index a550c608695..00000000000 --- a/platform/ui/src/utils/CustomizableRenderComponent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { useServices } from '@ohif/ui'; - -interface ICustomizableRenderComponent { - customizationId: string; - FallbackComponent: React.ElementType; - [key: string]: any; -} - -export default function CustomizableRenderComponent(props: ICustomizableRenderComponent) { - const { customizationId, FallbackComponent, ...rest } = props; - const { services } = useServices(); - const CustomizedComponent = services.customizationService.getCustomization(customizationId); - return CustomizedComponent ? : ; -} From e12d952111d76fb9851b26c5fde5ac72cee522f5 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Tue, 28 Jan 2025 17:13:28 +0530 Subject: [PATCH 26/40] updated the workflow of rendering custom component in UI --- .../components/Labelling/LabellingFlow.tsx | 28 ++++++++----------- .../LoadingIndicatorProgress.tsx | 7 ++--- .../LoadingIndicatorTotalPercent.tsx | 9 ++---- .../ProgressLoadingBar/ProgressLoadingBar.tsx | 7 ++--- .../src/utils/CustomizableRenderComponent.tsx | 14 ++++++++++ 5 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 platform/ui/src/utils/CustomizableRenderComponent.tsx diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index 52055025c79..ef3c19f09e7 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -1,7 +1,7 @@ -import { useServices } from '@ohif/ui'; import React, { Component } from 'react'; import LabellingTransition from './LabellingTransition'; import cloneDeep from 'lodash.clonedeep'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; interface PropType { labellingDoneCallback: (label: string) => void; @@ -87,22 +87,16 @@ class LabellingFlow extends Component { }; labellingStateFragment = () => { - const { services } = useServices(); - const Component = services.customizationService.getCustomization( - 'measurement.labellingComponent' - ); - return ( - - ); + return CustomizableRenderComponent({ + customizationId: 'measurement.labellingComponent', + items: this.currentItems, + onSelected: this.selectTreeSelectCalback, + closePopup: this.closePopup, + selectTreeFirstTitle: 'Annotation', + measurementData: this.props.measurementData, + label: this.state.label, + exclusive: this.props.exclusive, + }); }; } diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 3edbdcd4240..a252c14295e 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,4 +1,4 @@ -import { useServices } from '@ohif/ui'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; /** * A React component that renders a loading indicator. @@ -8,9 +8,8 @@ import { useServices } from '@ohif/ui'; */ function LoadingIndicatorProgress({ className, textBlock, progress }) { - const { services } = useServices(); - const Component = services.customizationService.getCustomization('ui.LoadingIndicatorProgress'); - return Component({ + return CustomizableRenderComponent({ + customizationId: 'ui.LoadingIndicatorProgress', className, textBlock, progress, diff --git a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx index 7ae25970320..458d225ccd6 100644 --- a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -1,4 +1,4 @@ -import { useServices } from '../../contextProviders'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export interface LoadingIndicatorTotalPercentProps { className?: string; @@ -19,11 +19,8 @@ function LoadingIndicatorTotalPercent({ loadingText, targetText, }: LoadingIndicatorTotalPercentProps) { - const { services } = useServices(); - const Component = services.customizationService.getCustomization( - 'ui.LoadingIndicatorTotalPercent' - ); - return Component({ + return CustomizableRenderComponent({ + customizationId: 'ui.LoadingIndicatorTotalPercent', className, totalNumbers, percentComplete, diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx index ba32b10d12c..71243371188 100644 --- a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -1,4 +1,4 @@ -import { useServices } from '@ohif/ui'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export type ProgressLoadingBarProps = { progress?: number; @@ -11,9 +11,8 @@ export type ProgressLoadingBarProps = { */ function ProgressLoadingBar({ progress }) { - const { services } = useServices(); - const Component = services.customizationService.getCustomization('ui.ProgressLoadingBar'); - return Component({ + return CustomizableRenderComponent({ + customizationId: 'ui.ProgressLoadingBar', progress, }); } diff --git a/platform/ui/src/utils/CustomizableRenderComponent.tsx b/platform/ui/src/utils/CustomizableRenderComponent.tsx new file mode 100644 index 00000000000..e3a3de53fd6 --- /dev/null +++ b/platform/ui/src/utils/CustomizableRenderComponent.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useServices } from '@ohif/ui'; + +interface ICustomizableRenderComponent { + customizationId: string; + [key: string]: any; +} + +export default function CustomizableRenderComponent(props: ICustomizableRenderComponent) { + const { customizationId, ...rest } = props; + const { services } = useServices(); + const Component = services.customizationService.getCustomization(customizationId); + return ; +} From 8dbde3810afa863fa0b8184c8ca2a92d70d1f2aa Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Tue, 28 Jan 2025 18:38:32 +0530 Subject: [PATCH 27/40] updated the doc of Customizable Render component --- .../platform/services/ui/customization-service.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md index a6baf90f447..1185b977c37 100644 --- a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -489,6 +489,17 @@ context menus. Currently it supports buttons 1-3, as well as modifier keys by associating a commands list with the button to click. See `initContextMenu` for more details. +## Customizable Render component + +The CustomizableRenderComponent renders a component based on the customizationId received through props. When the customizationId is provided, the component will attempt to fetch and render the corresponding available component. This approach helps eliminate the dependency on the customization service in UI components. + +```jsx +CustomizableRenderComponent({ + customizationId: 'customization-id', + props + }); +``` + ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. From 3c515830c90e25eb8f29089a005005628d802283 Mon Sep 17 00:00:00 2001 From: Arul Mozhi Date: Tue, 28 Jan 2025 18:40:03 +0530 Subject: [PATCH 28/40] minor update based on customizable render component --- platform/ui/src/components/ContextMenu/ContextMenu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index 903aab861c2..167ad98e3c2 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,9 +1,8 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import { useServices } from '@ohif/ui'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; const ContextMenu = ({ items, ...props }) => { - const { services } = useServices(); const contextMenuRef = useRef(null); useEffect(() => { if (!contextMenuRef?.current) { @@ -35,7 +34,8 @@ const ContextMenu = ({ items, ...props }) => { onContextMenu={e => e.preventDefault()} > {items.map((item, index) => { - return services.customizationService.getCustomization('ui.ContextMenuItem')({ + return CustomizableRenderComponent({ + customizationId: 'ui.ContextMenuItem', item, index, ...props, From 9f08a242d2e91cc5eb061b3383bf29c477bd32df Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Tue, 28 Jan 2025 19:29:31 +0530 Subject: [PATCH 29/40] removed unwanted changes from DialogProvider --- platform/ui/src/contextProviders/DialogProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index 90f408559db..3282e5c08b2 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -18,7 +18,6 @@ import classNames from 'classnames'; * we import to instantiate cornerstone */ import guid from './../../../core/src/utils/guid'; -import { CustomizationService } from '@ohif/core'; import './DialogProvider.css'; const DialogContext = createContext(null); From 8fd8eb93997d2699857de1e5b3c00fe84569ba13 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 30 Jan 2025 14:40:53 +0530 Subject: [PATCH 30/40] reverted the platoform ui modifications in components --- .../Components/ContextMenuItemComponent.tsx | 28 ------- .../default/src/Components/LabellingFlow.tsx | 42 ---------- .../Components/LoadingIndicatorProgress.tsx | 31 -------- .../LoadingIndicatorTotalPercent.tsx | 41 ---------- .../ProgressLoadingBar/ProgressLoadingBar.css | 26 ------- .../ProgressLoadingBar/ProgressLoadingBar.tsx | 29 ------- .../Components/ProgressLoadingBar/index.js | 2 - .../ViewportActionCorners.tsx | 76 ------------------- .../ViewportActionCorners/index.tsx | 4 - .../contextMenuItemCustomization.ts | 5 -- .../labellingFlowCustomization.tsx | 2 +- .../loadingIndicatorProgressCustomization.tsx | 2 +- ...dingIndicatorTotalPercentCustomization.tsx | 2 +- .../progressLoadingBarCustomization.tsx | 2 +- .../viewportActionCornersCustomization.ts | 2 +- .../default/src/getCustomizationModule.tsx | 2 - .../components/ContextMenu/ContextMenu.tsx | 57 +++++--------- .../components/Labelling/LabellingFlow.tsx | 25 +++--- .../LoadingIndicatorProgress.tsx | 31 ++++++-- .../LoadingIndicatorTotalPercent.tsx | 43 +++++++---- .../ProgressLoadingBar/ProgressLoadingBar.tsx | 26 +++++-- .../ViewportActionCorners.tsx | 59 ++++++++++++-- 22 files changed, 164 insertions(+), 373 deletions(-) delete mode 100644 extensions/default/src/Components/ContextMenuItemComponent.tsx delete mode 100644 extensions/default/src/Components/LabellingFlow.tsx delete mode 100644 extensions/default/src/Components/LoadingIndicatorProgress.tsx delete mode 100644 extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx delete mode 100644 extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css delete mode 100644 extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx delete mode 100644 extensions/default/src/Components/ProgressLoadingBar/index.js delete mode 100644 extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx delete mode 100644 extensions/default/src/Components/ViewportActionCorners/index.tsx delete mode 100644 extensions/default/src/customizations/contextMenuItemCustomization.ts diff --git a/extensions/default/src/Components/ContextMenuItemComponent.tsx b/extensions/default/src/Components/ContextMenuItemComponent.tsx deleted file mode 100644 index 12c59a3ad58..00000000000 --- a/extensions/default/src/Components/ContextMenuItemComponent.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Typography } from '@ohif/ui'; -import { Icons } from '@ohif/ui-next'; - -/** - * A React component that renders a context menu item. - */ -const ContextMenuItemComponent = ({ index, item, ...props }) => { - return ( -
item.action(item, props)} - style={{ justifyContent: 'space-between' }} - className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" - > - {item.label} - {item.iconRight && ( - - )} -
- ); -}; - -export default ContextMenuItemComponent; diff --git a/extensions/default/src/Components/LabellingFlow.tsx b/extensions/default/src/Components/LabellingFlow.tsx deleted file mode 100644 index 23671a0aea1..00000000000 --- a/extensions/default/src/Components/LabellingFlow.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { ReactElement } from 'react'; -import SelectTree from '@ohif/ui/src/components/SelectTree'; - -export type LabellingFlowProps = { - columns: number; - onSelected: () => void; - closePopup: () => void; - label: string; - measurementData: any; - items: any[]; - exclusive: boolean; - selectTreeFirstTitle: string; -}; -/** - * A React component that renders a loading progress bar. - * If progress is not provided, it will render an infinite loading bar - * If progress is provided, it will render a progress bar - * The progress text can be optionally displayed to the left of the bar. - */ -function LabellingFlow({ - items, - columns, - onSelected, - closePopup, - selectTreeFirstTitle, - exclusive, - label, -}: LabellingFlowProps): ReactElement { - return ( - - ); -} - -export default LabellingFlow; diff --git a/extensions/default/src/Components/LoadingIndicatorProgress.tsx b/extensions/default/src/Components/LoadingIndicatorProgress.tsx deleted file mode 100644 index 898c77ae0ef..00000000000 --- a/extensions/default/src/Components/LoadingIndicatorProgress.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; - -import { Icons } from '@ohif/ui-next'; -import ProgressLoadingBar from './ProgressLoadingBar'; - -/** - * A React component that renders a loading indicator. - * if progress is not provided, it will render an infinite loading indicator - * if progress is provided, it will render a progress bar - * Optionally a textBlock can be provided to display a message - */ - -function LoadingIndicatorProgress({ className, textBlock, progress }) { - return ( -
- -
- -
- {textBlock} -
- ); -} - -export default LoadingIndicatorProgress; diff --git a/extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx b/extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx deleted file mode 100644 index 60518cf5cf7..00000000000 --- a/extensions/default/src/Components/LoadingIndicatorTotalPercent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { LoadingIndicatorTotalPercentProps } from '@ohif/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent'; -import LoadingIndicatorProgress from './LoadingIndicatorProgress'; - -/** - * A React component that renders a loading indicator but accepts a totalNumbers - * and percentComplete to display a more detailed message. - */ -function LoadingIndicatorTotalPercent({ - className, - totalNumbers, - percentComplete, - loadingText, - targetText, -}: LoadingIndicatorTotalPercentProps): JSX.Element { - const progress = percentComplete; - const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; - const numTargetsLoadedText = - percentComplete !== null ? Math.floor((percentComplete * totalNumbers) / 100) : ''; - - const textBlock = - !totalNumbers && percentComplete === null ? ( -
{loadingText}
- ) : !totalNumbers && percentComplete !== null ? ( -
Loaded {percentComplete}%
- ) : ( -
- Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText} -
- ); - - return ( - - ); -} - -export default LoadingIndicatorTotalPercent; diff --git a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css deleted file mode 100644 index f9addbe9e91..00000000000 --- a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.css +++ /dev/null @@ -1,26 +0,0 @@ -.loading { - background-color: #091731; - height: 8px; - border-radius: 4px; - overflow: hidden; - position: relative; - width: 100%; -} - -.infinite-loading-bar { - animation: side2side 2s ease-in-out infinite; - height: 100%; - position: absolute; - border-radius: 4px; - width: 50%; -} - -@keyframes side2side { - 0%, - 100% { - transform: translateX(-50%); - } - 50% { - transform: translateX(150%); - } -} diff --git a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx b/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx deleted file mode 100644 index cd0674e96e2..00000000000 --- a/extensions/default/src/Components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { ReactElement } from 'react'; -import { ProgressLoadingBarProps } from '@ohif/ui/src/components/ProgressLoadingBar/ProgressLoadingBar'; -import './ProgressLoadingBar.css'; - -/** - * A React component that renders a loading progress bar. - * If progress is not provided, it will render an infinite loading bar - * If progress is provided, it will render a progress bar - * The progress text can be optionally displayed to the left of the bar. - */ -function ProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { - return ( -
- {progress === undefined || progress === null ? ( -
- ) : ( -
- )} -
- ); -} - -export default ProgressLoadingBar; diff --git a/extensions/default/src/Components/ProgressLoadingBar/index.js b/extensions/default/src/Components/ProgressLoadingBar/index.js deleted file mode 100644 index ab8869ffcc4..00000000000 --- a/extensions/default/src/Components/ProgressLoadingBar/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import ProgressLoadingBar from './ProgressLoadingBar'; -export default ProgressLoadingBar; diff --git a/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx b/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx deleted file mode 100644 index b8a2534b174..00000000000 --- a/extensions/default/src/Components/ViewportActionCorners/ViewportActionCorners.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import { Types } from '@ohif/ui'; - -export enum ViewportActionCornersLocations { - topLeft, - topRight, - bottomLeft, - bottomRight, -} - -export type ViewportActionCornersProps = { - cornerComponents: Record< - ViewportActionCornersLocations, - Array - >; -}; - -const commonClasses = 'pointer-events-auto flex items-center gap-1'; -const classes = { - [ViewportActionCornersLocations.topLeft]: classNames( - commonClasses, - 'absolute top-[4px] left-[0px] pl-[4px]' - ), - [ViewportActionCornersLocations.topRight]: classNames( - commonClasses, - 'absolute top-[4px] right-[4px] right-viewport-scrollbar' - ), - [ViewportActionCornersLocations.bottomLeft]: classNames( - commonClasses, - 'absolute bottom-[4px] left-[0px] pl-[4px]' - ), - [ViewportActionCornersLocations.bottomRight]: classNames( - commonClasses, - 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' - ), -}; - -/** - * A component that renders various action items/components to each corner of a viewport. - * The position of each corner's components is such that a single row of components are - * rendered absolutely without intersecting the ViewportOverlay component. - * Note that corner components are passed as an object mapping each corner location - * to an array of components for that location. The components in each array are - * rendered from left to right in the order that they appear in the array. - */ -function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { - if (!cornerComponents) { - return null; - } - - return ( -
{ - event.preventDefault(); - event.stopPropagation(); - }} - > - {Object.entries(cornerComponents).map(([location, locationComponents]) => { - return ( -
- {locationComponents.map(componentInfo => { - return
{componentInfo.component}
; - })} -
- ); - })} -
- ); -} - -export default ViewportActionCorners; diff --git a/extensions/default/src/Components/ViewportActionCorners/index.tsx b/extensions/default/src/Components/ViewportActionCorners/index.tsx deleted file mode 100644 index 9136db97de7..00000000000 --- a/extensions/default/src/Components/ViewportActionCorners/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners'; - -export { ViewportActionCornersLocations }; -export default ViewportActionCorners; diff --git a/extensions/default/src/customizations/contextMenuItemCustomization.ts b/extensions/default/src/customizations/contextMenuItemCustomization.ts deleted file mode 100644 index cb03600b475..00000000000 --- a/extensions/default/src/customizations/contextMenuItemCustomization.ts +++ /dev/null @@ -1,5 +0,0 @@ -import ContextMenuItemComponent from '../Components/ContextMenuItemComponent'; - -export default { - 'ui.ContextMenuItem': ContextMenuItemComponent, -}; diff --git a/extensions/default/src/customizations/labellingFlowCustomization.tsx b/extensions/default/src/customizations/labellingFlowCustomization.tsx index ff03a04af57..16af87effd5 100644 --- a/extensions/default/src/customizations/labellingFlowCustomization.tsx +++ b/extensions/default/src/customizations/labellingFlowCustomization.tsx @@ -1,4 +1,4 @@ -import LabellingFlow from '../Components/LabellingFlow'; +import { LabellingFlow } from '@ohif/ui'; export default { 'measurement.labellingComponent': LabellingFlow, diff --git a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx index a47e337e36d..aee5c260736 100644 --- a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx @@ -1,4 +1,4 @@ -import LoadingIndicatorProgress from '../Components/LoadingIndicatorProgress'; +import { LoadingIndicatorProgress } from '@ohif/ui'; export default { 'ui.LoadingIndicatorProgress': LoadingIndicatorProgress, diff --git a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx index 5a49c5a7973..4b80efd084d 100644 --- a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx @@ -1,4 +1,4 @@ -import LoadingIndicatorTotalPercent from '../Components/LoadingIndicatorTotalPercent'; +import { LoadingIndicatorTotalPercent } from '@ohif/ui'; export default { 'ui.LoadingIndicatorTotalPercent': LoadingIndicatorTotalPercent, diff --git a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx index 278524f70af..7a21fe87e84 100644 --- a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx +++ b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx @@ -1,4 +1,4 @@ -import ProgressLoadingBar from '../Components/ProgressLoadingBar'; +import { ProgressLoadingBar } from '@ohif/ui'; export default { 'ui.ProgressLoadingBar': ProgressLoadingBar, diff --git a/extensions/default/src/customizations/viewportActionCornersCustomization.ts b/extensions/default/src/customizations/viewportActionCornersCustomization.ts index 6273a05198e..e1dea5cf323 100644 --- a/extensions/default/src/customizations/viewportActionCornersCustomization.ts +++ b/extensions/default/src/customizations/viewportActionCornersCustomization.ts @@ -1,4 +1,4 @@ -import ViewportActionCorners from '../Components/ViewportActionCorners'; +import { ViewportActionCorners } from '@ohif/ui'; export default { 'ui.ViewportActionCorner': ViewportActionCorners, diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index cbd586f142a..d4b30618354 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -14,7 +14,6 @@ import onDropHandlerCustomization from './customizations/onDropHandlerCustomizat import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization'; import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; -import contextMenuItemCustomization from './customizations/contextMenuItemCustomization'; import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; import labellingFlowCustomization from './customizations/labellingFlowCustomization'; @@ -57,7 +56,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...loadingIndicatorProgressCustomization, ...loadingIndicatorTotalPercentCustomization, ...progressLoadingBarCustomization, - ...contextMenuItemCustomization, ...viewportActionCornersCustomization, ...labellingFlowCustomization, }, diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index 167ad98e3c2..a6c0fddbea3 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,55 +1,40 @@ -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import Typography from '../Typography'; +import Icon from '../Icon'; const ContextMenu = ({ items, ...props }) => { - const contextMenuRef = useRef(null); - useEffect(() => { - if (!contextMenuRef?.current) { - return; - } - - const contextMenu = contextMenuRef.current; - - const boundingClientRect = contextMenu.getBoundingClientRect(); - if (boundingClientRect.bottom + boundingClientRect.height > window.innerHeight) { - props.defaultPosition.y = props.defaultPosition.y - boundingClientRect.height; - } - if (boundingClientRect.right + boundingClientRect.width > window.innerWidth) { - props.defaultPosition.x = props.defaultPosition.x - boundingClientRect.width; - } - }, [props.defaultPosition]); - if (!items) { return null; } - return (
e.preventDefault()} > - {items.map((item, index) => { - return CustomizableRenderComponent({ - customizationId: 'ui.ContextMenuItem', - item, - index, - ...props, - }); - })} + {items.map((item, index) => ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ))}
); }; ContextMenu.propTypes = { - defaultPosition: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number, - }), items: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string.isRequired, diff --git a/platform/ui/src/components/Labelling/LabellingFlow.tsx b/platform/ui/src/components/Labelling/LabellingFlow.tsx index ef3c19f09e7..8d1dfe75f6f 100644 --- a/platform/ui/src/components/Labelling/LabellingFlow.tsx +++ b/platform/ui/src/components/Labelling/LabellingFlow.tsx @@ -1,7 +1,7 @@ +import SelectTree from '../SelectTree'; import React, { Component } from 'react'; import LabellingTransition from './LabellingTransition'; import cloneDeep from 'lodash.clonedeep'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; interface PropType { labellingDoneCallback: (label: string) => void; @@ -81,22 +81,23 @@ class LabellingFlow extends Component { }; selectTreeSelectCalback = (event, itemSelected) => { - const label = itemSelected.label || itemSelected.value; + const label = itemSelected.value; this.closePopup(); return this.props.labellingDoneCallback(label); }; labellingStateFragment = () => { - return CustomizableRenderComponent({ - customizationId: 'measurement.labellingComponent', - items: this.currentItems, - onSelected: this.selectTreeSelectCalback, - closePopup: this.closePopup, - selectTreeFirstTitle: 'Annotation', - measurementData: this.props.measurementData, - label: this.state.label, - exclusive: this.props.exclusive, - }); + return ( + + ); }; } diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index a252c14295e..8937d8c8eb2 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,4 +1,8 @@ -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import React from 'react'; +import classNames from 'classnames'; + +import Icon from '../Icon'; +import ProgressLoadingBar from '../ProgressLoadingBar'; /** * A React component that renders a loading indicator. @@ -6,13 +10,24 @@ import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent * if progress is provided, it will render a progress bar * Optionally a textBlock can be provided to display a message */ - function LoadingIndicatorProgress({ className, textBlock, progress }) { - return CustomizableRenderComponent({ - customizationId: 'ui.LoadingIndicatorProgress', - className, - textBlock, - progress, - }); + return ( +
+ +
+ +
+ {textBlock} +
+ ); } + export default LoadingIndicatorProgress; diff --git a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx index 458d225ccd6..199fb8ab6b5 100644 --- a/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx +++ b/platform/ui/src/components/LoadingIndicatorTotalPercent/LoadingIndicatorTotalPercent.tsx @@ -1,6 +1,8 @@ -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import React from 'react'; -export interface LoadingIndicatorTotalPercentProps { +import LoadingIndicatorProgress from '../LoadingIndicatorProgress'; + +interface Props { className?: string; totalNumbers: number | null; percentComplete: number | null; @@ -16,17 +18,32 @@ function LoadingIndicatorTotalPercent({ className, totalNumbers, percentComplete, - loadingText, - targetText, -}: LoadingIndicatorTotalPercentProps) { - return CustomizableRenderComponent({ - customizationId: 'ui.LoadingIndicatorTotalPercent', - className, - totalNumbers, - percentComplete, - loadingText, - targetText, - }); + loadingText = 'Loading...', + targetText = 'segments', +}: Props): JSX.Element { + const progress = percentComplete; + const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; + const numTargetsLoadedText = + percentComplete !== null ? Math.floor((percentComplete * totalNumbers) / 100) : ''; + + const textBlock = + !totalNumbers && percentComplete === null ? ( +
{loadingText}
+ ) : !totalNumbers && percentComplete !== null ? ( +
Loaded {percentComplete}%
+ ) : ( +
+ Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText} +
+ ); + + return ( + + ); } export default LoadingIndicatorTotalPercent; diff --git a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx index 71243371188..62034dc46d3 100644 --- a/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx +++ b/platform/ui/src/components/ProgressLoadingBar/ProgressLoadingBar.tsx @@ -1,4 +1,6 @@ -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; +import React, { ReactElement } from 'react'; + +import './ProgressLoadingBar.css'; export type ProgressLoadingBarProps = { progress?: number; @@ -9,12 +11,22 @@ export type ProgressLoadingBarProps = { * If progress is provided, it will render a progress bar * The progress text can be optionally displayed to the left of the bar. */ - -function ProgressLoadingBar({ progress }) { - return CustomizableRenderComponent({ - customizationId: 'ui.ProgressLoadingBar', - progress, - }); +function ProgressLoadingBar({ progress }: ProgressLoadingBarProps): ReactElement { + return ( +
+ {progress === undefined || progress === null ? ( +
+ ) : ( +
+ )} +
+ ); } export default ProgressLoadingBar; diff --git a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx index c3e52930160..48d872299e9 100644 --- a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx +++ b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx @@ -1,5 +1,7 @@ -import { useServices } from '@ohif/ui'; +import classNames from 'classnames'; +import React from 'react'; import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes'; +import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export enum ViewportActionCornersLocations { topLeft, @@ -15,6 +17,26 @@ export type ViewportActionCornersProps = { >; }; +const commonClasses = 'pointer-events-auto flex items-center gap-1'; +const classes = { + [ViewportActionCornersLocations.topLeft]: classNames( + commonClasses, + 'absolute top-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.topRight]: classNames( + commonClasses, + 'absolute top-[4px] right-[4px] right-viewport-scrollbar' + ), + [ViewportActionCornersLocations.bottomLeft]: classNames( + commonClasses, + 'absolute bottom-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.bottomRight]: classNames( + commonClasses, + 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' + ), +}; + /** * A component that renders various action items/components to each corner of a viewport. * The position of each corner's components is such that a single row of components are @@ -24,15 +46,40 @@ export type ViewportActionCornersProps = { * rendered from left to right in the order that they appear in the array. */ function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { - const { services } = useServices(); + return CustomizableRenderComponent({ + customizationId: 'ui.ViewportActionCorner', + FallbackComponent: FallbackViewportActionCorners, + cornerComponents, + }); +} + +function FallbackViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { if (!cornerComponents) { return null; } - const Component = services.customizationService.getCustomization('ui.ViewportActionCorner'); - return Component({ - cornerComponents, - }); + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + }} + > + {Object.entries(cornerComponents).map(([location, locationComponents]) => { + return ( +
+ {locationComponents.map(componentInfo => { + return
{componentInfo.component}
; + })} +
+ ); + })} +
+ ); } export default ViewportActionCorners; From 85cf0a9e20bd14d528418c9f54e1672965ab2adb Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 30 Jan 2025 14:44:04 +0530 Subject: [PATCH 31/40] reverted the ui chagnes in platform ui --- .../components/ContextMenu/ContextMenu.tsx | 59 +++++++------------ .../ViewportActionCorners.tsx | 9 --- 2 files changed, 21 insertions(+), 47 deletions(-) diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index a6c0fddbea3..b81c78dca9a 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,46 +1,29 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import Typography from '../Typography'; -import Icon from '../Icon'; +import classNames from 'classnames'; -const ContextMenu = ({ items, ...props }) => { - if (!items) { - return null; - } +import ProgressLoadingBar from '../ProgressLoadingBar'; +import { Icons } from '@ohif/ui-next'; +/** + * A React component that renders a loading indicator. + * if progress is not provided, it will render an infinite loading indicator + * if progress is provided, it will render a progress bar + * Optionally a textBlock can be provided to display a message + */ +function LoadingIndicatorProgress({ className, textBlock, progress }) { return (
e.preventDefault()} + className={classNames( + 'absolute top-0 left-0 z-50 flex flex-col items-center justify-center space-y-5', + className + )} > - {items.map((item, index) => ( -
item.action(item, props)} - style={{ justifyContent: 'space-between' }} - className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" - > - {item.label} - {item.iconRight && ( - - )} -
- ))} + +
+ +
+ {textBlock}
); -}; +} -ContextMenu.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - action: PropTypes.func.isRequired, - }) - ), -}; - -export default ContextMenu; +export default LoadingIndicatorProgress; diff --git a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx index 48d872299e9..78d5dd4dc2d 100644 --- a/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx +++ b/platform/ui/src/components/ViewportActionCorners/ViewportActionCorners.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import React from 'react'; import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes'; -import CustomizableRenderComponent from '../../utils/CustomizableRenderComponent'; export enum ViewportActionCornersLocations { topLeft, @@ -46,14 +45,6 @@ const classes = { * rendered from left to right in the order that they appear in the array. */ function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { - return CustomizableRenderComponent({ - customizationId: 'ui.ViewportActionCorner', - FallbackComponent: FallbackViewportActionCorners, - cornerComponents, - }); -} - -function FallbackViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) { if (!cornerComponents) { return null; } From 6b84abf9ea887ffa4532c731490106ff6bf1ef28 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 30 Jan 2025 14:56:46 +0530 Subject: [PATCH 32/40] update contextmenu component --- .../components/ContextMenu/ContextMenu.tsx | 84 ++++++++++++++----- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index b81c78dca9a..35474d95a98 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,29 +1,69 @@ -import React from 'react'; -import classNames from 'classnames'; - -import ProgressLoadingBar from '../ProgressLoadingBar'; +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Typography from '../Typography'; import { Icons } from '@ohif/ui-next'; -/** - * A React component that renders a loading indicator. - * if progress is not provided, it will render an infinite loading indicator - * if progress is provided, it will render a progress bar - * Optionally a textBlock can be provided to display a message - */ -function LoadingIndicatorProgress({ className, textBlock, progress }) { + +const ContextMenu = ({ items, ...props }) => { + const contextMenuRef = useRef(null); + useEffect(() => { + if (!contextMenuRef?.current) { + return; + } + + const contextMenu = contextMenuRef.current; + + const boundingClientRect = contextMenu.getBoundingClientRect(); + if (boundingClientRect.bottom + boundingClientRect.height > window.innerHeight) { + props.defaultPosition.y = props.defaultPosition.y - boundingClientRect.height; + } + if (boundingClientRect.right + boundingClientRect.width > window.innerWidth) { + props.defaultPosition.x = props.defaultPosition.x - boundingClientRect.width; + } + }, [props.defaultPosition]); + + if (!items) { + return null; + } + return (
e.preventDefault()} > - -
- -
- {textBlock} + {items.map((item, index) => ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ))}
); -} +}; + +ContextMenu.propTypes = { + defaultPosition: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + }) + ), +}; -export default LoadingIndicatorProgress; +export default ContextMenu; From 34ada91682f44017dcb70999593be68f80f9e3d7 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Thu, 30 Jan 2025 14:57:36 +0530 Subject: [PATCH 33/40] clean up oss files --- .../components/ContextMenu/ContextMenu.tsx | 84 ++++++++++++++----- .../LoadingIndicatorProgress.tsx | 8 +- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index b81c78dca9a..35474d95a98 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,29 +1,69 @@ -import React from 'react'; -import classNames from 'classnames'; - -import ProgressLoadingBar from '../ProgressLoadingBar'; +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Typography from '../Typography'; import { Icons } from '@ohif/ui-next'; -/** - * A React component that renders a loading indicator. - * if progress is not provided, it will render an infinite loading indicator - * if progress is provided, it will render a progress bar - * Optionally a textBlock can be provided to display a message - */ -function LoadingIndicatorProgress({ className, textBlock, progress }) { + +const ContextMenu = ({ items, ...props }) => { + const contextMenuRef = useRef(null); + useEffect(() => { + if (!contextMenuRef?.current) { + return; + } + + const contextMenu = contextMenuRef.current; + + const boundingClientRect = contextMenu.getBoundingClientRect(); + if (boundingClientRect.bottom + boundingClientRect.height > window.innerHeight) { + props.defaultPosition.y = props.defaultPosition.y - boundingClientRect.height; + } + if (boundingClientRect.right + boundingClientRect.width > window.innerWidth) { + props.defaultPosition.x = props.defaultPosition.x - boundingClientRect.width; + } + }, [props.defaultPosition]); + + if (!items) { + return null; + } + return (
e.preventDefault()} > - -
- -
- {textBlock} + {items.map((item, index) => ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ))}
); -} +}; + +ContextMenu.propTypes = { + defaultPosition: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + }) + ), +}; -export default LoadingIndicatorProgress; +export default ContextMenu; diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 8937d8c8eb2..b81c78dca9a 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -1,9 +1,8 @@ import React from 'react'; import classNames from 'classnames'; -import Icon from '../Icon'; import ProgressLoadingBar from '../ProgressLoadingBar'; - +import { Icons } from '@ohif/ui-next'; /** * A React component that renders a loading indicator. * if progress is not provided, it will render an infinite loading indicator @@ -18,10 +17,7 @@ function LoadingIndicatorProgress({ className, textBlock, progress }) { className )} > - +
From 79330a76b7c06f3a9ce76498c5019fe41c5ec690 Mon Sep 17 00:00:00 2001 From: Abhijith Sb <105038248+abhijith-trenser@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:36:22 +0530 Subject: [PATCH 34/40] revert additional changes in ContextMenu --- platform/ui/src/components/ContextMenu/ContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index 35474d95a98..c2bd83889a4 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -6,7 +6,7 @@ import { Icons } from '@ohif/ui-next'; const ContextMenu = ({ items, ...props }) => { const contextMenuRef = useRef(null); useEffect(() => { - if (!contextMenuRef?.current) { + if(!contextMenuRef?.current) { return; } From 88fe623385a37d43c72ec1ee000c3eaacf8dbbdb Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 31 Jan 2025 13:58:02 +0530 Subject: [PATCH 35/40] updating review comments - inital commit --- .../viewports/OHIFCornerstonePMAPViewport.tsx | 7 ++-- .../viewports/OHIFCornerstoneRTViewport.tsx | 6 +++- .../viewports/OHIFCornerstoneSEGViewport.tsx | 6 +++- .../DicomUpload/DicomUploadProgress.tsx | 6 +++- .../components/OHIFViewportActionCorners.tsx | 5 +-- .../src/Components/ItemListComponent.tsx | 7 +++- .../ContextMenuController.tsx | 9 +---- extensions/default/src/ViewerLayout/index.tsx | 8 +++-- .../contextMenuCustomization.ts | 24 ++----------- .../loadingIndicatorProgressCustomization.tsx | 2 +- ...dingIndicatorTotalPercentCustomization.tsx | 2 +- .../progressLoadingBarCustomization.tsx | 2 +- .../viewportActionCornersCustomization.ts | 2 +- .../default/src/getCustomizationModule.tsx | 2 -- .../src/DicomMicroscopyViewport.tsx | 10 ++++-- platform/app/src/routes/Local/Local.tsx | 9 +++-- platform/app/src/routes/WorkList/WorkList.tsx | 4 ++- .../sampleCustomizations.tsx | 35 ++++++++++++++----- .../services/ui/customization-service.md | 11 ------ .../src/utils/CustomizableRenderComponent.tsx | 14 -------- 20 files changed, 85 insertions(+), 86 deletions(-) delete mode 100644 platform/ui/src/utils/CustomizableRenderComponent.tsx diff --git a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx index 59a3609a4d4..2d33fe3ed3a 100644 --- a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx +++ b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { LoadingIndicatorTotalPercent } from '@ohif/ui'; import { useViewportGrid } from '@ohif/ui-next'; function OHIFCornerstonePMAPViewport(props: withAppTypes) { @@ -13,7 +12,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { extensionManager, } = props; const viewportId = viewportOptions.viewportId; - const { displaySetService, segmentationService, uiNotificationService } = + const { displaySetService, segmentationService, uiNotificationService, customizationService } = servicesManager.services; // PMAP viewport will always have a single display set @@ -21,6 +20,10 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { throw new Error('PMAP viewport must have a single display set'); } + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + const pmapDisplaySet = displaySets[0]; const [viewportGrid, viewportGridService] = useViewportGrid(); const referencedDisplaySetRef = useRef(null); diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 64b233252ba..e6b8d1ee88b 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui'; +import { ViewportActionArrows } from '@ohif/ui'; import { useViewportGrid } from '@ohif/ui-next'; import promptHydrateRT from '../utils/promptHydrateRT'; @@ -39,6 +39,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { throw new Error('RT viewport should only have a single display set'); } + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + const rtDisplaySet = displaySets[0]; const [viewportGrid, viewportGridService] = useViewportGrid(); diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 69c0d4f6f7d..311ada43800 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui'; +import { ViewportActionArrows } from '@ohif/ui'; import { useViewportGrid } from '@ohif/ui-next'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import promptHydrateSEG from '../utils/promptHydrateSEG'; @@ -31,6 +31,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { viewportActionCornersService, } = servicesManager.services; + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`; // SEG viewport will always have a single display set diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx index 2ab8b23024a..bddce98203a 100644 --- a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { Button, ProgressLoadingBar } from '@ohif/ui'; +import { Button, useServices } from '@ohif/ui'; import { Icons } from '@ohif/ui-next'; import DicomFileUploader, { EVENTS, @@ -40,6 +40,10 @@ function DicomUploadProgress({ dicomFileUploaderArr, onComplete, }: DicomUploadProgressProps): ReactElement { + const { customizationService } = useServices(); + + const ProgressLoadingBar = customizationService.getCustomization('ui.progressLoadingBar'); + const [totalUploadSize] = useState( dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0) ); diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx index 3e7380972da..513dae4fc02 100644 --- a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx +++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider'; -import { ViewportActionCorners } from '@ohif/ui'; +import { useServices } from '@ohif/ui'; export type OHIFViewportActionCornersProps = { viewportId: string; }; function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) { + const { customizationService } = useServices(); const [viewportActionCornersState] = useViewportActionCornersContext(); - + const ViewportActionCorners = customizationService.getCustomization('ui.viewportActionCorner'); if (!viewportActionCornersState[viewportId]) { return null; } diff --git a/extensions/default/src/Components/ItemListComponent.tsx b/extensions/default/src/Components/ItemListComponent.tsx index c3533684549..6c7f058bfb6 100644 --- a/extensions/default/src/Components/ItemListComponent.tsx +++ b/extensions/default/src/Components/ItemListComponent.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React, { ReactElement, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, InputFilterText, LoadingIndicatorProgress } from '@ohif/ui'; +import { Button, InputFilterText, useServices } from '@ohif/ui'; import { Icons } from '@ohif/ui-next'; import { Types } from '@ohif/core'; @@ -16,6 +16,7 @@ function ItemListComponent({ itemList, onItemClicked, }: ItemListComponentProps): ReactElement { + const { customizationService } = useServices(); const { t } = useTranslation('DataSourceConfiguration'); const [filterValue, setFilterValue] = useState(''); @@ -23,6 +24,10 @@ function ItemListComponent({ setFilterValue(''); }, [itemList]); + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + return (
diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx index c79045d110c..50303a4ba39 100644 --- a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx @@ -1,5 +1,4 @@ import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder'; -import ContextMenu from '../../../../platform/ui/src/components/ContextMenu/ContextMenu'; import { CommandsManager } from '@ohif/core'; import { annotation as CsAnnotation } from '@cornerstonejs/tools'; import { Menu, MenuItem, Point, ContextMenuProps } from './types'; @@ -72,12 +71,7 @@ export default class ContextMenuController { menuId ); - const menu = ContextMenuItemsBuilder.findMenu( - menus, - { selectorProps: selectorProps || contextMenuProps, event }, - menuId - ); - const className = menu?.className || ''; + const ContextMenu = this.services.customizationService.getCustomization('ui.contextMenu'); this.services.uiDialogService.dismiss({ id: 'context-menu' }); this.services.uiDialogService.create({ @@ -103,7 +97,6 @@ export default class ContextMenuController { menus, event, subMenu, - className, eventData: event?.detail || event, onClose: () => { diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index 64ecca5615c..a2ed2ba39cf 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { LoadingIndicatorProgress, InvestigationalUseDialog } from '@ohif/ui'; +import { InvestigationalUseDialog } from '@ohif/ui'; import { HangingProtocolService, CommandsManager } from '@ohif/core'; import { useAppConfig } from '@state'; import ViewerHeader from './ViewerHeader'; @@ -27,7 +27,7 @@ function ViewerLayout({ }: withAppTypes): React.FunctionComponent { const [appConfig] = useAppConfig(); - const { panelService, hangingProtocolService } = servicesManager.services; + const { panelService, hangingProtocolService, customizationService } = servicesManager.services; const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator); const hasPanels = useCallback( @@ -55,6 +55,10 @@ function ViewerLayout({ setRightPanelClosed ); + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + /** * Set body classes (tailwindcss) that don't allow vertical * or horizontal overflow (no scrolling). Also guarantee window diff --git a/extensions/default/src/customizations/contextMenuCustomization.ts b/extensions/default/src/customizations/contextMenuCustomization.ts index b19232b228b..4366df89c6e 100644 --- a/extensions/default/src/customizations/contextMenuCustomization.ts +++ b/extensions/default/src/customizations/contextMenuCustomization.ts @@ -1,25 +1,5 @@ -import { CustomizationService } from '@ohif/core'; +import { ContextMenu } from '@ohif/ui'; export default { - 'ohif.contextMenu': { - $transform: function (customizationService: CustomizationService) { - /** - * Applies the inheritsFrom to all the menu items. - * This function clones the object and child objects to prevent - * changes to the original customization object. - */ - // Don't modify the children, as those are copied by reference - const clonedObject = { ...this }; - clonedObject.menus = this.menus.map(menu => ({ ...menu })); - - for (const menu of clonedObject.menus) { - const { items: originalItems } = menu; - menu.items = []; - for (const item of originalItems) { - menu.items.push(customizationService.transform(item)); - } - } - return clonedObject; - }, - }, + 'ui.contextMenu': ContextMenu, }; diff --git a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx index aee5c260736..9bba0cc758c 100644 --- a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx @@ -1,5 +1,5 @@ import { LoadingIndicatorProgress } from '@ohif/ui'; export default { - 'ui.LoadingIndicatorProgress': LoadingIndicatorProgress, + 'ui.loadingIndicatorProgress': LoadingIndicatorProgress, }; diff --git a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx index 4b80efd084d..368834f6310 100644 --- a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx +++ b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx @@ -1,5 +1,5 @@ import { LoadingIndicatorTotalPercent } from '@ohif/ui'; export default { - 'ui.LoadingIndicatorTotalPercent': LoadingIndicatorTotalPercent, + 'ui.loadingIndicatorTotalPercent': LoadingIndicatorTotalPercent, }; diff --git a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx index 7a21fe87e84..d4de5097c21 100644 --- a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx +++ b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx @@ -1,5 +1,5 @@ import { ProgressLoadingBar } from '@ohif/ui'; export default { - 'ui.ProgressLoadingBar': ProgressLoadingBar, + 'ui.progressLoadingBar': ProgressLoadingBar, }; diff --git a/extensions/default/src/customizations/viewportActionCornersCustomization.ts b/extensions/default/src/customizations/viewportActionCornersCustomization.ts index e1dea5cf323..49b28bb214e 100644 --- a/extensions/default/src/customizations/viewportActionCornersCustomization.ts +++ b/extensions/default/src/customizations/viewportActionCornersCustomization.ts @@ -1,5 +1,5 @@ import { ViewportActionCorners } from '@ohif/ui'; export default { - 'ui.ViewportActionCorner': ViewportActionCorners, + 'ui.viewportActionCorner': ViewportActionCorners, }; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index d4b30618354..7bc3ee29ef7 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -15,7 +15,6 @@ import loadingIndicatorProgressCustomization from './customizations/loadingIndic import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; -import labellingFlowCustomization from './customizations/labellingFlowCustomization'; /** * @@ -57,7 +56,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...loadingIndicatorTotalPercentCustomization, ...progressLoadingBarCustomization, ...viewportActionCornersCustomization, - ...labellingFlowCustomization, }, }, ]; diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx index 2fcd0ca6643..c0c72bb1fcb 100644 --- a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { LoadingIndicatorProgress } from '@ohif/ui'; import { cleanDenaturalizedDataset } from '@ohif/extension-default'; import './DicomMicroscopyViewport.css'; @@ -8,6 +7,7 @@ import ViewportOverlay from './components/ViewportOverlay'; import getDicomWebClient from './utils/dicomWebClient'; import dcmjs from 'dcmjs'; import MicroscopyService from './services/MicroscopyService'; +import { CustomizationService } from '@ohif/core'; class DicomMicroscopyViewport extends Component { state = { @@ -16,6 +16,7 @@ class DicomMicroscopyViewport extends Component { }; microscopyService: MicroscopyService; + customizationService: CustomizationService; viewer: any = null; // dicom-microscopy-viewer instance managedViewer: any = null; // managed wrapper of microscopy-dicom extension @@ -25,8 +26,9 @@ class DicomMicroscopyViewport extends Component { constructor(props: any) { super(props); - const { microscopyService } = this.props.servicesManager.services; + const { microscopyService, customizationService } = this.props.servicesManager.services; this.microscopyService = microscopyService; + this.customizationService = customizationService; } static propTypes = { @@ -49,7 +51,6 @@ class DicomMicroscopyViewport extends Component { resizeRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]), }; - /** * Get the nearest ROI from the mouse click point * @@ -272,6 +273,9 @@ class DicomMicroscopyViewport extends Component { const style = { width: '100%', height: '100%' }; const displaySet = this.props.displaySets[0]; const firstInstance = displaySet.firstInstance || displaySet.instance; + const LoadingIndicatorProgress = this.customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); return (
{ @@ -49,10 +49,15 @@ type LocalProps = { }; function Local({ modePath }: LocalProps) { + const { customizationService } = servicesManager.services; const navigate = useNavigate(); const dropzoneRef = useRef(); const [dropInitiated, setDropInitiated] = React.useState(false); + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + // Initializing the dicom local dataSource const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE]; const localDataSources = dataSourceModules.reduce((acc, curr) => { diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 96f0676319d..a63a96ec226 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -22,7 +22,6 @@ import { useModal, AboutModal, UserPreferences, - LoadingIndicatorProgress, useSessionStorage, InvestigationalUseDialog, Button, @@ -525,6 +524,9 @@ function WorkList({ } const { customizationService } = servicesManager.services; + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); const DicomUploadComponent = customizationService.getCustomization('dicomUploadComponent'); const uploadProps = diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index 15c942722da..1652e8fe28d 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -614,7 +614,7 @@ window.config = { `, }, { - id: 'ui.LoadingIndicatorTotalPercent', + id: 'ui.loadingIndicatorTotalPercent', description: 'Customizes the LoadingIndicatorTotalPercent component.', image: loadingIndicator, default: null, @@ -623,7 +623,7 @@ window.config = { // rest of window config customizationService: [ { - 'ui.LoadingIndicatorTotalPercent': { + 'ui.loadingIndicatorTotalPercent': { $set: CustomizedComponent, }, }, @@ -632,7 +632,7 @@ window.config = { `, }, { - id: 'ui.LoadingIndicatorProgress', + id: 'ui.loadingIndicatorProgress', description: 'Customizes the LoadingIndicatorProgress component.', default: null, configuration: ` @@ -640,7 +640,7 @@ window.config = { // rest of window config customizationService: [ { - 'ui.LoadingIndicatorProgress': { + 'ui.loadingIndicatorProgress': { $set: CustomizedComponent, }, }, @@ -649,7 +649,7 @@ window.config = { `, }, { - id: 'ui.ProgressLoadingBar', + id: 'ui.progressLoadingBar', description: 'Customizes the ProgressLoadingBar component.', default: null, configuration: ` @@ -657,7 +657,7 @@ window.config = { // rest of window config customizationService: [ { - 'ui.ProgressLoadingBar': { + 'ui.progressLoadingBar': { $set: CustomizedComponent, }, }, @@ -666,15 +666,32 @@ window.config = { `, }, { - id: 'ui.ContextMenuItem', - description: 'Customizes the Context menu item component.', + id: 'ui.viewportActionCorner', + description: 'Customizes the viewportActionCorner component.', default: null, configuration: ` window.config = { // rest of window config customizationService: [ { - 'ui.ContextMenuItem': { + 'ui.viewportActionCorner': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.contextMenu', + description: 'Customizes the Context menu component.', + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.contextMenu': { $set: CustomizedComponent, }, }, diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md index 1185b977c37..a6baf90f447 100644 --- a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -489,17 +489,6 @@ context menus. Currently it supports buttons 1-3, as well as modifier keys by associating a commands list with the button to click. See `initContextMenu` for more details. -## Customizable Render component - -The CustomizableRenderComponent renders a component based on the customizationId received through props. When the customizationId is provided, the component will attempt to fetch and render the corresponding available component. This approach helps eliminate the dependency on the customization service in UI components. - -```jsx -CustomizableRenderComponent({ - customizationId: 'customization-id', - props - }); -``` - ## Please add additional customizations above this section > 3rd Party implementers may be added to this table via pull requests. diff --git a/platform/ui/src/utils/CustomizableRenderComponent.tsx b/platform/ui/src/utils/CustomizableRenderComponent.tsx deleted file mode 100644 index e3a3de53fd6..00000000000 --- a/platform/ui/src/utils/CustomizableRenderComponent.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { useServices } from '@ohif/ui'; - -interface ICustomizableRenderComponent { - customizationId: string; - [key: string]: any; -} - -export default function CustomizableRenderComponent(props: ICustomizableRenderComponent) { - const { customizationId, ...rest } = props; - const { services } = useServices(); - const Component = services.customizationService.getCustomization(customizationId); - return ; -} From 58726f0ae79da79442fb36bcfb0e2f2d95c20314 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 31 Jan 2025 16:44:11 +0530 Subject: [PATCH 36/40] implementation of SystemProvider and update usage logic --- extensions/cornerstone/src/commandsModule.ts | 27 +++++++++------- .../DicomUpload/DicomUploadProgress.tsx | 8 +++-- .../components/OHIFViewportActionCorners.tsx | 7 ++-- .../src/Components/ItemListComponent.tsx | 7 ++-- .../labellingFlowCustomization.tsx | 2 +- .../default/src/getCustomizationModule.tsx | 2 ++ .../default/src/utils/callInputDialog.tsx | 10 ++++-- .../src/utils/promptLabelAnnotation.js | 4 ++- platform/app/src/App.tsx | 7 ++-- .../src/contextProviders/SystemProvider.tsx | 32 +++++++++++++++++++ platform/core/src/index.ts | 3 ++ .../src/contextProviders/DialogProvider.tsx | 3 +- .../src/contextProviders/ServicesProvider.tsx | 18 ----------- platform/ui/src/contextProviders/index.js | 2 -- platform/ui/src/index.js | 2 -- 15 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 platform/core/src/contextProviders/SystemProvider.tsx delete mode 100644 platform/ui/src/contextProviders/ServicesProvider.tsx diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index a9d659cd193..41d4d909293 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -208,8 +208,9 @@ function commandsModule({ */ setMeasurementLabel: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); + const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then( + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow).then( (val: Map) => { measurementService.update( uid, @@ -325,16 +326,19 @@ function commandsModule({ renameMeasurement: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); + const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => { - measurementService.update( - uid, - { - ...val, - }, - true - ); - }); + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow).then( + val => { + measurementService.update( + uid, + { + ...val, + }, + true + ); + } + ); }, toggleLockMeasurement: ({ uid }) => { @@ -376,7 +380,8 @@ function commandsModule({ }, arrowTextCallback: ({ callback, data, uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig); + const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); + callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, LabellingFlow); }, toggleCine: () => { const { viewports } = viewportGridService.getState(); diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx index bddce98203a..e5d5f7915b9 100644 --- a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { Button, useServices } from '@ohif/ui'; +import { useSystem } from '@ohif/core'; +import { Button } from '@ohif/ui'; import { Icons } from '@ohif/ui-next'; import DicomFileUploader, { EVENTS, @@ -40,9 +41,10 @@ function DicomUploadProgress({ dicomFileUploaderArr, onComplete, }: DicomUploadProgressProps): ReactElement { - const { customizationService } = useServices(); + const { services } = useSystem(); - const ProgressLoadingBar = customizationService.getCustomization('ui.progressLoadingBar'); + const ProgressLoadingBar = + services.customizationService.getCustomization('ui.progressLoadingBar'); const [totalUploadSize] = useState( dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0) diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx index 513dae4fc02..a6dcea74dff 100644 --- a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx +++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider'; -import { useServices } from '@ohif/ui'; +import { useSystem } from '@ohif/core'; export type OHIFViewportActionCornersProps = { viewportId: string; }; function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) { - const { customizationService } = useServices(); + const { services } = useSystem(); const [viewportActionCornersState] = useViewportActionCornersContext(); - const ViewportActionCorners = customizationService.getCustomization('ui.viewportActionCorner'); + const ViewportActionCorners = + services.customizationService.getCustomization('ui.viewportActionCorner'); if (!viewportActionCornersState[viewportId]) { return null; } diff --git a/extensions/default/src/Components/ItemListComponent.tsx b/extensions/default/src/Components/ItemListComponent.tsx index 6c7f058bfb6..f98115ca7f5 100644 --- a/extensions/default/src/Components/ItemListComponent.tsx +++ b/extensions/default/src/Components/ItemListComponent.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; import React, { ReactElement, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, InputFilterText, useServices } from '@ohif/ui'; +import { useSystem } from '@ohif/core'; +import { Button, InputFilterText } from '@ohif/ui'; import { Icons } from '@ohif/ui-next'; import { Types } from '@ohif/core'; @@ -16,7 +17,7 @@ function ItemListComponent({ itemList, onItemClicked, }: ItemListComponentProps): ReactElement { - const { customizationService } = useServices(); + const { services } = useSystem(); const { t } = useTranslation('DataSourceConfiguration'); const [filterValue, setFilterValue] = useState(''); @@ -24,7 +25,7 @@ function ItemListComponent({ setFilterValue(''); }, [itemList]); - const LoadingIndicatorProgress = customizationService.getCustomization( + const LoadingIndicatorProgress = services.customizationService.getCustomization( 'ui.loadingIndicatorProgress' ); diff --git a/extensions/default/src/customizations/labellingFlowCustomization.tsx b/extensions/default/src/customizations/labellingFlowCustomization.tsx index 16af87effd5..00f97f8229b 100644 --- a/extensions/default/src/customizations/labellingFlowCustomization.tsx +++ b/extensions/default/src/customizations/labellingFlowCustomization.tsx @@ -1,5 +1,5 @@ import { LabellingFlow } from '@ohif/ui'; export default { - 'measurement.labellingComponent': LabellingFlow, + 'ui.labellingComponent': LabellingFlow, }; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 7bc3ee29ef7..d4b30618354 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -15,6 +15,7 @@ import loadingIndicatorProgressCustomization from './customizations/loadingIndic import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; +import labellingFlowCustomization from './customizations/labellingFlowCustomization'; /** * @@ -56,6 +57,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...loadingIndicatorTotalPercentCustomization, ...progressLoadingBarCustomization, ...viewportActionCornersCustomization, + ...labellingFlowCustomization, }, }, ]; diff --git a/extensions/default/src/utils/callInputDialog.tsx b/extensions/default/src/utils/callInputDialog.tsx index 321b308a864..c527674ecf9 100644 --- a/extensions/default/src/utils/callInputDialog.tsx +++ b/extensions/default/src/utils/callInputDialog.tsx @@ -89,7 +89,13 @@ export function callInputDialog( } } -export function callLabelAutocompleteDialog(uiDialogService, callback, dialogConfig, labelConfig) { +export function callLabelAutocompleteDialog( + uiDialogService, + callback, + dialogConfig, + labelConfig, + LabellingFlow +) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; @@ -123,7 +129,7 @@ export function callLabelAutocompleteDialog(uiDialogService, callback, dialogCon }); } -export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig) { +export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; return new Promise>((resolve, reject) => { diff --git a/extensions/default/src/utils/promptLabelAnnotation.js b/extensions/default/src/utils/promptLabelAnnotation.js index 8139da4ebfd..cb5d71fd734 100644 --- a/extensions/default/src/utils/promptLabelAnnotation.js +++ b/extensions/default/src/utils/promptLabelAnnotation.js @@ -5,11 +5,13 @@ function promptLabelAnnotation({ servicesManager }, ctx, evt) { const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt; return new Promise(async function (resolve) { const labelConfig = customizationService.getCustomization('measurementLabels'); + const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(measurementId); const value = await showLabelAnnotationPopup( measurement, servicesManager.services.uiDialogService, - labelConfig + labelConfig, + LabellingFlow ); measurementService.update( diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 093ea5f26e3..93eb870aebd 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -12,6 +12,7 @@ import { CommandsManager, HotkeysManager, ServiceProvidersManager, + SystemContextProvider, } from '@ohif/core'; import { DialogProvider, @@ -21,7 +22,6 @@ import { ViewportDialogProvider, CineProvider, UserAuthenticationProvider, - ServicesProvider, } from '@ohif/ui'; import { ThemeWrapper as ThemeWrapperNext, @@ -115,7 +115,10 @@ function App({ [I18nextProvider, { i18n }], [ThemeWrapperNext], [ThemeWrapper], - [ServicesProvider, { services: servicesManager.services }], + [ + SystemContextProvider, + { commandsManager, extensionManager, hotkeysManager, services: servicesManager.services }, + ], [ToolboxProvider], [ViewportGridProvider, { service: viewportGridService }], [ViewportDialogProvider, { service: uiViewportDialogService }], diff --git a/platform/core/src/contextProviders/SystemProvider.tsx b/platform/core/src/contextProviders/SystemProvider.tsx new file mode 100644 index 00000000000..9e84ec34562 --- /dev/null +++ b/platform/core/src/contextProviders/SystemProvider.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useContext } from 'react'; +import { CommandsManager, HotkeysManager } from '../classes'; +import { ExtensionManager } from '../extensions'; + +interface SystemContextProviderProps { + children: React.ReactNode | React.ReactNode[] | ((...args: any[]) => React.ReactNode); + services: AppTypes.Services; + commandsManager: CommandsManager; + extensionManager: ExtensionManager; + hotkeysManager: HotkeysManager; +} + +const systemContext = createContext(null); +const { Provider } = systemContext; + +export const useSystem = () => useContext(systemContext); + +export function SystemContextProvider({ + children, + services, + commandsManager, + extensionManager, + hotkeysManager, +}: SystemContextProviderProps) { + return ( + + {children} + + ); +} + +export default SystemContextProvider; diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts index 5f01c13446e..b9d8719f5d0 100644 --- a/platform/core/src/index.ts +++ b/platform/core/src/index.ts @@ -1,6 +1,7 @@ import { ExtensionManager, MODULE_TYPES } from './extensions'; import { ServiceProvidersManager, ServicesManager } from './services'; import classes, { CommandsManager, HotkeysManager } from './classes'; +import { SystemContextProvider, useSystem } from './contextProviders/SystemProvider'; import DICOMWeb from './DICOMWeb'; import errorHandler from './errorHandler.js'; @@ -99,6 +100,7 @@ export { HotkeysManager, ServicesManager, ServiceProvidersManager, + SystemContextProvider, // defaults, utils, @@ -134,6 +136,7 @@ export { PanelService, WorkflowStepsService, StudyPrefetcherService, + useSystem, useToolbar, useActiveViewportDisplaySets, }; diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index 3282e5c08b2..ecadd2bdb30 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -18,13 +18,14 @@ import classNames from 'classnames'; * we import to instantiate cornerstone */ import guid from './../../../core/src/utils/guid'; + import './DialogProvider.css'; const DialogContext = createContext(null); export const useDialog = () => useContext(DialogContext); -const DialogProvider = ({ children, service }) => { +const DialogProvider = ({ children, service = null }) => { const [isDragging, setIsDragging] = useState(false); const [dialogs, setDialogs] = useState([]); const [lastDialogId, setLastDialogId] = useState(null); diff --git a/platform/ui/src/contextProviders/ServicesProvider.tsx b/platform/ui/src/contextProviders/ServicesProvider.tsx deleted file mode 100644 index 5f3fa86ec0e..00000000000 --- a/platform/ui/src/contextProviders/ServicesProvider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import PropTypes from 'prop-types'; - -const servicesManagerContext = createContext(null); -const { Provider } = servicesManagerContext; - -export const useServices = () => useContext(servicesManagerContext); - -export function ServicesProvider({ children, services }) { - return {children}; -} - -ServicesProvider.propTypes = { - children: PropTypes.any, - services: PropTypes.any, -}; - -export default ServicesProvider; diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index 8288530a0a3..95a72df0801 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -16,8 +16,6 @@ export { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './Vi export { useToolbox, ToolboxProvider } from './Toolbox/ToolboxContext'; -export { useServices, ServicesProvider } from './ServicesProvider'; - export { UserAuthenticationContext, UserAuthenticationProvider, diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 9dcb53995aa..2efd0594d3a 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -10,7 +10,6 @@ export { ModalProvider, ModalConsumer, useModal, - useServices, withModal, ImageViewerContext, ImageViewerProvider, @@ -28,7 +27,6 @@ export { useUserAuthentication, useToolbox, ToolboxProvider, - ServicesProvider, } from './contextProviders'; /** COMPONENTS */ From a4583608bc0b25fc6b9043e31ee1a5f0ad32337c Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 31 Jan 2025 16:50:19 +0530 Subject: [PATCH 37/40] update document and added image --- .../src/CustomizableContextMenu/types.ts | 1 - platform/docs/docs/assets/img/context-menu.jpg | Bin 0 -> 10420 bytes .../sampleCustomizations.tsx | 6 ++++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 platform/docs/docs/assets/img/context-menu.jpg diff --git a/extensions/default/src/CustomizableContextMenu/types.ts b/extensions/default/src/CustomizableContextMenu/types.ts index 512c3fef5f6..e86d1a82483 100644 --- a/extensions/default/src/CustomizableContextMenu/types.ts +++ b/extensions/default/src/CustomizableContextMenu/types.ts @@ -98,7 +98,6 @@ export interface Menu { selector?: Types.Predicate; items: MenuItem[]; - className: string; } export type Point = { diff --git a/platform/docs/docs/assets/img/context-menu.jpg b/platform/docs/docs/assets/img/context-menu.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4fef53fa9640621ae8f030842d482b80c17b1c4f GIT binary patch literal 10420 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

vS2ZMfe6r~~iX zPCPRcOVm*-Nq20mF)mny`hFj=75c9;l;AgQX1FBQI`1p*Vk;*c9-)_{?OkARW8rfDHiJ9G+MP?XBMFM5wknUp3 zFfS%o2`#=o%~j2_86nyJ1k<@Npa5E#!$GqBB2XmR^QtLqENrSbFJz&MdX}u*Z4+7u z7Qx~(6T}|IP);>+mhlqpG6)=%dA-c_e%C-WGM?>8=Q@hFG755O1kdUZmf&CcMq39q zyHo}pGmSdbdRD5`9B6B!eCc&0LyX?asA)}>Kad4Z3yqaYRlO&rKU!OJ)?hDh~Ou(@(<6NJvL$Bz7yQ>tBCx#--QTT1AHckJV6KlX#fvgg4JJYjL6X9r33~!Bs%ZAp2a<-hzm~k}$5?#*;!#f~?69I>yEx z{8i$)hhGPxbXEGWt2DN8_R&jNXe80LoYkgnfM>O97A9yWfEwmvqq)ZnQnV94Ph=r^bMP&{;`yR9p zBhnUD6Nav{8Bg>kYZS7kWod=!{1c&r?4fSEqA0Vy70i>7LNJ&6sL${T0>j!}R-v+I zM&D<@U>_Gz#$Pb-B(8t7e|^m*(R3`u{0{=|ZzArWcbjwb!1Ih33aIp7Kj~#Df;8HBdl3zS%rTQ;Y?{{Cmw-Pi+W*UMg zpD{B|C!*_G<}~^l!91R@=Z1+^;*V~5f;wiDX>TIDc8@6#B|?l~+WWycn#Sh}XvS7a zdifO|IpAuYDehslMv>giY#;ApjKV1P=opO!Q_;#h0e zPg{j|G#=jp6D7KgAOZ~tGouC+ApXN`KvadlSpE1(pl()mA7_DVlMt{-*H(00&UES6 z!%|a!a=(IMEsE8Qa82*$^5$9P>6`nh;Xa>Fpt~LHW$dT(lBEvou{vggbdmCzn3o4( zyfC7jQ3W?D=E0-FE!51<-iCo5C<)xvoAd>vJZ^!|WB+0l1=4;5MYu^IQN`#-?wg5^ zt`tgY5=1^vS5kdR!PW%{wjCXKBVsM58=}`H;9_h^ z1afk=-2wh7z~R?_1W96d|L*AYcNrMBGo2ZJ&277}KxD}h9|2i!B@b+(>0HD+g%LK@ zo_)99^F~W37pG8@hJf1Iat7(zbW#PyYNjP~^``Z1vZYWH8$>L^7{Tul>>t*~Wv8DU z6J#!jF5TzcCrWkyYj67ZyMNywbOm(h*!j{KZLrLV32^fVx Date: Fri, 31 Jan 2025 19:20:38 +0530 Subject: [PATCH 38/40] improved optimization in code --- extensions/cornerstone/src/commandsModule.ts | 12 ++-- .../DicomUpload/DicomUploadProgress.tsx | 4 +- .../components/OHIFViewportActionCorners.tsx | 4 +- .../src/Components/ItemListComponent.tsx | 4 +- .../default/src/utils/callInputDialog.tsx | 10 ++-- .../src/utils/promptLabelAnnotation.js | 4 +- platform/app/src/App.tsx | 2 +- .../src/contextProviders/SystemProvider.tsx | 7 ++- .../assets/img/loading-indicator-icon.png | Bin 0 -> 2164 bytes .../assets/img/loading-indicator-percent.png | Bin 0 -> 3023 bytes .../assets/img/viewport-action-corners.png | Bin 0 -> 6867 bytes .../sampleCustomizations.tsx | 56 ++++++++++-------- 12 files changed, 55 insertions(+), 48 deletions(-) create mode 100644 platform/docs/docs/assets/img/loading-indicator-icon.png create mode 100644 platform/docs/docs/assets/img/loading-indicator-percent.png create mode 100644 platform/docs/docs/assets/img/viewport-action-corners.png diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 41d4d909293..ff0cc2894ad 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -208,9 +208,9 @@ function commandsModule({ */ setMeasurementLabel: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); + const labelSelector = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow).then( + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector).then( (val: Map) => { measurementService.update( uid, @@ -326,9 +326,9 @@ function commandsModule({ renameMeasurement: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); + const labelSelector = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow).then( + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector).then( val => { measurementService.update( uid, @@ -380,8 +380,8 @@ function commandsModule({ }, arrowTextCallback: ({ callback, data, uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); - callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, LabellingFlow); + const labelSelector = customizationService.getCustomization('ui.labellingComponent'); + callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, labelSelector); }, toggleCine: () => { const { viewports } = viewportGridService.getState(); diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx index e5d5f7915b9..7f58172f99a 100644 --- a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -41,10 +41,10 @@ function DicomUploadProgress({ dicomFileUploaderArr, onComplete, }: DicomUploadProgressProps): ReactElement { - const { services } = useSystem(); + const { servicesManager } = useSystem(); const ProgressLoadingBar = - services.customizationService.getCustomization('ui.progressLoadingBar'); + servicesManager.services.customizationService.getCustomization('ui.progressLoadingBar'); const [totalUploadSize] = useState( dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0) diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx index a6dcea74dff..041dfa5912d 100644 --- a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx +++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx @@ -7,10 +7,10 @@ export type OHIFViewportActionCornersProps = { }; function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) { - const { services } = useSystem(); + const { servicesManager } = useSystem(); const [viewportActionCornersState] = useViewportActionCornersContext(); const ViewportActionCorners = - services.customizationService.getCustomization('ui.viewportActionCorner'); + servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner'); if (!viewportActionCornersState[viewportId]) { return null; } diff --git a/extensions/default/src/Components/ItemListComponent.tsx b/extensions/default/src/Components/ItemListComponent.tsx index f98115ca7f5..796ad7659e7 100644 --- a/extensions/default/src/Components/ItemListComponent.tsx +++ b/extensions/default/src/Components/ItemListComponent.tsx @@ -17,7 +17,7 @@ function ItemListComponent({ itemList, onItemClicked, }: ItemListComponentProps): ReactElement { - const { services } = useSystem(); + const { servicesManager } = useSystem(); const { t } = useTranslation('DataSourceConfiguration'); const [filterValue, setFilterValue] = useState(''); @@ -25,7 +25,7 @@ function ItemListComponent({ setFilterValue(''); }, [itemList]); - const LoadingIndicatorProgress = services.customizationService.getCustomization( + const LoadingIndicatorProgress = servicesManager.services.customizationService.getCustomization( 'ui.loadingIndicatorProgress' ); diff --git a/extensions/default/src/utils/callInputDialog.tsx b/extensions/default/src/utils/callInputDialog.tsx index c527674ecf9..15519e0745f 100644 --- a/extensions/default/src/utils/callInputDialog.tsx +++ b/extensions/default/src/utils/callInputDialog.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui'; +import { Input, Dialog, ButtonEnums } from '@ohif/ui'; /** * @@ -94,7 +94,7 @@ export function callLabelAutocompleteDialog( callback, dialogConfig, labelConfig, - LabellingFlow + labelSelector ) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; @@ -118,7 +118,7 @@ export function callLabelAutocompleteDialog( centralize: true, isDraggable: false, showOverlay: true, - content: LabellingFlow, + content: labelSelector, contentProps: { labellingDoneCallback: labellingDoneCallback, measurementData: { label: '' }, @@ -129,7 +129,7 @@ export function callLabelAutocompleteDialog( }); } -export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, LabellingFlow) { +export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; return new Promise>((resolve, reject) => { @@ -145,7 +145,7 @@ export function showLabelAnnotationPopup(measurement, uiDialogService, labelConf id: 'select-annotation', isDraggable: false, showOverlay: true, - content: LabellingFlow, + content: labelSelector, defaultPosition: { x: window.innerWidth / 2, y: window.innerHeight / 2, diff --git a/extensions/default/src/utils/promptLabelAnnotation.js b/extensions/default/src/utils/promptLabelAnnotation.js index cb5d71fd734..fa0b195cc86 100644 --- a/extensions/default/src/utils/promptLabelAnnotation.js +++ b/extensions/default/src/utils/promptLabelAnnotation.js @@ -5,13 +5,13 @@ function promptLabelAnnotation({ servicesManager }, ctx, evt) { const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt; return new Promise(async function (resolve) { const labelConfig = customizationService.getCustomization('measurementLabels'); - const LabellingFlow = customizationService.getCustomization('ui.labellingComponent'); + const labelSelector = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(measurementId); const value = await showLabelAnnotationPopup( measurement, servicesManager.services.uiDialogService, labelConfig, - LabellingFlow + labelSelector ); measurementService.update( diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 93eb870aebd..802a3c3ab2b 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -117,7 +117,7 @@ function App({ [ThemeWrapper], [ SystemContextProvider, - { commandsManager, extensionManager, hotkeysManager, services: servicesManager.services }, + { commandsManager, extensionManager, hotkeysManager, services: servicesManager }, ], [ToolboxProvider], [ViewportGridProvider, { service: viewportGridService }], diff --git a/platform/core/src/contextProviders/SystemProvider.tsx b/platform/core/src/contextProviders/SystemProvider.tsx index 9e84ec34562..6f168608066 100644 --- a/platform/core/src/contextProviders/SystemProvider.tsx +++ b/platform/core/src/contextProviders/SystemProvider.tsx @@ -1,10 +1,11 @@ import React, { createContext, useContext } from 'react'; import { CommandsManager, HotkeysManager } from '../classes'; import { ExtensionManager } from '../extensions'; +import { ServicesManager } from '../services'; interface SystemContextProviderProps { children: React.ReactNode | React.ReactNode[] | ((...args: any[]) => React.ReactNode); - services: AppTypes.Services; + servicesManager: ServicesManager; commandsManager: CommandsManager; extensionManager: ExtensionManager; hotkeysManager: HotkeysManager; @@ -17,13 +18,13 @@ export const useSystem = () => useContext(systemContext); export function SystemContextProvider({ children, - services, + servicesManager, commandsManager, extensionManager, hotkeysManager, }: SystemContextProviderProps) { return ( - + {children} ); diff --git a/platform/docs/docs/assets/img/loading-indicator-icon.png b/platform/docs/docs/assets/img/loading-indicator-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..934d03aed0fcdc8a311698d5ae52b20a1a0410bf GIT binary patch literal 2164 zcmd6pdr;C@6vrvIrH^2w<&UX=RK5j5r(FMba z&}n*q*0CKyaWL5Y6Q9SFuER(Y3^sF?7shQ539k_Q9D%DBKI*~JD%S`LR%I`0@t`l2 z`8a)gji1kbcQDGG()j%CTk|Oxv-#9QOQ-*;3~FsVl$7p9=p*HZ$|@CuvlUvAvl3`g+W66L~J(%(j*{ zT)Ei5z(A0TEQ#xHFh?Re+77aY)M-T(78Mn}E{VoL5hZ0MkX0qmFl|`D?Pdt^ly&(m zmZbAxTmLq0{Lng>^x6 zvQqgJDGZ^sd>U=tMQ?9!FN-`ZPxCOczg$&SPTsxM_>-G@^%rcWY~Sqx z338ANAOX^(R{fa~`M94)L`1}3dofG3jFy_3`WGI?=!y0CL}ktXMBCuf_u8kwT*G_{ zO}ljIk|@QJu>`d!rd+uldK{>Jlv<%bO>o(XJC1QuZ!u(%6TtFa@CA-8%CJBWhm#%z zyDeLA#FtSOvA2K9-{9zIo{^CuXsi8oI7~A>ev{AVli%i!toI-i)%h;MFIbGh@wZtM z^RxZ2Sgb;oj6fiuj+e8$JqF3&yy6%N2NqC8z=VYW*qfi(Wobd#$+mn8RL7J^Nly(g*4zCT&(uTdegj}wMrTCxZdTl~5Q(-F zcxH7N$17;pTqYAI-kR~xcOlUT1PUdXou%lwe7RJ4jUzb_K&8K+IwN-;NDKn)b9);U zrE!|Wot3J+ok0)xqeSCpxK$5`la^(~Ie|DZv}0pqbyH08Vzg$AQ{ee*q>-;%rPFE9 z)h&H51S`_yZJR&D_P2#W0udZIvOcGRS3m&?Z~8ryqj8OUap5wms+~k8m5P8U@S})o z(i6Qt8Ud;gyl+q>HH-PTM9Tw&#$1lpxWGxL`ZMCQGK}&|lZwlP&ViZiTJUU4!MY9L zTH6{h=Bgy7o5s<4&g+_9Ox704@bZttgTaZ~_6UI~)6@okp2K1lg0*d*V-kSqWVJYC zk(*KhmKKvyqFfoRq(mjM=Lz!i_DxKD*cXCl3?a12Jw)OI;)B{BkEh+Oo{0wvmrmF+l=X%5%_6kuN4 Ld@*eIgFpWPAR!^4 literal 0 HcmV?d00001 diff --git a/platform/docs/docs/assets/img/loading-indicator-percent.png b/platform/docs/docs/assets/img/loading-indicator-percent.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f466b2113c70a898f807ca37409ca68808d549 GIT binary patch literal 3023 zcmeH}dpHw(8^_n1PIx3Ga@d|I3y-Eej$9Q!*1`n8UlS_rLeQuIs(tzurGSzx#LJ*ZsS$-*tbl`}eyOFQMQ+ zNvlW$0Dzwmb~a7`z%H<4|M&jAlGV0>;vzA-@J?`RKxMbuti+KDuyU{h0O%x{P0SvN zf8dtgRXhNo5cZv2e1A|R03hRsu(5K9M9=dalU4iGn-)iVH;3Z^sz<1%)#3IhA8mI` zD`YtH>}}+%ZmJSpKEXn zxM!N1&moPQFS)TIp0I$fwlajdmehoVr>doulpHt3Pba(RDAtEg-OsBJf96WdEPzI| zr>a9Ihv>dblj)8PIkWfQJr?kouBalI9*DSB;=v?zb#WW`e00=g8r*NBHhAe%5~RK4 zd;(WCvs51?dqH3lAl80ELPY#%+D&dk40_yHaHZm!;+E(0V7&pc_pCe^pmXSl=nUH0SP2QlXhF6K zD_{CQV=3$)W4(deqjRw@!tNcO{l+{td!U6&F~{u2y}o2qv4 zZjpjFs@~)D24*r3%c>t6NlUP7Q9E`lXN53{ZQQCA3|E1rKUiboz-{^EyGrN>GlkLJ zUEZd-t8c-3OY!jxO^~{JN&%CH6AaO#uyB~Uxf(QrIkC1l!A*_HIrv@BuY^gkV__KS zs1{c>cY!dORIe8TXT3TE?wmu(O?`^r@x|V#97%($*1m}OF`>Q-+nF?y=Bq6a?o5McpZXUE?Bv!`F5lW$rzri&2{(a)y>Ep#NMdY+df?05CbxM`$IaV$cA&Cq8AkF?IW zx!yVETI{QDwvVA1@XI@I&Vf7j`S^bnE8zZ149YWD%N@Jcd&aPnEWOdJepREofGqTS z51gm)!lW~0E_7l;v7R_9Q;%D*6kaW8AaZ7zP*EF)bkwiUkyx&SY%(sHsl2>YVW{di z-wJB;3|hc*zI<50Z7kA?LNA_+xcYi~Bm_aJB=!BZ5R8)qX0P_oWrlrMNH`vXs@&R()Acm7Bsgi0$fHc{kG5Xw}*AU1InS+8m!ViB46|R?QZ#aleorP!4 z58sP@NcVYfj{CyN_USiZE#zG@4d_i=_0T9Vn92n~!PMi%GdcFKIOlu?tpeS=Tu+Xu zoSZ$2r3K8Ky<5K6Tz`%2XJ7S>>iw$8;2kxXj2vz)uStndNj{U(UYu9u+t%71sRgo- z^POek*Q_C+*IL)J@`yb{>eSWSvJRT7h}Qe+_!jM+j}w6#iq1M2$Qt0#&ZiE4{rrm= z0;0XQO3h2{qZi2o`vt(Z2ksE;Ta4G2jQQI})+^-L>rfO%N`T3x9!Vm*tqKN4MnJL= z2?C&Fe+bQGF~#Cr?n#t!ir<=r3i+%uOn8s;A&4vLtOf?m6aKuwW5vB?oyIhh3K&Jy z&#SMLb>M>ZWJRby$@!%V$>#$kl>70$IkgR-EQ)=rSRJM%vMSMmnU9 zr-Inch~8_L!@Y)f zb1TWs%>2l+EH1{yXJdTOVa|3Yn=Ci?a8+I>CXnzu&!Z+})5(82A3CEL=Qc?1>OQ;} zVv|rt2zhE5!!;8api5$vRZ_lsZRV0Q@d;_;qGwqSy{8%7=@G2V67Lom+l^it4!hC7 zb{ht`H23jahu+dYN72T%b1W|Fqm|{vG4exxQQ>|0_wAJBR3QxX-(QolUeFayN;- zRi;!S`@Ic`UMA^-3&tdyUfD8da$KuN^ND@ET8i@pEy>%D5EQdGE&bRN4|n_jJPcds z0msF1b?N;wYO7V?0C5bxbhNBDsXDmzaVn4j0F>XdR=Z>0BKZjZ&mC)xo!wU+fa)1~ S&PtLY9DuMz*;HD4|M@SPQHRd} literal 0 HcmV?d00001 diff --git a/platform/docs/docs/assets/img/viewport-action-corners.png b/platform/docs/docs/assets/img/viewport-action-corners.png new file mode 100644 index 0000000000000000000000000000000000000000..887bfe0290fa28442c20c898b47af556ce3c6fa6 GIT binary patch literal 6867 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ⓈQ zUxj_it&rNzqo<`dv0Y+CD+*Bdt2|LCj-v-U_s{z;X+J21US&zC<2|C*B6uxO7kl~!=*TfXZ7dFOc&pACHvkF3=ME5)|a_ga~g8+8APu9x|A2cmI z(KynF+pe8FMK=bt$d4umg@+PGXS7%;2zl_E5bs1P5viaE0|gWhL-7+nuXUT zgAkGa=^>U!;e`TXTKD&j$@)azI!>(JWdF0bK^8Z7A@;CXZc@UeV=^@+`LTm)-e~Tq zhuS>N$$m5B7QHnBlVL6m>T2!Aqi5QET^y{PXyFa60%)0zSlTCRu=Hf?~#7Oq@+v6E{*1-x#y|S*n zGB{`l-WS&E=Sge94yvL@$dXU5I#WqURZ{dWVYPRNB9eggga$!|{86siv*25f*!_^p z=rf6V`itTz^oP*u*+AWj{j&uO=$)o9{&wHN*o z=y_Jlc)Y1}kjVaEIt5A+$Oa<5UV`V?${`cclAR6cqrF{Ep%sF}vHi1;%3h#2&g@-=6K{M!I!MZY#jGsN^780z2RAJI3ELC;?_)2U~qK zaf9_9hI4~lcSD_hH?tB#*oFGyL$xk@J&9h|jvO6qZI;071aR%>0ps0;Z*_Gz&^)03H?mrm- z?l0I*$;7+KSIr7r^MLF{K;!p&y8T2XMW&s5Yp5`_$SGHHt84bT^{@%Q8|4BhuI2LAR2gTHk z2|O5gaTB+1LZ?3t-Qz<9n&sNF@KzwT^KqrhfN3*<3~#lf)gj?YKja7hn_s>g3ZTgV zy?b1`vX?Z;x+!~e{4I`A-{k%F(ms2WA(K9>d$RY-_j^;r1{LRod9lYyES0-hCn7UO za$i#*Kb``cw9Brok(K_uhZV@i?a=Xy+TsR1`q@NF?G0q*n-*hJetUMTK+fjKvvOSx zCHmqwm=OVoOlpMWh!~#AOn?~xOsRP;lM7;Iap_-{Cz|z0iy)8@VV1d`8iiA?>2P|r zB27agr@UH`*pRX{XMS^n#QlBhwrcHr_jVja`Xh=M?5mVLEaS_Ono8V|rnRYuE`}76 z@h%k89}bdTEpXLU&m{?H)`-~j;E!hMwzCJj<`JUtTx`-c?CvpD?Ym7B_#Y4}vs|pC zBU}604jP*DW3~$ns-wg1OeHfRrVv34fN{$hZ`P}O+@LGoF5E=}EgNm{a+pG|C~>DZ z-7dpXR)$j6H|w(p`J~L;L6w*u1%frggq96pJpKqWkZG`N+0CDI*t8R!C%cinuF1xW zO^2rSx{oJIc1i%!?BqHDs0vIym?KN4gK#?opDN(C#K?_tFirDzXo*ejk!L{cqNSYZ zY834(RQe(;!*`0^mgwyzvFRQLN5olaELRRiFW}22Z~{ZvD=z!~>(Rkj$uAjKN-MuM z{*0SLKr~qkt>0Gx4QXsbkH7Ugpg`#}rab3Z9B+EuM;mDtp8!nKncxos$CWyjD1V0b zCHh$d+AFMUsP}eVBJp)`X7Tfx1vr&?gCL@wa>FV^{+?NM^;00V0jOu==~RE;cInUCaFbwGPmn1hL zKQ#JfqXt}AIk2FDZ~8yY8!_4W&I&z*kCAZ5B2W(JqeFF?pkGA{i~fv%OCEimd-jSg ziU(37Cav)~au?x$UOw(ox_bsWA6)&%TGjAnVZFs6%J`4`ILao$`53J+vA8dI-VW5!CJ; zQ(_pde$(*Qrz|Nb_&+o5ES}v;zqO23dv6yN`%&%r`frmxp|RiZ-(x+OG;#<3pDLBv z25+s1KS{NlyJ{#dA=pA|k+tE4p!xrlP;yt=H3?5%h2Y;3M5KPBu9Zn;9_^REEH^E0 h6ZGr!!n;ed2zJu?+j&V-SA{4NO*LIr*i+lE{{#6H#!UbK literal 0 HcmV?d00001 diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index 3d741bc3d1f..0a46a4b4a57 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -11,7 +11,10 @@ import segmentationShowAddSegmentImage from '../../../assets/img/segmentationSho import layoutSelectorCommonPresetsImage from '../../../assets/img/layoutSelectorCommonPresetsImage.png'; import layoutSelectorAdvancedPresetGeneratorImage from '../../../assets/img/layoutSelectorAdvancedPresetGeneratorImage.png'; import labellingFLow from '../../../assets/img/labelling-flow.png'; -import loadingIndicator from '../../../assets/img/Loading-Indicator.png'; +import progressLoading from '../../../assets/img/Loading-Indicator.png'; +import loadingIndicatorProgress from '../../../assets/img/loading-indicator-icon.png'; +import loadingIndicatorPercent from '../../../assets/img/loading-indicator-percent.png'; +import viewportActionCorners from '../../../assets/img/viewport-action-corners.png'; import contextMenu from '../../../assets/img/context-menu.jpg'; import segDisplayEditingTrue from '../../../assets/img/segDisplayEditingTrue.png'; @@ -617,8 +620,8 @@ window.config = { { id: 'ui.loadingIndicatorTotalPercent', description: 'Customizes the LoadingIndicatorTotalPercent component.', - image: loadingIndicator, - default: null, + image: loadingIndicatorPercent, + default: null, //use platform/ui component as default configuration: ` window.config = { // rest of window config @@ -635,7 +638,8 @@ window.config = { { id: 'ui.loadingIndicatorProgress', description: 'Customizes the LoadingIndicatorProgress component.', - default: null, + image: loadingIndicatorProgress, + default: null, //use platform/ui component as default configuration: ` window.config = { // rest of window config @@ -652,7 +656,8 @@ window.config = { { id: 'ui.progressLoadingBar', description: 'Customizes the ProgressLoadingBar component.', - default: null, + image: progressLoading, + default: null, //use platform/ui component as default configuration: ` window.config = { // rest of window config @@ -669,7 +674,8 @@ window.config = { { id: 'ui.viewportActionCorner', description: 'Customizes the viewportActionCorner component.', - default: null, + iamge: viewportActionCorners, + default: null, //use platform/ui component as default configuration: ` window.config = { // rest of window config @@ -687,7 +693,7 @@ window.config = { id: 'ui.contextMenu', description: 'Customizes the Context menu component.', image: contextMenu, - default: null, + default: null, //use platform/ui component as default configuration: ` window.config = { // rest of window config @@ -701,6 +707,24 @@ window.config = { }; `, }, + { + id: 'ui.labellingComponent', + description: 'Customizes the labelling flow component.', + image: labellingFLow, + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.labellingComponent': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, ]; export const segmentationCustomizations = [ @@ -1059,24 +1083,6 @@ window.config = { }; `, }, - { - id: 'ui.labellingComponent', - description: 'Customizes the labelling flow component.', - image: labellingFLow, - default: null, - configuration: ` - window.config = { - // rest of window config - customizationService: [ - { - 'ui.labellingComponent': { - $set: CustomizedComponent, - }, - }, - ], - }; - `, - }, ]; export const studyBrowserCustomizations = [ From 425a635cd283babe3ea38bbbb1c5d133b31d6574 Mon Sep 17 00:00:00 2001 From: Abhijith Sb <105038248+abhijith-trenser@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:14:08 +0530 Subject: [PATCH 39/40] reverting unwanted modification in DialogProvider --- platform/ui/src/contextProviders/DialogProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform/ui/src/contextProviders/DialogProvider.tsx b/platform/ui/src/contextProviders/DialogProvider.tsx index ecadd2bdb30..647816220f5 100644 --- a/platform/ui/src/contextProviders/DialogProvider.tsx +++ b/platform/ui/src/contextProviders/DialogProvider.tsx @@ -316,7 +316,9 @@ export const withDialog = Component => { DialogProvider.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node, PropTypes.func]) .isRequired, - service: PropTypes.shape({ setServiceImplementation: PropTypes.func }), + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), }; export default DialogProvider; From 89ed9c5b2febf3592e736b871dc74a40b8adca56 Mon Sep 17 00:00:00 2001 From: Abhijith SB Date: Fri, 31 Jan 2025 22:15:07 +0530 Subject: [PATCH 40/40] updating review comments --- extensions/cornerstone/src/commandsModule.ts | 12 +++++----- .../contextMenuCustomization.ts | 24 +++++++++++++++++-- .../contextMenuUICustomization.ts | 5 ++++ .../default/src/getCustomizationModule.tsx | 2 ++ .../default/src/utils/callInputDialog.tsx | 8 +++---- .../src/utils/promptLabelAnnotation.js | 4 ++-- platform/app/src/routes/Local/Local.tsx | 5 ++-- 7 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 extensions/default/src/customizations/contextMenuUICustomization.ts diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index ff0cc2894ad..de3bf342ff2 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -208,9 +208,9 @@ function commandsModule({ */ setMeasurementLabel: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const labelSelector = customizationService.getCustomization('ui.labellingComponent'); + const renderContent = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector).then( + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then( (val: Map) => { measurementService.update( uid, @@ -326,9 +326,9 @@ function commandsModule({ renameMeasurement: ({ uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const labelSelector = customizationService.getCustomization('ui.labellingComponent'); + const renderContent = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector).then( + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent).then( val => { measurementService.update( uid, @@ -380,8 +380,8 @@ function commandsModule({ }, arrowTextCallback: ({ callback, data, uid }) => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const labelSelector = customizationService.getCustomization('ui.labellingComponent'); - callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, labelSelector); + const renderContent = customizationService.getCustomization('ui.labellingComponent'); + callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig, renderContent); }, toggleCine: () => { const { viewports } = viewportGridService.getState(); diff --git a/extensions/default/src/customizations/contextMenuCustomization.ts b/extensions/default/src/customizations/contextMenuCustomization.ts index 4366df89c6e..b19232b228b 100644 --- a/extensions/default/src/customizations/contextMenuCustomization.ts +++ b/extensions/default/src/customizations/contextMenuCustomization.ts @@ -1,5 +1,25 @@ -import { ContextMenu } from '@ohif/ui'; +import { CustomizationService } from '@ohif/core'; export default { - 'ui.contextMenu': ContextMenu, + 'ohif.contextMenu': { + $transform: function (customizationService: CustomizationService) { + /** + * Applies the inheritsFrom to all the menu items. + * This function clones the object and child objects to prevent + * changes to the original customization object. + */ + // Don't modify the children, as those are copied by reference + const clonedObject = { ...this }; + clonedObject.menus = this.menus.map(menu => ({ ...menu })); + + for (const menu of clonedObject.menus) { + const { items: originalItems } = menu; + menu.items = []; + for (const item of originalItems) { + menu.items.push(customizationService.transform(item)); + } + } + return clonedObject; + }, + }, }; diff --git a/extensions/default/src/customizations/contextMenuUICustomization.ts b/extensions/default/src/customizations/contextMenuUICustomization.ts new file mode 100644 index 00000000000..4366df89c6e --- /dev/null +++ b/extensions/default/src/customizations/contextMenuUICustomization.ts @@ -0,0 +1,5 @@ +import { ContextMenu } from '@ohif/ui'; + +export default { + 'ui.contextMenu': ContextMenu, +}; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index d4b30618354..d0ba441fb0f 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -6,6 +6,7 @@ import customRoutesCustomization from './customizations/customRoutesCustomizatio import studyBrowserCustomization from './customizations/studyBrowserCustomization'; import overlayItemCustomization from './customizations/overlayItemCustomization'; import contextMenuCustomization from './customizations/contextMenuCustomization'; +import contextMenuUICustomization from './customizations/contextMenuUICustomization'; import menuContentCustomization from './customizations/menuContentCustomization'; import getDataSourceConfigurationCustomization from './customizations/dataSourceConfigurationCustomization'; import progressDropdownCustomization from './customizations/progressDropdownCustomization'; @@ -58,6 +59,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag ...progressLoadingBarCustomization, ...viewportActionCornersCustomization, ...labellingFlowCustomization, + ...contextMenuUICustomization, }, }, ]; diff --git a/extensions/default/src/utils/callInputDialog.tsx b/extensions/default/src/utils/callInputDialog.tsx index 15519e0745f..b7e74a2bf45 100644 --- a/extensions/default/src/utils/callInputDialog.tsx +++ b/extensions/default/src/utils/callInputDialog.tsx @@ -94,7 +94,7 @@ export function callLabelAutocompleteDialog( callback, dialogConfig, labelConfig, - labelSelector + renderContent ) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; @@ -118,7 +118,7 @@ export function callLabelAutocompleteDialog( centralize: true, isDraggable: false, showOverlay: true, - content: labelSelector, + content: renderContent, contentProps: { labellingDoneCallback: labellingDoneCallback, measurementData: { label: '' }, @@ -129,7 +129,7 @@ export function callLabelAutocompleteDialog( }); } -export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, labelSelector) { +export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig, renderContent) { const exclusive = labelConfig ? labelConfig.exclusive : false; const dropDownItems = labelConfig ? labelConfig.items : []; return new Promise>((resolve, reject) => { @@ -145,7 +145,7 @@ export function showLabelAnnotationPopup(measurement, uiDialogService, labelConf id: 'select-annotation', isDraggable: false, showOverlay: true, - content: labelSelector, + content: renderContent, defaultPosition: { x: window.innerWidth / 2, y: window.innerHeight / 2, diff --git a/extensions/default/src/utils/promptLabelAnnotation.js b/extensions/default/src/utils/promptLabelAnnotation.js index fa0b195cc86..28a39c87f82 100644 --- a/extensions/default/src/utils/promptLabelAnnotation.js +++ b/extensions/default/src/utils/promptLabelAnnotation.js @@ -5,13 +5,13 @@ function promptLabelAnnotation({ servicesManager }, ctx, evt) { const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt; return new Promise(async function (resolve) { const labelConfig = customizationService.getCustomization('measurementLabels'); - const labelSelector = customizationService.getCustomization('ui.labellingComponent'); + const renderContent = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(measurementId); const value = await showLabelAnnotationPopup( measurement, servicesManager.services.uiDialogService, labelConfig, - labelSelector + renderContent ); measurementService.update( diff --git a/platform/app/src/routes/Local/Local.tsx b/platform/app/src/routes/Local/Local.tsx index 65722281427..7f7806e5185 100644 --- a/platform/app/src/routes/Local/Local.tsx +++ b/platform/app/src/routes/Local/Local.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useRef } from 'react'; import classnames from 'classnames'; import { useNavigate } from 'react-router-dom'; -import { DicomMetadataStore, MODULE_TYPES } from '@ohif/core'; +import { DicomMetadataStore, MODULE_TYPES, useSystem } from '@ohif/core'; import Dropzone from 'react-dropzone'; import filesToStudies from './filesToStudies'; -import { extensionManager, servicesManager } from '../../App'; +import { extensionManager } from '../../App'; import { Button } from '@ohif/ui'; import { Icons } from '@ohif/ui-next'; @@ -49,6 +49,7 @@ type LocalProps = { }; function Local({ modePath }: LocalProps) { + const { servicesManager } = useSystem(); const { customizationService } = servicesManager.services; const navigate = useNavigate(); const dropzoneRef = useRef();