feat(toolbox): Refactor Toolbar and Toolbox to enable sections (#4825)
This commit is contained in:
1 parent
86c12246ac
commit
cc5cdfb081
79 files changed
+3527
-2992
No files matched your search
@@ -13,18 +13,23 @@ interface RadioOption {
|
||||
name: string;
|
||||
value: string;
|
||||
values: RadioValue[];
|
||||
commands?: (val: string) => void;
|
||||
onChange?: (val: string) => void;
|
||||
}
|
||||
|
||||
interface RowSegmentedControlProps {
|
||||
option: RadioOption;
|
||||
className?: string;
|
||||
onChange?: (val: string) => void;
|
||||
}
|
||||
|
||||
export const RowSegmentedControl: React.FC<RowSegmentedControlProps> = ({ option, className }) => {
|
||||
export const RowSegmentedControl: React.FC<RowSegmentedControlProps> = ({
|
||||
option,
|
||||
className,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleValueChange = (newVal: string) => {
|
||||
if (option.commands) {
|
||||
option.commands(newVal);
|
||||
if (onChange) {
|
||||
onChange(newVal);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const renderRangeSetting = option => {
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
value={option.value}
|
||||
onChange={value => option.commands?.(value)}
|
||||
onChange={value => option.onChange?.(value)}
|
||||
allowNumberEdit={true}
|
||||
inputClassName="ml-1 w-4/5 cursor-pointer"
|
||||
/>
|
||||
@@ -70,6 +70,7 @@ const renderRadioSetting = option => {
|
||||
<RowSegmentedControl
|
||||
key={option.id}
|
||||
option={option}
|
||||
onChange={option.onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -79,7 +80,7 @@ function renderDoubleRangeSetting(option) {
|
||||
<RowDoubleRange
|
||||
key={option.id}
|
||||
values={option.value}
|
||||
onChange={option.commands}
|
||||
onChange={option.onChange}
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { ToolboxUI, useToolbox } from '../../';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
* custom control panels to users. This component is a generic UI component that
|
||||
* interacts with services and commands in a generic fashion. While it might
|
||||
* seem unconventional to import it from the UI and integrate it into the JSX,
|
||||
* it belongs in the UI components as there isn't anything in this component that
|
||||
* couldn't be used for a completely different type of app. It plays a crucial
|
||||
* role in enhancing the app with a toolbox by providing a way to integrate
|
||||
* and display various tools and their corresponding options
|
||||
*/
|
||||
function Toolbox({
|
||||
servicesManager,
|
||||
buttonSectionId,
|
||||
commandsManager,
|
||||
title,
|
||||
...props
|
||||
}: withAppTypes) {
|
||||
// We should move these outside of the platform/ui-next, no file here
|
||||
// should rely on the managers and services
|
||||
const { state: toolboxState, api } = useToolbox(buttonSectionId);
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
const prevButtonIdsRef = useRef('');
|
||||
const prevToolboxStateRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
const currentButtonIdsStr = JSON.stringify(
|
||||
toolbarButtons.map(button => {
|
||||
const { id, componentProps } = button;
|
||||
if (componentProps.items?.length) {
|
||||
return componentProps.items.map(item => `${item.id}-${item.disabled}`);
|
||||
}
|
||||
return `${id}-${componentProps.disabled}`;
|
||||
})
|
||||
);
|
||||
|
||||
const currentToolBoxStateStr = JSON.stringify(
|
||||
Object.keys(toolboxState.toolOptions).map(tool => {
|
||||
const options = toolboxState.toolOptions[tool];
|
||||
if (Array.isArray(options)) {
|
||||
return options?.map(option => `${option.id}-${option.value}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
prevButtonIdsRef.current === currentButtonIdsStr &&
|
||||
prevToolboxStateRef.current === currentToolBoxStateStr
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevButtonIdsRef.current = currentButtonIdsStr;
|
||||
prevToolboxStateRef.current = currentToolBoxStateStr;
|
||||
|
||||
const initializeOptionsWithEnhancements = toolbarButtons.reduce(
|
||||
(accumulator, toolbarButton) => {
|
||||
const { id: buttonId, componentProps } = toolbarButton;
|
||||
|
||||
const createEnhancedOptions = (options, parentId) => {
|
||||
const optionsToUse = Array.isArray(options) ? options : [options];
|
||||
|
||||
return optionsToUse.map(option => {
|
||||
if (typeof option.optionComponent === 'function') {
|
||||
return option;
|
||||
}
|
||||
|
||||
const value =
|
||||
toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ??
|
||||
option.value;
|
||||
|
||||
const updatedOptions = toolboxState.toolOptions?.[parentId];
|
||||
|
||||
return {
|
||||
...option,
|
||||
value,
|
||||
commands: value => {
|
||||
api.handleToolOptionChange(parentId, option.id, value);
|
||||
|
||||
const { isArray } = Array;
|
||||
const cmds = isArray(option.commands) ? option.commands : [option.commands];
|
||||
|
||||
cmds.forEach(command => {
|
||||
const isString = typeof command === 'string';
|
||||
const isObject = typeof command === 'object';
|
||||
const isFunction = typeof command === 'function';
|
||||
|
||||
if (isString) {
|
||||
commandsManager.run(command, { value });
|
||||
} else if (isObject) {
|
||||
commandsManager.run({
|
||||
...command,
|
||||
commandOptions: {
|
||||
...command.commandOptions,
|
||||
...option,
|
||||
value,
|
||||
options: updatedOptions,
|
||||
},
|
||||
});
|
||||
} else if (isFunction) {
|
||||
command({ value, commandsManager, servicesManager, options: updatedOptions });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const { items, options } = componentProps;
|
||||
|
||||
if (items?.length) {
|
||||
items.forEach(({ options, id }) => {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
accumulator[id] = createEnhancedOptions(options, id);
|
||||
});
|
||||
} else if (options?.length) {
|
||||
accumulator[buttonId] = createEnhancedOptions(options, buttonId);
|
||||
} else if (options?.optionComponent) {
|
||||
accumulator[buttonId] = options.optionComponent;
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
api.initializeToolOptions(initializeOptionsWithEnhancements);
|
||||
}, [toolbarButtons, api, toolboxState, commandsManager, servicesManager]);
|
||||
|
||||
const handleToolOptionChange = (toolName, optionName, newValue) => {
|
||||
api.handleToolOptionChange(toolName, optionName, newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
api.handleToolSelect(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toolbox;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { PanelSection } from '../../components';
|
||||
@@ -6,52 +6,39 @@ import { ToolSettings } from '../OHIFToolSettings';
|
||||
|
||||
const ItemsPerRow = 4;
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
interface ToolbarButton {
|
||||
id: string;
|
||||
Component: React.ComponentType<{
|
||||
id: string;
|
||||
onInteraction: (details: { itemId: string }) => void;
|
||||
size: string;
|
||||
}>;
|
||||
componentProps: {
|
||||
isActive?: boolean;
|
||||
buttonSection?: string;
|
||||
options?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Just refactoring from the toolbox component to make it more readable
|
||||
*/
|
||||
function ToolboxUI(props: withAppTypes) {
|
||||
interface ToolboxProps {
|
||||
toolbarButtons: ToolbarButton[];
|
||||
numRows: number;
|
||||
title?: string;
|
||||
useCollapsedPanel?: boolean;
|
||||
onInteraction?: (details: { itemId: string }) => void;
|
||||
activeToolOptions?: unknown;
|
||||
}
|
||||
|
||||
function ToolboxUI(props: ToolboxProps) {
|
||||
const {
|
||||
toolbarButtons,
|
||||
handleToolSelect,
|
||||
toolboxState,
|
||||
toolbarButtons = [],
|
||||
numRows,
|
||||
servicesManager,
|
||||
title,
|
||||
useCollapsedPanel = true,
|
||||
onInteraction,
|
||||
activeToolOptions,
|
||||
} = props;
|
||||
|
||||
const { activeTool, toolOptions, selectedEvent } = toolboxState;
|
||||
const activeToolOptions = toolOptions?.[activeTool];
|
||||
|
||||
const prevToolOptions = usePrevious(activeToolOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeToolOptions || Array.isArray(activeToolOptions) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeToolOptions.forEach((option, index) => {
|
||||
const prevOption = prevToolOptions ? prevToolOptions[index] : undefined;
|
||||
if (!prevOption || option.value !== prevOption.value || selectedEvent) {
|
||||
const isOptionValid = option.condition
|
||||
? option.condition({ options: activeToolOptions })
|
||||
: true;
|
||||
if (isOptionValid) {
|
||||
const { commands } = option;
|
||||
commands(option.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [activeToolOptions, selectedEvent]);
|
||||
|
||||
const render = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -67,12 +54,9 @@ function ToolboxUI(props: withAppTypes) {
|
||||
|
||||
const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`;
|
||||
|
||||
const onInteraction = ({ itemId, id, commands }) => {
|
||||
const idToUse = itemId || id;
|
||||
handleToolSelect(idToUse);
|
||||
props.onInteraction({
|
||||
const handleInteraction = ({ itemId }: { itemId: string }) => {
|
||||
onInteraction?.({
|
||||
itemId,
|
||||
commands,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -85,10 +69,8 @@ function ToolboxUI(props: withAppTypes) {
|
||||
>
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
onInteraction={handleInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
import { ToolboxUI } from './ToolboxUI';
|
||||
import Toolbox from './Toolbox';
|
||||
export { ToolboxUI, Toolbox };
|
||||
export * from './ToolboxUI';
|
||||
@@ -41,7 +41,10 @@ const StudyItem = ({
|
||||
<div className="flex min-w-0 flex-col items-start text-[13px]">
|
||||
<Tooltip>
|
||||
<TooltipContent>{date}</TooltipContent>
|
||||
<TooltipTrigger className="w-full">
|
||||
<TooltipTrigger
|
||||
className="w-full"
|
||||
asChild
|
||||
>
|
||||
<div className="h-[18px] w-full max-w-[160px] overflow-hidden truncate whitespace-nowrap text-left text-white">
|
||||
{date}
|
||||
</div>
|
||||
@@ -49,7 +52,10 @@ const StudyItem = ({
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipContent>{description}</TooltipContent>
|
||||
<TooltipTrigger className="w-full">
|
||||
<TooltipTrigger
|
||||
className="w-full"
|
||||
asChild
|
||||
>
|
||||
<div className="text-muted-foreground h-[18px] w-full overflow-hidden truncate whitespace-nowrap text-left">
|
||||
{description}
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ function ToolButton(props: ToolButtonProps) {
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const { buttonSizeClass, iconSizeClass } = sizeClasses[size];
|
||||
const { buttonSizeClass, iconSizeClass } = sizeClasses[size] || sizeClasses.default;
|
||||
|
||||
const buttonClasses = cn(
|
||||
baseClasses,
|
||||
|
||||
@@ -52,7 +52,7 @@ import { ThumbnailList } from './ThumbnailList';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
||||
import { ToolboxUI, Toolbox } from './OHIFToolbox';
|
||||
import { ToolboxUI } from './OHIFToolbox';
|
||||
import Numeric from './Numeric';
|
||||
import { InputDialog, PresetDialog } from './OHIFDialogs';
|
||||
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
|
||||
@@ -105,6 +105,7 @@ import {
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButton';
|
||||
import { ToolSettings } from './OHIFToolSettings';
|
||||
|
||||
export {
|
||||
Numeric,
|
||||
@@ -173,7 +174,6 @@ export {
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
ToolboxUI,
|
||||
Toolbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@@ -236,4 +236,5 @@ export {
|
||||
ImageModal,
|
||||
UserPreferencesModal,
|
||||
FooterAction,
|
||||
ToolSettings,
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
export const initialState = {};
|
||||
|
||||
export const toolboxReducer = (state, action) => {
|
||||
const { toolbarSectionId } = action.payload;
|
||||
|
||||
if (!state[toolbarSectionId]) {
|
||||
state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false };
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
activeTool: action.payload.activeTool,
|
||||
selectedEvent: true,
|
||||
},
|
||||
};
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
const { toolName, optionName, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
selectedEvent: false,
|
||||
toolOptions: {
|
||||
...state[toolbarSectionId].toolOptions,
|
||||
[toolName]: state[toolbarSectionId].toolOptions[toolName].map(option =>
|
||||
option.id === optionName ? { ...option, value } : option
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'INITIALIZE_TOOL_OPTIONS':
|
||||
// Initialize tool options for each toolbarSectionId
|
||||
return {
|
||||
...state,
|
||||
selectedEvent: false,
|
||||
[action.toolbarSectionId]: {
|
||||
...state[action.toolbarSectionId],
|
||||
toolOptions: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const ToolboxContext = createContext();
|
||||
|
||||
export const ToolboxProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const handleToolSelect = (toolbarSectionId, toolName) => {
|
||||
dispatch({
|
||||
type: 'SET_ACTIVE_TOOL',
|
||||
payload: { toolbarSectionId, activeTool: toolName },
|
||||
});
|
||||
};
|
||||
|
||||
const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TOOL_OPTION',
|
||||
payload: { toolbarSectionId, toolName, optionName, value: newValue },
|
||||
});
|
||||
};
|
||||
|
||||
const initializeToolOptions = (toolbarSectionId, toolOptions) => {
|
||||
dispatch({
|
||||
type: 'INITIALIZE_TOOL_OPTIONS',
|
||||
toolbarSectionId,
|
||||
payload: toolOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions };
|
||||
|
||||
const value = { state, api };
|
||||
|
||||
return <ToolboxContext.Provider value={value}>{children}</ToolboxContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for accessing toolbox state and actions for a specific toolbar section.
|
||||
* You can use this hook to access the state and actions for a specific toolbar section (
|
||||
* defined by the toolbarSectionId) in your custom toolbar components. This hook
|
||||
* helps to manage the state and actions for the tools and their options in the toolbar.
|
||||
*/
|
||||
export const useToolbox = toolbarSectionId => {
|
||||
const context = useContext(ToolboxContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useToolbox must be used within a ToolboxProvider');
|
||||
}
|
||||
const { state, api } = context;
|
||||
|
||||
return {
|
||||
state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} },
|
||||
api: {
|
||||
handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName),
|
||||
handleToolOptionChange: (toolName, optionName, value) =>
|
||||
api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value),
|
||||
initializeToolOptions: toolOptions =>
|
||||
api.initializeToolOptions(toolbarSectionId, toolOptions),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,11 @@
|
||||
import NotificationProvider, { useNotification } from './NotificationProvider';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
import { ToolboxProvider, useToolbox } from './ToolboxContext';
|
||||
import { ModalProvider, useModal } from './ModalProvider';
|
||||
import { DialogProvider, useDialog } from './DialogProvider';
|
||||
import ManagedDialog from './ManagedDialog';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
export { ToolboxProvider, useToolbox };
|
||||
export { ModalProvider, useModal };
|
||||
export { DialogProvider, useDialog };
|
||||
export { ManagedDialog };
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
ToolboxUI,
|
||||
ToolSettings,
|
||||
DoubleSlider,
|
||||
Label,
|
||||
Slider,
|
||||
@@ -98,7 +99,6 @@ import {
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
Toolbox,
|
||||
Numeric,
|
||||
InputDialog,
|
||||
PresetDialog,
|
||||
@@ -113,8 +113,6 @@ import { DataRow } from './components/DataRow';
|
||||
import {
|
||||
useNotification,
|
||||
NotificationProvider,
|
||||
useToolbox,
|
||||
ToolboxProvider,
|
||||
useModal,
|
||||
ModalProvider,
|
||||
DialogProvider,
|
||||
@@ -232,9 +230,6 @@ export {
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
ToolboxProvider,
|
||||
Toolbox,
|
||||
useToolbox,
|
||||
utils,
|
||||
Numeric,
|
||||
AboutModal,
|
||||
@@ -249,4 +244,5 @@ export {
|
||||
DialogProvider,
|
||||
useDialog,
|
||||
ManagedDialog,
|
||||
ToolSettings,
|
||||
};
|
||||
Reference in new issue
Block a user