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
@@ -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"
|
||||
|
||||
@@ -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', ... ],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user