feat(PanelService): Left or right side panel to auto start as closed but auto open when needed (#3212)
* feat(SidePanel) OHIF issue #3135 - Added a method to set a callback for a Panel to invoke when it is ready to be shown (e.g. its data is loaded). - Implemented such methods for both the segmentation and measurement panels. - The SidePanel component now adds a callback to Panel components so that it will automatically open a Panel that was initially closed and yet to be opened. - Updated the OHIF documentation accordingly. * PR feedback - added a PanelService that centralized much of the logic that existed in panel PanelModule - the SidePanel subscribes to PanelService.EVENTS.ACTIVATE_PANEL for each of its child panels * Removed the PanelMeasurementTableTracking setMeasurementPanelContentReadyCallback method. * Made the forceActive flag in the PanelService optional and defaulted it to false. * Fixed failing top level exports unit test. * - PanelService subscriptions are now per panel (id) so subscribers do not necessarily need to check the panel id in the event when it is fired - PanelService activate panel trigger subscriptions are now returned so that they can be (better) managed outside of the service - updated/created the various documentation for panels and PanelService * Clarified various documentation. Moved the code to add the activate panel triggers out of the extensions and into the longitudinal mode. * Removed the openWhenPanelActivated flag. PanelService now conforms to extending PubSubService like the other services. Updated various documentation. * Fixed failing e2e, mpr test. * Renamed the ActivatePanelTriggers type properties. The ExtensionManager now sets the id of various modules as a property on each of those modules.
This commit is contained in:
parent
e4e62e9e14
commit
5670a4d8d1
@ -1,6 +1,8 @@
|
|||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
|
|
||||||
@ -39,13 +41,13 @@ const extension = {
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
configuration = {},
|
configuration = {},
|
||||||
}) => {},
|
}) => {},
|
||||||
/**
|
/**
|
||||||
* PanelModule should provide a list of panels that will be available in OHIF
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
* for Modes to consume and render. Each panel is defined by a {name,
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||||
*/
|
*/
|
||||||
getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => {
|
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
|
||||||
const wrappedPanelSegmentation = () => {
|
const wrappedPanelSegmentation = () => {
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { SegmentationGroupTable } from '@ohif/ui';
|
|||||||
import callInputDialog from './callInputDialog';
|
import callInputDialog from './callInputDialog';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import callColorPickerDialog from './callColorPickerDialog';
|
|
||||||
|
|
||||||
export default function PanelSegmentation({
|
export default function PanelSegmentation({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
|||||||
@ -158,6 +158,7 @@ function ViewerLayout({
|
|||||||
const { content, entry } = getComponent(id);
|
const { content, entry } = getComponent(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: entry.id,
|
||||||
iconName: entry.iconName,
|
iconName: entry.iconName,
|
||||||
iconLabel: entry.iconLabel,
|
iconLabel: entry.iconLabel,
|
||||||
label: entry.label,
|
label: entry.label,
|
||||||
@ -227,6 +228,7 @@ function ViewerLayout({
|
|||||||
side="left"
|
side="left"
|
||||||
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
||||||
tabs={leftPanelComponents}
|
tabs={leftPanelComponents}
|
||||||
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : null}
|
) : null}
|
||||||
@ -248,6 +250,7 @@ function ViewerLayout({
|
|||||||
side="right"
|
side="right"
|
||||||
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
||||||
tabs={rightPanelComponents}
|
tabs={rightPanelComponents}
|
||||||
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
PanelMeasurementTableTracking,
|
PanelMeasurementTableTracking,
|
||||||
PanelStudyBrowserTracking,
|
PanelStudyBrowserTracking,
|
||||||
@ -11,7 +12,7 @@ function getPanelModule({
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}) {
|
}): Types.Panel[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'seriesList',
|
name: 'seriesList',
|
||||||
@ -24,6 +25,7 @@ function getPanelModule({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'trackedMeasurements',
|
name: 'trackedMeasurements',
|
||||||
iconName: 'tab-linear',
|
iconName: 'tab-linear',
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const extensionDependencies = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function modeFactory() {
|
function modeFactory() {
|
||||||
|
let _activatePanelTriggersSubscriptions = [];
|
||||||
return {
|
return {
|
||||||
// TODO: We're using this as a route segment
|
// TODO: We're using this as a route segment
|
||||||
// We should not be.
|
// We should not be.
|
||||||
@ -71,6 +72,8 @@ function modeFactory() {
|
|||||||
measurementService,
|
measurementService,
|
||||||
toolbarService,
|
toolbarService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
|
panelService,
|
||||||
|
segmentationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
measurementService.clearMeasurements();
|
measurementService.clearMeasurements();
|
||||||
@ -121,6 +124,28 @@ function modeFactory() {
|
|||||||
'Crosshairs',
|
'Crosshairs',
|
||||||
'MoreTools',
|
'MoreTools',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// // ActivatePanel event trigger for when a segmentation or measurement is added.
|
||||||
|
// // Do not force activation so as to respect the state the user may have left the UI in.
|
||||||
|
// _activatePanelTriggersSubscriptions = [
|
||||||
|
// ...panelService.addActivatePanelTriggers(dicomSeg.panel, [
|
||||||
|
// {
|
||||||
|
// sourcePubSubService: segmentationService,
|
||||||
|
// sourceEvents: [
|
||||||
|
// segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ]),
|
||||||
|
// ...panelService.addActivatePanelTriggers(tracked.measurements, [
|
||||||
|
// {
|
||||||
|
// sourcePubSubService: measurementService,
|
||||||
|
// sourceEvents: [
|
||||||
|
// measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||||
|
// measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ]),
|
||||||
|
// ];
|
||||||
},
|
},
|
||||||
onModeExit: ({ servicesManager }) => {
|
onModeExit: ({ servicesManager }) => {
|
||||||
const {
|
const {
|
||||||
@ -131,6 +156,9 @@ function modeFactory() {
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
_activatePanelTriggersSubscriptions = [];
|
||||||
|
|
||||||
toolbarService.reset();
|
toolbarService.reset();
|
||||||
toolGroupService.destroy();
|
toolGroupService.destroy();
|
||||||
syncGroupService.destroy();
|
syncGroupService.destroy();
|
||||||
@ -162,7 +190,7 @@ function modeFactory() {
|
|||||||
props: {
|
props: {
|
||||||
leftPanels: [tracked.thumbnailList],
|
leftPanels: [tracked.thumbnailList],
|
||||||
rightPanels: [dicomSeg.panel, tracked.measurements],
|
rightPanels: [dicomSeg.panel, tracked.measurements],
|
||||||
// rightPanelDefaultClosed: true, // optional prop to start with collapse panels
|
rightPanelDefaultClosed: true,
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
namespace: tracked.viewport,
|
namespace: tracked.viewport,
|
||||||
|
|||||||
@ -274,9 +274,9 @@ export default class ExtensionManager {
|
|||||||
// Default for most extension points,
|
// Default for most extension points,
|
||||||
// Just adds each entry ready for consumption by mode.
|
// Just adds each entry ready for consumption by mode.
|
||||||
extensionModule.forEach(element => {
|
extensionModule.forEach(element => {
|
||||||
this.modulesMap[
|
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||||
`${extensionId}.${moduleType}.${element.name}`
|
element.id = id;
|
||||||
] = element;
|
this.modulesMap[id] = element;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -40,6 +40,7 @@ describe('Top level exports', () => {
|
|||||||
'DicomMetadataStore',
|
'DicomMetadataStore',
|
||||||
'pubSubServiceInterface',
|
'pubSubServiceInterface',
|
||||||
'PubSubService',
|
'PubSubService',
|
||||||
|
'PanelService',
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(OHIF).sort();
|
const exports = Object.keys(OHIF).sort();
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
PubSubService,
|
PubSubService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
|
PanelService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
@ -74,6 +75,7 @@ const OHIF = {
|
|||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
PubSubService,
|
PubSubService,
|
||||||
|
PanelService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -112,6 +114,7 @@ export {
|
|||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
PubSubService,
|
PubSubService,
|
||||||
Types,
|
Types,
|
||||||
|
PanelService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OHIF };
|
export { OHIF };
|
||||||
|
|||||||
61
platform/core/src/services/PanelService/PanelService.ts
Normal file
61
platform/core/src/services/PanelService/PanelService.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ActivatePanelTriggers } from '../../types';
|
||||||
|
import { Subscription } from '../../types/IPubSub';
|
||||||
|
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||||
|
|
||||||
|
export const EVENTS = {
|
||||||
|
ACTIVATE_PANEL: 'event::panelService:activatePanel',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PanelService extends PubSubService {
|
||||||
|
public static REGISTRATION = {
|
||||||
|
name: 'panelService',
|
||||||
|
create: (): PanelService => {
|
||||||
|
return new PanelService();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(EVENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the panel with the given id. If the forceActive flag is false
|
||||||
|
* then it is up to the component containing the panel whether to activate
|
||||||
|
* it immediately or not. For instance, the panel might not be activated when
|
||||||
|
* the forceActive flag is false in the case where the user might have
|
||||||
|
* activated/displayed and then closed the panel already.
|
||||||
|
* Note that this method simply fires a broadcast event: ActivatePanelEvent.
|
||||||
|
* @param panelId the panel's id
|
||||||
|
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||||
|
*/
|
||||||
|
activatePanel(panelId: string, forceActive = false): void {
|
||||||
|
this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by
|
||||||
|
* activatePanelTrigger.sourcePubSubService that
|
||||||
|
* when fired/broadcasted must in turn activate the panel with the given id.
|
||||||
|
* The subscriptions created are returned such that they can be managed and unsubscribed
|
||||||
|
* as appropriate.
|
||||||
|
* @param panelId the id of the panel to activate
|
||||||
|
* @param activatePanelTriggers an array of triggers
|
||||||
|
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||||
|
* @returns an array of the subscriptions subscribed to
|
||||||
|
*/
|
||||||
|
addActivatePanelTriggers(
|
||||||
|
panelId: string,
|
||||||
|
activatePanelTriggers: ActivatePanelTriggers[],
|
||||||
|
forceActive = false
|
||||||
|
): Subscription[] {
|
||||||
|
return activatePanelTriggers
|
||||||
|
.map(trigger =>
|
||||||
|
trigger.sourceEvents.map(eventName =>
|
||||||
|
trigger.sourcePubSubService.subscribe(eventName, () =>
|
||||||
|
this.activatePanel(panelId, forceActive)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
platform/core/src/services/PanelService/index.ts
Normal file
3
platform/core/src/services/PanelService/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import PanelService from './PanelService';
|
||||||
|
|
||||||
|
export default PanelService;
|
||||||
@ -17,6 +17,7 @@ import UserAuthenticationService from './UserAuthenticationService';
|
|||||||
import CustomizationService from './CustomizationService';
|
import CustomizationService from './CustomizationService';
|
||||||
|
|
||||||
import Services from '../types/Services';
|
import Services from '../types/Services';
|
||||||
|
import PanelService from './PanelService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Services,
|
Services,
|
||||||
@ -36,4 +37,5 @@ export {
|
|||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
PubSubService,
|
PubSubService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
|
PanelService,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import Consumer from './Consumer';
|
import Consumer from './Consumer';
|
||||||
|
|
||||||
|
|
||||||
export default interface IPubSub {
|
export default interface IPubSub {
|
||||||
subscribe: (eventName: string, callback: Consumer) => void;
|
subscribe: (eventName: string, callback: Consumer) => void;
|
||||||
_broadcastEvent: (
|
_broadcastEvent: (
|
||||||
@ -10,3 +9,7 @@ export default interface IPubSub {
|
|||||||
_unsubscribe: (eventName: string, listenerId: string) => void;
|
_unsubscribe: (eventName: string, listenerId: string) => void;
|
||||||
_isValidEvent: (eventName: string) => boolean;
|
_isValidEvent: (eventName: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Subscription = {
|
||||||
|
unsubscribe: () => void;
|
||||||
|
};
|
||||||
|
|||||||
30
platform/core/src/types/PanelModule.ts
Normal file
30
platform/core/src/types/PanelModule.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { PubSubService } from "../services";
|
||||||
|
|
||||||
|
type Panel = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
iconName: string;
|
||||||
|
iconLabel: string;
|
||||||
|
label: string;
|
||||||
|
component: React.FC;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActivatePanelTriggers = {
|
||||||
|
sourcePubSubService: PubSubService;
|
||||||
|
sourceEvents: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PanelEvent {
|
||||||
|
panelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivatePanelEvent extends PanelEvent {
|
||||||
|
forceActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ActivatePanelEvent,
|
||||||
|
ActivatePanelTriggers,
|
||||||
|
Panel,
|
||||||
|
PanelEvent,
|
||||||
|
};
|
||||||
@ -27,4 +27,5 @@ export default interface Services {
|
|||||||
syncGroupService?: Record<string, unknown>;
|
syncGroupService?: Record<string, unknown>;
|
||||||
cornerstoneCacheService?: Record<string, unknown>;
|
cornerstoneCacheService?: Record<string, unknown>;
|
||||||
segmentationService?: Record<string, unknown>;
|
segmentationService?: Record<string, unknown>;
|
||||||
|
panelService?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ export * from './AppConfig';
|
|||||||
export * from './Consumer';
|
export * from './Consumer';
|
||||||
export * from './Command';
|
export * from './Command';
|
||||||
export * from './StudyMetadata';
|
export * from './StudyMetadata';
|
||||||
|
export * from './PanelModule';
|
||||||
|
export * from './IPubSub';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the types used within the various services and managers, but
|
* Export the types used within the various services and managers, but
|
||||||
|
|||||||
@ -25,6 +25,10 @@ optionally provide its own `context` value.
|
|||||||
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
||||||
associated `ServicesManager` and `CommandsManager`.
|
associated `ServicesManager` and `CommandsManager`.
|
||||||
|
|
||||||
|
An extension can also trigger to activate/open a panel via the `PanelService` -
|
||||||
|
either by explicitly calling `PanelService.activatePanel` or triggering panel
|
||||||
|
activation when some other event fires.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import PanelMeasurementTable from './PanelMeasurementTable.js';
|
import PanelMeasurementTable from './PanelMeasurementTable.js';
|
||||||
|
|
||||||
@ -64,7 +68,15 @@ providing the component with its.
|
|||||||
|
|
||||||
New: You can easily add multiple panels to the left/right side of the viewer
|
New: You can easily add multiple panels to the left/right side of the viewer
|
||||||
using the mode configuration. As seen below, the `leftPanels` and `rightPanels`
|
using the mode configuration. As seen below, the `leftPanels` and `rightPanels`
|
||||||
accept an `Array` of the `IDs`.
|
accept an `Array` of the `IDs`. The mode configuration also allows for either (or
|
||||||
|
both) side panels to be closed by default. In the code below, the right panel
|
||||||
|
is closed by default. The mode can optionally add event triggers to
|
||||||
|
the `PanelService` that when fired will cause a side panel that was defaulted
|
||||||
|
closed to open. In the code below, the right side panel, that contains the
|
||||||
|
`trackedMeasurements` panel, is triggered to open when a measurement is added.
|
||||||
|
Note that once a default closed side panel has been opened once,
|
||||||
|
only a `PanelService.EVENTS.ACTIVATE_PANEL` event with `forceActive === true`
|
||||||
|
will cause it open (again).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
||||||
@ -79,6 +91,7 @@ const id = 'viewer'
|
|||||||
const version = '3.0.0
|
const version = '3.0.0
|
||||||
|
|
||||||
function modeFactory({ modeConfiguration }) {
|
function modeFactory({ modeConfiguration }) {
|
||||||
|
let _activatePanelTriggersSubscriptions = [];
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
routes: [
|
routes: [
|
||||||
@ -94,12 +107,35 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
rightPanels: [
|
rightPanels: [
|
||||||
'@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
|
'@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
|
||||||
],
|
],
|
||||||
|
rightPanelDefaultClosed: true,
|
||||||
viewports,
|
viewports,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
onModeEnter: ({ servicesManager }) => {
|
||||||
|
const {
|
||||||
|
measurementService,
|
||||||
|
panelService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
_activatePanelTriggersSubscriptions = [
|
||||||
|
...panelService.addActivatePanelTriggers('@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', [
|
||||||
|
{
|
||||||
|
sourcePubSubService: measurementService,
|
||||||
|
sourceEvents: [
|
||||||
|
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||||
|
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onModeExit: () => {
|
||||||
|
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
_activatePanelTriggersSubscriptions = [];
|
||||||
|
},
|
||||||
extensions: extensionDependencies
|
extensions: extensionDependencies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
49
platform/docs/docs/platform/services/data/PanelService.md
Normal file
49
platform/docs/docs/platform/services/data/PanelService.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 8
|
||||||
|
sidebar_label: Panel Service
|
||||||
|
---
|
||||||
|
|
||||||
|
# Panel Service
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Panel Service provides for activating/showing a panel that was registered
|
||||||
|
via the `getPanelModule` extension method. Such panels can be either explicitly
|
||||||
|
activated or implicitly triggered to activate when some other event occurs.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
The following events are published in `PanelService`.
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
| --------------------- | ------------------------------------------------------ |
|
||||||
|
| ACTIVATE__PANEL | Fires a `ActivatePanelEvent` when a particular panel should be activated (i.e. shown). |
|
||||||
|
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Panel Activation
|
||||||
|
|
||||||
|
- `activatePanel`: Fires the `ACTIVATE_PANEL` event for a particular panel (id).
|
||||||
|
An optional `forceActive` flag can be passed that when `true` "forces" a
|
||||||
|
panel to show. Ultimately, it is up to a panel's container whether it
|
||||||
|
is appropriate to activate/show the panel. For instance, if the user opened and then
|
||||||
|
closed a side panel that contains the panel to activate, that side panel
|
||||||
|
may decide that the user knows best and will not open the panel (again).
|
||||||
|
|
||||||
|
- `addActivatePanelTriggers`: Creates and returns event subscriptions that when
|
||||||
|
fired will activate the specified panel with an optional `forceActive` flag
|
||||||
|
(see `activatePanel`). This allows for panel activation to be directly triggered
|
||||||
|
by some other event(s). When the triggers are no longer needed, simply
|
||||||
|
unsubscribe to the returned subscriptions. For example, a panel
|
||||||
|
for tracking measurements might get activated every time the
|
||||||
|
`MeasurementService` fires a `MEASUREMENT_ADDED` event like this:
|
||||||
|
```js
|
||||||
|
panelService.addActivatePanelTriggers('measurement-tracking-panel-id', [
|
||||||
|
sourcePubSubService: measurementService,
|
||||||
|
sourceEvents: [
|
||||||
|
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||||
|
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
```
|
||||||
@ -20,6 +20,7 @@ We maintain the following non-ui Services:
|
|||||||
- [Toolbar Service](../data/ToolBarService.md)
|
- [Toolbar Service](../data/ToolBarService.md)
|
||||||
- [Measurement Service](../data/MeasurementService.md)
|
- [Measurement Service](../data/MeasurementService.md)
|
||||||
- [Customization Service](customization-service.md)
|
- [Customization Service](customization-service.md)
|
||||||
|
- [Panel Service](../data/PanelService.md)
|
||||||
|
|
||||||
## Service Architecture
|
## Service Architecture
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SwiperCore, {
|
import SwiperCore, {
|
||||||
A11y,
|
A11y,
|
||||||
@ -11,6 +11,8 @@ import SwiperCore, {
|
|||||||
} from 'swiper';
|
} from 'swiper';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
|
||||||
|
import { PanelService, ServicesManager, Types } from '@ohif/core';
|
||||||
|
|
||||||
import { Button, Icon, IconButton, Tooltip } from '../';
|
import { Button, Icon, IconButton, Tooltip } from '../';
|
||||||
|
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
@ -67,15 +69,23 @@ const position = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SidePanel = ({
|
const SidePanel = ({
|
||||||
|
servicesManager,
|
||||||
side,
|
side,
|
||||||
className,
|
className,
|
||||||
activeTabIndex: activeTabIndexProp,
|
activeTabIndex: activeTabIndexProp,
|
||||||
tabs,
|
tabs,
|
||||||
}) => {
|
}) => {
|
||||||
|
const panelService: PanelService = servicesManager.services.panelService;
|
||||||
|
|
||||||
const { t } = useTranslation('SidePanel');
|
const { t } = useTranslation('SidePanel');
|
||||||
|
|
||||||
|
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||||
|
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||||
|
const [hasBeenOpened, setHasBeenOpened] = useState(
|
||||||
|
activeTabIndexProp !== null
|
||||||
|
);
|
||||||
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp || 0);
|
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
|
||||||
const swiperRef = useRef() as any;
|
const swiperRef = useRef() as any;
|
||||||
const [swiper, setSwiper] = useState<any>();
|
const [swiper, setSwiper] = useState<any>();
|
||||||
|
|
||||||
@ -102,6 +112,41 @@ const SidePanel = ({
|
|||||||
}
|
}
|
||||||
}, [swiper]);
|
}, [swiper]);
|
||||||
|
|
||||||
|
const updatePanelOpen = useCallback((panelOpen: boolean) => {
|
||||||
|
setPanelOpen(panelOpen);
|
||||||
|
if (panelOpen) {
|
||||||
|
setHasBeenOpened(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateActiveTabIndex = useCallback(
|
||||||
|
(activeTabIndex: number) => {
|
||||||
|
setActiveTabIndex(activeTabIndex);
|
||||||
|
updatePanelOpen(true);
|
||||||
|
},
|
||||||
|
[updatePanelOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activatePanelSubscription = panelService.subscribe(
|
||||||
|
panelService.EVENTS.ACTIVATE_PANEL,
|
||||||
|
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||||
|
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||||
|
const tabIndex = tabs.findIndex(
|
||||||
|
tab => tab.id === activatePanelEvent.panelId
|
||||||
|
);
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
updateActiveTabIndex(tabIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
activatePanelSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [tabs, hasBeenOpened, panelService, updateActiveTabIndex]);
|
||||||
|
|
||||||
const getCloseStateComponent = () => {
|
const getCloseStateComponent = () => {
|
||||||
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
||||||
return (
|
return (
|
||||||
@ -112,7 +157,7 @@ const SidePanel = ({
|
|||||||
side === 'left' ? 'pr-2 justify-end' : 'pl-2 justify-start'
|
side === 'left' ? 'pr-2 justify-end' : 'pl-2 justify-start'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPanelOpen(prev => !prev);
|
updatePanelOpen(prev => !prev);
|
||||||
}}
|
}}
|
||||||
data-cy={`side-panel-header-${side}`}
|
data-cy={`side-panel-header-${side}`}
|
||||||
>
|
>
|
||||||
@ -142,8 +187,7 @@ const SidePanel = ({
|
|||||||
size="initial"
|
size="initial"
|
||||||
className="text-primary-active"
|
className="text-primary-active"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTabIndex(index);
|
updateActiveTabIndex(index);
|
||||||
setPanelOpen(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@ -180,7 +224,7 @@ const SidePanel = ({
|
|||||||
tabs.length === 1 && 'mb-1'
|
tabs.length === 1 && 'mb-1'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPanelOpen(prev => !prev);
|
updatePanelOpen(prev => !prev);
|
||||||
// slideToActivePanel();
|
// slideToActivePanel();
|
||||||
}}
|
}}
|
||||||
data-cy={`side-panel-header-${side}`}
|
data-cy={`side-panel-header-${side}`}
|
||||||
@ -215,8 +259,7 @@ const SidePanel = ({
|
|||||||
nextRef,
|
nextRef,
|
||||||
tabs,
|
tabs,
|
||||||
activeTabIndex,
|
activeTabIndex,
|
||||||
setActiveTabIndex,
|
updateActiveTabIndex
|
||||||
setPanelOpen
|
|
||||||
)}
|
)}
|
||||||
{/** carousel navigation with the arrows */}
|
{/** carousel navigation with the arrows */}
|
||||||
{/** only show carousel nav if tabs are more than 3 tabs */}
|
{/** only show carousel nav if tabs are more than 3 tabs */}
|
||||||
@ -250,6 +293,7 @@ SidePanel.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SidePanel.propTypes = {
|
SidePanel.propTypes = {
|
||||||
|
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||||
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
activeTabIndex: PropTypes.number,
|
activeTabIndex: PropTypes.number,
|
||||||
@ -273,8 +317,7 @@ function _getMoreThanOneTabLayout(
|
|||||||
nextRef: React.MutableRefObject<undefined>,
|
nextRef: React.MutableRefObject<undefined>,
|
||||||
tabs: any,
|
tabs: any,
|
||||||
activeTabIndex: any,
|
activeTabIndex: any,
|
||||||
setActiveTabIndex: React.Dispatch<any>,
|
updateActiveTabIndex
|
||||||
setPanelOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -309,8 +352,7 @@ function _getMoreThanOneTabLayout(
|
|||||||
)}
|
)}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTabIndex(index);
|
updateActiveTabIndex(index);
|
||||||
setPanelOpen(true);
|
|
||||||
}}
|
}}
|
||||||
data-cy={`${obj.name}-btn`}
|
data-cy={`${obj.name}-btn`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -27,9 +27,10 @@ describe('OHIF MPR', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render correctly the MPR', () => {
|
it('should render correctly the MPR', () => {
|
||||||
cy.wait(1000);
|
cy.wait(250);
|
||||||
|
|
||||||
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
|
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
|
||||||
|
cy.wait(250);
|
||||||
cy.get('[data-cy="MPR"]').click();
|
cy.get('[data-cy="MPR"]').click();
|
||||||
|
|
||||||
cy.get('[data-cy="thumbnail-viewport-labels"]').should('have.length', 3);
|
cy.get('[data-cy="thumbnail-viewport-labels"]').should('have.length', 3);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
|
PanelService,
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
HangingProtocolService.REGISTRATION,
|
HangingProtocolService.REGISTRATION,
|
||||||
CineService.REGISTRATION,
|
CineService.REGISTRATION,
|
||||||
UserAuthenticationService.REGISTRATION,
|
UserAuthenticationService.REGISTRATION,
|
||||||
|
PanelService.REGISTRATION,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
errorHandler.getHTTPErrorHandler = () => {
|
errorHandler.getHTTPErrorHandler = () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user