feat(toolbox): Refactor Toolbar and Toolbox to enable sections (#4825)

This commit is contained in:
Alireza authored and GitHub committed 2025-03-06 22:19:47 -05:00
1 parent 86c12246ac
commit cc5cdfb081
79 files changed
+3527 -2992

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.0.1",
"@cornerstonejs/dicom-image-loader": "^3.0.4",
"@ohif/ui": "3.10.0-beta.116",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
+49 -8
View File
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useSystem } from '../contextProviders/SystemProvider';
export function useToolbar({ servicesManager, buttonSection = 'primary' }: withAppTypes) {
export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
const { commandsManager, servicesManager } = useSystem();
const { toolbarService, viewportGridService } = servicesManager.services;
const { EVENTS } = toolbarService;
@@ -11,15 +13,54 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA
// Callback function for handling toolbar interactions
const onInteraction = useCallback(
args => {
args.event?.stopPropagation?.();
const viewportId = viewportGridService.getActiveViewportId();
const refreshProps = {
viewportId,
};
toolbarService.recordInteraction(args, {
refreshProps,
});
const refreshProps = { viewportId };
const buttonProps = toolbarService.getButtonProps(args.itemId);
if (buttonProps.commands || buttonProps.options) {
const allCommands = [];
const options = buttonProps.options || [];
const itemCommands = buttonProps.commands || [];
// Process item commands
if (itemCommands) {
Array.isArray(itemCommands)
? allCommands.push(...itemCommands)
: allCommands.push(itemCommands);
}
// Process commands from options
if (options.length > 0) {
options.forEach(option => {
if (!option.commands) {
return;
}
const valueToUse = option.value;
const commands = Array.isArray(option.commands) ? option.commands : [option.commands];
commands.forEach(command => {
const commandOptions = {
...option,
value: valueToUse,
options: buttonProps.options,
servicesManager: servicesManager,
commandsManager: commandsManager,
};
const processedCommand = () => commandsManager.run(command, commandOptions);
allCommands.push(processedCommand);
});
});
}
buttonProps.commands = allCommands;
}
toolbarService.recordInteraction({ ...args, ...buttonProps }, { refreshProps });
},
[toolbarService, viewportGridService]
[toolbarService, viewportGridService, toolbarButtons]
);
// Effect to handle toolbar modification events
@@ -134,7 +134,7 @@ export default class ToolbarService extends PubSubService {
buttons.forEach(button => {
if (replace || !this.state.buttons[button.id]) {
if (!button.props) {
button.props = {};
button.props = {} as ButtonProps;
}
this.state.buttons[button.id] = button;
@@ -173,12 +173,16 @@ export default class ToolbarService extends PubSubService {
? interaction.commands
: [interaction.commands];
commands = commands.filter(Boolean);
if (!commands?.length) {
this.refreshToolbarState({
...options?.refreshProps,
itemId,
interaction,
});
return;
}
const commandOptions = { ...options, ...interaction };
@@ -228,10 +232,6 @@ export default class ToolbarService extends PubSubService {
*/
public refreshToolbarState(refreshProps) {
const buttons = this.state.buttons;
// Tracks evaluated buttons to avoid re-evaluating them (this will
// cause issue for toggles where if the button is in primary
// and secondary it will be evaluated twice)
const evaluationResults = new Map();
const evaluateButtonProps = (button, props, refreshProps) => {
@@ -252,59 +252,50 @@ export default class ToolbarService extends PubSubService {
}
};
const refreshedButtons = Object.values(buttons).reduce((acc, button: Button) => {
const isNested = (button.props as NestedButtonProps)?.groupId;
Object.values(buttons).forEach(button => {
const hasSection = (button.props as NestedButtonProps)?.buttonSection;
if (!isNested) {
if (!hasSection) {
this.handleEvaluate(button.props);
const buttonProps = button.props as ButtonProps;
const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps);
acc[button.id] = {
buttons[button.id] = {
...button,
props: updatedProps,
};
} else {
let buttonProps = button.props as NestedButtonProps;
// if it is nested we should perform evaluate on each item in the group
this.handleEvaluateNested(buttonProps);
const { evaluate: groupEvaluate } = buttonProps;
const groupEvaluated =
typeof groupEvaluate === 'function'
? groupEvaluate({ ...refreshProps, button })
: undefined;
const groupEvaluated = groupEvaluate?.({ ...refreshProps, button });
// handle group evaluate function which might switch the primary
// item in the group
buttonProps = {
...buttonProps,
primary: groupEvaluated?.primary ?? buttonProps.primary,
disabled: groupEvaluated?.disabled ?? buttonProps.disabled,
disabledText: groupEvaluated?.disabledText ?? buttonProps.disabledText,
};
const { primary, items } = buttonProps;
const toolButtonIds = this.state.buttonSections[buttonProps.buttonSection];
// primary and items evaluate functions
let updatedPrimary;
if (primary) {
updatedPrimary = evaluateButtonProps(primary, primary, refreshProps);
if (!toolButtonIds) {
return;
}
const updatedItems = items.map(item => evaluateButtonProps(item, item, refreshProps));
buttonProps = {
...buttonProps,
primary: updatedPrimary,
items: updatedItems,
};
acc[button.id] = {
...button,
props: buttonProps,
};
toolButtonIds.forEach(buttonId => {
const button = buttons[buttonId];
const updatedProps = evaluateButtonProps(button, button.props, refreshProps);
buttons[buttonId] = {
...button,
props: updatedProps,
};
});
}
});
return acc;
}, {});
this.setButtons(refreshedButtons);
this.setButtons(buttons);
return this.state;
}
@@ -349,17 +340,6 @@ export default class ToolbarService extends PubSubService {
* @returns The button properties.
*/
public getButtonProps(id: string): ButtonProps {
for (const buttonId of Object.keys(this.state.buttons)) {
const { primary, items } = (this.state.buttons[buttonId].props as NestedButtonProps) || {};
if (primary?.id === id) {
return primary;
}
const found = items?.find(childButton => childButton.id === id);
if (found) {
return found;
}
}
// This should be checked after we checked the nested buttons, since
// we are checking based on the ids, the nested objects are higher priority
// and more specific
@@ -421,6 +401,12 @@ export default class ToolbarService extends PubSubService {
);
}
getButtonPropsInButtonSection(sectionId: string) {
const buttonSectionIds = this.state.buttonSections[sectionId];
return buttonSectionIds?.map(btnId => this.getButtonProps(btnId)) || [];
}
/**
* Retrieves the tool name for a given button.
* @param button - The button object.
@@ -448,41 +434,119 @@ export default class ToolbarService extends PubSubService {
* @param {*} metadata
* @param {*} props - Props set by the Viewer layer
*/
_mapButtonToDisplay(btn, props) {
_mapButtonToDisplay(btn: Button, props: Record<string, unknown>) {
if (!btn) {
return;
}
const { id, uiType, component } = btn;
const { groupId } = btn.props;
const { id, uiType } = btn;
const { groupId } = btn.props as NestedButtonProps;
const buttonTypes = this._getButtonUITypes();
const buttonType = buttonTypes[uiType];
if (!buttonType && !component) {
if (!btn.component) {
btn.component = buttonType.defaultComponent;
}
if (!buttonType) {
return;
}
!groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
const { id: buttonId, props: componentProps } = btn;
const createEnhancedOptions = (options, itemId) => {
const optionsToUse = Array.isArray(options) ? options : [options];
const toolProps = this.getButtonProps(itemId);
return optionsToUse.map(option => {
if (typeof option.optionComponent === 'function') {
return option;
}
return {
...option,
onChange: value => {
// Update the option's value for UI
option.value = value;
const cmds = Array.isArray(option.commands) ? option.commands : [option.commands];
// Find the parent button and update its options
if (toolProps && toolProps.options) {
// Find the option in the button's options array and update its value
const optionIndex = toolProps.options.findIndex(opt => opt.id === option.id);
if (optionIndex !== -1) {
toolProps.options[optionIndex].value = value;
}
}
cmds.forEach(command => {
const commandOptions = {
...option,
value,
options: toolProps.options,
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
};
this._commandsManager.run(command, commandOptions);
});
// Notify that toolbar state has been modified
this._broadcastEvent(EVENTS.TOOL_BAR_STATE_MODIFIED, {
buttons: this.state.buttons,
buttonSections: this.state.buttonSections,
});
},
};
});
};
if ((componentProps as NestedButtonProps)?.items?.length) {
const { items = [] } = componentProps as NestedButtonProps;
items.forEach(item => {
if (!item.options) {
return;
}
item.options = createEnhancedOptions(item.options, item.id);
});
} else if ((componentProps as ButtonProps).options?.length) {
(componentProps as ButtonProps).options = createEnhancedOptions(
(componentProps as ButtonProps).options,
buttonId
);
} else if ((componentProps as ButtonProps).optionComponent) {
(componentProps as ButtonProps).optionComponent = options.optionComponent;
}
return {
id,
Component: component || buttonType.defaultComponent,
componentProps: Object.assign({}, btn.props, props),
Component: btn.component,
componentProps: Object.assign({ id }, btn.props, props),
};
}
handleEvaluateNested = props => {
const { primary, items } = props;
// handle group evaluate function
this.handleEvaluate(props);
const { buttonSection } = props;
// primary and items evaluate functions
if (primary) {
this.handleEvaluate(primary);
if (!buttonSection) {
return;
}
items.forEach(item => this.handleEvaluate(item));
const toolbarButtons = this.getButtonSection(buttonSection);
if (!toolbarButtons?.length) {
return;
}
toolbarButtons.forEach(button => {
this.handleEvaluate(button.componentProps);
});
};
handleEvaluate = props => {
@@ -492,9 +556,7 @@ export default class ToolbarService extends PubSubService {
// get the custom option component from the extension manager and set it as the optionComponent
const buttonTypes = this._getButtonUITypes();
const optionComponent = buttonTypes[options]?.defaultComponent;
props.options = {
optionComponent,
};
props.options = optionComponent;
}
if (typeof evaluate === 'function') {
@@ -1,4 +1,5 @@
import type { RunCommand } from '../../types/Command';
import React from 'react';
export type EvaluatePublic =
| string
@@ -17,6 +18,20 @@ export type EvaluateObject = {
[key: string]: unknown;
};
export type ButtonOptions = {
id: string;
type: 'range' | 'radio' | 'double-range' | 'custom';
name?: string;
min?: number;
max?: number;
step?: number;
value?: number | number[] | string;
commands?: (value: unknown) => void;
condition?: (props: Record<string, unknown>) => boolean;
children?: React.ReactNode | (() => React.ReactNode);
options?: Array<{ value: string; label: string }>;
};
export type ButtonProps = {
id: string;
icon: string;
@@ -27,27 +42,15 @@ export type ButtonProps = {
className?: string;
evaluate?: EvaluatePublic;
listeners?: Record<string, RunCommand>;
};
export type NestedButtonProps = {
groupId: string;
// group evaluate which is different
// from the evaluate function for the primary and items
evaluate?: EvaluatePublic;
items: ButtonProps[];
primary: ButtonProps & {
// Todo: this is really ugly but really we don't have any other option
// the ui design requires this since the button should be rounded if
// active otherwise it should not be rounded
isActive?: boolean;
};
secondary: ButtonProps;
options?: ButtonOptions[];
buttonSection?: string;
};
export type Button = {
id: string;
props: ButtonProps | NestedButtonProps;
props: ButtonProps;
// button ui type (e.g. 'ohif.splitButton', 'ohif.radioGroup')
// extensions can provide custom components for these types
uiType: string;
component?: React.ComponentType<any>;
};
@@ -27,7 +27,7 @@ export const EVENTS = {
sections: [
{
key: 'primary',
buttons: [ 'MeasurementTools', 'Zoom', ... ],
buttons: [ 'measurementSection', 'Zoom', ... ],
},
],
},