feat(app): Decouple colorbar UI from service and enable action menu programatic control (#5018)
This commit is contained in:
1 parent
f14d9ebdd7
commit
1445e12124
41 files changed
+1872
-659
No files matched your search
@@ -26,6 +26,7 @@ import {
|
||||
ModalProvider,
|
||||
ViewportDialogProvider,
|
||||
UserAuthenticationProvider,
|
||||
ViewportActionCornersProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
@@ -104,6 +105,7 @@ function App({
|
||||
userAuthenticationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const providers = [
|
||||
@@ -119,6 +121,7 @@ function App({
|
||||
[TooltipProvider],
|
||||
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
|
||||
[ModalProvider, { service: uiModalService, modal: ModalNext }],
|
||||
[ViewportActionCornersProvider, { service: viewportActionCornersService }],
|
||||
[ShepherdJourneyProvider],
|
||||
];
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import EmptyViewport from './EmptyViewport';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useViewportActionCornersWithGrid } from '../hooks';
|
||||
|
||||
function ViewerViewportGrid(props: withAppTypes) {
|
||||
const { servicesManager, viewportComponents = [], dataSource, commandsManager } = props;
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [appConfig] = useAppConfig();
|
||||
const { initializeViewportCorners } = useViewportActionCornersWithGrid();
|
||||
|
||||
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
|
||||
const { numCols, numRows } = layout;
|
||||
@@ -415,8 +417,14 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
displaySetOptions={displaySetOptions}
|
||||
needsRerendering={displaySetsNeedsRerendering}
|
||||
isHangingProtocolLayout={isHangingProtocolLayout}
|
||||
onElementEnabled={() => {
|
||||
onElementEnabled={evt => {
|
||||
viewportGridService.setViewportIsReady(viewportId, true);
|
||||
|
||||
// Initialize the viewport action corners for this viewport
|
||||
if (evt?.detail?.element) {
|
||||
const elementRef = { current: evt.detail.element };
|
||||
initializeViewportCorners(viewportId, elementRef, displaySets, commandsManager);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -425,7 +433,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
}
|
||||
|
||||
return viewportPanes;
|
||||
}, [viewports, activeViewportId, viewportComponents, dataSource]);
|
||||
}, [viewports, activeViewportId, viewportComponents, dataSource, initializeViewportCorners]);
|
||||
|
||||
/**
|
||||
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useDebounce from './useDebounce.js';
|
||||
import useDebounce from './useDebounce';
|
||||
import useSearchParams from './useSearchParams';
|
||||
import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
|
||||
|
||||
export { useDebounce, useSearchParams };
|
||||
export { useDebounce, useSearchParams, useViewportActionCornersWithGrid };
|
||||
@@ -0,0 +1,5 @@
|
||||
import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
|
||||
import useDebounce from './useDebounce';
|
||||
import useSearchParams from './useSearchParams';
|
||||
|
||||
export { useViewportActionCornersWithGrid, useDebounce, useSearchParams };
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useCallback, MutableRefObject, useRef } from 'react';
|
||||
import { useViewportActionCorners, ViewportActionCornersLocations } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Hook that manages viewport action corner components for all viewports in the grid
|
||||
* @returns A function that can be called to initialize action corners for a viewport
|
||||
*/
|
||||
export default function useViewportActionCornersWithGrid() {
|
||||
const [, api] = useViewportActionCorners();
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
// Keep a ref to track processed viewports to avoid duplicates
|
||||
const processedViewports = useRef<Set<string>>(new Set());
|
||||
|
||||
// Map of customization keys to their corresponding enum values
|
||||
const locationMap = {
|
||||
'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
|
||||
'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
|
||||
'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
|
||||
'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
|
||||
};
|
||||
|
||||
// Function to process customizations for a viewport
|
||||
const initializeViewportCorners = useCallback(
|
||||
(
|
||||
viewportId: string,
|
||||
elementRef: MutableRefObject<HTMLDivElement>,
|
||||
displaySets: any[],
|
||||
commandsManager: any
|
||||
) => {
|
||||
if (!viewportId || !elementRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate processing
|
||||
if (processedViewports.current.has(viewportId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this viewport as processed
|
||||
processedViewports.current.add(viewportId);
|
||||
|
||||
// Clear any existing components for this viewport
|
||||
api.clear(viewportId);
|
||||
|
||||
// Process each location
|
||||
Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
|
||||
const items = customizationService.getCustomization(locationKey);
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
try {
|
||||
if (typeof item.component === 'function') {
|
||||
// Use the component renderer provided directly in the item
|
||||
const component = item.component({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
location: locationValue,
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
if (component) {
|
||||
api.addComponent({
|
||||
viewportId,
|
||||
id: item.id,
|
||||
component,
|
||||
location: locationValue,
|
||||
indexPriority: item.indexPriority,
|
||||
});
|
||||
}
|
||||
} else if (item.component) {
|
||||
// Handle static components
|
||||
api.addComponent({
|
||||
viewportId,
|
||||
id: item.id,
|
||||
component: item.component,
|
||||
location: locationValue,
|
||||
indexPriority: item.indexPriority,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error adding component ${item.id} to viewport corner:`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[api, customizationService]
|
||||
);
|
||||
|
||||
// Cleanup function for unmounting viewports
|
||||
const cleanupViewportCorners = useCallback(
|
||||
(viewportId: string) => {
|
||||
if (!viewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from processed set
|
||||
processedViewports.current.delete(viewportId);
|
||||
|
||||
// Clear from the store
|
||||
api.clear(viewportId);
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
return { initializeViewportCorners, cleanupViewportCorners };
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next';
|
||||
import { useSearchParams } from '@hooks';
|
||||
import { useSearchParams } from '../../hooks';
|
||||
import { useAppConfig } from '@state';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
//
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useDebounce, useSearchParams } from '@hooks';
|
||||
import { useDebounce, useSearchParams } from '../../hooks';
|
||||
import { utils, Types as coreTypes } from '@ohif/core';
|
||||
|
||||
import {
|
||||
|
||||
@@ -16,6 +16,8 @@ export type DisplaySet = {
|
||||
label?: string;
|
||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||
isOverlayDisplaySet?: boolean;
|
||||
/** flag indicating if it supports window level */
|
||||
supportsWindowLevel?: boolean;
|
||||
|
||||
// Details about how to display:
|
||||
/**
|
||||
|
||||
+27
-2
@@ -201,6 +201,31 @@ window.config = {
|
||||
],
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportActionMenu.windowLevelActionMenu',
|
||||
description:
|
||||
'Configures the display and location of the window level action menu in the viewport.',
|
||||
image: windowLevelActionMenu,
|
||||
default: null,
|
||||
configuration: `
|
||||
window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
$merge: {
|
||||
location: 0, // Set the location of the menu in the viewport.
|
||||
// 0: topLeft
|
||||
// 1: topRight
|
||||
// 2: bottomLeft
|
||||
// 3: bottomRight
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'measurementLabels',
|
||||
description: 'Labels for measurement tools in the viewer that are automatically asked for.',
|
||||
@@ -937,7 +962,7 @@ window.config = {
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportActionMenu.dataOverlay',
|
||||
id: 'viewportActionMenu.dataOverlayMenu',
|
||||
description: 'Configures the display and location of the data overlay in the viewport.',
|
||||
image: segmentationOverlay,
|
||||
default: null,
|
||||
@@ -946,7 +971,7 @@ window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
'viewportActionMenu.dataOverlayMenu': {
|
||||
$merge: {
|
||||
enabled: true,
|
||||
location: 1, // Set the location of the overlay in the viewport.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
ReactNode,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A small container that can render multiple "corner" items (like icons, status)
|
||||
* in each corner of the viewport: top-left, top-right, bottom-left, bottom-right.
|
||||
*/
|
||||
export enum ViewportActionCornersLocations {
|
||||
topLeft,
|
||||
topRight,
|
||||
@@ -33,35 +35,102 @@ const locationClasses = {
|
||||
),
|
||||
};
|
||||
|
||||
function ViewportActionCorners({ cornerComponents }) {
|
||||
if (!cornerComponents) {
|
||||
return null;
|
||||
}
|
||||
const ViewportActionCornersContext = createContext<{
|
||||
registerCorner: (location: ViewportActionCornersLocations, children: ReactNode) => void;
|
||||
} | null>(null);
|
||||
|
||||
function Container({ children }: { children: ReactNode }) {
|
||||
const [corners, setCorners] = useState({
|
||||
[ViewportActionCornersLocations.topLeft]: null,
|
||||
[ViewportActionCornersLocations.topRight]: null,
|
||||
[ViewportActionCornersLocations.bottomLeft]: null,
|
||||
[ViewportActionCornersLocations.bottomRight]: null,
|
||||
});
|
||||
|
||||
const registerCorner = useCallback(
|
||||
(location: ViewportActionCornersLocations, children: ReactNode) => {
|
||||
setCorners(prev => {
|
||||
// Only update if the children are different to avoid unnecessary renders
|
||||
if (prev[location] === children) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[location]: children,
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(cornerComponents).map(([location, locationArray]) => (
|
||||
<div
|
||||
key={location}
|
||||
className={locationClasses[location]}
|
||||
>
|
||||
{locationArray.map(componentInfo => (
|
||||
<div key={componentInfo.id}>{componentInfo.component}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ViewportActionCornersContext.Provider value={{ registerCorner }}>
|
||||
{children}
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(corners).map(([location, children]) => {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={location}
|
||||
className={locationClasses[location]}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ViewportActionCornersContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
function Corner({
|
||||
location,
|
||||
children,
|
||||
}: {
|
||||
location: ViewportActionCornersLocations;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const context = useContext(ViewportActionCornersContext);
|
||||
|
||||
export { ViewportActionCorners };
|
||||
if (!context) {
|
||||
throw new Error('Corner component must be used within a ViewportActionCorners.Container');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
context.registerCorner(location, children);
|
||||
}, [context, location, children]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function TopLeft({ children }: { children: ReactNode }) {
|
||||
return <Corner location={ViewportActionCornersLocations.topLeft}>{children}</Corner>;
|
||||
}
|
||||
|
||||
function TopRight({ children }: { children: ReactNode }) {
|
||||
return <Corner location={ViewportActionCornersLocations.topRight}>{children}</Corner>;
|
||||
}
|
||||
|
||||
function BottomLeft({ children }: { children: ReactNode }) {
|
||||
return <Corner location={ViewportActionCornersLocations.bottomLeft}>{children}</Corner>;
|
||||
}
|
||||
|
||||
function BottomRight({ children }: { children: ReactNode }) {
|
||||
return <Corner location={ViewportActionCornersLocations.bottomRight}>{children}</Corner>;
|
||||
}
|
||||
|
||||
export const ViewportActionCorners = {
|
||||
Container,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
};
|
||||
@@ -0,0 +1,550 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
|
||||
import { ActionComponentInfo, AlignAndSide } from '../types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
viewports: {} as Record<string, Record<ViewportActionCornersLocations, ActionComponentInfo[]>>,
|
||||
};
|
||||
|
||||
/**
|
||||
* API for managing viewport action corner items.
|
||||
* These methods control the state and behavior of the corner items themselves,
|
||||
* not the content they might affect in the viewport.
|
||||
*/
|
||||
interface ViewportActionCornersApi {
|
||||
getState: () => typeof DEFAULT_STATE;
|
||||
addComponent: (component: ActionComponentInfo) => void;
|
||||
addComponents: (components: Array<ActionComponentInfo>) => void;
|
||||
clear: (viewportId: string) => void;
|
||||
getAlignAndSide: (location: ViewportActionCornersLocations) => AlignAndSide;
|
||||
lockItem: (viewportId: string, itemId: string) => void;
|
||||
unlockItem: (viewportId: string, itemId: string) => void;
|
||||
toggleLock: (viewportId: string, itemId: string) => void;
|
||||
isItemLocked: (viewportId: string, itemId: string) => boolean;
|
||||
showItem: (viewportId: string, itemId: string) => void;
|
||||
hideItem: (viewportId: string, itemId: string) => void;
|
||||
toggleVisibility: (viewportId: string, itemId: string) => void;
|
||||
isItemVisible: (viewportId: string, itemId: string) => boolean;
|
||||
openItem: (viewportId: string, itemId: string) => void;
|
||||
closeItem: (viewportId: string, itemId: string) => void;
|
||||
closeAllItems: (viewportId: string) => void;
|
||||
isItemOpen: (viewportId: string, itemId: string) => boolean;
|
||||
}
|
||||
|
||||
export const ViewportActionCornersContext = createContext<
|
||||
[typeof DEFAULT_STATE, ViewportActionCornersApi]
|
||||
>([DEFAULT_STATE, {} as ViewportActionCornersApi]);
|
||||
|
||||
interface ViewportActionCornersProviderProps {
|
||||
children: ReactNode;
|
||||
service: any;
|
||||
}
|
||||
|
||||
export function ViewportActionCornersProvider({
|
||||
children,
|
||||
service,
|
||||
}: ViewportActionCornersProviderProps) {
|
||||
const viewportActionCornersReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD_COMPONENT': {
|
||||
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
|
||||
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
newState.viewports[viewportId] = {
|
||||
[ViewportActionCornersLocations.topLeft]: [],
|
||||
[ViewportActionCornersLocations.topRight]: [],
|
||||
[ViewportActionCornersLocations.bottomLeft]: [],
|
||||
[ViewportActionCornersLocations.bottomRight]: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (!newState.viewports[viewportId][location]) {
|
||||
newState.viewports[viewportId][location] = [];
|
||||
}
|
||||
|
||||
const componentInfo = {
|
||||
id,
|
||||
component,
|
||||
indexPriority,
|
||||
isOpen: false,
|
||||
isVisible: true,
|
||||
isLocked: false,
|
||||
};
|
||||
|
||||
const components = [...newState.viewports[viewportId][location]];
|
||||
const index = components.findIndex(item => item.indexPriority > indexPriority);
|
||||
|
||||
if (index === -1) {
|
||||
components.push(componentInfo);
|
||||
} else {
|
||||
components.splice(index, 0, componentInfo);
|
||||
}
|
||||
|
||||
newState.viewports[viewportId][location] = components;
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'ADD_COMPONENTS': {
|
||||
const components = action.payload;
|
||||
let newState = { ...state };
|
||||
|
||||
components.forEach(component => {
|
||||
newState = viewportActionCornersReducer(newState, {
|
||||
type: 'ADD_COMPONENT',
|
||||
payload: component,
|
||||
});
|
||||
});
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'CLEAR': {
|
||||
const viewportId = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (newState.viewports[viewportId]) {
|
||||
const newViewports = { ...newState.viewports };
|
||||
delete newViewports[viewportId];
|
||||
newState.viewports = newViewports;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_LOCKED': {
|
||||
const { viewportId, itemId, lockedStatus } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||
|
||||
Object.keys(viewportCopy).forEach(locationKey => {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = [...viewportCopy[location]];
|
||||
|
||||
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||
if (itemIndex !== -1) {
|
||||
const updatedItem = { ...components[itemIndex], isLocked: lockedStatus };
|
||||
components[itemIndex] = updatedItem;
|
||||
viewportCopy[location] = components;
|
||||
}
|
||||
});
|
||||
|
||||
newState.viewports[viewportId] = viewportCopy;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_VISIBLE': {
|
||||
const { viewportId, itemId, visibleStatus } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||
|
||||
Object.keys(viewportCopy).forEach(locationKey => {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = [...viewportCopy[location]];
|
||||
|
||||
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||
if (itemIndex !== -1) {
|
||||
const updatedItem = { ...components[itemIndex], isVisible: visibleStatus };
|
||||
components[itemIndex] = updatedItem;
|
||||
viewportCopy[location] = components;
|
||||
}
|
||||
});
|
||||
|
||||
newState.viewports[viewportId] = viewportCopy;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'OPEN_ITEM': {
|
||||
const { viewportId, itemId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||
|
||||
// Update isOpen flag for the component with matching id in any location
|
||||
Object.keys(viewportCopy).forEach(locationKey => {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = [...viewportCopy[location]];
|
||||
|
||||
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||
if (itemIndex !== -1) {
|
||||
const updatedItem = { ...components[itemIndex], isOpen: true };
|
||||
components[itemIndex] = updatedItem;
|
||||
viewportCopy[location] = components;
|
||||
}
|
||||
});
|
||||
|
||||
newState.viewports[viewportId] = viewportCopy;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'CLOSE_ITEM': {
|
||||
const { viewportId, itemId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||
|
||||
// Update isOpen flag for the component with matching id in any location
|
||||
Object.keys(viewportCopy).forEach(locationKey => {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = [...viewportCopy[location]];
|
||||
|
||||
const itemIndex = components.findIndex(item => item.id === itemId);
|
||||
if (itemIndex !== -1) {
|
||||
const updatedItem = { ...components[itemIndex], isOpen: false };
|
||||
components[itemIndex] = updatedItem;
|
||||
viewportCopy[location] = components;
|
||||
}
|
||||
});
|
||||
|
||||
newState.viewports[viewportId] = viewportCopy;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'CLOSE_ALL_ITEMS': {
|
||||
const viewportId = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState.viewports[viewportId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewportCopy = { ...newState.viewports[viewportId] };
|
||||
|
||||
// Set isOpen to false for all components in the viewport
|
||||
Object.keys(viewportCopy).forEach(locationKey => {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = viewportCopy[location].map(item => ({
|
||||
...item,
|
||||
isOpen: false,
|
||||
}));
|
||||
|
||||
viewportCopy[location] = components;
|
||||
});
|
||||
|
||||
newState.viewports[viewportId] = viewportCopy;
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(viewportActionCornersReducer, DEFAULT_STATE);
|
||||
|
||||
const getState = useCallback(() => {
|
||||
return state;
|
||||
}, [state]);
|
||||
|
||||
const addComponent = useCallback(
|
||||
(component: ActionComponentInfo) => {
|
||||
dispatch({
|
||||
type: 'ADD_COMPONENT',
|
||||
payload: component,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const addComponents = useCallback(
|
||||
(components: Array<ActionComponentInfo>) => {
|
||||
dispatch({
|
||||
type: 'ADD_COMPONENTS',
|
||||
payload: components,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const clear = useCallback(
|
||||
(viewportId: string) => {
|
||||
dispatch({
|
||||
type: 'CLEAR',
|
||||
payload: viewportId,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const lockItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_LOCKED',
|
||||
payload: { viewportId, itemId, lockedStatus: true },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const unlockItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_LOCKED',
|
||||
payload: { viewportId, itemId, lockedStatus: false },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const toggleLock = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
const currentLocked = isItemLocked(viewportId, itemId);
|
||||
dispatch({
|
||||
type: 'SET_LOCKED',
|
||||
payload: { viewportId, itemId, lockedStatus: !currentLocked },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const isItemLocked = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
if (!state.viewports[viewportId]) {
|
||||
return false; // Default to unlocked if viewport doesn't exist
|
||||
}
|
||||
|
||||
for (const locationKey in state.viewports[viewportId]) {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = state.viewports[viewportId][location];
|
||||
const item = components.find(item => item.id === itemId);
|
||||
|
||||
if (item && item.isLocked === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Default to unlocked if item not found or isLocked is undefined
|
||||
},
|
||||
[state.viewports]
|
||||
);
|
||||
|
||||
const showItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_VISIBLE',
|
||||
payload: { viewportId, itemId, visibleStatus: true },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const hideItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_VISIBLE',
|
||||
payload: { viewportId, itemId, visibleStatus: false },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const toggleVisibility = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
const currentVisible = isItemVisible(viewportId, itemId);
|
||||
dispatch({
|
||||
type: 'SET_VISIBLE',
|
||||
payload: { viewportId, itemId, visibleStatus: !currentVisible },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const isItemVisible = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
if (!state.viewports[viewportId]) {
|
||||
return true; // Default to visible if viewport doesn't exist
|
||||
}
|
||||
|
||||
for (const locationKey in state.viewports[viewportId]) {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = state.viewports[viewportId][location];
|
||||
const item = components.find(item => item.id === itemId);
|
||||
|
||||
if (item && item.isVisible === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Default to visible if item not found or isVisible is undefined
|
||||
},
|
||||
[state.viewports]
|
||||
);
|
||||
|
||||
const openItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'OPEN_ITEM',
|
||||
payload: { viewportId, itemId },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const closeItem = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
dispatch({
|
||||
type: 'CLOSE_ITEM',
|
||||
payload: { viewportId, itemId },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const closeAllItems = useCallback(
|
||||
(viewportId: string) => {
|
||||
dispatch({
|
||||
type: 'CLOSE_ALL_ITEMS',
|
||||
payload: viewportId,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const isItemOpen = useCallback(
|
||||
(viewportId: string, itemId: string) => {
|
||||
if (!state.viewports[viewportId]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const locationKey in state.viewports[viewportId]) {
|
||||
const location = Number(locationKey) as ViewportActionCornersLocations;
|
||||
const components = state.viewports[viewportId][location];
|
||||
const item = components.find(item => item.id === itemId);
|
||||
|
||||
if (item && item.isOpen === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[state.viewports]
|
||||
);
|
||||
|
||||
const getAlignAndSide = useCallback(
|
||||
(location: ViewportActionCornersLocations) => {
|
||||
return service.getAlignAndSide(location);
|
||||
},
|
||||
[service]
|
||||
);
|
||||
|
||||
const api = useMemo(() => {
|
||||
return {
|
||||
getState,
|
||||
addComponent,
|
||||
addComponents,
|
||||
clear,
|
||||
getAlignAndSide,
|
||||
lockItem,
|
||||
unlockItem,
|
||||
toggleLock,
|
||||
isItemLocked,
|
||||
showItem,
|
||||
hideItem,
|
||||
toggleVisibility,
|
||||
isItemVisible,
|
||||
openItem,
|
||||
closeItem,
|
||||
closeAllItems,
|
||||
isItemOpen,
|
||||
};
|
||||
}, [
|
||||
getState,
|
||||
addComponent,
|
||||
addComponents,
|
||||
clear,
|
||||
getAlignAndSide,
|
||||
lockItem,
|
||||
unlockItem,
|
||||
toggleLock,
|
||||
isItemLocked,
|
||||
showItem,
|
||||
hideItem,
|
||||
toggleVisibility,
|
||||
isItemVisible,
|
||||
openItem,
|
||||
closeItem,
|
||||
closeAllItems,
|
||||
isItemOpen,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (service && service.setServiceImplementation) {
|
||||
const implementation = {
|
||||
getState,
|
||||
addComponent,
|
||||
addComponents,
|
||||
clear,
|
||||
lockItem,
|
||||
unlockItem,
|
||||
toggleLock,
|
||||
isItemLocked,
|
||||
showItem,
|
||||
hideItem,
|
||||
toggleVisibility,
|
||||
isItemVisible,
|
||||
openItem,
|
||||
closeItem,
|
||||
closeAllItems,
|
||||
isItemOpen,
|
||||
};
|
||||
|
||||
service.setServiceImplementation(implementation);
|
||||
}
|
||||
}, [
|
||||
service,
|
||||
getState,
|
||||
addComponent,
|
||||
addComponents,
|
||||
clear,
|
||||
lockItem,
|
||||
unlockItem,
|
||||
toggleLock,
|
||||
isItemLocked,
|
||||
showItem,
|
||||
hideItem,
|
||||
toggleVisibility,
|
||||
isItemVisible,
|
||||
openItem,
|
||||
closeItem,
|
||||
closeAllItems,
|
||||
isItemOpen,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ViewportActionCornersContext.Provider value={[state, api]}>
|
||||
{children}
|
||||
</ViewportActionCornersContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook to use the ViewportActionCorners context
|
||||
export const useViewportActionCorners = () => useContext(ViewportActionCornersContext);
|
||||
@@ -9,6 +9,10 @@ import { UserAuthenticationProvider, useUserAuthentication } from './UserAuthent
|
||||
import { ImageViewerContext, ImageViewerProvider, useImageViewer } from './ImageViewerProvider';
|
||||
import DragAndDropProvider from './DragAndDropProvider';
|
||||
import CineProvider, { useCine } from './CineProvider';
|
||||
import {
|
||||
ViewportActionCornersProvider,
|
||||
useViewportActionCorners,
|
||||
} from './ViewportActionCornersProvider';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
@@ -20,3 +24,4 @@ export { UserAuthenticationProvider, useUserAuthentication };
|
||||
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
|
||||
export { DragAndDropProvider };
|
||||
export { CineProvider, useCine };
|
||||
export { ViewportActionCornersProvider, useViewportActionCorners };
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
|
||||
|
||||
export type ActionComponentInfo = {
|
||||
viewportId: string;
|
||||
id: string;
|
||||
component: ReactNode;
|
||||
location: ViewportActionCornersLocations;
|
||||
indexPriority?: number;
|
||||
isLocked?: boolean;
|
||||
isOpen?: boolean;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
|
||||
export type AlignAndSide = {
|
||||
align: 'start' | 'end' | 'center';
|
||||
side: 'top' | 'bottom' | 'left' | 'right';
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import ThumbnailType from './ThumbnailType';
|
||||
export * from './Predicate';
|
||||
export * from './ContextMenuItem';
|
||||
export * from './ViewportActionCornersTypes';
|
||||
export * from './ActionCorners';
|
||||
|
||||
/**
|
||||
* StringNumber often comes back from DICOMweb for integer valued items.
|
||||
|
||||
Reference in new issue
Block a user