feat(customization): new customization service api (#4688)
This commit is contained in:
1 parent
fab52808cb
commit
55ad8efbab
124 files changed
+4261
-2517
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user