@@ -344,7 +342,7 @@ function _getStatusComponent(isTracked) {
}
>
diff --git a/extensions/tmtv/src/commandsModule.js b/extensions/tmtv/src/commandsModule.js
index 32b368b8e..d00b33c89 100644
--- a/extensions/tmtv/src/commandsModule.js
+++ b/extensions/tmtv/src/commandsModule.js
@@ -508,13 +508,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
colormap: {
name: colormap,
- // TODO: This opacity mapping matches that in hpViewports, but
- // ideally making this editable in a side panel would be useful
- opacity: [
- { value: 0, opacity: 0 },
- { value: 0.1, opacity: 0.9 },
- { value: 1, opacity: 0.95 },
- ],
},
});
diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js
index 99d4135b6..eaa461533 100644
--- a/extensions/tmtv/src/init.js
+++ b/extensions/tmtv/src/init.js
@@ -1,10 +1,6 @@
import { addTool, RectangleROIStartEndThresholdTool } from '@cornerstonejs/tools';
-import { utilities } from '@cornerstonejs/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
-import colormaps from './utils/colormaps';
-
-const { registerColormap } = utilities.colormap;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
@@ -38,5 +34,4 @@ export default function init({ servicesManager }) {
RectangleROIStartEndThreshold.toAnnotation,
RectangleROIStartEndThreshold.toMeasurement
);
- colormaps.forEach(registerColormap);
}
diff --git a/extensions/tmtv/src/utils/hpViewports.ts b/extensions/tmtv/src/utils/hpViewports.ts
index b716a9ef1..a93392170 100644
--- a/extensions/tmtv/src/utils/hpViewports.ts
+++ b/extensions/tmtv/src/utils/hpViewports.ts
@@ -20,6 +20,9 @@ const ctAXIAL = {
id: 'ctWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
],
},
@@ -48,6 +51,9 @@ const ctSAGITTAL = {
id: 'ctWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
],
},
@@ -75,6 +81,9 @@ const ctCORONAL = {
id: 'ctWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
],
},
@@ -108,6 +117,9 @@ const ptAXIAL = {
id: 'ptWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -152,6 +164,9 @@ const ptSAGITTAL = {
id: 'ptWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -196,6 +211,9 @@ const ptCORONAL = {
id: 'ptWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -249,6 +267,9 @@ const fusionAXIAL = {
id: 'fusionWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -312,6 +333,9 @@ const fusionSAGITTAL = {
id: 'fusionWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -375,6 +399,9 @@ const fusionCORONAL = {
id: 'fusionWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
@@ -423,6 +450,9 @@ const mipSAGITTAL = {
id: 'ptWLSync',
source: true,
target: true,
+ options: {
+ syncColormap: true,
+ },
},
{
type: 'voi',
diff --git a/modes/basic-test-mode/src/toolbarButtons.ts b/modes/basic-test-mode/src/toolbarButtons.ts
index 81a0ceb57..988e639f7 100644
--- a/modes/basic-test-mode/src/toolbarButtons.ts
+++ b/modes/basic-test-mode/src/toolbarButtons.ts
@@ -114,7 +114,7 @@ const toolbarButtons: Button[] = [
evaluate: 'evaluate.cornerstoneTool',
},
},
- // Window Level + Presets...
+ // Window Level
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts
index 55d63b470..bce66af83 100644
--- a/modes/longitudinal/src/toolbarButtons.ts
+++ b/modes/longitudinal/src/toolbarButtons.ts
@@ -7,29 +7,6 @@ import type { Button } from '@ohif/core/types';
const { windowLevelPresets } = defaults;
const { createButton } = ToolbarService;
-/**
- *
- * @param {*} preset - preset number (from above import)
- * @param {*} title
- * @param {*} subtitle
- */
-function _createWwwcPreset(preset, title, subtitle) {
- return {
- id: preset.toString(),
- title,
- subtitle,
- commands: [
- {
- commandName: 'setWindowLevel',
- commandOptions: {
- ...windowLevelPresets[preset],
- },
- context: 'CORNERSTONE',
- },
- ],
- };
-}
-
export const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
commandOptions: {
@@ -111,33 +88,15 @@ const toolbarButtons: Button[] = [
evaluate: 'evaluate.cornerstoneTool',
},
},
- // Window Level + Presets...
+ // Window Level
{
id: 'WindowLevel',
- uiType: 'ohif.splitButton',
+ uiType: 'ohif.radioGroup',
props: {
- groupId: 'WindowLevel',
- primary: createButton({
- id: 'WindowLevel',
- icon: 'tool-window-level',
- label: 'Window Level',
- tooltip: 'Window Level',
- commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
- }),
- secondary: {
- icon: 'chevron-down',
- label: 'W/L Manual',
- tooltip: 'W/L Presets',
- },
- renderer: WindowLevelMenuItem,
- items: [
- _createWwwcPreset(1, 'Soft tissue', '400 / 40'),
- _createWwwcPreset(2, 'Lung', '1500 / -600'),
- _createWwwcPreset(3, 'Liver', '150 / 90'),
- _createWwwcPreset(4, 'Bone', '2500 / 480'),
- _createWwwcPreset(5, 'Brain', '80 / 40'),
- ],
+ icon: 'tool-window-level',
+ label: 'Window Level',
+ commands: setToolActiveToolbar,
+ evaluate: 'evaluate.cornerstoneTool',
},
},
// Pan...
diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts
index 4d2dacd06..3255570d1 100644
--- a/modes/segmentation/src/toolbarButtons.ts
+++ b/modes/segmentation/src/toolbarButtons.ts
@@ -49,29 +49,12 @@ const toolbarButtons: Button[] = [
},
{
id: 'WindowLevel',
- uiType: 'ohif.splitButton',
+ uiType: 'ohif.radioGroup',
props: {
- groupId: 'WindowLevel',
- primary: createButton({
- id: 'WindowLevel',
- icon: 'tool-window-level',
- label: 'Window Level',
- tooltip: 'Window Level',
- commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
- }),
- secondary: {
- icon: 'chevron-down',
- tooltip: 'W/L Presets',
- },
- renderer: WindowLevelMenuItem,
- items: [
- _createWwwcPreset(1, 'Soft tissue', '400 / 40'),
- _createWwwcPreset(2, 'Lung', '1500 / -600'),
- _createWwwcPreset(3, 'Liver', '150 / 90'),
- _createWwwcPreset(4, 'Bone', '2500 / 480'),
- _createWwwcPreset(5, 'Brain', '80 / 40'),
- ],
+ icon: 'tool-window-level',
+ label: 'Window Level',
+ commands: setToolActiveToolbar,
+ evaluate: 'evaluate.cornerstoneTool',
},
},
{
diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js
index 92ccb893e..9643fa971 100644
--- a/modes/tmtv/src/index.js
+++ b/modes/tmtv/src/index.js
@@ -90,7 +90,6 @@ function modeFactory({ modeConfiguration }) {
'Pan',
'SyncToggle',
'RectangleROIStartEndThreshold',
- 'fusionPTColormap',
]);
// For the hanging protocol we need to decide on the window level
diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js
index 5beaac21f..c605b4966 100644
--- a/modes/tmtv/src/toolbarButtons.js
+++ b/modes/tmtv/src/toolbarButtons.js
@@ -103,29 +103,12 @@ const toolbarButtons = [
// Window Level + Presets
{
id: 'WindowLevel',
- uiType: 'ohif.splitButton',
+ uiType: 'ohif.radioGroup',
props: {
- groupId: 'WindowLevel',
- primary: ToolbarService.createButton({
- id: 'WindowLevel',
- icon: 'tool-window-level',
- label: 'Window Level',
- tooltip: 'Window Level',
- commands: setToolActiveToolbar,
- evaluate: 'evaluate.cornerstoneTool',
- }),
- secondary: {
- icon: 'chevron-down',
- tooltip: 'W/L Presets',
- },
- renderer: WindowLevelMenuItem,
- items: [
- _createWwwcPreset(1, 'Soft tissue', '400 / 40'),
- _createWwwcPreset(2, 'Lung', '1500 / -600'),
- _createWwwcPreset(3, 'Liver', '150 / 90'),
- _createWwwcPreset(4, 'Bone', '2500 / 480'),
- _createWwwcPreset(5, 'Brain', '80 / 40'),
- ],
+ icon: 'tool-window-level',
+ label: 'Window Level',
+ commands: setToolActiveToolbar,
+ evaluate: 'evaluate.cornerstoneTool',
},
},
// Crosshairs Button
@@ -161,38 +144,6 @@ const toolbarButtons = [
evaluate: 'evaluate.cornerstoneTool',
},
},
- // Fusion PT Colormap Button
- {
- id: 'fusionPTColormap',
- uiType: 'ohif.splitButton',
- props: {
- groupId: 'fusionPTColormap',
- primary: ToolbarService.createButton({
- id: 'fusionPTColormap',
- icon: 'tool-fusion-color',
- label: 'Fusion PT Colormap',
- tooltip: 'Fusion PT Colormap',
- commands: [],
- evaluate: 'evaluate.action',
- }),
- secondary: {
- icon: 'chevron-down',
- tooltip: 'PET Image Colormap',
- },
- items: [
- _createColormap('HSV', 'hsv'),
- _createColormap('Hot Iron', 'hot_iron'),
- _createColormap('S PET', 's_pet'),
- _createColormap('Red Hot', 'red_hot'),
- _createColormap('Perfusion', 'perfusion'),
- _createColormap('Rainbow', 'rainbow_2'),
- _createColormap('SUV', 'suv'),
- _createColormap('GE 256', 'ge_256'),
- _createColormap('GE', 'ge'),
- _createColormap('Siemens', 'siemens'),
- ],
- },
- },
];
export default toolbarButtons;
diff --git a/platform/app/cypress/integration/MultiStudy.spec.js b/platform/app/cypress/integration/MultiStudy.spec.js
index 43fec70cf..3c7da1b37 100644
--- a/platform/app/cypress/integration/MultiStudy.spec.js
+++ b/platform/app/cypress/integration/MultiStudy.spec.js
@@ -15,7 +15,7 @@ describe('OHIF Multi Study', () => {
cy.get('[data-cy="viewport-pane"]').as('viewportPane');
cy.get('@viewportPane').its('length').should('be.eq', 4);
- cy.get('[data-cy="studyDate"]').as('studyDate');
+ cy.get('[data-cy="viewport-overlay-top-left"] [title="Study date"]').as('studyDate');
cy.get('@studyDate').should(studyDate => {
expect(studyDate.length).to.be.eq(4);
diff --git a/platform/app/cypress/integration/customization/HangingProtocol.spec.js b/platform/app/cypress/integration/customization/HangingProtocol.spec.js
index 4f55e620b..a30064acf 100644
--- a/platform/app/cypress/integration/customization/HangingProtocol.spec.js
+++ b/platform/app/cypress/integration/customization/HangingProtocol.spec.js
@@ -36,6 +36,6 @@ describe('OHIF HP', () => {
cy.initCommonElementsAliases();
// The specified series/sop UID's are index 101, so ensure that image is displayed
- cy.get('@viewportInfoTopRight').should('contains.text', 'I:6');
+ cy.get('@viewportInfoBottomRight').should('contains.text', 'I:6');
});
});
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js
index 984b038bc..b0d2f0bd7 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js
@@ -29,10 +29,10 @@ describe('OHIF Cornerstone Hotkeys', () => {
it('checks if hotkeys "ArrowUp" and "ArrowDown" can navigate in the stack', () => {
// Hotkey ArrowDown
cy.get('body').type('{downarrow}');
- cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/26)');
+ cy.get('@viewportInfoBottomRight').should('contains.text', 'I:2 (2/26)');
// Hotkey ArrowUp
cy.get('body').type('{uparrow}');
- cy.get('@viewportInfoTopRight').should('contains.text', 'I:1 (1/26)');
+ cy.get('@viewportInfoBottomRight').should('contains.text', 'I:1 (1/26)');
});
it('checks if hotkeys "V" and "H" can flip the image', () => {
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
index 45a60fa52..595cfd3dd 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js
@@ -83,7 +83,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.trigger('mouseup', { buttons: 1 });
// The exact text is slightly dependent on the viewport resolution, so leave a range
- cy.get('@viewportInfoTopLeft').should($txt => {
+ cy.get('@viewportInfoBottomLeft').should($txt => {
const text = $txt.text();
expect(text).to.include('W:118').include('L:479');
});
@@ -143,7 +143,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.resetViewport();
const expectedText = 'W:958L:479';
- cy.get('@viewportInfoTopLeft').should('have.text', expectedText);
+ cy.get('@viewportInfoBottomLeft').should('have.text', expectedText);
});
/*it('checks if CINE tool will prompt a modal with working controls', () => {
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js
index f23b2186e..c31a2b76f 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js
@@ -8,7 +8,7 @@ describe('OHIF General Viewer', function () {
it('scrolls series stack using scrollbar', function () {
cy.scrollToIndex(13);
- cy.get('@viewportInfoTopRight').should('contains.text', '14');
+ cy.get('@viewportInfoBottomRight').should('contains.text', '14');
});
it('performs right click to zoom', function () {
@@ -32,7 +32,7 @@ describe('OHIF General Viewer', function () {
.trigger('mouseup');
// make sure the new zoom level is less than the initial
- cy.get('@viewportInfoTopLeft').then($viewportInfo => {
+ cy.get('@viewportInfoBottomLeft').then($viewportInfo => {
const zoomLevelFinal = $viewportInfo.text().substring(6, 9);
expect(zoomLevelFinal < zoomLevelInitial).to.eq(true);
});
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
index 435915edf..944f562a6 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
@@ -51,13 +51,13 @@ describe('OHIF Measurement Panel', function () {
// Reset to default tool so that the new add length works
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
- cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
+ cy.get('@viewportInfoBottomRight').should('contains.text', '(14/');
// Click on first measurement item
cy.get('[data-cy="measurement-item"]').eq(0).click();
- cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
- cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
+ cy.get('@viewportInfoBottomRight').should('contains.text', '(1/');
+ cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/');
});
/*
diff --git a/platform/app/cypress/integration/volume/MPR.spec.js b/platform/app/cypress/integration/volume/MPR.spec.js
index 0d15e75e4..2ef026ec9 100644
--- a/platform/app/cypress/integration/volume/MPR.spec.js
+++ b/platform/app/cypress/integration/volume/MPR.spec.js
@@ -31,31 +31,8 @@ describe('OHIF MPR', () => {
cy.wait(250);
cy.get('[data-cy="MPR"]').click();
- cy.get('[data-cy="thumbnail-viewport-labels"]').should('have.length', 3);
-
cy.get('.cornerstone-canvas').should('have.length', 3);
- cy.get('[data-cy="thumbnail-viewport-labels"]')
- .eq(2)
- .find('div')
- .should('have.length', 3)
- .each(($div, index) => {
- const text = $div.text();
- switch (index) {
- case 0:
- expect(text).to.equal('A');
- break;
- case 1:
- expect(text).to.equal('B');
- break;
- case 2:
- expect(text).to.equal('C');
- break;
- default:
- throw new Error(`Unexpected div found with text: ${text}`);
- }
- });
-
// check cornerstone to see if each has images
// we can later do visual testing to match the images with a baseline
cy.window()
@@ -81,9 +58,6 @@ describe('OHIF MPR', () => {
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
-
- // should not have any div under it
- cy.get('[data-cy="thumbnail-viewport-labels"]').eq(2).find('div').should('have.length', 0);
});
it('should correctly render Crosshairs for MPR', () => {
diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx
index 23b18b2ed..ec170ab40 100644
--- a/platform/app/src/App.tsx
+++ b/platform/app/src/App.tsx
@@ -5,7 +5,13 @@ import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import Compose from './routes/Mode/Compose';
-import { ServicesManager, ExtensionManager, CommandsManager, HotkeysManager } from '@ohif/core';
+import {
+ ServicesManager,
+ ExtensionManager,
+ CommandsManager,
+ HotkeysManager,
+ ServiceProvidersManager,
+} from '@ohif/core';
import {
DialogProvider,
Modal,
@@ -28,6 +34,7 @@ import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
let commandsManager: CommandsManager,
extensionManager: ExtensionManager,
servicesManager: ServicesManager,
+ serviceProvidersManager: ServiceProvidersManager,
hotkeysManager: HotkeysManager;
function App({ config, defaultExtensions, defaultModes }) {
@@ -48,6 +55,7 @@ function App({ config, defaultExtensions, defaultModes }) {
commandsManager = init.commandsManager;
extensionManager = init.extensionManager;
servicesManager = init.servicesManager;
+ serviceProvidersManager = init.serviceProvidersManager;
hotkeysManager = init.hotkeysManager;
// Set appConfig
@@ -78,6 +86,15 @@ function App({ config, defaultExtensions, defaultModes }) {
[DialogProvider, { service: uiDialogService }],
[ModalProvider, { service: uiModalService, modal: Modal }],
];
+
+ // Loop through and register each of the service providers registered with the ServiceProvidersManager.
+ const providersFromManager = Object.entries(serviceProvidersManager.providers);
+ if (providersFromManager.length > 0) {
+ providersFromManager.forEach(([serviceName, provider]) => {
+ providers.push([provider, { service: servicesManager.services[serviceName] }]);
+ });
+ }
+
const CombinedProviders = ({ children }) => Compose({ components: providers, children });
let authRoutes = null;
diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js
index 0d93ae639..867534bd2 100644
--- a/platform/app/src/appInit.js
+++ b/platform/app/src/appInit.js
@@ -2,6 +2,7 @@ import {
CommandsManager,
ExtensionManager,
ServicesManager,
+ ServiceProvidersManager,
HotkeysManager,
UINotificationService,
UIModalService,
@@ -34,6 +35,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
const commandsManager = new CommandsManager(commandsManagerConfig);
const servicesManager = new ServicesManager(commandsManager);
+ const serviceProvidersManager = new ServiceProvidersManager();
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
const appConfig = {
@@ -45,6 +47,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
+ serviceProvidersManager,
hotkeysManager,
appConfig,
});
@@ -130,6 +133,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
commandsManager,
extensionManager,
servicesManager,
+ serviceProvidersManager,
hotkeysManager,
};
}
diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx
index c4c9c6b83..bfa4ecd70 100644
--- a/platform/app/src/routes/WorkList/WorkList.tsx
+++ b/platform/app/src/routes/WorkList/WorkList.tsx
@@ -32,8 +32,12 @@ import {
ButtonEnums,
} from '@ohif/ui';
+import { Types } from '@ohif/ui';
+
import i18n from '@ohif/i18n';
+const PatientInfoVisibility = Types.PatientInfoVisibility;
+
const { sortBySeriesDate } = utils;
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
@@ -527,6 +531,7 @@ function WorkList({
menuOptions={menuOptions}
isReturnEnabled={false}
WhiteLabeling={appConfig.whiteLabeling}
+ showPatientInfo={PatientInfoVisibility.DISABLED}
/>
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts
index 7f6138344..ed4a9c4c5 100644
--- a/platform/core/src/extensions/ExtensionManager.ts
+++ b/platform/core/src/extensions/ExtensionManager.ts
@@ -1,7 +1,7 @@
import MODULE_TYPES from './MODULE_TYPES';
import log from '../log';
import { AppConfig } from '../types/AppConfig';
-import { PubSubService, ServicesManager } from '../services';
+import { PubSubService, ServiceProvidersManager, ServicesManager } from '../services';
import { HotkeysManager, CommandsManager } from '../classes';
import { DataSourceDefinition } from '../types';
@@ -10,6 +10,7 @@ import { DataSourceDefinition } from '../types';
*/
export interface ExtensionConstructor {
servicesManager: ServicesManager;
+ serviceProvidersManager: ServiceProvidersManager;
commandsManager: CommandsManager;
hotkeysManager: HotkeysManager;
appConfig: AppConfig;
@@ -28,6 +29,7 @@ export type ExtensionConfiguration = Record
;
export interface ExtensionParams extends ExtensionConstructor {
extensionManager: ExtensionManager;
servicesManager: ServicesManager;
+ serviceProvidersManager: ServiceProvidersManager;
configuration?: ExtensionConfiguration;
}
@@ -71,6 +73,7 @@ export default class ExtensionManager extends PubSubService {
private _commandsManager: CommandsManager;
private _servicesManager: ServicesManager;
private _hotkeysManager: HotkeysManager;
+ private _serviceProvidersManager: ServiceProvidersManager;
private modulesMap: Record;
private modules: Record;
private registeredExtensionIds: string[];
@@ -88,6 +91,7 @@ export default class ExtensionManager extends PubSubService {
constructor({
commandsManager,
servicesManager,
+ serviceProvidersManager,
hotkeysManager,
appConfig = {},
}: ExtensionConstructor) {
@@ -98,6 +102,7 @@ export default class ExtensionManager extends PubSubService {
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
+ this._serviceProvidersManager = serviceProvidersManager;
this._hotkeysManager = hotkeysManager;
this._appConfig = appConfig;
@@ -256,6 +261,7 @@ export default class ExtensionManager extends PubSubService {
if (extension.preRegistration) {
await extension.preRegistration({
servicesManager: this._servicesManager,
+ serviceProvidersManager: this._serviceProvidersManager,
commandsManager: this._commandsManager,
hotkeysManager: this._hotkeysManager,
extensionManager: this,
diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js
index 3c0922200..6b2f6efae 100644
--- a/platform/core/src/index.test.js
+++ b/platform/core/src/index.test.js
@@ -9,6 +9,7 @@ describe('Top level exports', () => {
'ExtensionManager',
'HotkeysManager',
'ServicesManager',
+ 'ServiceProvidersManager',
//
'defaults',
'utils',
diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts
index 13612d775..a04b6569f 100644
--- a/platform/core/src/index.ts
+++ b/platform/core/src/index.ts
@@ -1,5 +1,5 @@
import { ExtensionManager, MODULE_TYPES } from './extensions';
-import { ServicesManager } from './services';
+import { ServiceProvidersManager, ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes';
import DICOMWeb from './DICOMWeb';
@@ -51,6 +51,7 @@ const OHIF = {
ExtensionManager,
HotkeysManager,
ServicesManager,
+ ServiceProvidersManager,
//
defaults,
utils,
@@ -92,6 +93,7 @@ export {
ExtensionManager,
HotkeysManager,
ServicesManager,
+ ServiceProvidersManager,
//
defaults,
utils,
diff --git a/platform/core/src/services/ServiceProvidersManager.ts b/platform/core/src/services/ServiceProvidersManager.ts
new file mode 100644
index 000000000..ce2150b6e
--- /dev/null
+++ b/platform/core/src/services/ServiceProvidersManager.ts
@@ -0,0 +1,31 @@
+import log from './../log.js';
+
+/**
+ * The ServiceProvidersManager allows for a React context provider class to be registered
+ * for a particular service. This allows for extensions to register services
+ * with context providers and the providers will be instantiated and added to the
+ * DOM dynamically.
+ */
+export default class ServiceProvidersManager {
+ public providers = {};
+
+ public constructor() {
+ this.providers = {};
+ }
+
+ registerProvider(serviceName, provider) {
+ if (!serviceName) {
+ log.warn(
+ 'Attempting to register a provider to a null/undefined service name. Exiting early.'
+ );
+ return;
+ }
+
+ if (!provider) {
+ log.warn('Attempting to register a null/undefined provider. Exiting early.');
+ return;
+ }
+
+ this.providers[serviceName] = provider;
+ }
+}
diff --git a/platform/core/src/services/UIModalService/index.ts b/platform/core/src/services/UIModalService/index.ts
index 865a2f4b4..eb56bb7ee 100644
--- a/platform/core/src/services/UIModalService/index.ts
+++ b/platform/core/src/services/UIModalService/index.ts
@@ -42,6 +42,9 @@ class UIModalService {
closeButton = true,
title = null,
customClassName = null,
+ movable = false,
+ containerDimensions = null,
+ contentDimensions = null,
}) {
return serviceImplementation._show({
content,
@@ -51,6 +54,9 @@ class UIModalService {
closeButton,
title,
customClassName,
+ movable,
+ containerDimensions,
+ contentDimensions,
});
}
diff --git a/platform/core/src/services/index.ts b/platform/core/src/services/index.ts
index 3921c7873..bee9afd54 100644
--- a/platform/core/src/services/index.ts
+++ b/platform/core/src/services/index.ts
@@ -1,5 +1,6 @@
import MeasurementService from './MeasurementService';
import ServicesManager from './ServicesManager';
+import ServiceProvidersManager from './ServiceProvidersManager';
import UIDialogService from './UIDialogService';
import UIModalService from './UIModalService';
import UINotificationService from './UINotificationService';
@@ -22,6 +23,7 @@ export {
Services,
MeasurementService,
ServicesManager,
+ ServiceProvidersManager,
CustomizationService,
StateSyncService,
UIDialogService,
diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts
index 0c0efa512..5fabe5417 100644
--- a/platform/core/src/types/HangingProtocol.ts
+++ b/platform/core/src/types/HangingProtocol.ts
@@ -133,6 +133,7 @@ export type SyncGroup = {
id: string;
source?: boolean;
target?: boolean;
+ options?: object;
};
/** Declares a custom option, that is a computed type value */
diff --git a/platform/core/src/types/Services.ts b/platform/core/src/types/Services.ts
index ece05c32a..bf5d78591 100644
--- a/platform/core/src/types/Services.ts
+++ b/platform/core/src/types/Services.ts
@@ -33,4 +33,5 @@ export default interface Services {
cornerstoneCacheService?: unknown;
segmentationService?: unknown;
panelService?: unknown;
+ colorbarService?: unknown;
}
diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md
index 959e8d5ae..8182f0c27 100644
--- a/platform/docs/docs/configuration/configurationFiles.md
+++ b/platform/docs/docs/configuration/configurationFiles.md
@@ -189,6 +189,7 @@ if auth headers are used, a preflight request is required.
- `allowMultiSelectExport`: (default to false), if set to true, the user will be able to select the datasource to export the report to.
- `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport.
- `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed
+- `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners
- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
Points to consider while using `dangerouslyUseDynamicConfig`:
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.
diff --git a/platform/i18n/src/locales/en-US/Common.json b/platform/i18n/src/locales/en-US/Common.json
index 2cbb92cd3..a0b5ce3b4 100644
--- a/platform/i18n/src/locales/en-US/Common.json
+++ b/platform/i18n/src/locales/en-US/Common.json
@@ -1,4 +1,5 @@
{
+ "Back to": "Back to {{location}}",
"Close": "Close",
"Image": "Image",
"Layout": "Layout",
diff --git a/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json b/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json
new file mode 100644
index 000000000..9eba89b7e
--- /dev/null
+++ b/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json
@@ -0,0 +1,5 @@
+{
+ "Back to Display Options": "Back to Display Options",
+ "Modality Presets": "{{modality}} Presets",
+ "Modality Window Presets": "{{modality}} Window Presets"
+}
diff --git a/platform/i18n/src/locales/en-US/index.js b/platform/i18n/src/locales/en-US/index.js
index eb3d442ba..6004ac655 100644
--- a/platform/i18n/src/locales/en-US/index.js
+++ b/platform/i18n/src/locales/en-US/index.js
@@ -20,6 +20,7 @@ import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json';
import UserPreferencesModal from './UserPreferencesModal.json';
import ViewportDownloadForm from './ViewportDownloadForm.json';
import Messages from './Messages.json';
+import WindowLevelActionMenu from './WindowLevelActionMenu.json';
export default {
'en-US': {
@@ -45,5 +46,6 @@ export default {
UserPreferencesModal,
ViewportDownloadForm,
Messages,
+ WindowLevelActionMenu,
},
};
diff --git a/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json b/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json
new file mode 100644
index 000000000..13c1c8228
--- /dev/null
+++ b/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json
@@ -0,0 +1,5 @@
+{
+ "Back to Display Options": "Test Back to Display Options",
+ "Modality Presets": "Test {{modality}} Presets",
+ "Modality Window Presets": "Test {{modality}} Window Presets"
+}
diff --git a/platform/i18n/src/locales/test-LNG/index.js b/platform/i18n/src/locales/test-LNG/index.js
index 561161d2f..16997ce50 100644
--- a/platform/i18n/src/locales/test-LNG/index.js
+++ b/platform/i18n/src/locales/test-LNG/index.js
@@ -22,6 +22,7 @@ import ThumbnailTracked from './ThumbnailTracked.json';
import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json';
import UserPreferencesModal from './UserPreferencesModal.json';
import ViewportDownloadForm from './ViewportDownloadForm.json';
+import WindowLevelActionMenu from './WindowLevelActionMenu.json';
export default {
'test-LNG': {
@@ -49,5 +50,6 @@ export default {
TrackedCornerstoneViewport,
UserPreferencesModal,
ViewportDownloadForm,
+ WindowLevelActionMenu,
},
};
diff --git a/platform/ui/src/assets/icons/CT-AAA.png b/platform/ui/src/assets/icons/CT-AAA.png
new file mode 100644
index 000000000..67c6bf778
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-AAA.png differ
diff --git a/platform/ui/src/assets/icons/CT-AAA2.png b/platform/ui/src/assets/icons/CT-AAA2.png
new file mode 100644
index 000000000..4c51a6c28
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-AAA2.png differ
diff --git a/platform/ui/src/assets/icons/CT-Air.png b/platform/ui/src/assets/icons/CT-Air.png
new file mode 100644
index 000000000..a65680aa4
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Air.png differ
diff --git a/platform/ui/src/assets/icons/CT-Bone.png b/platform/ui/src/assets/icons/CT-Bone.png
new file mode 100644
index 000000000..7c3f8c9ef
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Bone.png differ
diff --git a/platform/ui/src/assets/icons/CT-Bones.png b/platform/ui/src/assets/icons/CT-Bones.png
new file mode 100644
index 000000000..441d6bf3a
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Bones.png differ
diff --git a/platform/ui/src/assets/icons/CT-Cardiac.png b/platform/ui/src/assets/icons/CT-Cardiac.png
new file mode 100644
index 000000000..3f9daadb3
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Cardiac.png differ
diff --git a/platform/ui/src/assets/icons/CT-Cardiac2.png b/platform/ui/src/assets/icons/CT-Cardiac2.png
new file mode 100644
index 000000000..a281b2425
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Cardiac2.png differ
diff --git a/platform/ui/src/assets/icons/CT-Cardiac3.png b/platform/ui/src/assets/icons/CT-Cardiac3.png
new file mode 100644
index 000000000..0b8773ef8
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Cardiac3.png differ
diff --git a/platform/ui/src/assets/icons/CT-Chest-Contrast-Enhanced.png b/platform/ui/src/assets/icons/CT-Chest-Contrast-Enhanced.png
new file mode 100644
index 000000000..be165b4c0
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Chest-Contrast-Enhanced.png differ
diff --git a/platform/ui/src/assets/icons/CT-Chest-Vessels.png b/platform/ui/src/assets/icons/CT-Chest-Vessels.png
new file mode 100644
index 000000000..23f8732c5
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Chest-Vessels.png differ
diff --git a/platform/ui/src/assets/icons/CT-Coronary-Arteries-2.png b/platform/ui/src/assets/icons/CT-Coronary-Arteries-2.png
new file mode 100644
index 000000000..1b6b16100
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Coronary-Arteries-2.png differ
diff --git a/platform/ui/src/assets/icons/CT-Coronary-Arteries-3.png b/platform/ui/src/assets/icons/CT-Coronary-Arteries-3.png
new file mode 100644
index 000000000..088a28611
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Coronary-Arteries-3.png differ
diff --git a/platform/ui/src/assets/icons/CT-Coronary-Arteries.png b/platform/ui/src/assets/icons/CT-Coronary-Arteries.png
new file mode 100644
index 000000000..3b32f1b73
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Coronary-Arteries.png differ
diff --git a/platform/ui/src/assets/icons/CT-Cropped-Volume-Bone.png b/platform/ui/src/assets/icons/CT-Cropped-Volume-Bone.png
new file mode 100644
index 000000000..13c0922ed
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Cropped-Volume-Bone.png differ
diff --git a/platform/ui/src/assets/icons/CT-Fat.png b/platform/ui/src/assets/icons/CT-Fat.png
new file mode 100644
index 000000000..9cdd78a25
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Fat.png differ
diff --git a/platform/ui/src/assets/icons/CT-Liver-Vasculature.png b/platform/ui/src/assets/icons/CT-Liver-Vasculature.png
new file mode 100644
index 000000000..b33856d12
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Liver-Vasculature.png differ
diff --git a/platform/ui/src/assets/icons/CT-Lung.png b/platform/ui/src/assets/icons/CT-Lung.png
new file mode 100644
index 000000000..158f3d7b2
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Lung.png differ
diff --git a/platform/ui/src/assets/icons/CT-MIP.png b/platform/ui/src/assets/icons/CT-MIP.png
new file mode 100644
index 000000000..30a93561e
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-MIP.png differ
diff --git a/platform/ui/src/assets/icons/CT-Muscle.png b/platform/ui/src/assets/icons/CT-Muscle.png
new file mode 100644
index 000000000..76ecdc41d
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Muscle.png differ
diff --git a/platform/ui/src/assets/icons/CT-Pulmonary-Arteries.png b/platform/ui/src/assets/icons/CT-Pulmonary-Arteries.png
new file mode 100644
index 000000000..4558000e0
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Pulmonary-Arteries.png differ
diff --git a/platform/ui/src/assets/icons/CT-Soft-Tissue.png b/platform/ui/src/assets/icons/CT-Soft-Tissue.png
new file mode 100644
index 000000000..f03690019
Binary files /dev/null and b/platform/ui/src/assets/icons/CT-Soft-Tissue.png differ
diff --git a/platform/ui/src/assets/icons/DTI-FA-Brain.png b/platform/ui/src/assets/icons/DTI-FA-Brain.png
new file mode 100644
index 000000000..964354622
Binary files /dev/null and b/platform/ui/src/assets/icons/DTI-FA-Brain.png differ
diff --git a/platform/ui/src/assets/icons/MR-Angio.png b/platform/ui/src/assets/icons/MR-Angio.png
new file mode 100644
index 000000000..f54d6fa5a
Binary files /dev/null and b/platform/ui/src/assets/icons/MR-Angio.png differ
diff --git a/platform/ui/src/assets/icons/MR-Default.png b/platform/ui/src/assets/icons/MR-Default.png
new file mode 100644
index 000000000..f8bf302c1
Binary files /dev/null and b/platform/ui/src/assets/icons/MR-Default.png differ
diff --git a/platform/ui/src/assets/icons/MR-MIP.png b/platform/ui/src/assets/icons/MR-MIP.png
new file mode 100644
index 000000000..8b3e91a24
Binary files /dev/null and b/platform/ui/src/assets/icons/MR-MIP.png differ
diff --git a/platform/ui/src/assets/icons/MR-T2-Brain.png b/platform/ui/src/assets/icons/MR-T2-Brain.png
new file mode 100644
index 000000000..8b1f7a550
Binary files /dev/null and b/platform/ui/src/assets/icons/MR-T2-Brain.png differ
diff --git a/platform/ui/src/assets/icons/VolumeRendering.png b/platform/ui/src/assets/icons/VolumeRendering.png
new file mode 100644
index 000000000..8d7313ea2
Binary files /dev/null and b/platform/ui/src/assets/icons/VolumeRendering.png differ
diff --git a/platform/ui/src/assets/icons/action-new-dialog.svg b/platform/ui/src/assets/icons/action-new-dialog.svg
new file mode 100644
index 000000000..89331dc6b
--- /dev/null
+++ b/platform/ui/src/assets/icons/action-new-dialog.svg
@@ -0,0 +1,14 @@
+
+
+ action-new-dialog
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/content-next.svg b/platform/ui/src/assets/icons/content-next.svg
new file mode 100644
index 000000000..45f0b3985
--- /dev/null
+++ b/platform/ui/src/assets/icons/content-next.svg
@@ -0,0 +1,9 @@
+
+
+ chevron-next
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/content-prev.svg b/platform/ui/src/assets/icons/content-prev.svg
new file mode 100644
index 000000000..a01164b6b
--- /dev/null
+++ b/platform/ui/src/assets/icons/content-prev.svg
@@ -0,0 +1,9 @@
+
+
+ chevron-prev
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-chevron-patient.svg b/platform/ui/src/assets/icons/icon-chevron-patient.svg
new file mode 100644
index 000000000..b96478759
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-chevron-patient.svg
@@ -0,0 +1,12 @@
+
+
+ icon-chevron-patient
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-color-lut.svg b/platform/ui/src/assets/icons/icon-color-lut.svg
new file mode 100644
index 000000000..a339125e7
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-color-lut.svg
@@ -0,0 +1,23 @@
+
+
+ icon-color-lut
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-multiple-patients.svg b/platform/ui/src/assets/icons/icon-multiple-patients.svg
new file mode 100644
index 000000000..276662f9a
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-multiple-patients.svg
@@ -0,0 +1,21 @@
+
+
+ icon-multiple-patients
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-patient.svg b/platform/ui/src/assets/icons/icon-patient.svg
new file mode 100644
index 000000000..0ddc8f7e6
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-patient.svg
@@ -0,0 +1,13 @@
+
+
+ icon-patient
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-settings.svg b/platform/ui/src/assets/icons/icon-settings.svg
new file mode 100644
index 000000000..381dae6f2
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-settings.svg
@@ -0,0 +1,13 @@
+
+
+ icon-settings
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/icon-toolbar-back.svg b/platform/ui/src/assets/icons/icon-toolbar-back.svg
new file mode 100644
index 000000000..8ee453b2c
--- /dev/null
+++ b/platform/ui/src/assets/icons/icon-toolbar-back.svg
@@ -0,0 +1,12 @@
+
+
+ icon-toolbar-back
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/next-arrow.svg b/platform/ui/src/assets/icons/next-arrow.svg
new file mode 100644
index 000000000..027a17de3
--- /dev/null
+++ b/platform/ui/src/assets/icons/next-arrow.svg
@@ -0,0 +1,12 @@
+
+
+ arrow-right
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/prev-arrow.svg b/platform/ui/src/assets/icons/prev-arrow.svg
new file mode 100644
index 000000000..455afb6bf
--- /dev/null
+++ b/platform/ui/src/assets/icons/prev-arrow.svg
@@ -0,0 +1,12 @@
+
+
+ arrow-left
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/viewport-status-tracked.svg b/platform/ui/src/assets/icons/viewport-status-tracked.svg
new file mode 100644
index 000000000..68185761a
--- /dev/null
+++ b/platform/ui/src/assets/icons/viewport-status-tracked.svg
@@ -0,0 +1,13 @@
+
+
+ viewport-status-tracfked
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/icons/viewport-window-level.svg b/platform/ui/src/assets/icons/viewport-window-level.svg
new file mode 100644
index 000000000..ce0a562b2
--- /dev/null
+++ b/platform/ui/src/assets/icons/viewport-window-level.svg
@@ -0,0 +1,17 @@
+
+
+ viewport-window-level
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/assets/styles/styles.css b/platform/ui/src/assets/styles/styles.css
index addaa2375..05abcb151 100644
--- a/platform/ui/src/assets/styles/styles.css
+++ b/platform/ui/src/assets/styles/styles.css
@@ -1,7 +1,9 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
- scrollbar-color: #173239 transparent;
+ scrollbar-color: #041c4a transparent;
scrollbar-gutter: stable;
+ scrollbar-width: thin;
+
}
.study-min-height {
@@ -13,7 +15,7 @@
}
.ohif-scrollbar::-webkit-scrollbar {
- width: 8px;
+ scrollbar-width: thin;
}
.ohif-scrollbar::-webkit-scrollbar-track {
@@ -22,13 +24,13 @@
.ohif-scrollbar::-webkit-scrollbar-thumb {
@apply rounded;
- @apply bg-primary-main;
- background-color: #173239;
+ @apply bg-secondary-dark;
+ background-color: #041c4a;
}
.ohif-scrollbar::-webkit-scrollbar-thumb:window-inactive {
- @apply bg-primary-main;
- background-color: #173239;
+ @apply bg-secondary-dark;
+ background-color: #041c4a;
}
/* INVISIBLE SCROLLBAR */
diff --git a/platform/ui/src/assets/svgs/ohif-logo.svg b/platform/ui/src/assets/svgs/ohif-logo.svg
new file mode 100644
index 000000000..3692116b4
--- /dev/null
+++ b/platform/ui/src/assets/svgs/ohif-logo.svg
@@ -0,0 +1,18 @@
+
+
+ toolbar-branding-open-health-imaging-foundation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/ui/src/components/AllInOneMenu/BackItem.tsx b/platform/ui/src/components/AllInOneMenu/BackItem.tsx
new file mode 100644
index 000000000..ac5ca51b7
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/BackItem.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import { Icon } from '@ohif/ui';
+import DividerItem from './DividerItem';
+
+type BackItemProps = {
+ backLabel?: string;
+ onBackClick: () => void;
+};
+
+const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
+ return (
+ <>
+
+
+
+
{backLabel || 'Back to Display Options'}
+
+
+ >
+ );
+};
+
+export default BackItem;
diff --git a/platform/ui/src/components/AllInOneMenu/DividerItem.tsx b/platform/ui/src/components/AllInOneMenu/DividerItem.tsx
new file mode 100644
index 000000000..056d68e9c
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/DividerItem.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+const DividerItem = () => {
+ return (
+
+ );
+};
+
+export default DividerItem;
diff --git a/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx b/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx
new file mode 100644
index 000000000..43ba18314
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx
@@ -0,0 +1,13 @@
+import React, { ReactNode } from 'react';
+
+type HeaderItemProps = {
+ children: ReactNode;
+};
+
+const HeaderItem = ({ children }: HeaderItemProps) => {
+ return (
+ {children}
+ );
+};
+
+export default HeaderItem;
diff --git a/platform/ui/src/components/AllInOneMenu/IconMenu.tsx b/platform/ui/src/components/AllInOneMenu/IconMenu.tsx
new file mode 100644
index 000000000..7b57f6f2f
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/IconMenu.tsx
@@ -0,0 +1,82 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import OutsideClickHandler from 'react-outside-click-handler';
+import { MenuProps } from './Menu';
+import getIcon from '../Icon/getIcon';
+import classNames from 'classnames';
+import { AllInOneMenu } from '..';
+export interface IconMenuProps extends MenuProps {
+ icon: string;
+ iconClassName?: string;
+ horizontalDirection?: AllInOneMenu.HorizontalDirection;
+ verticalDirection?: AllInOneMenu.VerticalDirection;
+ menuKey?: number | string;
+}
+
+/**
+ * An IconMenu allows for a div wrapped icon to be clicked to show and hide
+ * an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
+ * positioned relative to the icon.
+ *
+ * HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
+ * HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
+ * VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
+ * VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
+ *
+ * For example, if an IconMenu were situated in the bottom-left corner of a container,
+ * it would be best to use BottomToTop and LeftToRight directions for it.
+ */
+export default function IconMenu({
+ icon,
+ iconClassName,
+ horizontalDirection,
+ verticalDirection,
+ children,
+ backLabel,
+ menuClassName,
+ menuStyle,
+ onVisibilityChange,
+ menuKey,
+}: IconMenuProps) {
+ const [isMenuVisible, setIsMenuVisible] = useState(false);
+
+ const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
+
+ return (
+
+
+
+ {getIcon(icon)}
+
+
{
+ setIsMenuVisible(isVis);
+ onVisibilityChange?.(isVis);
+ }}
+ horizontalDirection={horizontalDirection}
+ >
+ {children}
+
+
+
+ );
+}
diff --git a/platform/ui/src/components/AllInOneMenu/Item.tsx b/platform/ui/src/components/AllInOneMenu/Item.tsx
new file mode 100644
index 000000000..7ad4511a3
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/Item.tsx
@@ -0,0 +1,45 @@
+import React, { ReactNode, useCallback, useContext } from 'react';
+import { MenuContext } from './Menu';
+
+type ItemProps = {
+ label: string;
+ secondaryLabel?: string;
+ icon?: ReactNode;
+ onClick?: () => void;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
+ rightIcon?: ReactNode;
+};
+
+const Item = ({
+ label,
+ secondaryLabel,
+ icon,
+ rightIcon,
+ onClick,
+ onMouseEnter,
+ onMouseLeave,
+}: ItemProps) => {
+ const { hideMenu } = useContext(MenuContext);
+
+ const onClickHandler = useCallback(() => {
+ hideMenu();
+ onClick?.();
+ }, [hideMenu, onClick]);
+
+ return (
+
+ {icon &&
{icon}
}
+
{label}
+ {secondaryLabel != null &&
{secondaryLabel} }
+ {rightIcon &&
{rightIcon}
}
+
+ );
+};
+
+export default Item;
diff --git a/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx b/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx
new file mode 100644
index 000000000..abe407c7f
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx
@@ -0,0 +1,29 @@
+import React, { ReactNode, useContext, useEffect } from 'react';
+import { MenuContext } from './Menu';
+
+type ItemPanelProps = {
+ label?: string;
+ index?: number;
+ children: ReactNode;
+};
+
+const ItemPanel = ({ label, index = 0, children }: ItemPanelProps) => {
+ const { addItemPanel, activePanelIndex } = useContext(MenuContext);
+
+ useEffect(() => {
+ addItemPanel(index, label);
+ }, []);
+
+ return (
+ activePanelIndex === index && (
+
+ {children}
+
+ )
+ );
+};
+
+export default ItemPanel;
diff --git a/platform/ui/src/components/AllInOneMenu/Menu.tsx b/platform/ui/src/components/AllInOneMenu/Menu.tsx
new file mode 100644
index 000000000..7e506496b
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/Menu.tsx
@@ -0,0 +1,188 @@
+import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
+
+import './allInOneMenu.css';
+import DividerItem from './DividerItem';
+import PanelSelector from './PanelSelector';
+import classNames from 'classnames';
+import BackItem from './BackItem';
+
+/**
+ * The vertical direction that the menu will be opened/used with.
+ *
+ * A TopToBottom menu would be used for cases where the menu is opened "near"
+ * the top edge of its container. Likewise a BottomToTop menu would be used
+ * for cases where the menu is opened "near" the bottom edge of its container.
+ *
+ * See IconMenu for more information.
+ */
+export enum VerticalDirection {
+ TopToBottom,
+ BottomToTop,
+}
+
+/**
+ * The horizontal direction that the menu is opened/used with.
+ * This direction dictates the general direction sub-menus and
+ * back-to-menus are opened with. For example, a RightToLeft menu
+ * will have sub-menu items indicated with a left pointing chevron
+ * and aligned with the left edge of the menu. Similarly back-to items of a
+ * RightToLeft menu are indicated with a right pointing chevron and
+ * aligned with the right edge of the menu.
+ *
+ * It is also worth noting that a LeftToRight menu would be used for
+ * cases where a menu is opened "near" the left edge of its container.
+ * Likewise, a RightToLeft menu would be used for cases where a menu is opened
+ * "near" the right edge of its container.
+ *
+ * See IconMenu for more information.
+ */
+export enum HorizontalDirection {
+ LeftToRight,
+ RightToLeft,
+}
+
+export interface MenuProps {
+ menuStyle?: unknown;
+ menuClassName?: string;
+ isVisible?: boolean;
+ preventHideMenu?: boolean;
+ backLabel?: string;
+ headerComponent?: ReactNode;
+ showHeaderDivider?: boolean;
+ activePanelIndex?: number;
+ onVisibilityChange?: (isVisible: boolean) => void;
+ horizontalDirection?: HorizontalDirection;
+ children: ReactNode;
+}
+type MenuContextProps = {
+ showSubMenu: (subMenuProps: MenuProps) => void;
+ hideMenu: () => void;
+ addItemPanel: (index: number, label: string) => void;
+ horizontalDirection: HorizontalDirection;
+ activePanelIndex: number;
+};
+
+type MenuPathState = {
+ props: MenuProps;
+ activePanelIndex: number;
+};
+
+export const MenuContext = createContext(null);
+
+const Menu = (props: MenuProps) => {
+ const {
+ isVisible,
+ onVisibilityChange,
+ activePanelIndex,
+ preventHideMenu,
+ menuClassName,
+ menuStyle,
+ horizontalDirection = HorizontalDirection.LeftToRight,
+ } = props;
+
+ const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
+
+ // The menuPath is an array consisting of this top Menu and every SubMenu
+ // that has been traversed/opened by the user with the last item in the array
+ // being the current (sub)menu that is currently visible. This allows for the previously
+ // viewed menus to be returned to via the Back button at the top of the menu.
+ const [menuPath, setMenuPath] = useState>([
+ { props, activePanelIndex: activePanelIndex || 0 },
+ ]);
+ const [itemPanelLabels, setItemPanelLabels] = useState>([]);
+
+ const hideMenu = useCallback(() => {
+ if (preventHideMenu) {
+ return;
+ }
+ setMenuPath(path => [path[0]]);
+ setItemPanelLabels([]);
+ setIsMenuVisible(false);
+ onVisibilityChange?.(false);
+ }, [preventHideMenu, onVisibilityChange]);
+
+ useEffect(() => {
+ if (isVisible) {
+ setIsMenuVisible(isVisible);
+ onVisibilityChange?.(isVisible);
+ } else {
+ hideMenu();
+ }
+ }, [hideMenu, isVisible, onVisibilityChange]);
+
+ const showSubMenu = useCallback((subMenuProps: MenuProps) => {
+ setMenuPath(path => {
+ return [
+ ...path,
+ { props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
+ ];
+ });
+ setItemPanelLabels([]);
+ }, []);
+
+ const addItemPanel = useCallback((index, label) => {
+ setItemPanelLabels(labels => {
+ return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
+ });
+ }, []);
+
+ const onActivePanelIndexChange = useCallback(index => {
+ setMenuPath(path => {
+ return [
+ ...path.slice(0, path.length - 1),
+ { ...path[path.length - 1], activePanelIndex: index },
+ ];
+ });
+ }, []);
+
+ const onBackClick = useCallback(() => {
+ setMenuPath(path => [...path.slice(0, path.length - 1)]);
+ setItemPanelLabels([]);
+ }, []);
+
+ const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
+ menuPath[menuPath.length - 1];
+
+ return (
+ <>
+
+ {isMenuVisible && (
+
+ {menuPath.length > 1 && (
+
+ )}
+ {itemPanelLabels.length > 1 && (
+
+ )}
+ {currentMenuProps.headerComponent}
+ {currentMenuProps.showHeaderDivider &&
}
+ {currentMenuProps.children}
+
+ )}
+
+ >
+ );
+};
+
+export default Menu;
diff --git a/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx b/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx
new file mode 100644
index 000000000..c46207b54
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx
@@ -0,0 +1,31 @@
+import React, { ReactNode } from 'react';
+import { ButtonGroup } from '../../components';
+
+type PanelSelectorProps = {
+ panelLabels: Array;
+ onActiveIndexChange: (index: number) => void;
+ activeIndex: number;
+};
+
+const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
+ const getButtons = () => {
+ return panelLabels.map((panelLabel, index) => {
+ return {
+ children: panelLabel,
+ key: index,
+ };
+ });
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default PanelSelector;
diff --git a/platform/ui/src/components/AllInOneMenu/SubMenu.tsx b/platform/ui/src/components/AllInOneMenu/SubMenu.tsx
new file mode 100644
index 000000000..42786622a
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/SubMenu.tsx
@@ -0,0 +1,38 @@
+import React, { useCallback, useContext } from 'react';
+import { MenuContext, MenuProps } from './Menu';
+import Icon from '../Icon';
+
+export interface SubMenuProps extends MenuProps {
+ itemLabel: string;
+ onClick?: () => void;
+ itemIcon?: string;
+}
+
+const SubMenu = (props: SubMenuProps) => {
+ const { showSubMenu } = useContext(MenuContext);
+
+ const onClickHandler = useCallback(() => {
+ showSubMenu(props);
+ props.onClick?.();
+ }, [showSubMenu, props]);
+
+ return (
+
+ {props.itemIcon && (
+
+ )}
+
{props.itemLabel}
+
+
+ );
+};
+
+export default SubMenu;
diff --git a/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx b/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx
new file mode 100644
index 000000000..a513dc672
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx
@@ -0,0 +1,129 @@
+import { DividerItem, HeaderItem, Item, ItemPanel, Menu, SubMenu } from '..';
+import InputRange from '../../InputRange/index.js';
+import SwitchButton from '../../SwitchButton/index.js';
+
+import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
+
+export const argTypes = {
+ component: Menu,
+ title: 'Components/AllInOneMenu',
+};
+
+
+
+export const AllInOneMenuTemplate = args => (
+
+
+
+ - console.info('Item 1 clicked.')}
+ >
+
+
+
Arbitrary item component:
+
{}}
+ >
+
+
+ console.info('Sub menu item clicked.')}
+ showHeaderDivider={true}
+ headerComponent={
+
+
+
+ }
+ >
+
+
+
+
+
+ Header for scrolling list of items}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+
+
+- [Overview](#overview)
+- [Props](#props)
+- [Contribute](#contribute)
+
+## Overview
+
+AllInOneMenu is a component that renders a menu with various menu items, sub-menus and
+sub-components. The particular feature of the AllInOneMenu is its ability to render a sub-menu by
+replacing the parent menu on screen. It also provides the ability to return to the parent menu with
+a back menu item from the sub-menu. Furthermore, each menu level can be split into item panes - that
+is several panes of menu items that are switched in and out of view like a tabbed pane.
+
+
+ console.info(`The menu visibility: ${isVisible}`),
+ }}
+>
+
+ {AllInOneMenuTemplate.bind({})}
+
+
+
+
+## Props
+
+
+
+## Usage
+
+## Contribute
+
+
diff --git a/platform/ui/src/components/AllInOneMenu/allInOneMenu.css b/platform/ui/src/components/AllInOneMenu/allInOneMenu.css
new file mode 100644
index 000000000..ce8ea955c
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/allInOneMenu.css
@@ -0,0 +1,11 @@
+.all-in-one-menu-item {
+ @apply h-8 px-2 text-[14px] w-full;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ line-height: 18px;
+}
+
+.all-in-one-menu-item-effects {
+ @apply cursor-pointer hover:bg-primary-dark hover:rounded;
+}
diff --git a/platform/ui/src/components/AllInOneMenu/index.tsx b/platform/ui/src/components/AllInOneMenu/index.tsx
new file mode 100644
index 000000000..f0522cf86
--- /dev/null
+++ b/platform/ui/src/components/AllInOneMenu/index.tsx
@@ -0,0 +1,19 @@
+import Menu, { HorizontalDirection, VerticalDirection } from './Menu';
+import DividerItem from './DividerItem';
+import HeaderItem from './HeaderItem';
+import IconMenu from './IconMenu';
+import Item from './Item';
+import ItemPanel from './ItemPanel';
+import SubMenu from './SubMenu';
+
+export {
+ Menu,
+ DividerItem,
+ HeaderItem,
+ IconMenu,
+ Item,
+ ItemPanel,
+ SubMenu,
+ HorizontalDirection,
+ VerticalDirection,
+};
diff --git a/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx b/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx
index d47288bf2..ca080a138 100644
--- a/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx
+++ b/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx
@@ -41,7 +41,7 @@ const ButtonGroup = ({
return cloneElement(child, {
key: index,
className: classnames(
- 'rounded-[4px] px-2 py-1',
+ 'rounded-[4px] px-2 py-1 text-center',
index === activeIndex
? 'bg-customblue-40 text-white'
: 'text-primary-active bg-black',
diff --git a/platform/ui/src/components/Header/Header.tsx b/platform/ui/src/components/Header/Header.tsx
index 99b97b527..5d4cd63e9 100644
--- a/platform/ui/src/components/Header/Header.tsx
+++ b/platform/ui/src/components/Header/Header.tsx
@@ -8,6 +8,8 @@ import Svg from '../Svg';
import Icon from '../Icon';
import IconButton from '../IconButton';
import Dropdown from '../Dropdown';
+import HeaderPatientInfo from '../HeaderPatientInfo';
+import { PatientInfoVisibility } from '../../types/PatientInfoVisibility';
function Header({
children,
@@ -16,6 +18,8 @@ function Header({
onClickReturnButton,
isSticky,
WhiteLabeling,
+ showPatientInfo = PatientInfoVisibility.VISIBLE_COLLAPSED,
+ servicesManager,
...props
}): ReactNode {
const { t } = useTranslation('Header');
@@ -34,7 +38,7 @@ function Header({
{...props}
>
-
+
)}
-
+
{WhiteLabeling?.createLogoComponentFn?.(React, props) || }
@@ -58,32 +62,30 @@ function Header({
-
-
-
+ {(showPatientInfo === PatientInfoVisibility.VISIBLE ||
+ showPatientInfo === PatientInfoVisibility.VISIBLE_COLLAPSED) && (
+
+ )}
+
+
+
-
-
-
-
-
-
+
+
+
+
+
@@ -103,6 +105,8 @@ Header.propTypes = {
isSticky: PropTypes.bool,
onClickReturnButton: PropTypes.func,
WhiteLabeling: PropTypes.object,
+ showPatientInfo: PropTypes.string,
+ servicesManager: PropTypes.object,
};
Header.defaultProps = {
diff --git a/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx b/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx
new file mode 100644
index 000000000..ec665a7a9
--- /dev/null
+++ b/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx
@@ -0,0 +1,134 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Icon } from '@ohif/ui';
+import { utils } from '@ohif/core';
+import { useAppConfig } from '@state';
+
+const { formatDate, formatPN } = utils;
+
+const formatWithEllipsis = (str, maxLength) => {
+ if (str.length > maxLength) {
+ return str.substring(0, maxLength) + '...';
+ }
+ return str;
+};
+
+function usePatientInfo(servicesManager) {
+ const { displaySetService } = servicesManager.services;
+
+ const [patientInfo, setPatientInfo] = useState({
+ PatientName: '',
+ PatientID: '',
+ PatientSex: '',
+ PatientDOB: '',
+ });
+ const [isMixedPatients, setIsMixedPatients] = useState(false);
+ const displaySets = displaySetService.getActiveDisplaySets();
+
+ const checkMixedPatients = PatientID => {
+ const displaySets = displaySetService.getActiveDisplaySets();
+ let isMixedPatients = false;
+ displaySets.forEach(displaySet => {
+ const instance = displaySet?.instances?.[0] || displaySet?.instance;
+ if (!instance) {
+ return;
+ }
+ if (instance.PatientID !== PatientID) {
+ isMixedPatients = true;
+ }
+ });
+ setIsMixedPatients(isMixedPatients);
+ };
+
+ const updatePatientInfo = () => {
+ const displaySet = displaySets[0];
+ const instance = displaySet?.instances?.[0] || displaySet?.instance;
+ if (!instance) {
+ return;
+ }
+ setPatientInfo({
+ PatientID: instance.PatientID || '',
+ PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : '',
+ PatientSex: instance.PatientSex || '',
+ PatientDOB: formatDate(instance.PatientBirthDate) || '',
+ });
+ checkMixedPatients(instance.PatientID || '');
+ };
+
+ useEffect(() => {
+ const subscription = displaySetService.subscribe(
+ displaySetService.EVENTS.DISPLAY_SETS_ADDED,
+ () => updatePatientInfo()
+ );
+ return () => subscription.unsubscribe();
+ }, []);
+
+ useEffect(() => {
+ updatePatientInfo();
+ }, [displaySets]);
+
+ return { patientInfo, isMixedPatients };
+}
+
+function HeaderPatientInfo({ servicesManager }) {
+ const [appConfig] = useAppConfig();
+ const initialExpandedState = appConfig.showPatientInfo === 'visible';
+ const [expanded, setExpanded] = useState(initialExpandedState);
+ const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
+
+ useEffect(() => {
+ if (isMixedPatients && expanded) {
+ setExpanded(false);
+ }
+ }, [isMixedPatients, expanded]);
+
+ const handleOnClick = () => {
+ if (!isMixedPatients) {
+ setExpanded(!expanded);
+ }
+ };
+
+ const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27);
+ const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15);
+
+ return (
+
+
+
+ {expanded ? (
+ <>
+
+ {formattedPatientName}
+
+
+
{formattedPatientID}
+
{patientInfo.PatientSex}
+
{patientInfo.PatientDOB}
+
+ >
+ ) : (
+
+ {' '}
+ {isMixedPatients ? 'Multiple Patients' : 'Patient'}
+
+ )}
+
+
+
+ );
+}
+
+HeaderPatientInfo.propTypes = {
+ servicesManager: PropTypes.object.isRequired,
+};
+
+export default HeaderPatientInfo;
diff --git a/platform/ui/src/components/HeaderPatientInfo/index.js b/platform/ui/src/components/HeaderPatientInfo/index.js
new file mode 100644
index 000000000..cc989ba53
--- /dev/null
+++ b/platform/ui/src/components/HeaderPatientInfo/index.js
@@ -0,0 +1,3 @@
+import HeaderPatientInfo from './HeaderPatientInfo';
+
+export default HeaderPatientInfo;
diff --git a/platform/ui/src/components/Icon/getIcon.js b/platform/ui/src/components/Icon/getIcon.js
index 4940fb771..e02d77b20 100644
--- a/platform/ui/src/components/Icon/getIcon.js
+++ b/platform/ui/src/components/Icon/getIcon.js
@@ -17,6 +17,8 @@ import chevronMenu from './../../assets/icons/chevron-menu.svg';
import chevronNext from './../../assets/icons/chevron-next.svg';
import chevronPrev from './../../assets/icons/chevron-prev.svg';
import chevronRight from './../../assets/icons/chevron-right.svg';
+import contentNext from './../../assets/icons/content-next.svg';
+import contentPrev from './../../assets/icons/content-prev.svg';
import eyeVisible from './../../assets/icons/eye-visible.svg';
import eyeHidden from './../../assets/icons/eye-hidden.svg';
import exclamation from './../../assets/icons/exclamation.svg';
@@ -83,6 +85,9 @@ import notificationsInfo from './../../assets/icons/notifications-info.svg';
import notificationsWarning from './../../assets/icons/notifications-warning.svg';
import notificationsError from './../../assets/icons/notifications-error.svg';
import notificationsSuccess from './../../assets/icons/notifications-success.svg';
+import nextArrow from './../../assets/icons/next-arrow.svg';
+import prevArrow from './../../assets/icons/prev-arrow.svg';
+import viewportStatusTracked from './../../assets/icons/viewport-status-tracked.svg';
/** Tools */
import toggleDicomOverlay from './../../assets/icons/toggle-dicom-overlay.svg';
@@ -139,12 +144,53 @@ import iconToolEraser from './../../assets/icons/icon-tool-eraser.svg';
import iconToolScissor from './../../assets/icons/icon-tool-scissor.svg';
import iconToolShape from './../../assets/icons/icon-tool-shape.svg';
import iconToolThreshold from './../../assets/icons/icon-tool-threshold.svg';
+import viewportWindowLevel from './../../assets/icons/viewport-window-level.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
import oldPlay from './../../assets/icons/old-play.svg';
import oldStop from './../../assets/icons/old-stop.svg';
+/** ColorLut */
+import iconColorLUT from './../../assets/icons/icon-color-lut.svg';
+
+/** New Patient Info Toolbar */
+import iconChevronPatient from './../../assets/icons/icon-chevron-patient.svg';
+import iconPatient from './../../assets/icons/icon-patient.svg';
+import iconSettings from './../../assets/icons/icon-settings.svg';
+import iconToolbarBack from './../../assets/icons/icon-toolbar-back.svg';
+import iconMultiplePatients from './../../assets/icons/icon-multiple-patients.svg';
+
+/** Volume Rendering */
+import CTAAA from './../../assets/icons/CT-AAA.png';
+import CTAAA2 from './../../assets/icons/CT-AAA2.png';
+import CTAir from './../../assets/icons/CT-Air.png';
+import CTBone from './../../assets/icons/CT-Bone.png';
+import CTBones from './../../assets/icons/CT-Bones.png';
+import CTCardiac from './../../assets/icons/CT-Cardiac.png';
+import CTCardiac2 from './../../assets/icons/CT-Cardiac2.png';
+import CTCardiac3 from './../../assets/icons/CT-Cardiac3.png';
+import CTChestContrastEnhanced from './../../assets/icons/CT-Chest-Contrast-Enhanced.png';
+import CTChestVessels from './../../assets/icons/CT-Chest-Vessels.png';
+import CTCoronaryArteries from './../../assets/icons/CT-Coronary-Arteries.png';
+import CTCoronaryArteries2 from './../../assets/icons/CT-Coronary-Arteries-2.png';
+import CTCoronaryArteries3 from './../../assets/icons/CT-Coronary-Arteries-3.png';
+import CTCroppedVolumeBone from './../../assets/icons/CT-Cropped-Volume-Bone.png';
+import CTFat from './../../assets/icons/CT-Fat.png';
+import CTLiverVasculature from './../../assets/icons/CT-Liver-Vasculature.png';
+import CTLung from './../../assets/icons/CT-Lung.png';
+import CTMIP from './../../assets/icons/CT-MIP.png';
+import CTMuscle from './../../assets/icons/CT-Muscle.png';
+import CTPulmonaryArteries from './../../assets/icons/CT-Pulmonary-Arteries.png';
+import CTSoftTissue from './../../assets/icons/CT-Soft-Tissue.png';
+import DTIFABrain from './../../assets/icons/DTI-FA-Brain.png';
+import MRAngio from './../../assets/icons/MR-Angio.png';
+import MRDefault from './../../assets/icons/MR-Default.png';
+import MRMIP from './../../assets/icons/MR-MIP.png';
+import MRT2Brain from './../../assets/icons/MR-T2-Brain.png';
+import VolumeRendering from './../../assets/icons/VolumeRendering.png';
+import actionNewDialog from './../../assets/icons/action-new-dialog.svg';
+
/** LAYOUT */
import layoutAdvanced3DFourUp from './../../assets/icons/layout-advanced-3d-four-up.svg';
@@ -160,7 +206,6 @@ import layoutCommon2x3 from './../../assets/icons/layout-common-2x3.svg';
import iconToolRotate from './../../assets/icons/tool-3d-rotate.svg';
/** New investigational use */
-
import investigationalUse from './../../assets/icons/illustration-investigational-use.svg';
const ICONS = {
@@ -180,6 +225,8 @@ const ICONS = {
'chevron-next': chevronNext,
'chevron-prev': chevronPrev,
'chevron-right': chevronRight,
+ 'content-next': contentNext,
+ 'content-prev': contentPrev,
'eye-visible': eyeVisible,
'eye-hidden': eyeHidden,
'external-link': externalLink,
@@ -304,12 +351,51 @@ const ICONS = {
'icon-tool-scissor': iconToolScissor,
'icon-tool-shape': iconToolShape,
'icon-tool-threshold': iconToolThreshold,
-
+ 'next-arrow': nextArrow,
+ 'prev-arrow': prevArrow,
+ 'viewport-status-tracked': viewportStatusTracked,
+ 'viewport-window-level': viewportWindowLevel,
/** Old OHIF */
'old-trash': oldTrash,
'old-play': oldPlay,
'old-stop': oldStop,
-
+ /** ColorLut */
+ 'icon-color-lut': iconColorLUT,
+ /** New Patient Info Toolbar */
+ 'icon-chevron-patient': iconChevronPatient,
+ 'icon-patient': iconPatient,
+ 'icon-settings': iconSettings,
+ 'icon-toolbar-back': iconToolbarBack,
+ 'icon-multiple-patients': iconMultiplePatients,
+ /** Volume Rendering */
+ 'CT-AAA': CTAAA,
+ 'CT-AAA2': CTAAA2,
+ 'CT-Air': CTAir,
+ 'CT-Bone': CTBone,
+ 'CT-Bones': CTBones,
+ 'CT-Cardiac': CTCardiac,
+ 'CT-Cardiac2': CTCardiac2,
+ 'CT-Cardiac3': CTCardiac3,
+ 'CT-Chest-Contrast-Enhanced': CTChestContrastEnhanced,
+ 'CT-Chest-Vessels': CTChestVessels,
+ 'CT-Coronary-Arteries': CTCoronaryArteries,
+ 'CT-Coronary-Arteries-2': CTCoronaryArteries2,
+ 'CT-Coronary-Arteries-3': CTCoronaryArteries3,
+ 'CT-Cropped-Volume-Bone': CTCroppedVolumeBone,
+ 'CT-Fat': CTFat,
+ 'CT-Liver-Vasculature': CTLiverVasculature,
+ 'CT-Lung': CTLung,
+ 'CT-MIP': CTMIP,
+ 'CT-Muscle': CTMuscle,
+ 'CT-Pulmonary-Arteries': CTPulmonaryArteries,
+ 'CT-Soft-Tissue': CTSoftTissue,
+ 'DTI-FA-Brain': DTIFABrain,
+ 'MR-Angio': MRAngio,
+ 'MR-Default': MRDefault,
+ 'MR-MIP': MRMIP,
+ 'MR-T2-Brain': MRT2Brain,
+ VolumeRendering: VolumeRendering,
+ 'action-new-dialog': actionNewDialog,
/** LAYOUT */
'layout-advanced-3d-four-up': layoutAdvanced3DFourUp,
'layout-advanced-3d-main': layoutAdvanced3DMain,
@@ -340,11 +426,17 @@ function addIcon(iconName, iconSVG) {
* return `null`
*/
export default function getIcon(key, props) {
- if (!key || !ICONS[key]) {
+ const icon = ICONS[key];
+
+ if (!key || !icon) {
return React.createElement('div', null, 'Missing Icon');
}
- return React.createElement(ICONS[key], props);
+ if (typeof icon === 'string' && icon.endsWith('.png')) {
+ return React.createElement('img', { src: icon, ...props });
+ } else {
+ return React.createElement(icon, props);
+ }
}
export { getIcon, ICONS, addIcon };
diff --git a/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css
index 6d1aaf32d..695e11e9c 100644
--- a/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css
+++ b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css
@@ -1,13 +1,13 @@
.scroll {
- height: 100%;
+ height: calc(100% - 30px);
padding: 5px;
position: absolute;
right: 0;
- top: 0;
+ top: 30px;
}
.scroll .scroll-holder {
- height: calc(100% - 20px);
- margin-top: 5px;
+ height: calc(100%);
+ margin-bottom: 5px;
position: relative;
width: 12px;
}
diff --git a/platform/ui/src/components/InputRange/InputRange.tsx b/platform/ui/src/components/InputRange/InputRange.tsx
index 8986143a9..4ae168712 100644
--- a/platform/ui/src/components/InputRange/InputRange.tsx
+++ b/platform/ui/src/components/InputRange/InputRange.tsx
@@ -12,6 +12,12 @@ import getMaxDigits from '../../utils/getMaxDigits';
*
*
*/
+
+type Label = {
+ text: string;
+ position: number;
+};
+
type InputRangeProps = {
value: number;
onChange: (value: number) => void;
@@ -28,6 +34,7 @@ type InputRangeProps = {
trackColor?: string;
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
+ labels?: Label[];
};
const InputRange: React.FC
= ({
@@ -46,6 +53,7 @@ const InputRange: React.FC = ({
trackColor,
allowNumberEdit = false,
showAdjustmentArrows = true,
+ labels = [],
}) => {
const [rangeValue, setRangeValue] = useState(value);
@@ -114,6 +122,22 @@ const InputRange: React.FC = ({
{showLabel && labelPosition === 'right' && (
{LabelOrEditableNumber}
)}
+ {labels.length > 0 && (
+ <>
+ {labels.map((label, index) => {
+ const position = label.position;
+ return (
+
+ {label.text}
+
+ );
+ })}
+ >
+ )}
);
diff --git a/platform/ui/src/components/Modal/Modal.tsx b/platform/ui/src/components/Modal/Modal.tsx
index 783f9c9fb..1a717dc10 100644
--- a/platform/ui/src/components/Modal/Modal.tsx
+++ b/platform/ui/src/components/Modal/Modal.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
+import Draggable from 'react-draggable';
import { useModal } from '../../contextProviders';
import Icon from '../Icon';
@@ -20,6 +21,9 @@ const Modal = ({
onClose,
children,
shouldCloseOnOverlayClick,
+ movable = false,
+ containerDimensions = null,
+ contentDimensions = null,
}) => {
const { hide } = useModal();
@@ -27,44 +31,70 @@ const Modal = ({
hide();
};
- const renderHeader = () => {
- return (
- title && (
-
-
- {title}
-
- {closeButton && (
-
- )}
-
- )
+ const renderHeader = () =>
+ title && (
+
+
+ {title}
+
+ {closeButton && (
+
+ )}
+
);
- };
+
+ const modalContent = (
+ <>
+ {renderHeader()}
+
+ >
+ );
return (
- {renderHeader()}
-
+ {movable ? (
+
+ {modalContent}
+
+ ) : (
+ modalContent
+ )}
);
};
@@ -72,6 +102,9 @@ const Modal = ({
Modal.defaultProps = {
shouldCloseOnEsc: true,
shouldCloseOnOverlayClick: true,
+ movable: false,
+ containerDimensions: null,
+ contentDimensions: null,
};
Modal.propTypes = {
@@ -80,9 +113,11 @@ Modal.propTypes = {
isOpen: PropTypes.bool,
title: PropTypes.string,
onClose: PropTypes.func,
- /** The modal's content */
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
shouldCloseOnOverlayClick: PropTypes.bool,
+ movable: PropTypes.bool,
+ containerDimensions: PropTypes.string,
+ contentDimensions: PropTypes.string,
};
export default Modal;
diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx
index 671f1f628..c212de7ff 100644
--- a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx
+++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx
@@ -149,7 +149,6 @@ StudyBrowser.propTypes = {
componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage'])
.isRequired,
isTracked: PropTypes.bool,
- viewportIdentificator: PropTypes.arrayOf(PropTypes.string),
/**
* Data the thumbnail should expose to a receiving drop target. Use a matching
* `dragData.type` to identify which targets can receive this draggable item.
diff --git a/platform/ui/src/components/StudyBrowser/__stories__/studyBrowser.stories.mdx b/platform/ui/src/components/StudyBrowser/__stories__/studyBrowser.stories.mdx
index e60692f3f..0c0c10132 100644
--- a/platform/ui/src/components/StudyBrowser/__stories__/studyBrowser.stories.mdx
+++ b/platform/ui/src/components/StudyBrowser/__stories__/studyBrowser.stories.mdx
@@ -99,7 +99,6 @@ With Studies
displaySetInstanceUID: '0b66675a-a4e3-a718-21f4-035d2d178d62',
},
isTracked: false,
- viewportIdentificator: [],
},
],
},
diff --git a/platform/ui/src/components/Svg/getSvg.tsx b/platform/ui/src/components/Svg/getSvg.tsx
index dc5e0cb9a..22ebc3b84 100644
--- a/platform/ui/src/components/Svg/getSvg.tsx
+++ b/platform/ui/src/components/Svg/getSvg.tsx
@@ -1,6 +1,6 @@
import React from 'react';
// Svgs
-import logoOhif from './../../assets/svgs/logo-ohif.svg';
+import logoOhif from './../../assets/svgs/ohif-logo.svg';
const SVGS = {
'logo-ohif': logoOhif,
diff --git a/platform/ui/src/components/SwitchButton/SwitchButton.tsx b/platform/ui/src/components/SwitchButton/SwitchButton.tsx
new file mode 100644
index 000000000..0e6a80483
--- /dev/null
+++ b/platform/ui/src/components/SwitchButton/SwitchButton.tsx
@@ -0,0 +1,52 @@
+import React, { useCallback, useState } from 'react';
+
+import './switchButton.css';
+
+export enum SwitchLabelLocation {
+ left,
+ right,
+}
+
+export type SwitchButtonProps = {
+ checked?: boolean;
+ label?: string;
+ labelLocation?: SwitchLabelLocation;
+ onChange?: (checked: boolean) => void;
+};
+
+const SwitchButton = ({
+ label,
+ checked = false,
+ onChange,
+ labelLocation = SwitchLabelLocation.left,
+}: SwitchButtonProps) => {
+ const [isInputChecked, setIsInputChecked] = useState(checked);
+
+ const onHandleChange = useCallback(
+ event => {
+ setIsInputChecked(event.target.checked);
+ onChange?.(event.target.checked);
+ },
+ [onChange]
+ );
+
+ // Thanks goes to https://codepen.io/lhermann/pen/EBGZRZ for the inspiration to the code below.
+ return (
+
+ {label && labelLocation === SwitchLabelLocation.left && {label}
}
+
+ {label && labelLocation === SwitchLabelLocation.right && {label}
}
+
+ );
+};
+
+export default SwitchButton;
diff --git a/platform/ui/src/components/SwitchButton/__stories__/switchButton.stories.mdx b/platform/ui/src/components/SwitchButton/__stories__/switchButton.stories.mdx
new file mode 100644
index 000000000..acbdcc094
--- /dev/null
+++ b/platform/ui/src/components/SwitchButton/__stories__/switchButton.stories.mdx
@@ -0,0 +1,58 @@
+import SwitchButton, { SwitchLabelLocation } from '../SwitchButton';
+
+import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
+import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
+
+export const argTypes = {
+ component: SwitchButton,
+ title: 'Components/SwitchButton',
+};
+
+
+
+export const SwitchButtonTemplate = args => (
+
+
+
+);
+
+
+
+- [Overview](#overview)
+- [Props](#props)
+- [Contribute](#contribute)
+
+## Overview
+
+A SwitchButton is a checkbox type input/button that can either be on or off (like a light switch).
+
+
+ console.info(`${checked ? 'The switch is on.' : 'The switch is off.'}`)
+ }}
+>
+
+ {SwitchButtonTemplate.bind({})}
+
+
+
+
+## Props
+
+
+
+## Usage
+
+## Contribute
+
+
diff --git a/platform/ui/src/components/SwitchButton/index.js b/platform/ui/src/components/SwitchButton/index.js
new file mode 100644
index 000000000..0f44a7e6e
--- /dev/null
+++ b/platform/ui/src/components/SwitchButton/index.js
@@ -0,0 +1,3 @@
+import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
+export default SwitchButton;
+export { SwitchLabelLocation };
diff --git a/platform/ui/src/components/SwitchButton/switchButton.css b/platform/ui/src/components/SwitchButton/switchButton.css
new file mode 100644
index 000000000..2eff3baa2
--- /dev/null
+++ b/platform/ui/src/components/SwitchButton/switchButton.css
@@ -0,0 +1,7 @@
+.switch-button input:checked ~ .switch-button-outer {
+ @apply border-primary-light;
+}
+.switch-button input:checked ~ .switch-button-dot {
+ @apply bg-primary-light;
+ transform: translateX(12px);
+}
diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx
index dc4bd8322..cf949d202 100644
--- a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx
+++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx
@@ -29,7 +29,6 @@ const ThumbnailList = ({
componentType,
seriesDate,
countIcon,
- viewportIdentificator,
isTracked,
canReject,
onReject,
@@ -53,7 +52,6 @@ const ThumbnailList = ({
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
- viewportIdentificator={viewportIdentificator}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
@@ -72,7 +70,6 @@ const ThumbnailList = ({
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
- viewportIdentificator={viewportIdentificator}
isTracked={isTracked}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
@@ -96,7 +93,6 @@ const ThumbnailList = ({
onReject={onReject}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
- viewportIdentificator={viewportIdentificator}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
/>
);
@@ -120,7 +116,6 @@ ThumbnailList.propTypes = {
numInstances: PropTypes.number,
description: PropTypes.string,
componentType: Types.ThumbnailType.isRequired,
- viewportIdentificator: Types.StringArray,
isTracked: PropTypes.bool,
/**
* Data the thumbnail should expose to a receiving drop target. Use a matching
diff --git a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx
index 286d723b4..e1bd9acb3 100644
--- a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx
+++ b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx
@@ -24,43 +24,11 @@ function ThumbnailTracked({
onClick,
onDoubleClick,
onClickUntrack,
- viewportIdentificator,
isTracked,
isActive,
}) {
const { t } = useTranslation('ThumbnailTracked');
const trackedIcon = isTracked ? 'circled-checkmark' : 'dotted-circle';
- const viewportIdentificatorLabel = viewportIdentificator.join(', ');
- const renderViewportLabels = () => {
- const MAX_LABELS_PER_COL = 3;
- const shouldShowStack = viewportIdentificator.length > MAX_LABELS_PER_COL;
- if (shouldShowStack) {
- return (
-
-
- {viewportIdentificator.slice(0, MAX_LABELS_PER_COL).map(label => (
-
{label}
- ))}
-
-
- {`${t('Viewport')}: ${viewportIdentificatorLabel}`}
-
- }
- >
-
-
-
- );
- }
-
- return viewportIdentificator.map(label =>
{label}
);
- };
return (
- {!!viewportIdentificator.length && (
- {`${t('Viewport')}: ${viewportIdentificatorLabel}`}
- )}
}
@@ -105,13 +70,6 @@ function ThumbnailTracked({
className="text-primary-light mb-2 w-4"
/>
-
-