feat(customization): new customization service api (#4688)
@ -18,7 +18,8 @@
|
||||
},
|
||||
"rules": {
|
||||
// Enforce consistent brace style for all control statements for readability
|
||||
"curly": "error"
|
||||
"curly": "error",
|
||||
"import/no-anonymous-default-export": "off"
|
||||
},
|
||||
"globals": {
|
||||
"cy": true,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
@ -118,12 +118,11 @@ const commandsModule = (props: withAppTypes) => {
|
||||
throw new Error('Invalid report, no content');
|
||||
}
|
||||
|
||||
const onBeforeDicomStore =
|
||||
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
|
||||
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
|
||||
|
||||
let dicomDict;
|
||||
if (typeof onBeforeDicomStore === 'function') {
|
||||
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
|
||||
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
|
||||
@ -151,7 +150,7 @@ const commandsModule = (props: withAppTypes) => {
|
||||
*/
|
||||
loadSRMeasurements: ({ displaySetInstanceUID }) => {
|
||||
const { SeriesInstanceUIDs } = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
{ servicesManager, extensionManager, commandsManager },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
|
||||
@ -263,9 +263,9 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
let measurement = unloadedMeasurements[j];
|
||||
|
||||
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
|
||||
const onBeforeSRAddMeasurement = customizationService.getCustomization(
|
||||
'onBeforeSRAddMeasurement'
|
||||
)?.value;
|
||||
);
|
||||
|
||||
if (typeof onBeforeSRAddMeasurement === 'function') {
|
||||
measurement = onBeforeSRAddMeasurement({
|
||||
|
||||
@ -41,22 +41,14 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager }: withAppTypes,
|
||||
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||
|
||||
const codingValues = customizationService.getCustomization('codingValues', {});
|
||||
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelMeasurement.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
const codingValues = customizationService.getCustomization('codingValues');
|
||||
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
@ -107,8 +99,7 @@ export default function hydrateStructuredReport(
|
||||
metaData
|
||||
);
|
||||
|
||||
const onBeforeSRHydration =
|
||||
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
|
||||
const onBeforeSRHydration = customizationService.getCustomization('onBeforeSRHydration')?.value;
|
||||
|
||||
if (typeof onBeforeSRHydration === 'function') {
|
||||
storedMeasurementByAnnotationType = onBeforeSRHydration({
|
||||
@ -230,8 +221,7 @@ export default function hydrateStructuredReport(
|
||||
});
|
||||
|
||||
if (disableEditing) {
|
||||
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
|
||||
locking.setAnnotationLocked(addedAnnotation, true);
|
||||
locking.setAnnotationLocked(newAnnotationUID, true);
|
||||
}
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
|
||||
@ -5,7 +5,7 @@ function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }
|
||||
const ProgressDropdownWithService =
|
||||
servicesManager.services.customizationService.getCustomization(
|
||||
'progressDropdownWithServiceComponent'
|
||||
).component;
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -47,80 +47,6 @@ const OverlayItemComponents = {
|
||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||
};
|
||||
|
||||
const studyDateItem = {
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
|
||||
formatDate(referenceInstance.StudyDate),
|
||||
};
|
||||
|
||||
const seriesDescriptionItem = {
|
||||
id: 'SeriesDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Series description',
|
||||
condition: ({ referenceInstance }) => {
|
||||
return referenceInstance && referenceInstance.SeriesDescription;
|
||||
},
|
||||
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
|
||||
};
|
||||
|
||||
const topLeftItems = {
|
||||
id: 'cornerstoneOverlayTopLeft',
|
||||
items: [studyDateItem, seriesDescriptionItem],
|
||||
};
|
||||
|
||||
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
||||
|
||||
const bottomLeftItems = {
|
||||
id: 'cornerstoneOverlayBottomLeft',
|
||||
items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: props => {
|
||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const bottomRightItems = {
|
||||
id: 'cornerstoneOverlayBottomRight',
|
||||
items: [
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* The @ohif/cornerstoneOverlay is a default value for a customization
|
||||
* for the cornerstone overlays. The intent is to allow it to be extended
|
||||
* without needing to re-write the individual overlays by using the append
|
||||
* mechanism. Individual attributes can be modified individually without
|
||||
* affecting the other items by using the append as well, with position
|
||||
* based replacement.
|
||||
* This is used as a default in the getCustomizationModule so that it
|
||||
* is available early for additional customization extensions.
|
||||
*/
|
||||
const CornerstoneOverlay = {
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
topLeftItems,
|
||||
topRightItems,
|
||||
bottomLeftItems,
|
||||
bottomRightItems,
|
||||
};
|
||||
|
||||
/**
|
||||
* Customizable Viewport Overlay
|
||||
*/
|
||||
@ -143,26 +69,18 @@ function CustomizableViewportOverlay({
|
||||
const [scale, setScale] = useState(1);
|
||||
const { imageIndex } = imageSliceData;
|
||||
|
||||
// The new customization is 'cornerstoneOverlay', with an append or replace
|
||||
// on the individual items rather than defining individual items.
|
||||
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
||||
|
||||
// Historical usage defined the overlays as separate items due to lack of
|
||||
// append functionality. This code enables the historical usage, but
|
||||
// the recommended functionality is to append to the default values in
|
||||
// cornerstoneOverlay rather than defining individual items.
|
||||
const topLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
||||
cornerstoneOverlay?.topLeftItems;
|
||||
const topRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
||||
cornerstoneOverlay?.topRightItems;
|
||||
const bottomLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
||||
cornerstoneOverlay?.bottomLeftItems;
|
||||
const bottomRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
||||
cornerstoneOverlay?.bottomRightItems;
|
||||
const topLeftCustomization = customizationService.getCustomization('viewportOverlay.topLeft');
|
||||
const topRightCustomization = customizationService.getCustomization('viewportOverlay.topRight');
|
||||
const bottomLeftCustomization = customizationService.getCustomization(
|
||||
'viewportOverlay.bottomLeft'
|
||||
);
|
||||
const bottomRightCustomization = customizationService.getCustomization(
|
||||
'viewportOverlay.bottomRight'
|
||||
);
|
||||
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
@ -264,8 +182,8 @@ function CustomizableViewportOverlay({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { customizationType } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[customizationType];
|
||||
const { inheritsFrom } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[inheritsFrom];
|
||||
|
||||
if (OverlayItemComponent) {
|
||||
return <OverlayItemComponent {...overlayItemProps} />;
|
||||
@ -293,10 +211,6 @@ function CustomizableViewportOverlay({
|
||||
|
||||
const getContent = useCallback(
|
||||
(customization, keyPrefix) => {
|
||||
if (!customization?.items) {
|
||||
return null;
|
||||
}
|
||||
const { items } = customization;
|
||||
const props = {
|
||||
...displaySetProps,
|
||||
formatters: { formatDate: formatDICOMDate },
|
||||
@ -309,7 +223,7 @@ function CustomizableViewportOverlay({
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
{customization.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
|
||||
null}
|
||||
@ -533,4 +447,4 @@ CustomizableViewportOverlay.propTypes = {
|
||||
|
||||
export default CustomizableViewportOverlay;
|
||||
|
||||
export { CustomizableViewportOverlay, CornerstoneOverlay };
|
||||
export { CustomizableViewportOverlay };
|
||||
|
||||
@ -194,7 +194,7 @@ function commandsModule({
|
||||
* on the measurement with a response if not cancelled.
|
||||
*/
|
||||
setMeasurementLabel: ({ uid }) => {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
|
||||
(val: Map<any, any>) => {
|
||||
@ -311,7 +311,7 @@ function commandsModule({
|
||||
},
|
||||
|
||||
renameMeasurement: ({ uid }) => {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
|
||||
measurementService.update(
|
||||
@ -362,7 +362,7 @@ function commandsModule({
|
||||
viewportGridService.setActiveViewportId(viewportId);
|
||||
},
|
||||
arrowTextCallback: ({ callback, data, uid }) => {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
|
||||
},
|
||||
toggleCine: () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { useCine } from '@ohif/ui';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
@ -160,8 +160,7 @@ function RenderCinePlayer({
|
||||
dynamicInfo: dynamicInfoProp,
|
||||
customizationService,
|
||||
}) {
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
const CinePlayerComponent = customizationService.getCustomization('cinePlayer');
|
||||
|
||||
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
|
||||
|
||||
|
||||
@ -2,21 +2,21 @@
|
||||
// configured via the customization service.
|
||||
const defaultWindowLevelPresets = {
|
||||
CT: [
|
||||
{ description: 'Soft tissue', window: '400', level: '40' },
|
||||
{ description: 'Lung', window: '1500', level: '-600' },
|
||||
{ description: 'Liver', window: '150', level: '90' },
|
||||
{ description: 'Bone', window: '2500', level: '480' },
|
||||
{ description: 'Brain', window: '80', level: '40' },
|
||||
{ id: 'ct-soft-tissue', description: 'Soft tissue', window: '400', level: '40' },
|
||||
{ id: 'ct-lung', description: 'Lung', window: '1500', level: '-600' },
|
||||
{ id: 'ct-liver', description: 'Liver', window: '150', level: '90' },
|
||||
{ id: 'ct-bone', description: 'Bone', window: '2500', level: '480' },
|
||||
{ id: 'ct-brain', description: 'Brain', window: '80', level: '40' },
|
||||
],
|
||||
|
||||
PT: [
|
||||
{ description: 'Default', window: '5', level: '2.5' },
|
||||
{ description: 'SUV', window: '0', level: '3' },
|
||||
{ description: 'SUV', window: '0', level: '5' },
|
||||
{ description: 'SUV', window: '0', level: '7' },
|
||||
{ description: 'SUV', window: '0', level: '8' },
|
||||
{ description: 'SUV', window: '0', level: '10' },
|
||||
{ description: 'SUV', window: '0', level: '15' },
|
||||
{ id: 'pt-default', description: 'Default', window: '5', level: '2.5' },
|
||||
{ id: 'pt-suv-3', description: 'SUV', window: '0', level: '3' },
|
||||
{ id: 'pt-suv-5', description: 'SUV', window: '0', level: '5' },
|
||||
{ id: 'pt-suv-7', description: 'SUV', window: '0', level: '7' },
|
||||
{ id: 'pt-suv-8', description: 'SUV', window: '0', level: '8' },
|
||||
{ id: 'pt-suv-10', description: 'SUV', window: '0', level: '10' },
|
||||
{ id: 'pt-suv-15', description: 'SUV', window: '0', level: '15' },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -16,11 +16,10 @@ export function getWindowLevelActionMenu({
|
||||
}>): ReactNode {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
|
||||
const colorbarProperties = customizationService.get('cornerstone.colorbar');
|
||||
const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.get(
|
||||
'cornerstone.3dVolumeRendering'
|
||||
);
|
||||
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
|
||||
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
|
||||
const { volumeRenderingPresets, volumeRenderingQualityRange } =
|
||||
customizationService.getCustomization('cornerstone.3dVolumeRendering');
|
||||
|
||||
const displaySetPresets = displaySets
|
||||
.filter(displaySet => presets[displaySet.Modality])
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { colormaps } from '../utils/colormaps';
|
||||
|
||||
const DefaultColormap = 'Grayscale';
|
||||
|
||||
export default {
|
||||
'cornerstone.colorbar': {
|
||||
width: '16px',
|
||||
colorbarTickPosition: 'left',
|
||||
colormaps,
|
||||
colorbarContainerPosition: 'right',
|
||||
colorbarInitialColormap: DefaultColormap,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,99 @@
|
||||
export default {
|
||||
'layoutSelector.advancedPresetGenerator': ({
|
||||
servicesManager,
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}) => {
|
||||
const _areSelectorsValid = (
|
||||
hp: AppTypes.HangingProtocol.Protocol,
|
||||
displaySets: AppTypes.DisplaySet[],
|
||||
hangingProtocolService: AppTypes.HangingProtocolService
|
||||
) => {
|
||||
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hangingProtocolService.areRequiredSelectorsValid(
|
||||
Object.values(hp.displaySetSelectors),
|
||||
displaySets[0]
|
||||
);
|
||||
};
|
||||
|
||||
const generateAdvancedPresets = ({
|
||||
servicesManager,
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}) => {
|
||||
const { hangingProtocolService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
|
||||
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
|
||||
if (!viewportId) {
|
||||
return [];
|
||||
}
|
||||
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
if (!displaySetInsaneUIDs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const displaySets = displaySetInsaneUIDs.map(uid =>
|
||||
displaySetService.getDisplaySetByUID(uid)
|
||||
);
|
||||
|
||||
return hangingProtocols
|
||||
.map(hp => {
|
||||
if (!hp.isPreset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
|
||||
|
||||
return {
|
||||
icon: hp.icon,
|
||||
title: hp.name,
|
||||
commandOptions: {
|
||||
protocolId: hp.id,
|
||||
},
|
||||
disabled: !areValid,
|
||||
};
|
||||
})
|
||||
.filter(preset => preset !== null);
|
||||
};
|
||||
|
||||
return generateAdvancedPresets({ servicesManager });
|
||||
},
|
||||
'layoutSelector.commonPresets': [
|
||||
{
|
||||
icon: 'layout-common-1x1',
|
||||
commandOptions: {
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-1x2',
|
||||
commandOptions: {
|
||||
numRows: 1,
|
||||
numCols: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-2x2',
|
||||
commandOptions: {
|
||||
numRows: 2,
|
||||
numCols: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-2x3',
|
||||
commandOptions: {
|
||||
numRows: 2,
|
||||
numCols: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -0,0 +1,124 @@
|
||||
export default {
|
||||
'cornerstone.measurements': {
|
||||
Angle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CobbAngle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
ArrowAnnotate: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
RectangleROi: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CircleROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
EllipticalROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Bidirectional: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Length: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
LivewireContour: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
SplineROI: {
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Areas',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'areaUnits',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
PlanarFreehandROI: {
|
||||
displayTextOpen: [
|
||||
{
|
||||
displayName: 'Length',
|
||||
value: 'length',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'pixelValueUnits',
|
||||
for: ['mean', 'max'],
|
||||
type: 'unit',
|
||||
},
|
||||
{
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'unit',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { CinePlayer } from '@ohif/ui';
|
||||
import DicomUpload from '../components/DicomUpload/DicomUpload';
|
||||
|
||||
export default {
|
||||
cinePlayer: CinePlayer,
|
||||
autoCineModalities: ['OT', 'US'],
|
||||
'panelMeasurement.disableEditing': false,
|
||||
onBeforeSRAddMeasurement: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => {
|
||||
return measurement;
|
||||
},
|
||||
onBeforeDicomStore: ({ dicomDict, measurementData, naturalizedReport }) => {
|
||||
return dicomDict;
|
||||
},
|
||||
dicomUploadComponent: DicomUpload,
|
||||
codingValues: {},
|
||||
};
|
||||
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
|
||||
return {
|
||||
'panelSegmentation.customDropdownMenuContent': ({
|
||||
activeSegmentation,
|
||||
onSegmentationAdd,
|
||||
onSegmentationRemoveFromViewport,
|
||||
onSegmentationEdit,
|
||||
onSegmentationDelete,
|
||||
allowExport,
|
||||
storeSegmentation,
|
||||
onSegmentationDownload,
|
||||
onSegmentationDownloadRTSS,
|
||||
t,
|
||||
}) => (
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Create New Segmentation')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
|
||||
>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">{t('Remove from Viewport')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">{t('Rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={!allowExport}
|
||||
className="pl-1"
|
||||
>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Export & Download')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
|
||||
>
|
||||
{t('Export DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
|
||||
>
|
||||
{t('Download DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">{t('Delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
),
|
||||
'panelSegmentation.disableEditing': false,
|
||||
'panelSegmentation.showAddSegment': true,
|
||||
'panelSegmentation.onSegmentationAdd': () => {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
const viewportId = viewportGridService.getState().activeViewportId;
|
||||
commandsManager.run('createLabelmapForViewport', { viewportId });
|
||||
},
|
||||
'panelSegmentation.tableMode': 'collapsed',
|
||||
'panelSegmentation.readableText': {
|
||||
lesionStats: 'Lesion Statistics',
|
||||
minValue: 'Minimum Value',
|
||||
maxValue: 'Maximum Value',
|
||||
meanValue: 'Mean Value',
|
||||
volume: 'Volume (ml)',
|
||||
suvPeak: 'SUV Peak',
|
||||
suvMax: 'Maximum SUV',
|
||||
suvMaxIJK: 'SUV Max IJK',
|
||||
lesionGlyoclysisStats: 'Lesion Glycolysis',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
export default {
|
||||
cornerstoneViewportClickCommands: {
|
||||
doubleClick: ['toggleOneUp'],
|
||||
button1: ['closeContextMenu'],
|
||||
button3: [
|
||||
{
|
||||
commandName: 'showCornerstoneContextMenu',
|
||||
commandOptions: {
|
||||
requireNearbyToolData: true,
|
||||
menuId: 'measurementsContextMenu',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
export default {
|
||||
'viewportOverlay.topLeft': [
|
||||
{
|
||||
id: 'StudyDate',
|
||||
inheritsFrom: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
|
||||
formatDate(referenceInstance.StudyDate),
|
||||
},
|
||||
{
|
||||
id: 'SeriesDescription',
|
||||
inheritsFrom: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Series description',
|
||||
condition: ({ referenceInstance }) => {
|
||||
return referenceInstance && referenceInstance.SeriesDescription;
|
||||
},
|
||||
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
|
||||
},
|
||||
],
|
||||
'viewportOverlay.topRight': [],
|
||||
'viewportOverlay.bottomLeft': [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
inheritsFrom: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
inheritsFrom: 'ohif.overlayItem.zoomLevel',
|
||||
condition: props => {
|
||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
],
|
||||
'viewportOverlay.bottomRight': [
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
inheritsFrom: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { toolNames } from '../initCornerstoneTools';
|
||||
|
||||
export default {
|
||||
'cornerstone.overlayViewportTools': {
|
||||
active: [
|
||||
{
|
||||
toolName: toolNames.WindowLevel,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Pan,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Zoom,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.StackScroll,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
},
|
||||
],
|
||||
enabled: [
|
||||
{
|
||||
toolName: toolNames.PlanarFreehandContourSegmentation,
|
||||
configuration: {
|
||||
displayOnePointAsCrosshairs: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
import { CONSTANTS } from '@cornerstonejs/core';
|
||||
|
||||
const { VIEWPORT_PRESETS } = CONSTANTS;
|
||||
|
||||
export default {
|
||||
'cornerstone.3dVolumeRendering': {
|
||||
volumeRenderingPresets: VIEWPORT_PRESETS,
|
||||
volumeRenderingQualityRange: {
|
||||
min: 1,
|
||||
max: 4,
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
import defaultWindowLevelPresets from '../components/WindowLevelActionMenu/defaultWindowLevelPresets';
|
||||
|
||||
export default {
|
||||
'cornerstone.windowLevelPresets': defaultWindowLevelPresets,
|
||||
};
|
||||
@ -1,213 +0,0 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
import { CONSTANTS } from '@cornerstonejs/core';
|
||||
import { CornerstoneOverlay } from './Viewport/Overlays/CustomizableViewportOverlay';
|
||||
|
||||
const DefaultColormap = 'Grayscale';
|
||||
const { VIEWPORT_PRESETS } = CONSTANTS;
|
||||
|
||||
const tools = {
|
||||
active: [
|
||||
{
|
||||
toolName: toolNames.WindowLevel,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Pan,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Zoom,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.StackScroll,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
|
||||
},
|
||||
],
|
||||
enabled: [
|
||||
{
|
||||
toolName: toolNames.PlanarFreehandContourSegmentation,
|
||||
configuration: {
|
||||
displayOnePointAsCrosshairs: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
CornerstoneOverlay,
|
||||
{
|
||||
id: 'cornerstone.overlayViewportTools',
|
||||
tools,
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.windowLevelPresets',
|
||||
presets: defaultWindowLevelPresets,
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.colorbar',
|
||||
width: '16px',
|
||||
colorbarTickPosition: 'left',
|
||||
colormaps,
|
||||
colorbarContainerPosition: 'right',
|
||||
colorbarInitialColormap: DefaultColormap,
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.3dVolumeRendering',
|
||||
volumeRenderingPresets: VIEWPORT_PRESETS,
|
||||
volumeRenderingQualityRange: {
|
||||
min: 1,
|
||||
max: 4,
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.measurements',
|
||||
Angle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CobbAngle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
ArrowAnnotate: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
RectangleROi: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CircleROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
EllipticalROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Bidirectional: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Length: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
LivewireContour: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
SplineROI: {
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
/**
|
||||
{
|
||||
displayName: 'Modality',
|
||||
value: 'Modality',
|
||||
type: 'value',
|
||||
},
|
||||
*/
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'areaUnits',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
PlanarFreehandROI: {
|
||||
displayTextOpen: [
|
||||
{
|
||||
displayName: 'Length',
|
||||
value: 'length',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'pixelValueUnits',
|
||||
for: ['mean', 'max' /** 'stdDev **/],
|
||||
type: 'unit',
|
||||
},
|
||||
{
|
||||
value: 'areaUnits',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
/**
|
||||
{
|
||||
displayName: 'Std Dev',
|
||||
value: 'stdDev',
|
||||
type: 'value',
|
||||
},
|
||||
*/
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'unit',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getCustomizationModule;
|
||||
32
extensions/cornerstone/src/getCustomizationModule.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import viewportOverlayCustomization from './customizations/viewportOverlayCustomization';
|
||||
import getSegmentationPanelCustomization from './customizations/segmentationPanelCustomization';
|
||||
import layoutSelectorCustomization from './customizations/layoutSelectorCustomization';
|
||||
import viewportToolsCustomization from './customizations/viewportToolsCustomization';
|
||||
import viewportClickCommandsCustomization from './customizations/viewportClickCommandsCustomization';
|
||||
import measurementsCustomization from './customizations/measurementsCustomization';
|
||||
import volumeRenderingCustomization from './customizations/volumeRenderingCustomization';
|
||||
import colorbarCustomization from './customizations/colorbarCustomization';
|
||||
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
|
||||
import miscCustomization from './customizations/miscCustomization';
|
||||
|
||||
function getCustomizationModule({ commandsManager, servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
...viewportOverlayCustomization,
|
||||
...getSegmentationPanelCustomization({ commandsManager, servicesManager }),
|
||||
...layoutSelectorCustomization,
|
||||
...viewportToolsCustomization,
|
||||
...viewportClickCommandsCustomization,
|
||||
...measurementsCustomization,
|
||||
...volumeRenderingCustomization,
|
||||
...colorbarCustomization,
|
||||
...windowLevelPresetsCustomization,
|
||||
...miscCustomization,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getCustomizationModule;
|
||||
@ -12,10 +12,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
const { label, segments } = segmentation;
|
||||
|
||||
// Get the readable text mapping once
|
||||
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||
'PanelSegmentation.readableText',
|
||||
{}
|
||||
);
|
||||
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
|
||||
|
||||
// Helper function to recursively map cachedStats to readable display text
|
||||
function mapStatsToDisplay(stats, indent = 0) {
|
||||
|
||||
@ -7,10 +7,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
const { label, segments } = segmentation;
|
||||
|
||||
// Get the readable text mapping once
|
||||
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||
'PanelSegmentation.readableText',
|
||||
{}
|
||||
);
|
||||
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
|
||||
|
||||
// Helper function to recursively map cachedStats to readable display text
|
||||
function mapStatsToDisplay(stats, indent = 0) {
|
||||
|
||||
@ -160,11 +160,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
ViewportActionCornersProvider
|
||||
);
|
||||
|
||||
const { syncGroupService } = servicesManager.services;
|
||||
const { syncGroupService, customizationService } = servicesManager.services;
|
||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
|
||||
component: props => <DicomUpload {...props} />,
|
||||
});
|
||||
|
||||
return init.call(this, props);
|
||||
},
|
||||
getToolbarModule,
|
||||
|
||||
@ -5,27 +5,6 @@ import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
const DEFAULT_CONTEXT_MENU_CLICKS = {
|
||||
button1: {
|
||||
commands: [
|
||||
{
|
||||
commandName: 'closeContextMenu',
|
||||
},
|
||||
],
|
||||
},
|
||||
button3: {
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showCornerstoneContextMenu',
|
||||
commandOptions: {
|
||||
requireNearbyToolData: true,
|
||||
menuId: 'measurementsContextMenu',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a name, consisting of:
|
||||
* * alt when the alt key is down
|
||||
@ -60,8 +39,10 @@ function initContextMenu({
|
||||
* defaults on button1 and button2
|
||||
*/
|
||||
const cornerstoneViewportHandleEvent = (name, evt) => {
|
||||
const customizations =
|
||||
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_CONTEXT_MENU_CLICKS;
|
||||
const customizations = customizationService.getCustomization(
|
||||
'cornerstoneViewportClickCommands'
|
||||
);
|
||||
|
||||
const toRun = customizations[name];
|
||||
|
||||
if (!toRun) {
|
||||
@ -71,7 +52,7 @@ function initContextMenu({
|
||||
// only find nearbyToolData if required, for the click (which closes the context menu
|
||||
// we don't need to find nearbyToolData)
|
||||
let nearbyToolData = null;
|
||||
if (toRun.commands.some(command => command.commandOptions?.requireNearbyToolData)) {
|
||||
if (toRun.some(command => command.commandOptions?.requireNearbyToolData)) {
|
||||
nearbyToolData = findNearbyToolData(commandsManager, evt);
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
import { eventTarget, EVENTS } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { CommandsManager, CustomizationService, Types } from '@ohif/core';
|
||||
import { CommandsManager, CustomizationService } from '@ohif/core';
|
||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
const DEFAULT_DOUBLE_CLICK = {
|
||||
doubleClick: {
|
||||
commandName: 'toggleOneUp',
|
||||
commandOptions: {},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a double click event name, consisting of:
|
||||
* * alt when the alt key is down
|
||||
@ -50,8 +43,9 @@ function initDoubleClick({ customizationService, commandsManager }: initDoubleCl
|
||||
const eventName = getDoubleClickEventName(evt);
|
||||
|
||||
// Allows for the customization of the double click on a viewport.
|
||||
const customizations =
|
||||
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_DOUBLE_CLICK;
|
||||
const customizations = customizationService.getCustomization(
|
||||
'cornerstoneViewportClickCommands'
|
||||
);
|
||||
|
||||
const toRun = customizations[eventName];
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { utils } from '@ohif/core';
|
||||
import { MeasurementTable } from '@ohif/ui-next';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
@ -1,16 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
Icons,
|
||||
SegmentationTable,
|
||||
DropdownMenuSubTrigger,
|
||||
} from '@ohif/ui-next';
|
||||
import { SegmentationTable } from '@ohif/ui-next';
|
||||
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
|
||||
@ -19,7 +8,7 @@ export default function PanelSegmentation({
|
||||
commandsManager,
|
||||
children,
|
||||
}: withAppTypes) {
|
||||
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
|
||||
const { customizationService, displaySetService } = servicesManager.services;
|
||||
|
||||
const { segmentationsWithRepresentations, disabled } =
|
||||
useActiveViewportSegmentationRepresentations({
|
||||
@ -27,11 +16,6 @@ export default function PanelSegmentation({
|
||||
});
|
||||
|
||||
const handlers = {
|
||||
onSegmentationAdd: async () => {
|
||||
const viewportId = viewportGridService.getState().activeViewportId;
|
||||
commandsManager.run('createLabelmapForViewport', { viewportId });
|
||||
},
|
||||
|
||||
onSegmentationClick: (segmentationId: string) => {
|
||||
commandsManager.run('setActiveSegmentation', { segmentationId });
|
||||
},
|
||||
@ -125,107 +109,19 @@ export default function PanelSegmentation({
|
||||
},
|
||||
};
|
||||
|
||||
const { mode: SegmentationTableMode } = customizationService.getCustomization(
|
||||
'PanelSegmentation.tableMode',
|
||||
{
|
||||
id: 'default.segmentationTable.mode',
|
||||
mode: 'collapsed',
|
||||
}
|
||||
const segmentationTableMode = customizationService.getCustomization(
|
||||
'panelSegmentation.tableMode'
|
||||
);
|
||||
|
||||
// custom onSegmentationAdd if provided
|
||||
const { onSegmentationAdd } = customizationService.getCustomization(
|
||||
'PanelSegmentation.onSegmentationAdd',
|
||||
{
|
||||
id: 'segmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: handlers.onSegmentationAdd,
|
||||
}
|
||||
const onSegmentationAdd = customizationService.getCustomization(
|
||||
'panelSegmentation.onSegmentationAdd'
|
||||
);
|
||||
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelSegmentation.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { showAddSegment } = customizationService.getCustomization(
|
||||
'PanelSegmentation.showAddSegment',
|
||||
{
|
||||
id: 'default.showAddSegment',
|
||||
showAddSegment: true,
|
||||
}
|
||||
);
|
||||
|
||||
const CustomDropdownMenuContent = customizationService.getCustomComponent(
|
||||
'PanelSegmentation.CustomDropdownMenuContent',
|
||||
({
|
||||
activeSegmentation,
|
||||
onSegmentationAdd,
|
||||
onSegmentationRemoveFromViewport,
|
||||
onSegmentationEdit,
|
||||
onSegmentationDelete,
|
||||
allowExport,
|
||||
storeSegmentation,
|
||||
onSegmentationDownload,
|
||||
onSegmentationDownloadRTSS,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Create New Segmentation')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
|
||||
>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">{t('Remove from Viewport')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">{t('Rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={!allowExport}
|
||||
className="pl-1"
|
||||
>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Export & Download')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
|
||||
>
|
||||
{t('Export DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
|
||||
>
|
||||
{t('Download DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
{/* <DropdownMenuItem
|
||||
onClick={() => onSegmentationDownloadRTSS(activeSegmentation.segmentationId)}
|
||||
>
|
||||
{t('Download DICOM RTSTRUCT')}
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">{t('Delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
}
|
||||
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
|
||||
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
|
||||
const CustomDropdownMenuContent = customizationService.getCustomization(
|
||||
'panelSegmentation.customDropdownMenuContent'
|
||||
);
|
||||
|
||||
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
|
||||
@ -272,7 +168,7 @@ export default function PanelSegmentation({
|
||||
<SegmentationTable
|
||||
disabled={disabled}
|
||||
data={segmentationsWithRepresentations}
|
||||
mode={SegmentationTableMode}
|
||||
mode={segmentationTableMode}
|
||||
title="Segmentations"
|
||||
exportOptions={exportOptions}
|
||||
disableEditing={disableEditing}
|
||||
@ -308,7 +204,7 @@ export default function PanelSegmentation({
|
||||
<SegmentationTable.Config />
|
||||
<SegmentationTable.AddSegmentationRow />
|
||||
|
||||
{SegmentationTableMode === 'collapsed' ? (
|
||||
{segmentationTableMode === 'collapsed' ? (
|
||||
<SegmentationTable.Collapsed>
|
||||
<SegmentationTable.SelectorHeader>
|
||||
<CustomDropdownMenuContent />
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { useEffect, useState, memo } from 'react';
|
||||
|
||||
const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US'];
|
||||
|
||||
const ActiveViewportBehavior = memo(
|
||||
({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => {
|
||||
const { displaySetService, cineService, viewportGridService, customizationService } =
|
||||
@ -41,13 +39,7 @@ const ActiveViewportBehavior = memo(
|
||||
const modalities = displaySets.map(displaySet => displaySet?.Modality);
|
||||
const isDynamicVolume = displaySets.some(displaySet => displaySet?.isDynamicVolume);
|
||||
|
||||
const { modalities: sourceModalities } = customizationService.getModeCustomization(
|
||||
'autoCineModalities',
|
||||
{
|
||||
id: 'autoCineModalities',
|
||||
modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT,
|
||||
}
|
||||
);
|
||||
const sourceModalities = customizationService.getCustomization('autoCineModalities');
|
||||
|
||||
const requiresCine = modalities.some(modality => sourceModalities.includes(modality));
|
||||
|
||||
|
||||
@ -142,7 +142,7 @@ function getMappedAnnotations(annotation, displaySetService) {
|
||||
* @returns {object} Report's content.
|
||||
*/
|
||||
function getColumnValueReport(annotation, customizationService) {
|
||||
const { PlanarFreehandROI } = customizationService.get('cornerstone.measurements');
|
||||
const { PlanarFreehandROI } = customizationService.getCustomization('cornerstone.measurements');
|
||||
const { report } = PlanarFreehandROI;
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
@ -145,7 +145,7 @@ function getMappedAnnotations(annotation, displaySetService) {
|
||||
* @returns {object} Report's content.
|
||||
*/
|
||||
function getColumnValueReport(annotation, customizationService) {
|
||||
const { SplineROI } = customizationService.get('cornerstone.measurements');
|
||||
const { SplineROI } = customizationService.getCustomization('cornerstone.measurements');
|
||||
const { report } = SplineROI;
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
@ -29,8 +29,9 @@ function DataSourceConfigurationComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const { factory: configurationAPIFactory } =
|
||||
customizationService.get(activeDataSourceDef.configuration.configurationAPI) ?? {};
|
||||
const { factory: configurationAPIFactory } = customizationService.getCustomization(
|
||||
activeDataSourceDef.configuration.configurationAPI
|
||||
) ?? { factory: () => null };
|
||||
|
||||
if (!configurationAPIFactory) {
|
||||
return;
|
||||
|
||||
@ -56,9 +56,9 @@ export default function MoreDropdownMenu(bindProps) {
|
||||
} = bindProps;
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const items = customizationService.getCustomization(menuItemsKey)?.value;
|
||||
const items = customizationService.getCustomization(menuItemsKey);
|
||||
|
||||
if (!items) {
|
||||
if (!items?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
const defaultContextMenu = {
|
||||
id: 'measurementsContextMenu',
|
||||
customizationType: 'ohif.contextMenu',
|
||||
menus: [
|
||||
// Get the items from the UI Customization for the menu name (and have a custom name)
|
||||
{
|
||||
id: 'forExistingMeasurement',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'deleteMeasurement',
|
||||
// we only have support for cornerstoneTools context menu since
|
||||
// they are svg based
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setMeasurementLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default defaultContextMenu;
|
||||
@ -1,11 +1,5 @@
|
||||
import ContextMenuController from './ContextMenuController';
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import defaultContextMenu from './defaultContextMenu';
|
||||
import * as CustomizableContextMenuTypes from './types';
|
||||
|
||||
export {
|
||||
ContextMenuController,
|
||||
CustomizableContextMenuTypes,
|
||||
ContextMenuItemsBuilder,
|
||||
defaultContextMenu,
|
||||
};
|
||||
export { ContextMenuController, CustomizableContextMenuTypes, ContextMenuItemsBuilder };
|
||||
|
||||
@ -45,10 +45,11 @@ export type UIMenuItem = {
|
||||
*/
|
||||
export interface MenuItem {
|
||||
id?: string;
|
||||
/** The customization type is used to apply preset values to this item
|
||||
/**
|
||||
* The customization type is used to apply preset values to this item
|
||||
* when registered with the customization service.
|
||||
*/
|
||||
customizationType?: string;
|
||||
inheritsFrom?: string;
|
||||
|
||||
// The label is the value to show in the menu for this item
|
||||
label?: string;
|
||||
@ -91,7 +92,7 @@ export interface Menu {
|
||||
/** The customization type is used to apply preset values to this item
|
||||
* when registered with the customization service.
|
||||
*/
|
||||
customizationType?: string;
|
||||
inheritsFrom?: string;
|
||||
|
||||
// Choose whether this menu applies.
|
||||
selector?: Types.Predicate;
|
||||
|
||||
@ -582,8 +582,11 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
|
||||
return dicomWebConfigCopy;
|
||||
},
|
||||
getStudyInstanceUIDs({ params, query }) {
|
||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||
const queryStudyInstanceUIDs = utils.splitComma(query.getAll('StudyInstanceUIDs'));
|
||||
const paramsStudyInstanceUIDs = params.StudyInstanceUIDs || params.studyInstanceUIDs;
|
||||
|
||||
const queryStudyInstanceUIDs = utils.splitComma(
|
||||
query.getAll('StudyInstanceUIDs').concat(query.getAll('studyInstanceUIDs'))
|
||||
);
|
||||
|
||||
const StudyInstanceUIDs =
|
||||
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || paramsStudyInstanceUIDs;
|
||||
|
||||
@ -6,7 +6,7 @@ import { utils } from '@ohif/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
|
||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||
import { defaultActionIcons } from './constants';
|
||||
import MoreDropdownMenu from '../../Components/MoreDropdownMenu';
|
||||
|
||||
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
|
||||
@ -43,7 +43,7 @@ function PanelStudyBrowser({
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
|
||||
const [viewPresets, setViewPresets] = useState(
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')
|
||||
);
|
||||
|
||||
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
|
||||
|
||||
@ -2,87 +2,6 @@ import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton, LayoutPreset } from '@ohif/ui';
|
||||
|
||||
const defaultCommonPresets = [
|
||||
{
|
||||
icon: 'layout-common-1x1',
|
||||
commandOptions: {
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-1x2',
|
||||
commandOptions: {
|
||||
numRows: 1,
|
||||
numCols: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-2x2',
|
||||
commandOptions: {
|
||||
numRows: 2,
|
||||
numCols: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'layout-common-2x3',
|
||||
commandOptions: {
|
||||
numRows: 2,
|
||||
numCols: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const _areSelectorsValid = (hp, displaySets, hangingProtocolService) => {
|
||||
if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hangingProtocolService.areRequiredSelectorsValid(
|
||||
Object.values(hp.displaySetSelectors),
|
||||
displaySets[0]
|
||||
);
|
||||
};
|
||||
|
||||
const generateAdvancedPresets = ({ servicesManager }: withAppTypes) => {
|
||||
const { hangingProtocolService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const hangingProtocols = Array.from(hangingProtocolService.protocols.values());
|
||||
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
|
||||
if (!viewportId) {
|
||||
return [];
|
||||
}
|
||||
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
if (!displaySetInsaneUIDs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const displaySets = displaySetInsaneUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
|
||||
|
||||
return hangingProtocols
|
||||
.map(hp => {
|
||||
if (!hp.isPreset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService);
|
||||
|
||||
return {
|
||||
icon: hp.icon,
|
||||
title: hp.name,
|
||||
commandOptions: {
|
||||
protocolId: hp.id,
|
||||
},
|
||||
disabled: !areValid,
|
||||
};
|
||||
})
|
||||
.filter(preset => preset !== null);
|
||||
};
|
||||
|
||||
function ToolbarLayoutSelectorWithServices({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
@ -138,9 +57,13 @@ function LayoutSelector({
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
const commonPresets = customizationService.get('commonPresets') || defaultCommonPresets;
|
||||
const advancedPresets =
|
||||
customizationService.get('advancedPresets') || generateAdvancedPresets({ servicesManager });
|
||||
|
||||
const commonPresets = customizationService.getCustomization('layoutSelector.commonPresets');
|
||||
const advancedPresetsGenerator = customizationService.getCustomization(
|
||||
'layoutSelector.advancedPresetGenerator'
|
||||
);
|
||||
|
||||
const advancedPresets = advancedPresetsGenerator({ servicesManager });
|
||||
|
||||
const closeOnOutsideClick = event => {
|
||||
if (isOpen && dropdownRef.current) {
|
||||
@ -216,7 +139,7 @@ function LayoutSelector({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-primary-dark flex flex-col gap-2.5 border-l-2 border-solid border-black p-2">
|
||||
<div className="bg-primary-dark flex flex-col gap-2.5 border-l-2 border-solid border-black p-2">
|
||||
<div className="text-aqua-pale text-xs">Custom</div>
|
||||
<DropdownContent
|
||||
rows={rows}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Types, DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
|
||||
import { ContextMenuController } from './CustomizableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||
import findViewportsByPosition, {
|
||||
@ -113,10 +113,7 @@ const commandsModule = ({
|
||||
const optionsToUse = { ...options };
|
||||
|
||||
if (menuCustomizationId) {
|
||||
Object.assign(
|
||||
optionsToUse,
|
||||
customizationService.get(menuCustomizationId, defaultContextMenu)
|
||||
);
|
||||
Object.assign(optionsToUse, customizationService.getCustomization(menuCustomizationId));
|
||||
}
|
||||
|
||||
// TODO - make the selectorProps richer by including the study metadata and display set.
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { CustomizationService } from '@ohif/core';
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
'routes.customRoutes': {
|
||||
routes: [],
|
||||
notFoundRoute: null,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import DataSourceConfigurationComponent from '../Components/DataSourceConfigurationComponent';
|
||||
import { GoogleCloudDataSourceConfigurationAPI } from '../DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
||||
|
||||
export default function getDataSourceConfigurationCustomization({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
return {
|
||||
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
|
||||
'ohif.dataSourceConfigurationComponent': DataSourceConfigurationComponent.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
|
||||
// The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare
|
||||
'ohif.dataSourceConfigurationAPI.google': (dataSourceName: string) =>
|
||||
new GoogleCloudDataSourceConfigurationAPI(dataSourceName, servicesManager, extensionManager),
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import DataSourceSelector from '../Panels/DataSourceSelector';
|
||||
|
||||
export default {
|
||||
'routes.customRoutes': {
|
||||
routes: {
|
||||
$push: [
|
||||
{
|
||||
path: '/datasources',
|
||||
children: DataSourceSelector,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
export default {
|
||||
measurementsContextMenu: {
|
||||
inheritsFrom: 'ohif.contextMenu',
|
||||
menus: [
|
||||
// Get the items from the UI Customization for the menu name (and have a custom name)
|
||||
{
|
||||
id: 'forExistingMeasurement',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
commands: 'deleteMeasurement',
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: 'setMeasurementLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
'routes.customRoutes': {
|
||||
routes: {
|
||||
$push: [
|
||||
{
|
||||
path: '/custom',
|
||||
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuItem,
|
||||
Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
export default {
|
||||
'ohif.menuContent': function (props) {
|
||||
const { item: topLevelItem, commandsManager, servicesManager, ...rest } = props;
|
||||
|
||||
const content = function (subProps) {
|
||||
const { item: subItem } = subProps;
|
||||
|
||||
// Regular menu item
|
||||
const isDisabled = subItem.selector && !subItem.selector({ servicesManager });
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
disabled={isDisabled}
|
||||
onSelect={() => {
|
||||
commandsManager.runAsync(subItem.commands, {
|
||||
...subItem.commandOptions,
|
||||
...rest,
|
||||
});
|
||||
}}
|
||||
className="gap-[6px]"
|
||||
>
|
||||
{subItem.iconName && (
|
||||
<Icons.ByName
|
||||
name={subItem.iconName}
|
||||
className="-ml-1"
|
||||
/>
|
||||
)}
|
||||
{subItem.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
// If item has sub-items, render a submenu
|
||||
if (topLevelItem.items) {
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="gap-[6px]">
|
||||
{topLevelItem.iconName && (
|
||||
<Icons.ByName
|
||||
name={topLevelItem.iconName}
|
||||
className="-ml-1"
|
||||
/>
|
||||
)}
|
||||
{topLevelItem.label}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
{topLevelItem.items.map(subItem => content({ ...props, item: subItem }))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
return content({ ...props, item: topLevelItem });
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
export default {
|
||||
'studyBrowser.studyMenuItems': {
|
||||
$push: [
|
||||
{
|
||||
id: 'applyHangingProtocol',
|
||||
label: 'Apply Hanging Protocol',
|
||||
iconName: 'ViewportViews',
|
||||
items: [
|
||||
{
|
||||
id: 'applyDefaultProtocol',
|
||||
label: 'Default',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'applyMPRProtocol',
|
||||
label: '2x2 Grid',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'showInOtherMonitor',
|
||||
label: 'Launch On Second Monitor',
|
||||
iconName: 'DicomTagBrowser',
|
||||
selector: ({ servicesManager }) => {
|
||||
const { multiMonitorService } = servicesManager.services;
|
||||
return multiMonitorService.isMultimonitor;
|
||||
},
|
||||
commands: {
|
||||
commandName: 'multimonitor',
|
||||
commandOptions: {
|
||||
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
'ohif.overlayItem': 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 (
|
||||
<span
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: this.color || undefined }}
|
||||
title={this.title || ''}
|
||||
>
|
||||
{this.label && <span className="mr-1 shrink-0">{this.label}</span>}
|
||||
<span className="font-light">{value}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
import { ProgressDropdownWithService } from '../Components/ProgressDropdownWithService';
|
||||
|
||||
export default {
|
||||
progressDropdownWithServiceComponent: ProgressDropdownWithService,
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { sortingCriteria } = utils;
|
||||
|
||||
export default {
|
||||
sortingCriteria: sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { utils } from '@ohif/core';
|
||||
const { formatDate } = utils;
|
||||
|
||||
export default {
|
||||
'studyBrowser.studyMenuItems': [],
|
||||
'studyBrowser.thumbnailMenuItems': [
|
||||
{
|
||||
id: 'tagBrowser',
|
||||
label: 'Tag Browser',
|
||||
iconName: 'DicomTagBrowser',
|
||||
commands: 'openDICOMTagViewer',
|
||||
},
|
||||
],
|
||||
'studyBrowser.sortFunctions': [
|
||||
{
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesNumber - b?.SeriesNumber;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Series Date',
|
||||
sortFunction: (a, b) => {
|
||||
const dateA = new Date(formatDate(a?.SeriesDate));
|
||||
const dateB = new Date(formatDate(b?.SeriesDate));
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
},
|
||||
},
|
||||
],
|
||||
'studyBrowser.viewPresets': [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
'studyBrowser.studyMode': 'all',
|
||||
};
|
||||
@ -1,20 +1,15 @@
|
||||
import { CustomizationService } from '@ohif/core';
|
||||
import React from 'react';
|
||||
import DataSourceSelector from './Panels/DataSourceSelector';
|
||||
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
|
||||
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
||||
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuItem,
|
||||
Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
const formatDate = utils.formatDate;
|
||||
import defaultContextMenuCustomization from './customizations/defaultContextMenuCustomization';
|
||||
import helloPageCustomization from './customizations/helloPageCustomization';
|
||||
import datasourcesCustomization from './customizations/datasourcesCustomization';
|
||||
import multimonitorCustomization from './customizations/multimonitorCustomization';
|
||||
import customRoutesCustomization from './customizations/customRoutesCustomization';
|
||||
import studyBrowserCustomization from './customizations/studyBrowserCustomization';
|
||||
import overlayItemCustomization from './customizations/overlayItemCustomization';
|
||||
import contextMenuCustomization from './customizations/contextMenuCustomization';
|
||||
import menuContentCustomization from './customizations/menuContentCustomization';
|
||||
import getDataSourceConfigurationCustomization from './customizations/dataSourceConfigurationCustomization';
|
||||
import progressDropdownCustomization from './customizations/progressDropdownCustomization';
|
||||
import sortingCriteriaCustomization from './customizations/sortingCriteriaCustomization';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -29,315 +24,29 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
return [
|
||||
{
|
||||
name: 'helloPage',
|
||||
merge: 'Append',
|
||||
value: {
|
||||
id: 'customRoutes',
|
||||
routes: [
|
||||
{
|
||||
path: '/custom',
|
||||
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
|
||||
},
|
||||
],
|
||||
},
|
||||
value: helloPageCustomization,
|
||||
},
|
||||
|
||||
// Example customization to list a set of datasources
|
||||
{
|
||||
name: 'datasources',
|
||||
merge: 'Append',
|
||||
value: {
|
||||
id: 'customRoutes',
|
||||
routes: [
|
||||
{
|
||||
path: '/datasources',
|
||||
children: DataSourceSelector,
|
||||
},
|
||||
],
|
||||
},
|
||||
value: datasourcesCustomization,
|
||||
},
|
||||
{
|
||||
name: 'multimonitor',
|
||||
merge: 'Append',
|
||||
value: {
|
||||
id: 'studyBrowser.studyMenuItems',
|
||||
customizationType: 'ohif.menuContent',
|
||||
value: [
|
||||
{
|
||||
id: 'applyHangingProtocol',
|
||||
label: 'Apply Hanging Protocol',
|
||||
iconName: 'ViewportViews',
|
||||
items: [
|
||||
{
|
||||
id: 'applyDefaultProtocol',
|
||||
label: 'Default',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'applyMPRProtocol',
|
||||
label: '2x2 Grid',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'showInOtherMonitor',
|
||||
label: 'Launch On Second Monitor',
|
||||
iconName: 'DicomTagBrowser',
|
||||
// we should use evaluator for this, as these are basically toolbar buttons
|
||||
selector: ({ servicesManager }) => {
|
||||
const { multiMonitorService } = servicesManager.services;
|
||||
return multiMonitorService.isMultimonitor;
|
||||
},
|
||||
commands: {
|
||||
commandName: 'multimonitor',
|
||||
commandOptions: {
|
||||
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
|
||||
commands: [
|
||||
'loadStudy',
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
value: multimonitorCustomization,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
/**
|
||||
* Customization Component Type definition for overlay items.
|
||||
* Overlay items are texts (or other components) that will be displayed
|
||||
* on a Viewport Overlay, which contains the information panels on the
|
||||
* four corners of a viewport.
|
||||
*
|
||||
* @definition of a overlay item using this type
|
||||
* The value to be displayed is defined by
|
||||
* - setting DICOM image instance's property to this field,
|
||||
* - or defining contentF()
|
||||
*
|
||||
* {
|
||||
* id: string - unique id for the overlay item
|
||||
* customizationType: string - indicates customization type definition to this
|
||||
* label: string - Label, to be displayed for the item
|
||||
* title: string - Tooltip, for the item
|
||||
* color: string - Color of the text
|
||||
* condition: ({ instance }) => boolean - decides whether to display the overlay item or not
|
||||
* attribute: string - property name of the DICOM image instance
|
||||
* contentF: ({ instance, formatters }) => string | component,
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* id: 'PatientNameOverlay',
|
||||
* customizationType: 'ohif.overlayItem',
|
||||
* label: 'PN:',
|
||||
* title: 'Patient Name',
|
||||
* color: 'yellow',
|
||||
* condition: ({ instance }) => instance && instance.PatientName && instance.PatientName.Alphabetic,
|
||||
* attribute: 'PatientName',
|
||||
* contentF: ({ instance, formatters: { formatPN } }) => `${formatPN(instance.PatientName.Alphabetic)} ${(instance.PatientSex ? '(' + instance.PatientSex + ')' : '')}`,
|
||||
* },
|
||||
*
|
||||
* @see CustomizableViewportOverlay
|
||||
*/
|
||||
{
|
||||
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 (
|
||||
<span
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: this.color || undefined }}
|
||||
title={this.title || ''}
|
||||
>
|
||||
{this.label && <span className="mr-1 shrink-0">{this.label}</span>}
|
||||
<span className="font-light">{value}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ohif.contextMenu',
|
||||
|
||||
/** Applies the customizationType to all the menu items.
|
||||
* This function clones the object and child objects to prevent
|
||||
* changes to the original customization object.
|
||||
*/
|
||||
transform: function (customizationService: CustomizationService) {
|
||||
// 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;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
|
||||
id: 'ohif.dataSourceConfigurationComponent',
|
||||
component: DataSourceConfigurationComponent.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
// The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare
|
||||
id: 'ohif.dataSourceConfigurationAPI.google',
|
||||
factory: (dataSourceName: string) =>
|
||||
new GoogleCloudDataSourceConfigurationAPI(
|
||||
dataSourceName,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: 'progressDropdownWithServiceComponent',
|
||||
component: ProgressDropdownWithService,
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.sortFunctions',
|
||||
values: [
|
||||
{
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesNumber - b?.SeriesNumber;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Series Date',
|
||||
sortFunction: (a, b) => {
|
||||
const dateA = new Date(formatDate(a?.SeriesDate));
|
||||
const dateB = new Date(formatDate(b?.SeriesDate));
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.viewPresets',
|
||||
// change your default selected preset here
|
||||
value: [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.thumbnailMenuItems',
|
||||
value: [
|
||||
{
|
||||
id: 'tagBrowser',
|
||||
label: 'Tag Browser',
|
||||
iconName: 'DicomTagBrowser',
|
||||
commands: 'openDICOMTagViewer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ohif.menuContent',
|
||||
content: function (props) {
|
||||
const { item, commandsManager, servicesManager, ...rest } = props;
|
||||
|
||||
// If item has sub-items, render a submenu
|
||||
if (item.items) {
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="gap-[6px]">
|
||||
{item.iconName && (
|
||||
<Icons.ByName
|
||||
name={item.iconName}
|
||||
className="-ml-1"
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
{item.items.map(subItem => this.content({ ...props, item: subItem }))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
// Regular menu item
|
||||
const isDisabled = item.selector && !item.selector({ servicesManager });
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
disabled={isDisabled}
|
||||
onSelect={() => {
|
||||
commandsManager.runAsync(item.commands, {
|
||||
...item.commandOptions,
|
||||
...rest,
|
||||
});
|
||||
}}
|
||||
className="gap-[6px]"
|
||||
>
|
||||
{item.iconName && (
|
||||
<Icons.ByName
|
||||
name={item.iconName}
|
||||
className="-ml-1"
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
value: {
|
||||
...customRoutesCustomization,
|
||||
...studyBrowserCustomization,
|
||||
...overlayItemCustomization,
|
||||
...contextMenuCustomization,
|
||||
...menuContentCustomization,
|
||||
...getDataSourceConfigurationCustomization({ servicesManager, extensionManager }),
|
||||
...progressDropdownCustomization,
|
||||
...sortingCriteriaCustomization,
|
||||
...defaultContextMenuCustomization,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ function promptLabelAnnotation({ servicesManager }, ctx, evt) {
|
||||
const { measurementService, customizationService } = servicesManager.services;
|
||||
const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId } = evt;
|
||||
return new Promise(async function (resolve) {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const measurement = measurementService.getMeasurement(measurementId);
|
||||
const value = await showLabelAnnotationPopup(
|
||||
measurement,
|
||||
|
||||
@ -159,6 +159,7 @@ function TrackedMeasurementsContextProvider(
|
||||
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
}),
|
||||
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
|
||||
@ -173,11 +174,11 @@ function TrackedMeasurementsContextProvider(
|
||||
});
|
||||
machineOptions.guards = Object.assign({}, machineOptions.guards, {
|
||||
isLabelOnMeasure: (ctx, evt, condMeta) => {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
return labelConfig?.labelOnMeasure;
|
||||
},
|
||||
isLabelOnMeasureAndShouldKillMachine: (ctx, evt, condMeta) => {
|
||||
const labelConfig = customizationService.get('measurementLabels');
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
return evt.data && evt.data.userResponse === RESPONSE.NO_NEVER && labelConfig?.labelOnMeasure;
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
|
||||
function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig }: withAppTypes,
|
||||
ctx,
|
||||
evt
|
||||
) {
|
||||
@ -11,7 +11,7 @@ function hydrateStructuredReport(
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const hydrationResult = baseHydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
|
||||
@ -11,7 +11,11 @@ const RESPONSE = {
|
||||
HYDRATE_REPORT: 5,
|
||||
};
|
||||
|
||||
function promptHydrateStructuredReport({ servicesManager, extensionManager, appConfig }, ctx, evt) {
|
||||
function promptHydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig },
|
||||
ctx,
|
||||
evt
|
||||
) {
|
||||
const { uiViewportDialogService, displaySetService } = servicesManager.services;
|
||||
const { viewportId, displaySetInstanceUID } = evt;
|
||||
const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
@ -26,7 +30,7 @@ function promptHydrateStructuredReport({ servicesManager, extensionManager, appC
|
||||
if (promptResult === RESPONSE.HYDRATE_REPORT) {
|
||||
console.warn('!! HYDRATING STRUCTURED REPORT');
|
||||
const hydrationResult = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { Button, Icons } from '@ohif/ui-next';
|
||||
import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone';
|
||||
import { useTrackedMeasurements } from '../getContextModule';
|
||||
|
||||
const { filterAnd, filterPlanarMeasurement, filterAny, filterMeasurementsBySeriesUID } =
|
||||
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
|
||||
utils.MeasurementFilters;
|
||||
|
||||
function PanelMeasurementTableTracking({
|
||||
@ -21,13 +21,7 @@ function PanelMeasurementTableTracking({
|
||||
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
|
||||
: filterPlanarMeasurement;
|
||||
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelMeasurement.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -4,14 +4,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { useViewportGrid, DropdownMenu, DropdownMenuTrigger, Icons, Button } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { StudyBrowser } from '@ohif/ui-next';
|
||||
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { PanelStudyBrowserHeader, MoreDropdownMenu } from '@ohif/extension-default';
|
||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||
import { MoreDropdownMenu, PanelStudyBrowserHeader } from '@ohif/extension-default';
|
||||
import { defaultActionIcons } from './constants';
|
||||
const { formatDate, createStudyBrowserTabs } = utils;
|
||||
|
||||
const thumbnailNoImageModalities = [
|
||||
'SR',
|
||||
'SEG',
|
||||
@ -46,10 +47,7 @@ export default function PanelStudyBrowserTracking({
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const { mode: studyMode } = customizationService.getCustomization('PanelStudyBrowser.studyMode', {
|
||||
id: 'default',
|
||||
mode: 'all',
|
||||
});
|
||||
const studyMode = customizationService.getCustomization('studyBrowser.studyMode');
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
|
||||
@ -73,7 +71,7 @@ export default function PanelStudyBrowserTracking({
|
||||
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
|
||||
|
||||
const [viewPresets, setViewPresets] = useState(
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')
|
||||
);
|
||||
|
||||
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
|
||||
|
||||
@ -16,81 +16,79 @@
|
||||
* See https://dicom.nema.org/medical/dicom/current/output/html/part16.html
|
||||
* for definitions of SCT and other code values.
|
||||
*/
|
||||
const codingValues = {
|
||||
id: 'codingValues',
|
||||
export default {
|
||||
codingValues: {
|
||||
// Sites
|
||||
'SCT:69536005': {
|
||||
text: 'Head',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'red',
|
||||
},
|
||||
},
|
||||
'SCT:45048000': {
|
||||
text: 'Neck',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
'SCT:818981001': {
|
||||
text: 'Abdomen',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'orange',
|
||||
},
|
||||
},
|
||||
'SCT:816092008': {
|
||||
text: 'Pelvis',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'cyan',
|
||||
},
|
||||
},
|
||||
|
||||
// Sites
|
||||
'SCT:69536005': {
|
||||
text: 'Head',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'red',
|
||||
// Findings
|
||||
'SCT:371861004': {
|
||||
text: 'Mild intimal coronary irregularities',
|
||||
style: {
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
},
|
||||
'SCT:45048000': {
|
||||
text: 'Neck',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'blue',
|
||||
'SCT:194983005': {
|
||||
text: 'Aortic insufficiency',
|
||||
style: {
|
||||
color: 'darkred',
|
||||
},
|
||||
},
|
||||
},
|
||||
'SCT:818981001': {
|
||||
text: 'Abdomen',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'orange',
|
||||
'SCT:399232001': {
|
||||
text: '2-chamber',
|
||||
},
|
||||
},
|
||||
'SCT:816092008': {
|
||||
text: 'Pelvis',
|
||||
type: 'site',
|
||||
style: {
|
||||
color: 'cyan',
|
||||
'SCT:103340004': {
|
||||
text: 'SAX',
|
||||
},
|
||||
'SCT:91134007': {
|
||||
text: 'MV',
|
||||
},
|
||||
'SCT:122972007': {
|
||||
text: 'PV',
|
||||
},
|
||||
},
|
||||
|
||||
// Findings
|
||||
'SCT:371861004': {
|
||||
text: 'Mild intimal coronary irregularities',
|
||||
style: {
|
||||
color: 'green',
|
||||
// Orientations
|
||||
'SCT:24422004': {
|
||||
text: 'Axial',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
},
|
||||
'SCT:194983005': {
|
||||
text: 'Aortic insufficiency',
|
||||
style: {
|
||||
color: 'darkred',
|
||||
'SCT:81654009': {
|
||||
text: 'Coronal',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
'SCT:30730003': {
|
||||
text: 'Sagittal',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
},
|
||||
'SCT:399232001': {
|
||||
text: '2-chamber',
|
||||
},
|
||||
'SCT:103340004': {
|
||||
text: 'SAX',
|
||||
},
|
||||
'SCT:91134007': {
|
||||
text: 'MV',
|
||||
},
|
||||
'SCT:122972007': {
|
||||
text: 'PV',
|
||||
},
|
||||
|
||||
// Orientations
|
||||
'SCT:24422004': {
|
||||
text: 'Axial',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
'SCT:81654009': {
|
||||
text: 'Coronal',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
'SCT:30730003': {
|
||||
text: 'Sagittal',
|
||||
color: '#000000',
|
||||
type: 'orientation',
|
||||
},
|
||||
};
|
||||
|
||||
export default codingValues;
|
||||
|
||||
@ -1,26 +1,24 @@
|
||||
const codeMenuItem = {
|
||||
id: '@ohif/contextMenuAnnotationCode',
|
||||
|
||||
/** Applies the code value setup for this item */
|
||||
transform: function (customizationService) {
|
||||
const { code: codeRef } = this;
|
||||
if (!codeRef) {
|
||||
throw new Error(`item ${this} has no code ref`);
|
||||
}
|
||||
const codingValues = customizationService.get('codingValues');
|
||||
const code = codingValues[codeRef];
|
||||
return {
|
||||
...this,
|
||||
codeRef,
|
||||
code: { ref: codeRef, ...code },
|
||||
label: this.label || code.text || codeRef,
|
||||
commands: [
|
||||
{
|
||||
commandName: 'updateMeasurement',
|
||||
},
|
||||
],
|
||||
};
|
||||
export default {
|
||||
'@ohif/contextMenuAnnotationCode': {
|
||||
/** Applies the code value setup for this item */
|
||||
$transform: function (customizationService) {
|
||||
const { code: codeRef } = this;
|
||||
if (!codeRef) {
|
||||
throw new Error(`item ${this} has no code ref`);
|
||||
}
|
||||
const codingValues = customizationService.getCustomization('codingValues');
|
||||
const code = codingValues[codeRef];
|
||||
return {
|
||||
...this,
|
||||
codeRef,
|
||||
code: { ref: codeRef, ...code },
|
||||
label: this.label || code.text || codeRef,
|
||||
commands: [
|
||||
{
|
||||
commandName: 'updateMeasurement',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default codeMenuItem;
|
||||
|
||||
@ -1,100 +1,98 @@
|
||||
const findingsContextMenu = {
|
||||
id: 'measurementsContextMenu',
|
||||
customizationType: 'ohif.contextMenu',
|
||||
menus: [
|
||||
{
|
||||
id: 'forExistingMeasurement',
|
||||
// selector restricts context menu to when there is nearbyToolData
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
export default {
|
||||
measurementsContextMenu: {
|
||||
$set: {
|
||||
inheritsFrom: 'ohif.contextMenu',
|
||||
menus: [
|
||||
{
|
||||
customizationType: 'ohif.contextSubMenu',
|
||||
label: 'Site',
|
||||
actionType: 'ShowSubMenu',
|
||||
subMenu: 'siteSelectionSubMenu',
|
||||
},
|
||||
{
|
||||
customizationType: 'ohif.contextSubMenu',
|
||||
label: 'Finding',
|
||||
actionType: 'ShowSubMenu',
|
||||
subMenu: 'findingSelectionSubMenu',
|
||||
},
|
||||
{
|
||||
// customizationType is implicit here in the configuration setup
|
||||
label: 'Delete Measurement',
|
||||
commands: [
|
||||
// selector restricts context menu to when there is nearbyToolData
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
commandName: 'deleteMeasurement',
|
||||
label: 'Site',
|
||||
actionType: 'ShowSubMenu',
|
||||
subMenu: 'siteSelectionSubMenu',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setMeasurementLabel',
|
||||
label: 'Finding',
|
||||
actionType: 'ShowSubMenu',
|
||||
subMenu: 'findingSelectionSubMenu',
|
||||
},
|
||||
{
|
||||
// inheritsFrom is implicit here in the configuration setup
|
||||
label: 'Delete Measurement',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'deleteMeasurement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setMeasurementLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// The example below shows how to include a delegating sub-menu,
|
||||
// Only available on the @ohif/mnGrid hanging protocol
|
||||
// To demonstrate, select the 3x1 layout from the protocol menu
|
||||
// and right click on a measurement.
|
||||
{
|
||||
label: 'IncludeSubMenu',
|
||||
selector: ({ protocol }) => protocol?.id === '@ohif/mnGrid',
|
||||
delegating: true,
|
||||
subMenu: 'orientationSelectionSubMenu',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// The example below shows how to include a delegating sub-menu,
|
||||
// Only available on the @ohif/mnGrid hanging protocol
|
||||
// To demonstrate, select the 3x1 layout from the protocol menu
|
||||
// and right click on a measurement.
|
||||
{
|
||||
label: 'IncludeSubMenu',
|
||||
selector: ({ protocol }) => protocol?.id === '@ohif/mnGrid',
|
||||
delegating: true,
|
||||
subMenu: 'orientationSelectionSubMenu',
|
||||
id: 'orientationSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:24422004',
|
||||
},
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:81654009',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'orientationSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => false,
|
||||
items: [
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:24422004',
|
||||
id: 'findingSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:371861004',
|
||||
},
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:194983005',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:81654009',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'findingSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => false,
|
||||
items: [
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:371861004',
|
||||
},
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:194983005',
|
||||
id: 'siteSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:69536005',
|
||||
},
|
||||
{
|
||||
inheritsFrom: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:45048000',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'siteSelectionSubMenu',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:69536005',
|
||||
},
|
||||
{
|
||||
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||
code: 'SCT:45048000',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default findingsContextMenu;
|
||||
|
||||
@ -4,11 +4,17 @@ export default function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'custom-context-menu',
|
||||
value: [codingValues, contextMenuCodeItem, findingsContextMenu],
|
||||
value: {
|
||||
...codingValues,
|
||||
...contextMenuCodeItem,
|
||||
...findingsContextMenu,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'contextMenuCodeItem',
|
||||
value: [contextMenuCodeItem],
|
||||
value: {
|
||||
...contextMenuCodeItem,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -5,13 +5,12 @@ import { id } from './id';
|
||||
import hpTestSwitch from './hpTestSwitch';
|
||||
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
// import {setViewportZoomPan, storeViewportZoomPan } from './custom-viewport/setViewportZoomPan';
|
||||
import sameAs from './custom-attribute/sameAs';
|
||||
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
|
||||
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
|
||||
|
||||
/**
|
||||
* The test extension provides additional behaviour for testing various
|
||||
* The test extension provides additional behavior for testing various
|
||||
* customizations and settings for OHIF.
|
||||
*/
|
||||
const testExtension: Types.Extensions.Extension = {
|
||||
@ -20,7 +19,8 @@ const testExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
/** Register additional behaviour:
|
||||
/**
|
||||
* Register additional behavior:
|
||||
* * HP custom attribute seriesDescriptions to retrieve an array of all series descriptions
|
||||
* * HP custom attribute numberOfDisplaySets to retrieve the number of display sets
|
||||
* * HP custom attribute numberOfDisplaySetsWithImages to retrieve the number of display sets containing images
|
||||
|
||||
@ -67,7 +67,7 @@ export default function PanelRoiThresholdSegmentation({
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue.toFixed(3)} mL`}</div>
|
||||
<div className="text-white">{`${tmtvValue?.toFixed(3)} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@ -420,7 +420,13 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
return;
|
||||
}
|
||||
|
||||
return await workerManager.executeTask('suv-peak-worker', 'calculateTMTV', labelmapProps);
|
||||
const tmtv = await workerManager.executeTask(
|
||||
'suv-peak-worker',
|
||||
'calculateTMTV',
|
||||
labelmapProps
|
||||
);
|
||||
|
||||
return tmtv;
|
||||
},
|
||||
exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
|
||||
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
||||
|
||||
@ -86,7 +86,7 @@ function modeFactory() {
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
// init customizations
|
||||
customizationService.addModeCustomizations([
|
||||
customizationService.setCustomizations([
|
||||
'@ohif/extension-test.customizationModule.custom-context-menu',
|
||||
]);
|
||||
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuLabel,
|
||||
Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
export const performCustomizations = customizationService => {
|
||||
// Set the custom SegmentationTable
|
||||
customizationService.addModeCustomizations([
|
||||
// To disable editing in the SegmentationTable
|
||||
{
|
||||
id: 'PanelSegmentation.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
// to only show current study in the panel study browser
|
||||
// {
|
||||
// id: 'PanelStudyBrowser.studyMode',
|
||||
// mode: 'primary',
|
||||
// },
|
||||
// To disable editing in the MeasurementTable
|
||||
// {
|
||||
// id: 'PanelMeasurement.disableEditing',
|
||||
// disableEditing: true,
|
||||
// },
|
||||
// {
|
||||
// id: 'measurementLabels',
|
||||
// labelOnMeasure: true,
|
||||
// exclusive: true,
|
||||
// items: [
|
||||
// { value: 'Head', label: 'Head' },
|
||||
// { value: 'Shoulder', label: 'Shoulder' },
|
||||
// { value: 'Knee', label: 'Knee' },
|
||||
// { value: 'Toe', label: 'Toe' },
|
||||
// ],
|
||||
// },
|
||||
/**
|
||||
* Custom Dropdown Menu Item
|
||||
*/
|
||||
// {
|
||||
// id: 'PanelSegmentation.CustomDropdownMenuContent',
|
||||
// content: ({
|
||||
// activeSegmentation,
|
||||
// onSegmentationAdd,
|
||||
// onSegmentationRemoveFromViewport,
|
||||
// onSegmentationEdit,
|
||||
// onSegmentationDelete,
|
||||
// allowExport,
|
||||
// storeSegmentation,
|
||||
// onSegmentationDownload,
|
||||
// onSegmentationDownloadRTSS,
|
||||
// t,
|
||||
// }) => (
|
||||
// <DropdownMenuContent align="start">
|
||||
// <DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.id)}>
|
||||
// <Icons.Delete className="text-red-600" />
|
||||
// <span className="pl-2 text-red-600">{t('My Custom Dropdown Menu Item')}</span>
|
||||
// </DropdownMenuItem>
|
||||
// </DropdownMenuContent>
|
||||
// ),
|
||||
// },
|
||||
]);
|
||||
};
|
||||
@ -4,7 +4,6 @@ import { id } from './id';
|
||||
import initToolGroups from './initToolGroups';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import moreTools from './moreTools';
|
||||
import { performCustomizations } from './customizations';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
@ -89,15 +88,12 @@ function modeFactory({ modeConfiguration }) {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
panelService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
performCustomizations(customizationService);
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig);
|
||||
|
||||
|
||||
@ -62,36 +62,19 @@ function modeFactory({ modeConfiguration }) {
|
||||
|
||||
// the primary button section is created in the workflow steps
|
||||
// specific to the step
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
customizationService.setCustomizations({
|
||||
'panelSegmentation.tableMode': {
|
||||
$set: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
'panelSegmentation.onSegmentationAdd': {
|
||||
$set: () => {
|
||||
commandsManager.run('createNewLabelMapForDynamicVolume');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.showAddSegment',
|
||||
showAddSegment: false,
|
||||
'panelSegmentation.showAddSegment': {
|
||||
$set: false,
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.readableText',
|
||||
// remove following if you are not interested in that stats
|
||||
readableText: {
|
||||
lesionStats: 'Lesion Statistics',
|
||||
minValue: 'Minimum Value',
|
||||
maxValue: 'Maximum Value',
|
||||
meanValue: 'Mean Value (ml)',
|
||||
volume: 'Volume',
|
||||
suvPeak: 'SUV Peak',
|
||||
suvMax: 'Maximum SUV',
|
||||
suvMaxIJK: 'SUV Max IJK',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Auto play the clip initially when the volumes are loaded
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||
|
||||
@ -101,33 +101,16 @@ function modeFactory({ modeConfiguration }) {
|
||||
'BrushTools',
|
||||
]);
|
||||
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
customizationService.setCustomizations({
|
||||
'panelSegmentation.tableMode': {
|
||||
$set: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
'panelSegmentation.onSegmentationAdd': {
|
||||
$set: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.readableText',
|
||||
// remove following if you are not interested in that stats
|
||||
readableText: {
|
||||
lesionStats: 'Lesion Statistics',
|
||||
minValue: 'Minimum Value',
|
||||
maxValue: 'Maximum Value',
|
||||
meanValue: 'Mean Value',
|
||||
volume: 'Volume (ml)',
|
||||
suvPeak: 'SUV Peak',
|
||||
suvMax: 'Maximum SUV',
|
||||
suvMaxIJK: 'SUV Max IJK',
|
||||
lesionGlyoclysisStats: 'Lesion Glycolysis',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// For the hanging protocol we need to decide on the window level
|
||||
// based on whether the SUV is corrected or not, hence we can't hard
|
||||
|
||||
@ -15,52 +15,6 @@ window.config = {
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.datasources',
|
||||
'@ohif/extension-default.customizationModule.helloPage',
|
||||
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
defaultDataSourceName: 'e2e',
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import getStudies from './studiesList';
|
||||
import { DicomMetadataStore, log } from '@ohif/core';
|
||||
import { DicomMetadataStore, log, utils, Enums } from '@ohif/core';
|
||||
import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
|
||||
|
||||
import { utils, Enums } from '@ohif/core';
|
||||
|
||||
const { sortingCriteria, getSplitParam } = utils;
|
||||
const { getSplitParam } = utils;
|
||||
|
||||
/**
|
||||
* Initialize the route.
|
||||
@ -85,9 +83,7 @@ export async function defaultRouteInit(
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
returnPromises: true,
|
||||
sortCriteria:
|
||||
customizationService.get('sortingCriteria') ||
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
sortCriteria: customizationService.getCustomization('sortingCriteria'),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@ -525,8 +525,7 @@ function WorkList({
|
||||
}
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
const { component: DicomUploadComponent } =
|
||||
customizationService.getCustomization('dicomUploadComponent') || {};
|
||||
const DicomUploadComponent = customizationService.getCustomization('dicomUploadComponent');
|
||||
|
||||
const uploadProps =
|
||||
DicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled
|
||||
@ -554,8 +553,9 @@ function WorkList({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { component: dataSourceConfigurationComponent } =
|
||||
customizationService.get('ohif.dataSourceConfigurationComponent') ?? {};
|
||||
const dataSourceConfigurationComponent = customizationService.getCustomization(
|
||||
'ohif.dataSourceConfigurationComponent'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-black">
|
||||
|
||||
@ -108,7 +108,8 @@ const createRoutes = ({
|
||||
props: { children: WorkList, servicesManager, extensionManager },
|
||||
};
|
||||
|
||||
const customRoutes = customizationService.getGlobalCustomization('customRoutes');
|
||||
const customRoutes = customizationService.getCustomization('routes.customRoutes');
|
||||
|
||||
const allRoutes = [
|
||||
...routes,
|
||||
...(showStudyList ? [WorkListRoute] : []),
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
"dicomweb-client": "^0.10.4",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
|
||||
@ -94,16 +94,29 @@ export class CommandsManager {
|
||||
* @param {CommandDefinition} definition - {@link CommandDefinition}
|
||||
*/
|
||||
registerCommand(contextName, commandName, definition) {
|
||||
if (typeof definition !== 'object') {
|
||||
if (typeof definition !== 'object' && typeof definition !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and restrict keys to prevent prototype pollution
|
||||
const isSafeKey = key => {
|
||||
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
||||
};
|
||||
|
||||
if (!isSafeKey(contextName) || !isSafeKey(commandName)) {
|
||||
throw new Error('Invalid key name to prevent prototype pollution');
|
||||
}
|
||||
|
||||
const context = this.getContext(contextName);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
context[commandName] = definition;
|
||||
if (typeof definition === 'function') {
|
||||
context[commandName] = { commandFn: definition, options: {} };
|
||||
} else {
|
||||
context[commandName] = definition;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,6 +151,11 @@ export class CommandsManager {
|
||||
* @param {String} [contextName]
|
||||
*/
|
||||
public runCommand(commandName: string, options = {}, contextName?: string | string[]) {
|
||||
if (typeof commandName === 'function') {
|
||||
// If commandName is a function, run it directly
|
||||
return commandName(options);
|
||||
}
|
||||
|
||||
const definition = this.getCommand(commandName, contextName);
|
||||
if (!definition) {
|
||||
log.warn(`Command "${commandName}" not found in current context`);
|
||||
@ -147,7 +165,7 @@ export class CommandsManager {
|
||||
const { commandFn } = definition;
|
||||
const commandParams = Object.assign(
|
||||
{},
|
||||
definition.options, // "Command configuration"
|
||||
definition.options || {}, // "Command configuration"
|
||||
options // "Time of call" info
|
||||
);
|
||||
|
||||
@ -159,13 +177,16 @@ export class CommandsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static convertCommands(toRun: Command | Commands | Command[] | string) {
|
||||
public static convertCommands(toRun: Command | Commands | Command[] | string | Function) {
|
||||
if (typeof toRun === 'string') {
|
||||
return [{ commandName: toRun }];
|
||||
}
|
||||
if ('commandName' in toRun) {
|
||||
return [toRun as ComplexCommand];
|
||||
}
|
||||
if (typeof toRun === 'function') {
|
||||
return [{ commandName: toRun }];
|
||||
}
|
||||
if ('commands' in toRun) {
|
||||
const commandsInput = (toRun as Commands).commands;
|
||||
return this.convertCommands(commandsInput);
|
||||
@ -203,7 +224,7 @@ export class CommandsManager {
|
||||
*
|
||||
* Example commands to run are:
|
||||
* * 'updateMeasurement'
|
||||
* * `{commandName: 'displayWhatever'}`
|
||||
* * `{ commandName: 'displayWhatever'}`
|
||||
* * `['updateMeasurement', {commandName: 'displayWhatever'}]`
|
||||
* * `{ commands: 'updateMeasurement' }`
|
||||
* * `{ commands: ['updateMeasurement', {commandName: 'displayWhatever'}]}`
|
||||
|
||||
@ -1,313 +1,497 @@
|
||||
import CustomizationService, { CustomizationType, MergeEnum } from './CustomizationService';
|
||||
import log from '../../log';
|
||||
|
||||
jest.mock('../../log.js', () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
// File: CustomizationService.registrationAndOperations.test.js
|
||||
import CustomizationService, { CustomizationScope } from './CustomizationService';
|
||||
|
||||
const commandsManager = {};
|
||||
const extensionManager = {
|
||||
registeredExtensionIds: [],
|
||||
moduleEntries: {},
|
||||
|
||||
getRegisteredExtensionIds: () => extensionManager.registeredExtensionIds,
|
||||
|
||||
getModuleEntry: function (id) {
|
||||
return this.moduleEntries[id];
|
||||
},
|
||||
};
|
||||
|
||||
const commandsManager = {};
|
||||
const noop = () => {};
|
||||
|
||||
const ohifOverlayItem = {
|
||||
id: 'ohif.overlayItem',
|
||||
content: function (props) {
|
||||
return {
|
||||
label: this.label,
|
||||
value: props[this.attribute],
|
||||
ver: 'default',
|
||||
};
|
||||
},
|
||||
};
|
||||
// A helper default customization module that mimics the structure returned by the module.
|
||||
function getDefaultCustomizationModule() {
|
||||
return {
|
||||
// Simple types
|
||||
showAddSegment: true,
|
||||
somethingFalse: false,
|
||||
onAddSegment: () => 'default add',
|
||||
// Array of primitives
|
||||
NumbersList: [1, 2, 3, 4],
|
||||
// Object
|
||||
SeriesInfo: {
|
||||
label: 'Series Date',
|
||||
sortFunction: noop,
|
||||
views: ['sagittal', 'coronal', 'axial'],
|
||||
advanced: {
|
||||
subKey: 'original',
|
||||
anotherKey: 42,
|
||||
},
|
||||
},
|
||||
// Array of objects
|
||||
studyBrowser: [
|
||||
{
|
||||
id: 'seriesDate',
|
||||
label: 'Series Date',
|
||||
sortFunction: noop,
|
||||
},
|
||||
],
|
||||
advanced: {
|
||||
firstLabel: 'hello',
|
||||
functions: [
|
||||
{
|
||||
id: 'seriesDate',
|
||||
label: 'Series Date',
|
||||
sortFunction: () => {},
|
||||
viewFunctions: [
|
||||
{ id: 'sagittal', label: 'Sagittal', sortFunction: () => {} },
|
||||
{ id: 'coronal', label: 'Coronal', sortFunction: () => {} },
|
||||
{ id: 'axial', label: 'Axial', sortFunction: () => {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const testItem = {
|
||||
id: 'testItem',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
attribute: 'testAttribute',
|
||||
label: 'testItemLabel',
|
||||
};
|
||||
|
||||
describe('CustomizationService.ts', () => {
|
||||
describe('CustomizationService - Registration + API Operations', () => {
|
||||
let customizationService;
|
||||
|
||||
let configuration;
|
||||
|
||||
beforeEach(() => {
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
configuration = {};
|
||||
customizationService = new CustomizationService({
|
||||
configuration,
|
||||
commandsManager,
|
||||
});
|
||||
extensionManager.registeredExtensionIds = [];
|
||||
extensionManager.moduleEntries = {};
|
||||
customizationService = new CustomizationService({ commandsManager, configuration: {} });
|
||||
|
||||
// Simulate default registrations.
|
||||
customizationService.addReferences(getDefaultCustomizationModule(), CustomizationScope.Default);
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('init succeeds', () => {
|
||||
customizationService.init(extensionManager);
|
||||
afterEach(() => {
|
||||
customizationService.onModeExit();
|
||||
});
|
||||
|
||||
// Check that defaults are registered
|
||||
it('has registered default customizations', () => {
|
||||
const defaultShowAddSegment = customizationService.getCustomization('showAddSegment');
|
||||
expect(defaultShowAddSegment).toBe(true);
|
||||
|
||||
const defaultNumbersList = customizationService.getCustomization('NumbersList');
|
||||
expect(defaultNumbersList).toEqual([1, 2, 3, 4]);
|
||||
|
||||
const defaultSeriesInfo = customizationService.getCustomization('SeriesInfo');
|
||||
expect(defaultSeriesInfo.label).toBe('Series Date');
|
||||
expect(defaultSeriesInfo.advanced.subKey).toBe('original');
|
||||
|
||||
const defaultStudyBrowser = customizationService.getCustomization('studyBrowser');
|
||||
expect(Array.isArray(defaultStudyBrowser)).toBe(true);
|
||||
expect(defaultStudyBrowser.length).toBe(1);
|
||||
|
||||
//
|
||||
const advanced = customizationService.getCustomization('advanced');
|
||||
expect(advanced.firstLabel).toBe('hello');
|
||||
expect(advanced.functions.length).toBe(1);
|
||||
expect(advanced.functions[0].id).toBe('seriesDate');
|
||||
expect(advanced.functions[0].viewFunctions.length).toBe(3);
|
||||
expect(advanced.functions[0].viewFunctions[0].id).toBe('sagittal');
|
||||
expect(advanced.functions[0].viewFunctions[1].id).toBe('coronal');
|
||||
expect(advanced.functions[0].viewFunctions[2].id).toBe('axial');
|
||||
});
|
||||
|
||||
// 1. Simple Data Types
|
||||
describe('Simple Data Types', () => {
|
||||
it('replaces boolean value using $set over the default', () => {
|
||||
// Update the default value with a new one using $set.
|
||||
customizationService.setCustomizations({
|
||||
showAddSegment: { $set: false },
|
||||
});
|
||||
const result = customizationService.getCustomization('showAddSegment');
|
||||
|
||||
// Mode/global should override the default.
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('configurationRegistered', () => {
|
||||
configuration.testItem = testItem;
|
||||
customizationService.init(extensionManager);
|
||||
expect(customizationService.getGlobalCustomization('testItem')).toBe(testItem);
|
||||
it('replaces boolean value using $set over the default false', () => {
|
||||
// Update the default value with a new one using $set.
|
||||
customizationService.setCustomizations({
|
||||
somethingFalse: { $set: true },
|
||||
});
|
||||
const result = customizationService.getCustomization('somethingFalse');
|
||||
|
||||
// Mode/global should override the default.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('defaultRegistered', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||
name: 'default',
|
||||
value: [testItem],
|
||||
};
|
||||
customizationService.init(extensionManager);
|
||||
expect(customizationService.getGlobalCustomization('testItem')).toBe(testItem);
|
||||
it('replaces function value using $set over the default', () => {
|
||||
// Original default returns "default add"
|
||||
const original = customizationService.getCustomization('onAddSegment');
|
||||
expect(original()).toBe('default add');
|
||||
|
||||
// Now update the function
|
||||
customizationService.setCustomizations({
|
||||
onAddSegment: { $set: () => 999 },
|
||||
});
|
||||
const updated = customizationService.getCustomization('onAddSegment');
|
||||
expect(updated()).toBe(999);
|
||||
});
|
||||
|
||||
it('replaces two properties at once', () => {
|
||||
// Original default returns "default add"
|
||||
const original = customizationService.getCustomization('onAddSegment');
|
||||
expect(original()).toBe('default add');
|
||||
|
||||
// Now update the function
|
||||
customizationService.setCustomizations({
|
||||
onAddSegment: { $set: () => 998 },
|
||||
showAddSegment: { $set: false },
|
||||
});
|
||||
expect(customizationService.getCustomization('onAddSegment')).toBeDefined();
|
||||
expect(customizationService.getCustomization('showAddSegment')).toBe(false);
|
||||
expect(customizationService.getCustomization('onAddSegment')()).toBe(998);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customizationType', () => {
|
||||
it('inherits type', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||
name: 'default',
|
||||
value: [ohifOverlayItem],
|
||||
};
|
||||
configuration.testItem = testItem;
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
const item = customizationService.getGlobalCustomization('testItem');
|
||||
|
||||
const props = { testAttribute: 'testAttrValue' };
|
||||
const result = item.content(props);
|
||||
expect(result.label).toBe(testItem.label);
|
||||
expect(result.value).toBe(props.testAttribute);
|
||||
expect(result.ver).toBe('default');
|
||||
// 2. Arrays of Primitives
|
||||
describe('Arrays of Primitives', () => {
|
||||
it('replaces entire array with $set over default', () => {
|
||||
customizationService.setCustomizations({
|
||||
NumbersList: { $set: [5, 6, 7, 8, 9] },
|
||||
});
|
||||
const result = customizationService.getCustomization('NumbersList');
|
||||
expect(result).toEqual([5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('inline default inherits type', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||
name: 'default',
|
||||
value: [ohifOverlayItem],
|
||||
};
|
||||
configuration.testItem = testItem;
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
const item = customizationService.getCustomization('testItem2', {
|
||||
id: 'testItem2',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'otherLabel',
|
||||
attribute: 'otherAttr',
|
||||
it('applies $push, $unshift, and $splice to default array', () => {
|
||||
// Update array using merge commands
|
||||
customizationService.setCustomizations({
|
||||
NumbersList: {
|
||||
$push: [5, 6],
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('NumbersList');
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
// Customizes the default value, as this is testItem2
|
||||
const props = { otherAttr: 'other attribute value' };
|
||||
const result = item.content(props);
|
||||
expect(result.label).toBe('otherLabel');
|
||||
expect(result.value).toBe(props.otherAttr);
|
||||
expect(result.ver).toBe('default');
|
||||
it('applies $push, $unshift, and $splice to default array', () => {
|
||||
// Update array using merge commands
|
||||
customizationService.setCustomizations({
|
||||
NumbersList: {
|
||||
$unshift: [0],
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('NumbersList');
|
||||
expect(result).toEqual([0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('applies $push, $unshift, and $splice to default array', () => {
|
||||
// Update array using merge commands
|
||||
customizationService.setCustomizations({
|
||||
NumbersList: {
|
||||
$splice: [
|
||||
[2, 1, 99], // At index 2, remove
|
||||
],
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('NumbersList');
|
||||
expect(result).toEqual([1, 2, 99, 4]);
|
||||
});
|
||||
|
||||
it('applies $push, $unshift, and $splice to default array', () => {
|
||||
// Update array using merge commands
|
||||
customizationService.setCustomizations({
|
||||
NumbersList: {
|
||||
$push: [5, 6],
|
||||
$unshift: [0],
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('NumbersList');
|
||||
|
||||
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mode customization', () => {
|
||||
it('onModeEnter can add extensions', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||
name: 'default',
|
||||
value: [ohifOverlayItem],
|
||||
};
|
||||
customizationService.init(extensionManager);
|
||||
// 3. Objects
|
||||
describe('Objects', () => {
|
||||
it('replaces entire object with $set', () => {
|
||||
customizationService.setCustomizations({
|
||||
SeriesInfo: {
|
||||
$set: {
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber,
|
||||
views: ['3D'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('SeriesInfo');
|
||||
|
||||
expect(customizationService.getModeCustomization('testItem')).toBeUndefined();
|
||||
|
||||
customizationService.addModeCustomizations([testItem]);
|
||||
|
||||
expect(customizationService.getGlobalCustomization('testItem')).toBeUndefined();
|
||||
|
||||
const item = customizationService.getModeCustomization('testItem');
|
||||
expect(item).not.toBeUndefined();
|
||||
|
||||
const props = { testAttribute: 'testAttrValue' };
|
||||
const result = item.content(props);
|
||||
expect(result.label).toBe(testItem.label);
|
||||
expect(result.value).toBe(props.testAttribute);
|
||||
expect(result.ver).toBe('default');
|
||||
expect(result.label).toBe('Series Number');
|
||||
expect(result.sortFunction).not.toEqual(noop);
|
||||
expect(result.views).toEqual(['3D']);
|
||||
});
|
||||
|
||||
it('global customizations override modes', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.global'] = {
|
||||
name: 'default',
|
||||
value: [ohifOverlayItem],
|
||||
};
|
||||
configuration.testItem = testItem;
|
||||
customizationService.init(extensionManager);
|
||||
it('merges object fields with $merge over default', () => {
|
||||
// Merge basic fields (in mode should override defaults)
|
||||
customizationService.setCustomizations({
|
||||
SeriesInfo: {
|
||||
$merge: {
|
||||
label: 'New Label',
|
||||
extraField: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
let result = customizationService.getCustomization('SeriesInfo');
|
||||
|
||||
// Add a mode customization that would otherwise fail below
|
||||
customizationService.addModeCustomizations([{ ...testItem, label: 'other' }]);
|
||||
expect(result.label).toBe('New Label');
|
||||
expect(result.extraField).toBe(true);
|
||||
|
||||
const item = customizationService.getModeCustomization('testItem');
|
||||
|
||||
const props = { testAttribute: 'testAttrValue' };
|
||||
const result = item.content(props);
|
||||
expect(result.label).toBe(testItem.label);
|
||||
expect(result.value).toBe(props.testAttribute);
|
||||
// Merge deeper nested fields on the "advanced" property.
|
||||
customizationService.setCustomizations({
|
||||
SeriesInfo: {
|
||||
advanced: {
|
||||
$merge: {
|
||||
subKey: 'updatedSubValue',
|
||||
newSubKey: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
result = customizationService.getCustomization('SeriesInfo');
|
||||
expect(result.advanced.subKey).toBe('updatedSubValue');
|
||||
expect(result.advanced.newSubKey).toBe(123);
|
||||
expect(result.advanced.anotherKey).toBe(42);
|
||||
});
|
||||
|
||||
it('mode customizations override default', () => {
|
||||
extensionManager.registeredExtensionIds.push('@testExtension');
|
||||
extensionManager.moduleEntries['@testExtension.customizationModule.default'] = {
|
||||
name: 'default',
|
||||
value: [ohifOverlayItem, testItem],
|
||||
};
|
||||
customizationService.init(extensionManager);
|
||||
it('applies a function to modify a property with $apply', () => {
|
||||
customizationService.setCustomizations({
|
||||
SeriesInfo: {
|
||||
$apply: oldValue => ({
|
||||
...oldValue,
|
||||
label: 'Series Number (via $apply)',
|
||||
}),
|
||||
},
|
||||
});
|
||||
const result = customizationService.getCustomization('SeriesInfo');
|
||||
|
||||
// Add a mode customization that would otherwise fail below
|
||||
customizationService.addModeCustomizations([{ ...testItem, label: 'other' }]);
|
||||
|
||||
const item = customizationService.getCustomization('testItem');
|
||||
|
||||
const props = { testAttribute: 'testAttrValue' };
|
||||
const result = item.content(props);
|
||||
expect(result.label).toBe('other');
|
||||
expect(result.value).toBe(props.testAttribute);
|
||||
expect(result.label).toBe('Series Number (via $apply)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge', () => {
|
||||
it('appends to global configuration', () => {
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
customizationService.setGlobalCustomization('appendSet', {
|
||||
values: [{ id: 'one' }, { id: 'two' }],
|
||||
});
|
||||
const appendSet = customizationService.getCustomization('appendSet');
|
||||
expect(appendSet.values.length).toBe(2);
|
||||
|
||||
customizationService.setGlobalCustomization(
|
||||
'appendSet',
|
||||
{
|
||||
values: [{ id: 'three' }],
|
||||
// 4. Arrays of Objects
|
||||
describe('Arrays of Objects', () => {
|
||||
it('replaces entire array of objects using $set', () => {
|
||||
customizationService.setCustomizations({
|
||||
studyBrowser: {
|
||||
$set: [
|
||||
{
|
||||
id: 'seriesNumber',
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber,
|
||||
},
|
||||
{
|
||||
id: 'seriesDate',
|
||||
label: 'Series Date',
|
||||
sortFunction: (a, b) => new Date(b?.SeriesDate) - new Date(a?.SeriesDate),
|
||||
},
|
||||
],
|
||||
},
|
||||
MergeEnum.Append
|
||||
);
|
||||
const appendSet2 = customizationService.getCustomization('appendSet');
|
||||
expect(appendSet2.values.length).toBe(3);
|
||||
});
|
||||
const result = customizationService.getCustomization('studyBrowser');
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].label).toBe('Series Number');
|
||||
expect(result[1].label).toBe('Series Date');
|
||||
});
|
||||
|
||||
it('appends mode to default without touching default', () => {
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
customizationService.setDefaultCustomization('appendSet', {
|
||||
values: [{ id: 'one' }, { id: 'two' }],
|
||||
});
|
||||
const appendSet = customizationService.get('appendSet');
|
||||
expect(appendSet.values.length).toBe(2);
|
||||
|
||||
customizationService.setModeCustomization(
|
||||
'appendSet',
|
||||
{
|
||||
values: [{ id: 'three' }],
|
||||
it('updates array of objects with $push and $splice', () => {
|
||||
// Append a new item using $push
|
||||
customizationService.setCustomizations({
|
||||
studyBrowser: {
|
||||
$push: [
|
||||
{
|
||||
id: 'seriesNumber',
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber,
|
||||
},
|
||||
],
|
||||
},
|
||||
MergeEnum.Append
|
||||
);
|
||||
});
|
||||
|
||||
expect(appendSet.values.length).toBe(2);
|
||||
const appendSet2 = customizationService.getModeCustomization('appendSet');
|
||||
expect(appendSet2.values.length).toBe(3);
|
||||
let result = customizationService.getCustomization('studyBrowser');
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].label).toBe('Series Date');
|
||||
expect(result[1].label).toBe('Series Number');
|
||||
|
||||
// Insert at index 1 with $splice
|
||||
customizationService.setCustomizations({
|
||||
studyBrowser: {
|
||||
$splice: [
|
||||
[
|
||||
1,
|
||||
0,
|
||||
{
|
||||
id: 'anotherItem',
|
||||
label: 'Another Item',
|
||||
sortFunction: noop,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
result = customizationService.getCustomization('studyBrowser');
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0].label).toBe('Series Date');
|
||||
expect(result[1].label).toBe('Another Item');
|
||||
});
|
||||
});
|
||||
|
||||
// 5. Advanced Nested Structures
|
||||
describe('Advanced Nested Structures', () => {
|
||||
it('updates first level properties in advanced object', () => {
|
||||
customizationService.setCustomizations({
|
||||
advanced: {
|
||||
firstLabel: {
|
||||
$set: 'newLabel',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = customizationService.getCustomization('advanced');
|
||||
expect(result.firstLabel).toBe('newLabel');
|
||||
expect(result.functions).toBeDefined();
|
||||
});
|
||||
|
||||
it('merges values by name/position', () => {
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
customizationService.setDefaultCustomization('appendSet', {
|
||||
values: [{ id: 'one', obj: { v: '5' }, list: [1, 2, 3] }, { id: 'two' }],
|
||||
});
|
||||
const appendSet = customizationService.get('appendSet');
|
||||
expect(appendSet.values.length).toBe(2);
|
||||
|
||||
customizationService.setModeCustomization(
|
||||
'appendSet',
|
||||
{
|
||||
values: [{ id: 'three', obj: { v: 2 }, list: [3, 2, 1, 4] }],
|
||||
it('updates nested objects within functions array using $filter and $merge', () => {
|
||||
customizationService.setCustomizations({
|
||||
advanced: {
|
||||
// filter an object that is inside the advanced object
|
||||
// and then merge the object
|
||||
$filter: {
|
||||
match: { id: 'seriesDate' },
|
||||
$merge: {
|
||||
label: 'Series Data (via $filter)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MergeEnum.Merge,
|
||||
);
|
||||
});
|
||||
|
||||
const appendSet2 = customizationService.get('appendSet');
|
||||
const [value0] = appendSet2.values;
|
||||
expect(value0.id).toBe('three');
|
||||
expect(value0.list).toEqual([3, 2, 1, 4]);
|
||||
const result = customizationService.getCustomization('advanced');
|
||||
|
||||
expect(result.functions.length).toBe(1);
|
||||
expect(result.functions[0].label).toBe('Series Data (via $filter)');
|
||||
});
|
||||
|
||||
it('merges functions', () => {
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
customizationService.setDefaultCustomization('appendSet', {
|
||||
values: [{ f: () => 0, id: '0' }, { f: () => 5, id: '5' }],
|
||||
it('updates deeply nested view functions using $filter', () => {
|
||||
customizationService.setCustomizations({
|
||||
advanced: {
|
||||
$filter: {
|
||||
match: { id: 'axial' },
|
||||
$merge: {
|
||||
label: 'Axial (via $filter)',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const appendSet = customizationService.get('appendSet');
|
||||
expect(appendSet.values.length).toBe(2);
|
||||
|
||||
customizationService.setModeCustomization(
|
||||
'appendSet',
|
||||
{
|
||||
values: [{ f: () => 2, id: '2' }]
|
||||
},
|
||||
MergeEnum.Merge,
|
||||
);
|
||||
const result = customizationService.getCustomization('advanced');
|
||||
|
||||
const appendSet2 = customizationService.get('appendSet');
|
||||
const [value0, value1] = appendSet2.values;
|
||||
expect(value0.f()).toBe(2);
|
||||
expect(value1.f()).toBe(5);
|
||||
expect(result.functions.length).toBe(1);
|
||||
expect(result.functions[0].viewFunctions[2].label).toBe('Axial (via $filter)');
|
||||
});
|
||||
});
|
||||
|
||||
it('merges list with object', () => {
|
||||
customizationService.init(extensionManager);
|
||||
// 6. Multiple Default Registrations
|
||||
describe('Multiple Default Registrations', () => {
|
||||
it('allows subsequent default registrations to enhance previous ones', () => {
|
||||
customizationService = new CustomizationService({ commandsManager, configuration: {} });
|
||||
|
||||
const destination = [
|
||||
1,
|
||||
{ id: 'two', value: 2, list: [5, 6], },
|
||||
{ id: 'three', value: 3 }
|
||||
];
|
||||
|
||||
const source = {
|
||||
two: { value: 'updated2', list: { 0: 8 } },
|
||||
1: { extraValue: 2, list: [7], },
|
||||
1.0001: { id: 'inserted', value: 1.0001 },
|
||||
'-1': {
|
||||
value: -3
|
||||
},
|
||||
// First extension registers its defaults
|
||||
const firstExtensionDefaults = {
|
||||
simpleList: [1, 2, 3],
|
||||
};
|
||||
customizationService.addReferences(firstExtensionDefaults, CustomizationScope.Default);
|
||||
|
||||
customizationService.setDefaultCustomization('appendSet', {
|
||||
values: destination,
|
||||
// Second extension enhances the first one's defaults
|
||||
const secondExtensionDefaults = {
|
||||
simpleList: { $push: [4, 5] },
|
||||
};
|
||||
customizationService.addReferences(secondExtensionDefaults, CustomizationScope.Default);
|
||||
|
||||
// Verify the final state combines both extensions' contributions
|
||||
const result = customizationService.getCustomization('simpleList');
|
||||
expect(result).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomizationService - Inheritance (`inheritsFrom`)', () => {
|
||||
it('inherits properties from the parent customization', () => {
|
||||
// Register a parent customization
|
||||
customizationService.setCustomizations(
|
||||
{
|
||||
'test.overlayItem': {
|
||||
label: 'Default Label',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
CustomizationScope.Default
|
||||
);
|
||||
|
||||
// Register a child customization with `inheritsFrom`
|
||||
customizationService.setCustomizations({
|
||||
'viewportOverlay.topLeft.StudyDate': {
|
||||
$set: {
|
||||
inheritsFrom: 'test.overlayItem',
|
||||
label: 'Study Date',
|
||||
title: ' date',
|
||||
},
|
||||
},
|
||||
});
|
||||
customizationService.setModeCustomization('appendSet', {
|
||||
values: source,
|
||||
}, MergeEnum.Append);
|
||||
|
||||
const { values } = customizationService.getCustomization('appendSet');
|
||||
const [zero, one, two, three] = values;
|
||||
expect(zero).toBe(1);
|
||||
expect(one.value).toBe('updated2');
|
||||
expect(one.extraValue).toBe(2);
|
||||
expect(one.list).toEqual([8, 6, 7]);
|
||||
expect(two.id).toBe('inserted');
|
||||
expect(three.value).toBe(-3);
|
||||
const customization = customizationService.getCustomization(
|
||||
'viewportOverlay.topLeft.StudyDate'
|
||||
);
|
||||
|
||||
// Check that the inherited and overridden properties exist
|
||||
expect(customization.label).toBe('Study Date'); // Overridden
|
||||
expect(customization.color).toBe('blue'); // Inherited
|
||||
});
|
||||
|
||||
it('executes transform methods from the parent customization', () => {
|
||||
// Register a parent customization
|
||||
customizationService.setCustomizations(
|
||||
{
|
||||
'test.overlayItem': {
|
||||
$set: {
|
||||
$transform: function () {
|
||||
return {
|
||||
label: this.label,
|
||||
additionalKey: 'transformedValue',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CustomizationScope.Default
|
||||
);
|
||||
|
||||
// Register a child customization with `inheritsFrom`
|
||||
customizationService.setCustomizations({
|
||||
'viewportOverlay.bottomRight.InstanceNumber': {
|
||||
$set: {
|
||||
inheritsFrom: 'test.overlayItem',
|
||||
label: 'Instance Number',
|
||||
title: 'Instance Title',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customization = customizationService.getCustomization(
|
||||
'viewportOverlay.bottomRight.InstanceNumber'
|
||||
);
|
||||
|
||||
// Verify that the transform function from the parent is executed
|
||||
expect(customization.additionalKey).toBe('transformedValue');
|
||||
expect(customization.label).toBe('Instance Number');
|
||||
expect(customization.title).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,54 +1,36 @@
|
||||
import { mergeWith, cloneDeepWith } from 'lodash';
|
||||
|
||||
import update, { extend } from 'immutability-helper';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { Customization, NestedStrings } from './types';
|
||||
import type { Customization } from './types';
|
||||
import type { CommandsManager } from '../../classes';
|
||||
import type { ExtensionManager } from '../../extensions';
|
||||
|
||||
const EVENTS = {
|
||||
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
|
||||
GLOBAL_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:globalModified',
|
||||
DEFAULT_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:defaultModified',
|
||||
};
|
||||
|
||||
const flattenNestedStrings = (
|
||||
strs: NestedStrings | string,
|
||||
ret?: Record<string, string>
|
||||
): Record<string, string> => {
|
||||
if (!ret) {
|
||||
ret = {};
|
||||
}
|
||||
if (!strs) {
|
||||
return ret;
|
||||
}
|
||||
if (Array.isArray(strs)) {
|
||||
for (const val of strs) {
|
||||
flattenNestedStrings(val, ret);
|
||||
}
|
||||
} else {
|
||||
ret[strs] = strs;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
/**
|
||||
* Enum representing the different scopes of customizations available in the system.
|
||||
*/
|
||||
export enum CustomizationScope {
|
||||
/**
|
||||
* Global customizations that override both mode and default customizations.
|
||||
* These are applied universally across the application.
|
||||
*/
|
||||
Global = 'global',
|
||||
|
||||
export enum MergeEnum {
|
||||
/**
|
||||
* Append values in the nested arrays
|
||||
* Mode-specific customizations that are only active during a particular mode.
|
||||
* These are cleared and reset when switching between modes.
|
||||
*/
|
||||
Append = 'Append',
|
||||
/**
|
||||
* Merge values, replacing arrays
|
||||
*/
|
||||
Merge = 'Merge',
|
||||
/**
|
||||
* Replace the given value - this is the default
|
||||
*/
|
||||
Replace = 'Replace',
|
||||
}
|
||||
Mode = 'mode',
|
||||
|
||||
export enum CustomizationType {
|
||||
Global = 'Global',
|
||||
Mode = 'Mode',
|
||||
Default = 'Default',
|
||||
/**
|
||||
* Default customizations that serve as fallbacks when no global or mode-specific
|
||||
* customizations are defined. These can only be defined once.
|
||||
*/
|
||||
Default = 'default',
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,9 +60,12 @@ export enum CustomizationType {
|
||||
* every module for the given id and to load it/add it to the extensions.
|
||||
*/
|
||||
export default class CustomizationService extends PubSubService {
|
||||
public static EVENTS = EVENTS;
|
||||
public Scope = CustomizationScope;
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'customizationService',
|
||||
create: ({ configuration = {}, commandsManager }) => {
|
||||
create: ({ configuration, commandsManager }) => {
|
||||
return new CustomizationService({ configuration, commandsManager });
|
||||
},
|
||||
};
|
||||
@ -89,22 +74,25 @@ export default class CustomizationService extends PubSubService {
|
||||
extensionManager: ExtensionManager;
|
||||
|
||||
/**
|
||||
* mode customizations are changes to the default behaviour which are reset
|
||||
* every time a new mode is entered. This allows the mode to define custom
|
||||
* behaviour, and not interfere with other modes.
|
||||
*/
|
||||
private modeCustomizations = new Map<string, Customization>();
|
||||
/**
|
||||
* global customizations, are customizations which are set as a global default
|
||||
* This allows changes across the board to be applied, essentially as a priority
|
||||
* setting.
|
||||
* A collection of global customizations that act as a priority layer.
|
||||
* These customizations are applied universally, overriding both mode-specific
|
||||
* and default customizations. Ideal for system-wide changes.
|
||||
*/
|
||||
private globalCustomizations = new Map<string, Customization>();
|
||||
|
||||
/**
|
||||
* Default customizations allow applying default values. The intent is that
|
||||
* there is only one customization of that type, and it is registered at setup
|
||||
* time.
|
||||
* A collection of mode-specific customizations. These allow modes to define
|
||||
* their own behavior without impacting other modes. These customizations
|
||||
* are cleared and redefined whenever a mode changes, ensuring isolation
|
||||
* between modes. Read more about modes in the modes documentation.
|
||||
*/
|
||||
private modeCustomizations = new Map<string, Customization>();
|
||||
|
||||
/**
|
||||
* A collection of default customizations used as fallbacks. These serve as
|
||||
* the base configuration and are registered at setup. Default customizations
|
||||
* provide baseline values that can be overridden by mode or global customizations.
|
||||
* Use these for cases where default values are necessary for predictable behavior.
|
||||
*/
|
||||
private defaultCustomizations = new Map<string, Customization>();
|
||||
|
||||
@ -113,13 +101,12 @@ export default class CustomizationService extends PubSubService {
|
||||
* transform every time a customization is requested.
|
||||
*/
|
||||
private transformedCustomizations = new Map<string, Customization>();
|
||||
|
||||
configuration: any;
|
||||
private configuration: AppTypes.Config;
|
||||
|
||||
constructor({ configuration, commandsManager }) {
|
||||
super(EVENTS);
|
||||
this.configuration = configuration;
|
||||
this.commandsManager = commandsManager;
|
||||
this.configuration = configuration || {};
|
||||
}
|
||||
|
||||
public init(extensionManager: ExtensionManager): void {
|
||||
@ -128,33 +115,36 @@ export default class CustomizationService extends PubSubService {
|
||||
this.defaultCustomizations.clear();
|
||||
// Clear modes because those are defined in onModeEnter functions.
|
||||
this.modeCustomizations.clear();
|
||||
this.initDefaults();
|
||||
|
||||
this.extensionManager.getRegisteredExtensionIds().forEach(extensionId => {
|
||||
const keyDefault = `${extensionId}.customizationModule.default`;
|
||||
const defaultCustomizations = this._findExtensionValue(keyDefault);
|
||||
if (defaultCustomizations) {
|
||||
const { value } = defaultCustomizations;
|
||||
this._addReference(value, CustomizationScope.Default);
|
||||
}
|
||||
const keyGlobal = `${extensionId}.customizationModule.global`;
|
||||
const globalCustomizations = this._findExtensionValue(keyGlobal);
|
||||
if (globalCustomizations) {
|
||||
const { value } = globalCustomizations;
|
||||
this._addReference(value, CustomizationScope.Global);
|
||||
}
|
||||
});
|
||||
|
||||
this.addReferences(this.configuration);
|
||||
}
|
||||
|
||||
initDefaults(): void {
|
||||
this.extensionManager.getRegisteredExtensionIds().forEach(extensionId => {
|
||||
const keyDefault = `${extensionId}.customizationModule.default`;
|
||||
const defaultCustomizations = this.findExtensionValue(keyDefault);
|
||||
if (defaultCustomizations) {
|
||||
const { value } = defaultCustomizations;
|
||||
this.addReference(value, CustomizationType.Default);
|
||||
}
|
||||
const keyGlobal = `${extensionId}.customizationModule.global`;
|
||||
const globalCustomizations = this.findExtensionValue(keyGlobal);
|
||||
if (globalCustomizations) {
|
||||
const { value } = globalCustomizations;
|
||||
this.addReference(value, CustomizationType.Global);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findExtensionValue(value: string) {
|
||||
const entry = this.extensionManager.getModuleEntry(value);
|
||||
return entry as { value: Customization };
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
this.clearTransformedCustomizations();
|
||||
|
||||
this.init(this.extensionManager);
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.clearTransformedCustomizations();
|
||||
}
|
||||
|
||||
private clearTransformedCustomizations(): void {
|
||||
super.reset();
|
||||
|
||||
const modeCustomizationKeys = Array.from(this.modeCustomizations.keys());
|
||||
@ -165,65 +155,19 @@ export default class CustomizationService extends PubSubService {
|
||||
this.modeCustomizations.clear();
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.onModeEnter();
|
||||
}
|
||||
|
||||
public getModeCustomizations(): Map<string, Customization> {
|
||||
return this.modeCustomizations;
|
||||
}
|
||||
|
||||
public setModeCustomization(
|
||||
customizationId: string,
|
||||
customization: Customization,
|
||||
merge = MergeEnum.Merge
|
||||
): void {
|
||||
const defaultCustomization = this.defaultCustomizations.get(customizationId);
|
||||
const modeCustomization = this.modeCustomizations.get(customizationId);
|
||||
const globCustomization = this.globalCustomizations.get(customizationId);
|
||||
const sourceCustomization =
|
||||
modeCustomization ||
|
||||
(globCustomization && cloneDeepWith(globCustomization, cloneCustomizer)) ||
|
||||
defaultCustomization ||
|
||||
{};
|
||||
|
||||
// use the source merge type if not provided then fallback to merge
|
||||
this.modeCustomizations.set(
|
||||
customizationId,
|
||||
this.mergeValue(sourceCustomization, customization, sourceCustomization.merge ?? merge)
|
||||
);
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
||||
buttons: this.modeCustomizations,
|
||||
button: this.modeCustomizations.get(customizationId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the preferred getter for all customizations,
|
||||
* getting global (priority) customizations first,
|
||||
* then mode customizations, and finally the default customization.
|
||||
* Unified getter for customizations.
|
||||
*
|
||||
* @param customizationId - the customization id to look for
|
||||
* @param defaultValue - is the default value to return.
|
||||
* This value will be assigned as the default customization if there isn't
|
||||
* currently a default customization, and thus, the first default provided
|
||||
* will be used as the default - you cannot update this after or have it depend
|
||||
* on changing values.
|
||||
* Also, the value returned by the get customization has merges/updates applied,
|
||||
* and is thus may be modified from the value provided, and may not be the original
|
||||
* default provided. This allows applying the defaults for things like inheritance.
|
||||
* @return A customization to use if one is found, or the default customization,
|
||||
* both enhanced with any customizationType inheritance (see transform)
|
||||
* @param customizationId - The ID of the customization to retrieve.
|
||||
* @param scope - (Optional) The scope to retrieve from: 'global', 'mode', or 'default'.
|
||||
* If not specified, it retrieves based on priority: global > mode > default.
|
||||
* @returns The requested customization.
|
||||
*/
|
||||
public getCustomization(customizationId: string, defaultValue?: Customization): Customization {
|
||||
public getCustomization(customizationId: string): Customization {
|
||||
const transformed = this.transformedCustomizations.get(customizationId);
|
||||
if (transformed) {
|
||||
return transformed;
|
||||
}
|
||||
if (defaultValue && !this.defaultCustomizations.has(customizationId)) {
|
||||
this.setDefaultCustomization(customizationId, defaultValue);
|
||||
}
|
||||
const customization =
|
||||
this.globalCustomizations.get(customizationId) ??
|
||||
this.modeCustomizations.get(customizationId) ??
|
||||
@ -235,197 +179,254 @@ export default class CustomizationService extends PubSubService {
|
||||
return newTransformed;
|
||||
}
|
||||
|
||||
/** Mode customizations are changes to the behavior of the extensions
|
||||
* when running in a given mode. Reset clears mode customizations.
|
||||
/**
|
||||
* Takes an object with multiple properties, each property containing
|
||||
* immutability-helper commands, and applies them one by one.
|
||||
*
|
||||
* Note that global customizations over-ride mode customizations
|
||||
* Example:
|
||||
* customizationService.setCustomizations({
|
||||
* showAddSegment: { $set: false },
|
||||
* NumbersList: { $push: [99] },
|
||||
* }, CustomizationScope.Mode)
|
||||
*
|
||||
* @param defaultValue to return if no customization specified.
|
||||
* Or you can simply apply a list of strings that are customization module items in the
|
||||
* extension.
|
||||
*
|
||||
* Example:
|
||||
* customizationService.setCustomizations(['@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts'], CustomizationScope.Mode)
|
||||
*/
|
||||
public getModeCustomization = this.getCustomization;
|
||||
public setCustomizations(
|
||||
customizations: string[] | Record<string, Customization>,
|
||||
scope: CustomizationScope = CustomizationScope.Mode
|
||||
): void {
|
||||
if (Array.isArray(customizations)) {
|
||||
customizations.forEach(customization => {
|
||||
this._addReference(customization, scope);
|
||||
});
|
||||
} else {
|
||||
Object.entries(customizations).forEach(([key, value]) => {
|
||||
this._setCustomization(key, value, scope);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use setCustomizations instead
|
||||
*/
|
||||
public setCustomization(
|
||||
customizationId: string,
|
||||
customization: Customization | string,
|
||||
scope: CustomizationScope = CustomizationScope.Mode
|
||||
): void {
|
||||
console.warn(
|
||||
'setCustomization is deprecated. Please use setCustomizations with an object instead.'
|
||||
);
|
||||
this._setCustomization(customizationId, customization, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to set a single customization
|
||||
*/
|
||||
private _setCustomization(
|
||||
customizationId: string,
|
||||
customization: Customization,
|
||||
scope: CustomizationScope = CustomizationScope.Mode
|
||||
): void {
|
||||
// if (typeof customization === 'string') {
|
||||
// const extensionValue = this._findExtensionValue(customization);
|
||||
// customization = extensionValue.value;
|
||||
// }
|
||||
|
||||
switch (scope) {
|
||||
case CustomizationScope.Global:
|
||||
this.setGlobalCustomization(customizationId, customization);
|
||||
break;
|
||||
case CustomizationScope.Mode:
|
||||
this.setModeCustomization(customizationId, customization);
|
||||
break;
|
||||
case CustomizationScope.Default:
|
||||
this.setDefaultCustomization(customizationId, customization);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid customization scope: ${scope}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all customizations for a given scope.
|
||||
*
|
||||
* @param scope - The scope to retrieve customizations from: 'global', 'mode', or 'default'
|
||||
* @returns A Map containing all customizations for the specified scope
|
||||
*/
|
||||
public getCustomizations(scope: CustomizationScope): Map<string, Customization> {
|
||||
if (scope === CustomizationScope.Global) {
|
||||
return this.globalCustomizations;
|
||||
}
|
||||
if (scope === CustomizationScope.Mode) {
|
||||
return this.modeCustomizations;
|
||||
}
|
||||
return this.defaultCustomizations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a mode customization. Doesn't include defaults, but
|
||||
* does return global overrides.
|
||||
*/
|
||||
public hasModeCustomization(customizationId: string) {
|
||||
public hasCustomization(customizationId: string) {
|
||||
return (
|
||||
this.globalCustomizations.has(customizationId) || this.modeCustomizations.has(customizationId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* get is an alias for getModeCustomization, as it is the generic getter
|
||||
* which will return both mode and global customizations, and should be
|
||||
* used generally.
|
||||
* Note that the second parameter, defaultValue, will be expanded to include
|
||||
* any customizationType values defined in it, so it is not the same as doing:
|
||||
* `customizationService.get('key') || defaultValue`
|
||||
* unless the defaultValue does not contain any customizationType definitions.
|
||||
*/
|
||||
public get = this.getModeCustomization;
|
||||
|
||||
/**
|
||||
* Applies any inheritance due to UI Type customization.
|
||||
* This will look for customizationType in the customization object
|
||||
* This will look for inheritsFrom in the customization object
|
||||
* and if that is found, will assign all iterable values from that
|
||||
* type into the new type, allowing default behaviour to be configured.
|
||||
* type into the new type, allowing default behavior to be configured.
|
||||
*/
|
||||
public transform(customization: Customization): Customization {
|
||||
if (!customization) {
|
||||
return customization;
|
||||
}
|
||||
const { customizationType } = customization;
|
||||
if (!customizationType) {
|
||||
const { inheritsFrom } = customization;
|
||||
if (!inheritsFrom) {
|
||||
return customization;
|
||||
}
|
||||
const parent = this.getCustomization(customizationType);
|
||||
const parent = this.getCustomization(inheritsFrom);
|
||||
const result = parent ? Object.assign({}, parent, customization) : customization;
|
||||
// Execute an nested type information
|
||||
return result.transform?.(this) || result;
|
||||
return result.$transform?.(this) || result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to easily add and retrieve customizations
|
||||
* @param id The unique identifier for the customization
|
||||
* @param defaultComponent The default component to use if no customization is set
|
||||
* @param customComponent Optional custom component to set
|
||||
* @returns The custom component if set, otherwise the default component
|
||||
*
|
||||
* Sets a mode-specific customization.
|
||||
*
|
||||
* This method allows you to define or update a customization that applies only to the current mode.
|
||||
* Mode customizations are temporary and isolated, reset whenever a mode changes.
|
||||
*
|
||||
* @param customizationId - The unique identifier for the customization.
|
||||
* @param customization - The customization object containing the desired settings.
|
||||
*/
|
||||
public getCustomComponent(
|
||||
id: string,
|
||||
defaultComponent: React.ComponentType<any>,
|
||||
customComponent?: React.ComponentType<any>
|
||||
) {
|
||||
const customization = this.getCustomization(id, {
|
||||
id: `default-${id}`,
|
||||
content: defaultComponent,
|
||||
});
|
||||
private setModeCustomization(customizationId: string, customization: Customization): void {
|
||||
const defaultCustomization = this.defaultCustomizations.get(customizationId);
|
||||
const modeCustomization = this.modeCustomizations.get(customizationId);
|
||||
const globCustomization = this.globalCustomizations.get(customizationId);
|
||||
|
||||
if (customComponent) {
|
||||
this.setModeCustomization(id, { content: customComponent });
|
||||
}
|
||||
const sourceCustomization =
|
||||
modeCustomization || this._cloneIfNeeded(globCustomization) || defaultCustomization;
|
||||
|
||||
return customization.content;
|
||||
}
|
||||
const result = this._update(sourceCustomization, customization);
|
||||
this.modeCustomizations.set(customizationId, result);
|
||||
|
||||
public addModeCustomizations(modeCustomizations): void {
|
||||
if (!modeCustomizations) {
|
||||
return;
|
||||
}
|
||||
this.addReferences(modeCustomizations, CustomizationType.Mode);
|
||||
|
||||
this._broadcastModeCustomizationModified();
|
||||
}
|
||||
|
||||
_broadcastModeCustomizationModified(): void {
|
||||
this._broadcastEvent(EVENTS.MODE_CUSTOMIZATION_MODIFIED, {
|
||||
modeCustomizations: this.modeCustomizations,
|
||||
globalCustomizations: this.globalCustomizations,
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
||||
buttons: this.modeCustomizations,
|
||||
button: this.modeCustomizations.get(customizationId),
|
||||
});
|
||||
}
|
||||
|
||||
/** Global customizations are those that affect parts of the GUI other than
|
||||
* the modes. They include things like settings for the search screen.
|
||||
* Reset does NOT clear global customizations.
|
||||
*/
|
||||
getGlobalCustomization(id: string, defaultValue?: Customization): Customization | void {
|
||||
return this.transform(
|
||||
this.globalCustomizations.get(id) ?? this.defaultCustomizations.get(id) ?? defaultValue
|
||||
);
|
||||
private setGlobalCustomization(id: string, value: Customization): void {
|
||||
const defaultCustomization = this.defaultCustomizations.get(id);
|
||||
const globCustomization = this.globalCustomizations.get(id);
|
||||
|
||||
const sourceCustomization = this._cloneIfNeeded(globCustomization) || defaultCustomization;
|
||||
this.globalCustomizations.set(id, this._update(sourceCustomization, value));
|
||||
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastEvent(this.EVENTS.DEFAULT_CUSTOMIZATION_MODIFIED, {
|
||||
buttons: this.defaultCustomizations,
|
||||
button: this.defaultCustomizations.get(id),
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultCustomization(id: string, value: Customization): void {
|
||||
if (this.defaultCustomizations.has(id)) {
|
||||
console.warn(`Trying to update existing default for customization ${id}`);
|
||||
}
|
||||
this.transformedCustomizations.clear();
|
||||
|
||||
const sourceCustomization = this.defaultCustomizations.get(id);
|
||||
this.defaultCustomizations.set(id, this._update(sourceCustomization, value));
|
||||
|
||||
this._broadcastEvent(this.EVENTS.DEFAULT_CUSTOMIZATION_MODIFIED, {
|
||||
buttons: this.defaultCustomizations,
|
||||
button: this.defaultCustomizations.get(id),
|
||||
});
|
||||
}
|
||||
|
||||
private _findExtensionValue(value: string) {
|
||||
const entry = this.extensionManager.getModuleEntry(value);
|
||||
return entry as { value: Customization };
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a merge, creating a new instance value - that is, not referencing
|
||||
* the old one. This only works if you run once for the merge, so in general,
|
||||
* the source value should be global, while the appends should be mode based.
|
||||
* However, you can append to a global value too, as long as you ensure it
|
||||
* only gets merged once.
|
||||
* Registers a custom command to be used in customization updates.
|
||||
* @param commandName - The name of the command (without the $ prefix)
|
||||
* it will be prefixed with $
|
||||
* @param handler - Function that handles the command it receives the value and the original value
|
||||
*/
|
||||
private mergeValue(oldValue, newValue, mergeType = MergeEnum.Replace) {
|
||||
if (mergeType === MergeEnum.Replace) {
|
||||
public registerCustomUpdateCommand(
|
||||
commandName: string,
|
||||
handler: (value: Customization, original: Customization) => Customization
|
||||
): void {
|
||||
if (!commandName.startsWith('$')) {
|
||||
commandName = '$' + commandName;
|
||||
}
|
||||
extend(commandName, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses immutability-helper to apply the user's commands (e.g. $set, $push, $apply, etc.)
|
||||
* Takes into account the 'mergeType' if it's explicitly 'Replace'; otherwise does a normal update.
|
||||
*/
|
||||
private _update(oldValue: Customization | undefined, newValue: Customization): Customization {
|
||||
if (!oldValue) {
|
||||
oldValue = undefined;
|
||||
}
|
||||
|
||||
// Use immutability-helper to apply the commands
|
||||
// if $ is not part of the value in the json string, then we just return the newValue
|
||||
if (!hasDollarKey(newValue)) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
const returnValue = mergeWith(
|
||||
{},
|
||||
oldValue,
|
||||
newValue,
|
||||
mergeType === MergeEnum.Append ? appendCustomizer : mergeCustomizer
|
||||
);
|
||||
return returnValue;
|
||||
const result = update(oldValue, newValue);
|
||||
return result;
|
||||
}
|
||||
|
||||
public setGlobalCustomization(id: string, value: Customization, merge = MergeEnum.Replace): void {
|
||||
const defaultCustomization = this.defaultCustomizations.get(id);
|
||||
const globCustomization = this.globalCustomizations.get(id);
|
||||
const sourceCustomization =
|
||||
(globCustomization && cloneDeepWith(globCustomization, cloneCustomizer)) ||
|
||||
defaultCustomization ||
|
||||
{};
|
||||
this.globalCustomizations.set(
|
||||
id,
|
||||
this.mergeValue(sourceCustomization, value, value.merge ?? merge)
|
||||
);
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastGlobalCustomizationModified();
|
||||
}
|
||||
|
||||
public setDefaultCustomization(
|
||||
id: string,
|
||||
value: Customization,
|
||||
merge = MergeEnum.Replace
|
||||
): void {
|
||||
if (this.defaultCustomizations.has(id)) {
|
||||
throw new Error(`Trying to update existing default for customization ${id}`);
|
||||
private _cloneIfNeeded(value: any) {
|
||||
// If it's null/undefined or not an object, return as is
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
this.transformedCustomizations.clear();
|
||||
this.defaultCustomizations.set(
|
||||
id,
|
||||
this.mergeValue(this.defaultCustomizations.get(id), value, merge)
|
||||
);
|
||||
|
||||
// If it's an array, create a shallow copy
|
||||
if (Array.isArray(value)) {
|
||||
return [...value];
|
||||
}
|
||||
|
||||
// Otherwise create a shallow copy of the object
|
||||
return { ...value };
|
||||
}
|
||||
|
||||
protected setConfigGlobalCustomization(configuration: AppConfigCustomization): void {
|
||||
this.globalCustomizations.clear();
|
||||
const keys = flattenNestedStrings(configuration.globalCustomizations);
|
||||
this.readCustomizationTypes(v => keys[v.name] && v.customization, this.globalCustomizations);
|
||||
|
||||
// TODO - iterate over customizations, loading them from the extension
|
||||
// manager.
|
||||
this._broadcastGlobalCustomizationModified();
|
||||
}
|
||||
|
||||
_broadcastGlobalCustomizationModified(): void {
|
||||
this._broadcastEvent(EVENTS.GLOBAL_CUSTOMIZATION_MODIFIED, {
|
||||
modeCustomizations: this.modeCustomizations,
|
||||
globalCustomizations: this.globalCustomizations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A single reference is either an string to be loaded from a module,
|
||||
* or a customization itself.
|
||||
*/
|
||||
addReference(value?, type = CustomizationType.Global, id?: string, merge?: MergeEnum): void {
|
||||
_addReference(value?: any, type = CustomizationScope.Global): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const extensionValue = this.findExtensionValue(value);
|
||||
// The child of a reference is only a set of references when an array,
|
||||
// so call the addReference direct. It could be a secondary reference perhaps
|
||||
this.addReference(extensionValue.value, type, extensionValue.name, extensionValue.merge);
|
||||
} else if (Array.isArray(value)) {
|
||||
this.addReferences(value, type);
|
||||
} else {
|
||||
const useId = value.id || id;
|
||||
const setName =
|
||||
(type === CustomizationType.Global && 'setGlobalCustomization') ||
|
||||
(type === CustomizationType.Default && 'setDefaultCustomization') ||
|
||||
'setModeCustomization';
|
||||
this[setName](useId as string, value, merge);
|
||||
const extensionValue = this._findExtensionValue(value);
|
||||
value = extensionValue.value;
|
||||
}
|
||||
|
||||
Object.entries(value).forEach(([id, customization]) => {
|
||||
const setName =
|
||||
(type === CustomizationScope.Global && 'setGlobalCustomization') ||
|
||||
(type === CustomizationScope.Default && 'setDefaultCustomization') ||
|
||||
'setModeCustomization';
|
||||
this[setName](id as string, customization as Customization);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -433,94 +434,106 @@ export default class CustomizationService extends PubSubService {
|
||||
* or as an object whose key is the reference id, and the value is the string
|
||||
* or customization.
|
||||
*/
|
||||
addReferences(references?, type = CustomizationType.Global): void {
|
||||
addReferences(references?: any, type = CustomizationScope.Global): void {
|
||||
if (!references) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(references)) {
|
||||
references.forEach(item => {
|
||||
this.addReference(item, type);
|
||||
this._addReference(item, type);
|
||||
});
|
||||
} else {
|
||||
for (const key of Object.keys(references)) {
|
||||
const value = references[key];
|
||||
this.addReference(value, type, key);
|
||||
}
|
||||
this._addReference(references, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom merging function, to handle merging arrays and copying functions
|
||||
*/
|
||||
function appendCustomizer(obj, src) {
|
||||
if (Array.isArray(obj)) {
|
||||
const srcArray = Array.isArray(src);
|
||||
if (srcArray) {
|
||||
return obj.concat(...src);
|
||||
}
|
||||
if (typeof src === 'object') {
|
||||
const newList = obj.map(value => cloneDeepWith(value, cloneCustomizer));
|
||||
for (const [key, value] of Object.entries(src)) {
|
||||
const { position, isMerge } = findPosition(key, value, newList);
|
||||
if (isMerge) {
|
||||
if (typeof obj[position] === 'object') {
|
||||
newList[position] = mergeWith(
|
||||
Array.isArray(newList[position]) ? [] : {},
|
||||
newList[position],
|
||||
value,
|
||||
appendCustomizer
|
||||
);
|
||||
} else {
|
||||
newList[position] = value;
|
||||
/** Add custom $filter command */
|
||||
extend('$filter', (query, original) => {
|
||||
// This helper checks if an object matches all key/value pairs in `match`
|
||||
function objectMatches(item, matchObj) {
|
||||
return (
|
||||
item && typeof item === 'object' && Object.entries(matchObj).every(([k, v]) => item[k] === v)
|
||||
);
|
||||
}
|
||||
|
||||
// Recursively walk objects/arrays. Whenever we hit an array, we either filter
|
||||
// or update items that match, depending on what was passed in via `query`.
|
||||
function deepFilter(value, filterQuery) {
|
||||
// If it's an array, apply the filtering/updating logic to each item
|
||||
if (Array.isArray(value)) {
|
||||
let result = value;
|
||||
|
||||
// 1) If it's a function, filter array items
|
||||
if (typeof filterQuery === 'function') {
|
||||
return value.filter(filterQuery);
|
||||
}
|
||||
|
||||
// 2) If it's a string, remove items whose .id matches that string
|
||||
if (typeof filterQuery === 'string') {
|
||||
return value.filter(item => item.id !== filterQuery);
|
||||
}
|
||||
|
||||
// 3) If it's an object with .match and .merge, apply the merge to matched items
|
||||
if (typeof filterQuery === 'object' && filterQuery.match && filterQuery.$merge) {
|
||||
// First recurse into sub-objects/arrays so we handle deeply nested arrays
|
||||
result = value.map(item => deepFilter(item, filterQuery));
|
||||
// Then update items that match
|
||||
return result.map(item => {
|
||||
if (objectMatches(item, filterQuery.match)) {
|
||||
return { ...item, ...filterQuery.$merge };
|
||||
}
|
||||
} else {
|
||||
newList.splice(position, 0, value);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return newList;
|
||||
|
||||
// 4) If it's an object with .id and .$merge, for backwards-compat
|
||||
if (typeof filterQuery === 'object' && filterQuery.id && filterQuery.$merge) {
|
||||
result = value.map(item => deepFilter(item, filterQuery));
|
||||
return result.map(item => {
|
||||
if (item.id === filterQuery.id) {
|
||||
return { ...item, ...filterQuery.$merge };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, just recurse into sub-objects without filtering
|
||||
return value.map(item => deepFilter(item, filterQuery));
|
||||
}
|
||||
return obj.concat(src);
|
||||
}
|
||||
return cloneCustomizer(src);
|
||||
}
|
||||
|
||||
function mergeCustomizer(obj, src) {
|
||||
return cloneCustomizer(src);
|
||||
}
|
||||
|
||||
function findPosition(key, value, newList) {
|
||||
const numVal = Number(key);
|
||||
const isNumeric = !isNaN(numVal);
|
||||
const { length: len } = newList;
|
||||
|
||||
if (isNumeric) {
|
||||
if (newList[numVal < 0 ? numVal + len : numVal]) {
|
||||
return { isMerge: true, position: (numVal + len) % len };
|
||||
// If it's a plain object, recurse into its properties
|
||||
if (value && typeof value === 'object') {
|
||||
const newObj = { ...value };
|
||||
for (const [key, val] of Object.entries(newObj)) {
|
||||
newObj[key] = deepFilter(val, filterQuery);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
const absPosition = Math.ceil(numVal < 0 ? len + numVal : numVal);
|
||||
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) };
|
||||
}
|
||||
const findIndex = newList.findIndex(it => it.id === key);
|
||||
if (findIndex !== -1) {
|
||||
return { isMerge: true, position: findIndex };
|
||||
}
|
||||
const { _priority: priority } = value;
|
||||
if (priority !== undefined) {
|
||||
if (newList[(priority + len) % len]) {
|
||||
return { isMerge: true, position: (priority + len) % len };
|
||||
}
|
||||
const absPosition = Math.ceil(priority < 0 ? len + priority : priority);
|
||||
return { isMerge: false, position: Math.min(len, Math.max(absPosition, 0)) };
|
||||
}
|
||||
return { isMerge: false, position: len };
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom cloning function to just copy function reference
|
||||
*/
|
||||
function cloneCustomizer(value) {
|
||||
if (typeof value === 'function') {
|
||||
// If it's neither array nor object, just return it
|
||||
return value;
|
||||
}
|
||||
|
||||
return deepFilter(original, query);
|
||||
});
|
||||
|
||||
function hasDollarKey(value) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (hasDollarKey(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (value && typeof value === 'object') {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (key.startsWith('$') && key !== '$transform') {
|
||||
return true;
|
||||
}
|
||||
if (hasDollarKey(value[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ import { ComponentType } from 'react';
|
||||
export type Obj = Record<string, unknown>;
|
||||
|
||||
export interface BaseCustomization extends Obj {
|
||||
id: string;
|
||||
customizationType?: string;
|
||||
id?: string;
|
||||
inheritsFrom?: string;
|
||||
description?: string;
|
||||
label?: string;
|
||||
commands?: Command[];
|
||||
|
||||
@ -434,7 +434,9 @@ export default class HangingProtocolService extends PubSubService {
|
||||
this._setProtocol(matchedProtocol);
|
||||
}
|
||||
|
||||
this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter);
|
||||
if (this.protocol?.callbacks?.onProtocolEnter) {
|
||||
this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1028,7 +1030,9 @@ export default class HangingProtocolService extends PubSubService {
|
||||
// before reassigning the protocol, we need to check if there is a callback
|
||||
// on the old protocol that needs to be called
|
||||
// Send the notification about updating the state
|
||||
this._commandsManager.run(this.protocol?.callbacks?.onProtocolExit);
|
||||
if (this.protocol?.callbacks?.onProtocolExit) {
|
||||
this._commandsManager.run(this.protocol.callbacks.onProtocolExit);
|
||||
}
|
||||
|
||||
this.protocol = protocol;
|
||||
|
||||
|
||||
BIN
platform/docs/docs/assets/img/colorbarImage.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 130 KiB |
BIN
platform/docs/docs/assets/img/measurement-labels-auto.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
platform/docs/docs/assets/img/segDisplayEditingFalse.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
platform/docs/docs/assets/img/segDisplayEditingTrue.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
platform/docs/docs/assets/img/segmentationTableModeImage.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
platform/docs/docs/assets/img/segmentationTableModeImage2.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
platform/docs/docs/assets/img/seriesSort.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
platform/docs/docs/assets/img/studyMenuItemsImage.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
platform/docs/docs/assets/img/thumbnailMenuItemsImage.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
platform/docs/docs/assets/img/viewportOverlay-customization.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
platform/docs/docs/assets/img/windowLevelPresets.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
@ -67,12 +67,12 @@ customizationService.addModeCustomizations([
|
||||
customizationService.addModeCustomizations([
|
||||
// To disable editing in the SegmentationTable
|
||||
{
|
||||
id: 'PanelSegmentation.disableEditing',
|
||||
id: 'panelSegmentation.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
// To disable editing in the MeasurementTable
|
||||
{
|
||||
id: 'PanelMeasurement.disableEditing',
|
||||
id: 'panelMeasurement.disableEditing',
|
||||
disableEditing: true,
|
||||
},
|
||||
])
|
||||
@ -106,11 +106,11 @@ customizationService.addModeCustomizations([
|
||||
```js
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'PanelSegmentation.tableMode',
|
||||
id: 'panelSegmentation.tableMode',
|
||||
mode: 'expanded',
|
||||
},
|
||||
{
|
||||
id: 'PanelSegmentation.onSegmentationAdd',
|
||||
id: 'panelSegmentation.onSegmentationAdd',
|
||||
onSegmentationAdd: () => {
|
||||
commandsManager.run('createNewLabelmapFromPT');
|
||||
},
|
||||
|
||||