diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 0c69487ee..12b108800 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -29,6 +29,19 @@ jobs:
run: |
export NODE_OPTIONS="--max_old_space_size=10192"
bun run test:e2e:coverage
+ - name: Create directory of test results
+ if: ${{ !cancelled() }}
+ run: |
+ mkdir -p packaged-test-results
+ cp -r ./tests/test-results packaged-test-results/ || true
+ cp ./tests/playwright-report.json packaged-test-results/ || true
+ - name: Upload directory of test results artifact
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-results
+ path: packaged-test-results/
+ retention-days: 5
- name: create the coverage report
run: |
bun nyc report --reporter=lcov --reporter=text
diff --git a/extensions/cornerstone/src/hps/fourUp.ts b/extensions/cornerstone/src/hps/fourUp.ts
index 9a72b049d..11facdf9a 100644
--- a/extensions/cornerstone/src/hps/fourUp.ts
+++ b/extensions/cornerstone/src/hps/fourUp.ts
@@ -64,7 +64,7 @@ export const fourUp = {
customViewportProps: {
hideOverlays: true,
},
- syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
+ syncGroups: [HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
diff --git a/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts b/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts
index 002600f27..c26a6d5ee 100644
--- a/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts
+++ b/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts
@@ -71,7 +71,7 @@ export const mprAnd3DVolumeViewport = {
customViewportProps: {
hideOverlays: true,
},
- syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
+ syncGroups: [HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
diff --git a/extensions/cornerstone/src/hps/only3D.ts b/extensions/cornerstone/src/hps/only3D.ts
index 6e5782ebe..36f3534ef 100644
--- a/extensions/cornerstone/src/hps/only3D.ts
+++ b/extensions/cornerstone/src/hps/only3D.ts
@@ -47,7 +47,7 @@ export const only3D = {
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
- syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
+ syncGroups: [HYDRATE_SEG_SYNC_GROUP],
},
},
displaySets: [
diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx
index 397ba63c7..8916de073 100644
--- a/extensions/cornerstone/src/init.tsx
+++ b/extensions/cornerstone/src/init.tsx
@@ -39,6 +39,7 @@ import { useLutPresentationStore } from './stores/useLutPresentationStore';
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities';
+import { initializeWebWorkerProgressHandler } from './utils/initWebWorkerProgressHandler';
const { registerColormap } = csUtilities.colormap;
@@ -307,76 +308,6 @@ export default async function init({
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
}
-function initializeWebWorkerProgressHandler(uiNotificationService) {
- // Use a single map to track all active worker tasks
- const activeWorkerTasks = new Map();
-
- // Create a normalized task key that doesn't include the random ID
- // This helps us identify and deduplicate the same type of task
- const getNormalizedTaskKey = type => {
- return `worker-task-${type.toLowerCase().replace(/\s+/g, '-')}`;
- };
-
- eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
- const { progress, type, id } = detail;
-
- // Skip notifications for compute statistics
- if (type === cornerstoneTools.Enums.WorkerTypes.COMPUTE_STATISTICS) {
- return;
- }
-
- const normalizedKey = getNormalizedTaskKey(type);
-
- if (progress === 0) {
- // Check if we're already tracking a task of this type
- if (!activeWorkerTasks.has(normalizedKey)) {
- const progressPromise = new Promise((resolve, reject) => {
- activeWorkerTasks.set(normalizedKey, {
- resolve,
- reject,
- originalId: id,
- type,
- });
- });
-
- uiNotificationService.show({
- id: normalizedKey, // Use the normalized key as ID for better deduplication
- title: `${type}`,
- message: `Computing...`,
- autoClose: false,
- allowDuplicates: false,
- deduplicationInterval: 60000, // 60 seconds - prevent frequent notifications of same type
- promise: progressPromise,
- promiseMessages: {
- loading: `Computing...`,
- success: `Completed successfully`,
- error: 'Web Worker failed',
- },
- });
- } else {
- // Already tracking this type of task, just let it continue
- console.debug(`Already tracking a "${type}" task, skipping duplicate notification`);
- }
- }
- // Task completed
- else if (progress === 100) {
- // Check if we have this task type in our tracking map
- const taskData = activeWorkerTasks.get(normalizedKey);
-
- if (taskData) {
- // Resolve the promise to update the notification
- const { resolve } = taskData;
- resolve({ progress, type });
-
- // Remove from tracking
- activeWorkerTasks.delete(normalizedKey);
-
- console.debug(`Worker task "${type}" completed successfully`);
- }
- }
- });
-}
-
/**
* Creates a wrapped image load strategy with metadata handling
* @param strategyFn - The image loading strategy function to wrap
diff --git a/extensions/cornerstone/src/utils/initWebWorkerProgressHandler.ts b/extensions/cornerstone/src/utils/initWebWorkerProgressHandler.ts
new file mode 100644
index 000000000..a57404fef
--- /dev/null
+++ b/extensions/cornerstone/src/utils/initWebWorkerProgressHandler.ts
@@ -0,0 +1,110 @@
+import { eventTarget, EVENTS } from '@cornerstonejs/core';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+
+/**
+ * Initializes a handler for web worker progress events.
+ * Tracks active worker tasks and shows notifications for their progress.
+ *
+ * @param uiNotificationService - The UI notification service for showing progress notifications
+ */
+export function initializeWebWorkerProgressHandler(uiNotificationService: any) {
+ // Use a single map to track all active worker tasks
+ const activeWorkerTasks = new Map();
+
+ // Create a normalized task key that doesn't include the random ID
+ // This helps us identify and deduplicate the same type of task
+ const getNormalizedTaskKey = (type: string) => {
+ return `worker-task-${type.toLowerCase().replace(/\s+/g, '-')}`;
+ };
+
+ eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
+ let normalizedKey: string | undefined;
+ let shouldCleanup = false;
+
+ try {
+ const { progress, type, id } = detail;
+
+ // Skip notifications for compute statistics
+ if (type === cornerstoneTools.Enums.WorkerTypes.COMPUTE_STATISTICS) {
+ return;
+ }
+
+ normalizedKey = getNormalizedTaskKey(type);
+
+ if (progress === 0) {
+ // Check if we're already tracking a task of this type
+ if (!activeWorkerTasks.has(normalizedKey)) {
+ const progressPromise = new Promise((resolve, reject) => {
+ try {
+ activeWorkerTasks.set(normalizedKey, {
+ resolve,
+ reject,
+ originalId: id,
+ type,
+ });
+ } catch (error) {
+ console.error(`Error setting active worker task for type "${type}":`, error);
+ reject(error);
+ throw error; // Re-throw to trigger outer catch and cleanup
+ }
+ });
+
+ try {
+ uiNotificationService.show({
+ id: normalizedKey, // Use the normalized key as ID for better deduplication
+ title: `${type}`,
+ message: `Computing...`,
+ autoClose: false,
+ allowDuplicates: false,
+ deduplicationInterval: 60000, // 60 seconds - prevent frequent notifications of same type
+ promise: progressPromise,
+ promiseMessages: {
+ loading: `Computing...`,
+ success: `Completed successfully`,
+ error: 'Web Worker failed',
+ },
+ });
+ } catch (error) {
+ console.error(`Error showing web worker notification for type "${type}":`, error);
+ shouldCleanup = true;
+ throw error;
+ }
+ } else {
+ // Already tracking this type of task, just let it continue
+ console.debug(`Already tracking a "${type}" task, skipping duplicate notification`);
+ }
+ }
+ // Task completed
+ else if (progress === 100) {
+ // Check if we have this task type in our tracking map
+ const taskData = activeWorkerTasks.get(normalizedKey);
+
+ if (taskData) {
+ // Resolve the promise to update the notification
+ const { resolve } = taskData;
+ resolve({ progress, type });
+
+ // Mark for cleanup
+ shouldCleanup = true;
+
+ console.debug(`Worker task "${type}" completed successfully`);
+ }
+ }
+ } catch (error) {
+ console.error(`Error in web worker progress handler for type "${detail?.type}":`, error);
+ shouldCleanup = true;
+ } finally {
+ // Clean up if needed
+ if (shouldCleanup && normalizedKey) {
+ try {
+ activeWorkerTasks.delete(normalizedKey);
+ } catch (cleanupError) {
+ console.error(
+ `Error cleaning up active worker task for type "${detail?.type}":`,
+ cleanupError
+ );
+ }
+ }
+ }
+ });
+}
diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx
index 183fd417d..2827790c0 100644
--- a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx
+++ b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx
@@ -80,6 +80,7 @@ export default class ContextMenuController {
this.services.uiDialogService.hide('context-menu');
this.services.uiDialogService.show({
id: 'context-menu',
+ showOverlay: false,
defaultPosition: ContextMenuController._getDefaultPosition(
defaultPointsPosition,
event?.detail || event,
diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts
index f11d3d237..133d5878e 100644
--- a/extensions/default/src/index.ts
+++ b/extensions/default/src/index.ts
@@ -37,6 +37,8 @@ import * as utils from './utils';
import { Toolbox } from './utils';
import MoreDropdownMenu from './Components/MoreDropdownMenu';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
+import { Toolbar } from './Toolbar/Toolbar';
+
const defaultExtension: Types.Extensions.Extension = {
/**
* Only required property. Should be a unique value across all extensions.
@@ -103,4 +105,5 @@ export {
requestDisplaySetCreationForStudy,
callInputDialog,
createReportDialogPrompt,
+ Toolbar,
};
diff --git a/platform/core/src/services/UIDialogService/UIDialogService.ts b/platform/core/src/services/UIDialogService/UIDialogService.ts
index 2f291d331..15f50c2e1 100644
--- a/platform/core/src/services/UIDialogService/UIDialogService.ts
+++ b/platform/core/src/services/UIDialogService/UIDialogService.ts
@@ -14,6 +14,8 @@ const serviceImplementation = {
console.warn('isEmpty() NOT IMPLEMENTED');
return true;
},
+ _updatePosition: (id: string, position: { x: number; y: number }) =>
+ console.warn('updatePosition() NOT IMPLEMENTED'),
_customComponent: null,
};
@@ -63,6 +65,16 @@ class UIDialogService {
return serviceImplementation._isEmpty();
}
+ /**
+ * Update the position of a specific dialog by id
+ *
+ * @param {string} id - The dialog id to update
+ * @param {{ x: number; y: number }} position - The new position
+ */
+ updatePosition(id: string, position: { x: number; y: number }): void {
+ return serviceImplementation._updatePosition(id, position);
+ }
+
/**
* This provides flexibility in customizing the Modal's default component
*
@@ -75,7 +87,14 @@ class UIDialogService {
/**
* Set the service implementation
*/
- setServiceImplementation({ show, hide, hideAll, isEmpty, customComponent }: any): void {
+ setServiceImplementation({
+ show,
+ hide,
+ hideAll,
+ isEmpty,
+ updatePosition,
+ customComponent,
+ }: any): void {
if (show) {
serviceImplementation._show = show;
}
@@ -88,6 +107,9 @@ class UIDialogService {
if (isEmpty) {
serviceImplementation._isEmpty = isEmpty;
}
+ if (updatePosition) {
+ serviceImplementation._updatePosition = updatePosition;
+ }
if (customComponent) {
serviceImplementation._customComponent = customComponent;
}
diff --git a/platform/docs/src/pages/components-list.tsx b/platform/docs/src/pages/components-list.tsx
index 189e9f1a4..2ec48c375 100644
--- a/platform/docs/src/pages/components-list.tsx
+++ b/platform/docs/src/pages/components-list.tsx
@@ -139,4 +139,4 @@ export default function ComponentsList() {
);
-}
\ No newline at end of file
+}
diff --git a/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx b/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx
index 4de9a9fff..a4996e021 100644
--- a/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx
+++ b/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx
@@ -6,8 +6,8 @@ export const PowerOff = (props: IconProps) => (
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 28"
aria-labelledby="title"
- width="1em"
- height="1em"
+ width="28px"
+ height="28px"
fill="currentColor"
{...props}
>
diff --git a/platform/ui-next/src/components/Viewport/ViewportPane.tsx b/platform/ui-next/src/components/Viewport/ViewportPane.tsx
index e5c6f4b91..2610ad241 100644
--- a/platform/ui-next/src/components/Viewport/ViewportPane.tsx
+++ b/platform/ui-next/src/components/Viewport/ViewportPane.tsx
@@ -67,9 +67,10 @@ function ViewportPane({
{/* Border overlay */}
diff --git a/platform/ui-next/src/contextProviders/DialogProvider.tsx b/platform/ui-next/src/contextProviders/DialogProvider.tsx
index 940aed9b5..442cd7284 100644
--- a/platform/ui-next/src/contextProviders/DialogProvider.tsx
+++ b/platform/ui-next/src/contextProviders/DialogProvider.tsx
@@ -1,11 +1,20 @@
-import React, { useState, createContext, useContext, useCallback, useEffect, useMemo } from 'react';
-import ManagedDialog, { ManagedDialogProps } from './ManagedDialog';
+import React, {
+ useState,
+ createContext,
+ useContext,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+} from 'react';
+import ManagedDialog, { ManagedDialogProps, ManagedDialogRef } from './ManagedDialog';
interface DialogContextType {
show: (options: ManagedDialogProps) => string;
hide: (id: string) => void;
hideAll: () => void;
isEmpty: () => boolean;
+ updatePosition: (id: string, position: { x: number; y: number }) => void;
}
interface DialogService {
@@ -35,6 +44,7 @@ const DialogProvider: React.FC = ({
service = null,
}) => {
const [dialogs, setDialogs] = useState<(ManagedDialogProps & { id: string })[]>([]);
+ const dialogRefs = useRef