diff --git a/extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx b/extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx
index 43ec800c455..d00e241cff3 100644
--- a/extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx
+++ b/extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar';
import CustomizableViewportOverlay from './CustomizableViewportOverlay';
-import ViewportOrientationMarkers from './ViewportOrientationMarkers';
+// import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props: withAppTypes) {
@@ -68,6 +68,7 @@ function CornerstoneOverlays(props: withAppTypes) {
element={element}
/>
+ {/* Orientation markers disabled
+ */}
);
}
diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts
index 53d987ceb84..37f2ddc4354 100644
--- a/extensions/cornerstone/src/commandsModule.ts
+++ b/extensions/cornerstone/src/commandsModule.ts
@@ -805,7 +805,7 @@ function commandsModule({
const label = await callInputDialog({
uiDialogService,
title: i18n.t('Tools:Edit Arrow Text'),
- placeholder: data?.data?.label || i18n.t('Tools:Enter new text'),
+ placeholder: data?.data?.label || i18n.t('Tools:Enter annotation text'),
defaultValue: data?.data?.label || '',
});
diff --git a/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx
index 9d3e2e1b244..7b13dfd1486 100644
--- a/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx
+++ b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx
@@ -1,24 +1,25 @@
export default {
'viewportOverlay.topLeft': [
- {
- id: 'StudyDate',
- inheritsFrom: 'ohif.overlayItem',
- label: '',
- title: 'Study date',
- condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
- contentF: ({ referenceInstance, formatters: { formatDate } }) =>
- formatDate(referenceInstance.StudyDate),
- },
- {
- id: 'SeriesDescription',
- inheritsFrom: 'ohif.overlayItem',
- label: '',
- title: 'Series description',
- condition: ({ referenceInstance }) => {
- return referenceInstance && referenceInstance.SeriesDescription;
- },
- contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
- },
+ // Study date and series description disabled
+ // {
+ // id: 'StudyDate',
+ // inheritsFrom: 'ohif.overlayItem',
+ // label: '',
+ // title: 'Study date',
+ // condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
+ // contentF: ({ referenceInstance, formatters: { formatDate } }) =>
+ // formatDate(referenceInstance.StudyDate),
+ // },
+ // {
+ // id: 'SeriesDescription',
+ // inheritsFrom: 'ohif.overlayItem',
+ // label: '',
+ // title: 'Series description',
+ // condition: ({ referenceInstance }) => {
+ // return referenceInstance && referenceInstance.SeriesDescription;
+ // },
+ // contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
+ // },
],
'viewportOverlay.topRight': [],
'viewportOverlay.bottomLeft': [
@@ -26,19 +27,21 @@ export default {
id: 'WindowLevel',
inheritsFrom: 'ohif.overlayItem.windowLevel',
},
- {
- id: 'ZoomLevel',
- inheritsFrom: 'ohif.overlayItem.zoomLevel',
- condition: props => {
- const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
- return activeToolName === 'Zoom';
- },
- },
+ // Zoom level disabled
+ // {
+ // id: 'ZoomLevel',
+ // inheritsFrom: 'ohif.overlayItem.zoomLevel',
+ // condition: props => {
+ // const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
+ // return activeToolName === 'Zoom';
+ // },
+ // },
],
'viewportOverlay.bottomRight': [
- {
- id: 'InstanceNumber',
- inheritsFrom: 'ohif.overlayItem.instanceNumber',
- },
+ // Instance number disabled
+ // {
+ // id: 'InstanceNumber',
+ // inheritsFrom: 'ohif.overlayItem.instanceNumber',
+ // },
],
};
diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DicomWebDataSource/index.ts
index 5fb351767d8..acdcb370bdd 100644
--- a/extensions/default/src/DicomWebDataSource/index.ts
+++ b/extensions/default/src/DicomWebDataSource/index.ts
@@ -16,7 +16,7 @@ import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStu
import StaticWadoClient from './utils/StaticWadoClient';
import getDirectURL from '../utils/getDirectURL';
import { fixBulkDataURI } from './utils/fixBulkDataURI';
-import {HeadersInterface} from '@ohif/core/src/types/RequestHeaders';
+import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
@@ -143,10 +143,13 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
getAuthorizationHeader = () => {
- const xhrRequestHeaders: HeadersInterface = {};
+ let xhrRequestHeaders: HeadersInterface = {};
const authHeaders = userAuthenticationService.getAuthorizationHeader();
- if (authHeaders && authHeaders.Authorization) {
- xhrRequestHeaders.Authorization = authHeaders.Authorization;
+ if (authHeaders) {
+ xhrRequestHeaders = {
+ ...xhrRequestHeaders,
+ ...authHeaders,
+ };
}
return xhrRequestHeaders;
};
@@ -158,7 +161,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
*/
generateWadoHeader = (options: HeaderOptions): HeadersInterface => {
const authorizationHeader = getAuthorizationHeader();
- if (options?.includeTransferSyntax!==false) {
+ if (options?.includeTransferSyntax !== false) {
//Generate accept header depending on config params
const formattedAcceptHeader = utils.generateAcceptHeader(
dicomWebConfig.acceptHeader,
@@ -175,7 +178,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
// which the server expects Accept: application/dicom+json will still include that in the
// header.
return {
- ...authorizationHeader
+ ...authorizationHeader,
};
}
};
diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js
index ee13fa8c4b0..20ed59351d2 100644
--- a/extensions/default/src/DicomWebDataSource/qido.js
+++ b/extensions/default/src/DicomWebDataSource/qido.js
@@ -197,7 +197,7 @@ function mapParams(params, options = {}) {
if (params.studyInstanceUid) {
let studyUids = params.studyInstanceUid;
studyUids = Array.isArray(studyUids) ? studyUids.join() : studyUids;
- studyUids = studyUids.replace(/[^0-9.]+/g, '\\');
+ // studyUids = studyUids.replace(/[^0-9\.]+/g, '\\');
parameters.StudyInstanceUID = studyUids;
}
diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx
index c1bbb42cfe1..2aa0442249f 100644
--- a/extensions/default/src/ViewerLayout/index.tsx
+++ b/extensions/default/src/ViewerLayout/index.tsx
@@ -34,6 +34,9 @@ function ViewerLayout({
const { panelService, hangingProtocolService, customizationService } = servicesManager.services;
const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator);
+ // Check if UI should be hidden based on config
+ const hideUI = !!appConfig.hideUI;
+
const hasPanels = useCallback(
(side): boolean => !!panelService.getPanels(side).length,
[panelService]
@@ -151,21 +154,23 @@ function ViewerLayout({
return (
-
+ {!hideUI && (
+
+ )}
{showLoadingIndicator && }
{/* LEFT SIDEPANELS */}
- {hasLeftPanels ? (
+ {hasLeftPanels && !hideUI ? (
<>
- {hasRightPanels ? (
+ {hasRightPanels && !hideUI ? (
<>
void;
@@ -15,52 +15,37 @@ interface InputDialogDefaultProps {
function InputDialogDefault({
hide,
onSave,
- placeholder = 'Enter value',
+ placeholder = 'Enter annotation text',
defaultValue = '',
submitOnEnter,
}: InputDialogDefaultProps) {
return (
-
-
-
-
-
- Cancel
- {
- onSave(value);
- hide();
- }}
- >
- Save
-
-
-
+ placeholder={placeholder}
+ submitOnEnter={submitOnEnter}
+ />
);
}
/**
- * Shows an input dialog for entering text with customizable options
+ * Shows an inline annotation input for entering text
* @param uiDialogService - Service for showing UI dialogs
- * @param onSave - Callback function called when save button is clicked with entered value
* @param defaultValue - Initial value to show in input field
- * @param title - Title text to show in dialog header
* @param placeholder - Placeholder text for input field
* @param submitOnEnter - Whether to submit dialog when Enter key is pressed
*/
export async function callInputDialog({
uiDialogService,
defaultValue = '',
- title = 'Annotation',
- placeholder = '',
+ placeholder = 'Enter annotation text',
submitOnEnter = true,
}: {
uiDialogService: AppTypes.UIDialogService;
defaultValue?: string;
- title?: string;
placeholder?: string;
submitOnEnter?: boolean;
}) {
@@ -70,7 +55,7 @@ export async function callInputDialog({
uiDialogService.show({
id: dialogId,
content: InputDialogDefault,
- title: title,
+ unstyled: true,
shouldCloseOnEsc: true,
contentProps: {
onSave: value => {
diff --git a/modes/basic/src/index.tsx b/modes/basic/src/index.tsx
index 9e61fbb6442..3e956c180d4 100644
--- a/modes/basic/src/index.tsx
+++ b/modes/basic/src/index.tsx
@@ -214,20 +214,23 @@ export const toolbarSections = {
'Zoom',
'Pan',
'TrackballRotate',
- 'WindowLevel',
+ // 'WindowLevel', // Window level button disabled
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
],
- [TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'],
+ // Orientation and data overlay menus disabled
+ [TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: [
+ /* 'orientationMenu', 'dataOverlayMenu' */
+ ],
[TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'],
AdvancedRenderingControls: [
- 'windowLevelMenuEmbedded',
- 'voiManualControlMenu',
+ // 'windowLevelMenuEmbedded', // Window level menu disabled
+ // 'voiManualControlMenu', // VOI manual control disabled
'Colorbar',
'opacityMenu',
'thresholdMenu',
@@ -239,7 +242,10 @@ export const toolbarSections = {
'navigationComponent',
],
- [TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: ['windowLevelMenu'],
+ // Window level menu disabled in bottom left
+ [TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: [
+ /* 'windowLevelMenu' */
+ ],
MeasurementTools: [
'Length',
diff --git a/modes/basic/src/initToolGroups.ts b/modes/basic/src/initToolGroups.ts
index 6278f62619c..abd32513e11 100644
--- a/modes/basic/src/initToolGroups.ts
+++ b/modes/basic/src/initToolGroups.ts
@@ -19,13 +19,14 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
const tools = {
active: [
- {
- toolName: toolNames.WindowLevel,
- bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
- },
+ // WindowLevel tool disabled - no mouse binding
+ // {
+ // toolName: toolNames.WindowLevel,
+ // bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
+ // },
{
toolName: toolNames.Pan,
- bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
+ bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Zoom,
@@ -37,6 +38,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
},
],
passive: [
+ { toolName: toolNames.WindowLevel },
{ toolName: toolNames.Length },
{
toolName: toolNames.ArrowAnnotate,
@@ -82,10 +84,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
{ toolName: toolNames.LivewireContour },
{ toolName: toolNames.WindowLevelRegion },
],
- enabled: [
- { toolName: toolNames.ImageOverlayViewer },
- { toolName: toolNames.ReferenceLines },
- ],
+ enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
disabled: [
{
toolName: toolNames.AdvancedMagnify,
@@ -115,19 +114,20 @@ function initSRToolGroup(extensionManager, toolGroupService) {
const { toolNames, Enums } = CS3DUtilityModule.exports;
const tools = {
active: [
- {
- toolName: toolNames.WindowLevel,
- bindings: [
- {
- mouseButton: Enums.MouseBindings.Primary,
- },
- ],
- },
+ // WindowLevel tool disabled - no mouse binding
+ // {
+ // toolName: toolNames.WindowLevel,
+ // bindings: [
+ // {
+ // mouseButton: Enums.MouseBindings.Primary,
+ // },
+ // ],
+ // },
{
toolName: toolNames.Pan,
bindings: [
{
- mouseButton: Enums.MouseBindings.Auxiliary,
+ mouseButton: Enums.MouseBindings.Primary,
},
],
},
@@ -179,13 +179,14 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
const tools = {
active: [
- {
- toolName: toolNames.WindowLevel,
- bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
- },
+ // WindowLevel tool disabled - no mouse binding
+ // {
+ // toolName: toolNames.WindowLevel,
+ // bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
+ // },
{
toolName: toolNames.Pan,
- bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
+ bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Zoom,
@@ -197,6 +198,7 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
},
],
passive: [
+ { toolName: toolNames.WindowLevel },
{ toolName: toolNames.Length },
{
toolName: toolNames.ArrowAnnotate,
diff --git a/package.json b/package.json
index c699ea88994..583670bfca9 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
},
"scripts": {
"cm": "npx git-cz",
- "build": "lerna run build:viewer --stream",
+ "build": "lerna run build:viewer --stream --skip-nx-cache",
"build:dev": "lerna run build:dev --stream",
"build:ci": "lerna run build:viewer:ci --stream",
"build:qa": "lerna run build:viewer:qa --stream",
diff --git a/platform/app/.webpack/writePluginImportsFile.js b/platform/app/.webpack/writePluginImportsFile.js
index ba00dacf6d6..047ec12177f 100644
--- a/platform/app/.webpack/writePluginImportsFile.js
+++ b/platform/app/.webpack/writePluginImportsFile.js
@@ -10,8 +10,16 @@ const extractName = val => (typeof val === 'string' ? val : val.packageName);
const publicURL = process.env.PUBLIC_URL || '/';
-function isAbsolutePath(path) {
- return path.startsWith('http') || path.startsWith('/');
+function makeImportPath(path) {
+ if (path.startsWith('http')) {
+ return path;
+ }
+
+ if ( path.startsWith('/')) {
+ path = path.slice(1);
+ }
+
+ return `${publicURL}${path}`
}
function constructLines(input, categoryName) {
@@ -77,7 +85,7 @@ function getRuntimeLoadModesExtensions(modules) {
if (module.importPath) {
dynamicLoad.push(
` if( module==="${packageName}") {`,
- ` const imported = await window.browserImportFunction('${isAbsolutePath(module.importPath) ? '' : publicURL}${module.importPath}');`,
+ ` const imported = await window.browserImportFunction('${makeImportPath(module.importPath)}');`,
' return ' +
(module.globalName
? `window["${module.globalName}"];`
diff --git a/platform/app/public/config/butterfly.js b/platform/app/public/config/butterfly.js
new file mode 100644
index 00000000000..9f7ed38fb88
--- /dev/null
+++ b/platform/app/public/config/butterfly.js
@@ -0,0 +1,158 @@
+/** @type {AppTypes.Config} */
+
+window.config = {
+ name: 'config/butterfly.js',
+ routerBasename: '/static/ohif',
+ // whiteLabeling: {},
+ extensions: [],
+ modes: [],
+ customizationService: {},
+ showStudyList: false,
+ // some windows systems have issues with more than 3 web workers
+ maxNumberOfWebWorkers: 3,
+ // below flag is for performance reasons, but it might not work for all servers
+ showWarningMessageForCrossOrigin: true,
+ showCPUFallbackMessage: true,
+ showLoadingIndicator: true,
+ hideUI: true, // Hide toolbar and side panels by default for minimal UI
+ experimentalStudyBrowserSort: false,
+ strictZSpacingForVolumeViewport: true,
+ groupEnabledModesFirst: true,
+ allowMultiSelectExport: false,
+ maxNumRequests: {
+ interaction: 100,
+ thumbnail: 75,
+ // Prefetch number is dependent on the http protocol. For http 2 or
+ // above, the number of requests can be go a lot higher.
+ prefetch: 25,
+ },
+ // filterQueryParam: false,
+ // Defines multi-monitor layouts
+ multimonitor: [
+ {
+ id: 'split',
+ test: ({ multimonitor }) => multimonitor === 'split',
+ screens: [
+ {
+ id: 'ohif0',
+ screen: null,
+ location: {
+ screen: 0,
+ width: 0.5,
+ height: 1,
+ left: 0,
+ top: 0,
+ },
+ options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
+ },
+ {
+ id: 'ohif1',
+ screen: null,
+ location: {
+ width: 0.5,
+ height: 1,
+ left: 0.5,
+ top: 0,
+ },
+ options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
+ },
+ ],
+ },
+
+ {
+ id: '2',
+ test: ({ multimonitor }) => multimonitor === '2',
+ screens: [
+ {
+ id: 'ohif0',
+ screen: 0,
+ location: {
+ width: 1,
+ height: 1,
+ left: 0,
+ top: 0,
+ },
+ options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
+ },
+ {
+ id: 'ohif1',
+ screen: 1,
+ location: {
+ width: 1,
+ height: 1,
+ left: 0,
+ top: 0,
+ },
+ options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
+ },
+ ],
+ },
+ ],
+ defaultDataSourceName: 'dicomweb',
+ /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
+ // dangerouslyUseDynamicConfig: {
+ // enabled: true,
+ // // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only.
+ // // Example 1, to allow numbers and letters in an absolute or sub-path only.
+ // // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/
+ // // Example 2, to restricts to either hosptial.com or othersite.com.
+ // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/
+ // regex: /.*/,
+ // },
+ dataSources: [
+ {
+ namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
+ sourceName: 'dicomweb',
+ configuration: {
+ friendlyName: 'BFLY DICOM WEB',
+ name: 'bfly',
+ wadoUriRoot: 'https://api.dev.cloud.butterflynetwork.com/api/v1/dicomweb',
+ qidoRoot: 'https://api.dev.cloud.butterflynetwork.com/api/v1/dicomweb',
+ wadoRoot: 'https://api.dev.cloud.butterflynetwork.com/api/v1/dicomweb',
+ qidoSupportsIncludeField: false,
+ imageRendering: 'wadors',
+ thumbnailRendering: 'wadors',
+ enableStudyLazyLoad: false,
+ supportsFuzzyMatching: true,
+ supportsWildcard: false,
+ staticWado: true,
+ // whether the data source should use retrieveBulkData to grab metadata,
+ // and in case of relative path, what would it be relative to, options
+ // are in the series level or study level (some servers like series some study)
+ bulkDataURI: {
+ enabled: true,
+ transform: url => {
+ console.log('original url', url);
+
+ return url;
+ },
+ },
+ omitQuotationForMultipartRequest: true,
+ },
+ },
+ ],
+ httpErrorHandler: error => {
+ // This is 429 when rejected from the public idc sandbox too often.
+ console.warn(error.status);
+
+ // Could use services manager here to bring up a dialog/modal if needed.
+ console.warn('test, navigate to https://ohif.org/');
+ },
+ // whiteLabeling: {
+ // createLogoComponentFn: function (React) {
+ // return React.createElement(
+ // 'a',
+ // {
+ // target: '_self',
+ // rel: 'noopener noreferrer',
+ // className: 'text-purple-600 line-through',
+ // href: '_X___IDC__LOGO__LINK___Y_',
+ // },
+ // React.createElement('img', {
+ // src: './Logo.svg',
+ // className: 'w-14 h-14',
+ // })
+ // );
+ // },
+ // },
+};
diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js
index f29a70acb4f..4788900aa45 100644
--- a/platform/app/public/config/default.js
+++ b/platform/app/public/config/default.js
@@ -2,7 +2,7 @@
window.config = {
name: 'config/default.js',
- routerBasename: null,
+ routerBasename: '/static/ohif', // For 'olympus-frontend' setup (return to 'null' once we start using butterfly.js)
// whiteLabeling: {},
extensions: [],
modes: [],
@@ -14,6 +14,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
+ hideUI: true, // Hide toolbar and side panels by default for minimal UI
experimentalStudyBrowserSort: false,
strictZSpacingForVolumeViewport: true,
groupEnabledModesFirst: true,
diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx
index 446308d0092..a0f60eff42a 100644
--- a/platform/app/src/App.tsx
+++ b/platform/app/src/App.tsx
@@ -36,6 +36,7 @@ import appInit from './appInit.js';
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
import { ShepherdJourneyProvider } from 'react-shepherd';
import './App.css';
+import ButterflyProvider from './butterfly/ButterflyProvider';
let commandsManager: CommandsManager,
extensionManager: ExtensionManager,
@@ -173,8 +174,15 @@ function App({
basename={routerBasename}
future={routerFutureFlags}
>
- {authRoutes}
- {appRoutes}
+
+ {authRoutes}
+ {appRoutes}
+
);
diff --git a/platform/app/src/butterfly/ButterflyProvider.tsx b/platform/app/src/butterfly/ButterflyProvider.tsx
new file mode 100644
index 00000000000..927e0f0dd50
--- /dev/null
+++ b/platform/app/src/butterfly/ButterflyProvider.tsx
@@ -0,0 +1,439 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ CommandsManager,
+ ExtensionManager,
+ ServicesManager,
+ UserAuthenticationService,
+} from '@ohif/core';
+import React from 'react';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+// TODO Remove when the BE is ready
+const isInTesting = true;
+
+type MessageData =
+ | {
+ type: 'navigate';
+ studyId: string;
+ }
+ | {
+ type: 'command';
+ commandName: string;
+ options?: Record;
+ context?: string | string[];
+ }
+ | {
+ type: 'setTool';
+ toolName: string;
+ toolGroupId?: string;
+ }
+ | {
+ type: 'captureViewport';
+ }
+ | {
+ type: 'setZoomLevel';
+ level: '50%' | '70%' | '100%' | '150%' | '200%' | 'fit';
+ }
+ | {
+ type: 'clearFrameAnnotations';
+ }
+ | {
+ type: 'enableWindowLevel';
+ };
+
+type ButterflyProviderProps = {
+ userAuthenticationService: UserAuthenticationService;
+ commandsManager: CommandsManager;
+ extensionsManager: ExtensionManager;
+ servicesManager: ServicesManager;
+};
+
+export default function ButterflyProvider({
+ children,
+ commandsManager,
+ extensionsManager,
+ servicesManager,
+ userAuthenticationService,
+}: React.PropsWithChildren) {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const onMessage = (ev: MessageEvent) => {
+ console.log('ButterflyProvider: Received message', ev.data);
+
+ if (!(typeof ev.data === 'object')) {
+ return;
+ }
+
+ if (!('type' in ev.data)) {
+ return;
+ }
+
+ const { type } = ev.data;
+
+ if (isInTesting && type === 'navigate' && 'studyUuid' in ev.data) {
+ const { studyUuid } = ev.data;
+ const url = `/viewer?StudyInstanceUIDs=${studyUuid}`;
+ navigate(url);
+
+ // INFO Notify parent that navigation occurred
+ window.top.postMessage(
+ {
+ type: 'navigated',
+ studyId: studyUuid,
+ url: url,
+ },
+ '*'
+ );
+ }
+
+ // INFO Handle navigation to a specific study
+ if (!isInTesting && type === 'navigate' && 'studyId' in ev.data) {
+ const { studyId } = ev.data;
+ const query = new URLSearchParams({ StudyInstanceUIDs: studyId });
+ const dataSource = extensionsManager.getActiveDataSource()[0];
+ dataSource.initialize({ params: {}, query });
+
+ const url = `/viewer?${query.toString()}`;
+ navigate(url);
+
+ // INFO Notify parent that navigation occurred
+ window.top.postMessage(
+ {
+ type: 'navigated',
+ studyId: studyId,
+ url: url,
+ },
+ '*'
+ );
+
+ if (!isInTesting) {
+ // INFO Add BF auth token for requests
+ userAuthenticationService.setServiceImplementation({
+ getAuthorizationHeader() {
+ const bflyToken = JSON.parse(localStorage.getItem('bfly:token')).accessToken;
+ return {
+ Authorization: `JWT ${bflyToken}`,
+ 'olympus-organization': 'slug bni-slug',
+ };
+ },
+ getState() {
+ return {};
+ },
+ getUser() {},
+ handleUnauthenticated() {},
+ reset() {},
+ set() {},
+ setUser() {},
+ });
+ }
+ }
+
+ if (type === 'command' && 'commandName' in ev.data) {
+ const { commandName, options = {}, context } = ev.data;
+ try {
+ const result = commandsManager.runCommand(commandName, options, context || 'CORNERSTONE');
+ window.top.postMessage(
+ {
+ type: 'commandResult',
+ commandName,
+ success: true,
+ result,
+ },
+ '*'
+ );
+ } catch (error) {
+ console.error('ButterflyProvider: Command failed', commandName, error);
+ window.top.postMessage(
+ {
+ type: 'commandResult',
+ commandName,
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+
+ if (type === 'setTool' && 'toolName' in ev.data) {
+ const { toolName, toolGroupId } = ev.data;
+ try {
+ commandsManager.runCommand(
+ 'setToolActiveToolbar',
+ {
+ toolName,
+ toolGroupIds: toolGroupId ? [toolGroupId] : [],
+ },
+ 'CORNERSTONE'
+ );
+
+ if (toolName === 'PlanarFreehandROI') {
+ window.top.postMessage(
+ {
+ type: 'freehandROIToolActivated',
+ toolName,
+ },
+ '*'
+ );
+ }
+
+ window.top.postMessage(
+ {
+ type: 'toolChanged',
+ toolName,
+ success: true,
+ },
+ '*'
+ );
+ } catch (error) {
+ console.error('ButterflyProvider: Tool activation failed', toolName, error);
+ window.top.postMessage(
+ {
+ type: 'toolChanged',
+ toolName,
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+
+ if (type === 'captureViewport') {
+ try {
+ // INFO Add a small delay to ensure viewport and segmentation state are ready
+ setTimeout(() => {
+ commandsManager.runCommand('showDownloadViewportModal', {}, 'CORNERSTONE');
+ window.top.postMessage(
+ {
+ type: 'captureViewportOpened',
+ success: true,
+ },
+ '*'
+ );
+ }, 100);
+ } catch (error) {
+ console.error('ButterflyProvider: Failed to open capture viewport modal', error);
+ window.top.postMessage(
+ {
+ type: 'captureViewportOpened',
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+
+ if (type === 'setZoomLevel' && 'level' in ev.data) {
+ const { level } = ev.data;
+ try {
+ const viewportGridService = servicesManager?.services?.viewportGridService;
+ const cornerstoneViewportService = servicesManager?.services?.cornerstoneViewportService;
+
+ if (viewportGridService && cornerstoneViewportService) {
+ const activeViewportId = viewportGridService.getActiveViewportId();
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
+
+ if (viewport) {
+ let zoomScale: number;
+
+ switch (level) {
+ case '50%':
+ zoomScale = 0.5;
+ break;
+ case '70%':
+ zoomScale = 0.7;
+ break;
+ case '100%':
+ zoomScale = 1.0;
+ break;
+ case '150%':
+ zoomScale = 1.5;
+ break;
+ case '200%':
+ zoomScale = 2.0;
+ break;
+ case 'fit':
+ // INFO Reset to fit the viewport
+ viewport.resetCamera();
+ viewport.render();
+ window.top.postMessage(
+ {
+ type: 'zoomLevelSet',
+ level,
+ success: true,
+ },
+ '*'
+ );
+ return;
+ default:
+ throw new Error(`Invalid zoom level: ${level}`);
+ }
+
+ const currentCamera = viewport.getCamera();
+ const savedCamera = { ...currentCamera };
+ viewport.resetCamera();
+ const defaultCamera = viewport.getCamera();
+ const defaultParallelScale = defaultCamera.parallelScale;
+
+ const newParallelScale = defaultParallelScale / zoomScale;
+
+ viewport.setCamera({
+ ...savedCamera,
+ parallelScale: newParallelScale,
+ });
+ viewport.render();
+
+ window.top.postMessage(
+ {
+ type: 'zoomLevelSet',
+ level,
+ success: true,
+ },
+ '*'
+ );
+ } else {
+ throw new Error('No active viewport found');
+ }
+ } else {
+ throw new Error('Required services not available');
+ }
+ } catch (error) {
+ console.error('ButterflyProvider: Failed to set zoom level', error);
+ window.top.postMessage(
+ {
+ type: 'zoomLevelSet',
+ level,
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+
+ // INFO Handle clearing annotations for current frame only
+ if (type === 'clearFrameAnnotations') {
+ try {
+ const viewportGridService = servicesManager?.services?.viewportGridService;
+ const cornerstoneViewportService = servicesManager?.services?.cornerstoneViewportService;
+ const measurementService = servicesManager?.services?.measurementService;
+
+ if (viewportGridService && cornerstoneViewportService && measurementService) {
+ const activeViewportId = viewportGridService.getActiveViewportId();
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
+
+ if (viewport) {
+ // INFO Using 'any' as viewport type doesn't include all Cornerstone methods
+ const currentImageIdIndex = (viewport as any).getCurrentImageIdIndex
+ ? (viewport as any).getCurrentImageIdIndex()
+ : 0;
+ const currentImageId = (viewport as any).getImageIds
+ ? (viewport as any).getImageIds()[currentImageIdIndex]
+ : null;
+
+ if (currentImageId) {
+ const allMeasurements = measurementService.getMeasurements();
+
+ // INFO Filter and remove measurements that belong to the current frame
+ allMeasurements.forEach(measurement => {
+ // INFO Check if measurement belongs to the current image/frame
+ if (
+ measurement.referenceImageId === currentImageId ||
+ (measurement.metadata &&
+ measurement.metadata.referencedImageId === currentImageId) ||
+ (measurement.data && measurement.data.imageId === currentImageId)
+ ) {
+ try {
+ commandsManager.runCommand(
+ 'removeMeasurement',
+ { uid: measurement.uid },
+ 'CORNERSTONE'
+ );
+ } catch (e) {
+ console.warn('Failed to remove measurement:', measurement.uid, e);
+ }
+ }
+ });
+
+ // INFO Force viewport render to update display
+ viewport.render();
+
+ window.top.postMessage(
+ {
+ type: 'frameAnnotationsCleared',
+ frameIndex: currentImageIdIndex + 1,
+ success: true,
+ },
+ '*'
+ );
+ } else {
+ throw new Error('No current image found');
+ }
+ } else {
+ throw new Error('No active viewport found');
+ }
+ } else {
+ throw new Error('Required services not available');
+ }
+ } catch (error) {
+ console.error('ButterflyProvider: Failed to clear frame annotations', error);
+ window.top.postMessage(
+ {
+ type: 'frameAnnotationsCleared',
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+
+ if (type === 'enableWindowLevel') {
+ try {
+ commandsManager.runCommand(
+ 'setToolActiveToolbar',
+ {
+ toolName: 'WindowLevel',
+ toolGroupIds: [],
+ },
+ 'CORNERSTONE'
+ );
+
+ window.top.postMessage(
+ {
+ type: 'windowLevelEnabled',
+ success: true,
+ },
+ '*'
+ );
+ } catch (error) {
+ console.error('ButterflyProvider: Failed to enable window level tool', error);
+ window.top.postMessage(
+ {
+ type: 'windowLevelEnabled',
+ success: false,
+ error: error.message,
+ },
+ '*'
+ );
+ }
+ }
+ };
+
+ window.addEventListener('message', onMessage);
+
+ // INFO Notify parent that OHIF is ready to receive messages
+ window.top.postMessage({ type: 'ready' }, '*');
+
+ return () => {
+ window.removeEventListener('message', onMessage);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return <>{children}>;
+}
diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx
index 9c0d326a4ba..c1129085ca9 100644
--- a/platform/app/src/components/ViewportGrid.tsx
+++ b/platform/app/src/components/ViewportGrid.tsx
@@ -305,7 +305,7 @@ function ViewerViewportGrid(props: withAppTypes) {
}
return (
-
+
void;
+ onCancel: () => void;
+ defaultValue?: string;
+ placeholder?: string;
+ submitOnEnter?: boolean;
+ className?: string;
+ hide?: () => void;
+}
+
+export function InlineAnnotationInput({
+ onSave,
+ onCancel,
+ defaultValue = '',
+ placeholder = 'Enter annotation text',
+ submitOnEnter = true,
+ className,
+ hide,
+}: InlineAnnotationInputProps) {
+ const [value, setValue] = useState(defaultValue);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ if (defaultValue) {
+ inputRef.current.select();
+ }
+ }
+ }, [defaultValue]);
+
+ const handleSave = () => {
+ onSave(value);
+ hide?.();
+ };
+
+ const handleCancel = () => {
+ onCancel();
+ hide?.();
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (submitOnEnter && e.key === 'Enter') {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ handleCancel();
+ }
+ };
+
+ // TODO Try to use our components library
+
+ return (
+
+ setValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ className="min-w-[200px] flex-1 rounded-md border border-[rgba(51,51,51,1)] bg-[rgba(51,51,51,1)] px-3 py-2 text-sm text-white placeholder-[rgba(128,128,128,1)] outline-none focus:border-[rgba(39,121,255,1)] focus:ring-1 focus:ring-[rgba(39,121,255,1)]"
+ />
+
+
+
+ );
+}
+
+export default InlineAnnotationInput;
diff --git a/platform/ui-next/src/components/OHIFDialogs/index.ts b/platform/ui-next/src/components/OHIFDialogs/index.ts
index f0657dc4c08..4d1d4d03bc5 100644
--- a/platform/ui-next/src/components/OHIFDialogs/index.ts
+++ b/platform/ui-next/src/components/OHIFDialogs/index.ts
@@ -1,2 +1,3 @@
export { InputDialog } from './InputDialog';
export { PresetDialog } from './PresetDialog';
+export { InlineAnnotationInput } from './InlineAnnotationInput';
diff --git a/platform/ui-next/src/components/Viewport/ViewportPane.tsx b/platform/ui-next/src/components/Viewport/ViewportPane.tsx
index 2610ad2414c..4139d888346 100644
--- a/platform/ui-next/src/components/Viewport/ViewportPane.tsx
+++ b/platform/ui-next/src/components/Viewport/ViewportPane.tsx
@@ -66,13 +66,7 @@ function ViewportPane({
{children}
{/* Border overlay */}
-
+
);
}
diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts
index 5fc7af79029..d2a8c4339cb 100644
--- a/platform/ui-next/src/components/index.ts
+++ b/platform/ui-next/src/components/index.ts
@@ -56,7 +56,7 @@ import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
import { ToolboxUI } from './OHIFToolbox';
import Numeric from './Numeric';
-import { InputDialog, PresetDialog } from './OHIFDialogs';
+import { InputDialog, PresetDialog, InlineAnnotationInput } from './OHIFDialogs';
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
import Modal from './Modal/Modal';
import { FooterAction } from './FooterAction';
@@ -254,6 +254,7 @@ export {
ToolButtonListDivider,
InputDialog,
PresetDialog,
+ InlineAnnotationInput,
Modal,
AboutModal,
ImageModal,
@@ -269,5 +270,5 @@ export {
ProgressLoadingBar,
ViewportDialog,
CinePlayer,
- LayoutSelector
+ LayoutSelector,
};