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
+84
-97
@@ -1,108 +1,95 @@
|
||||
describe('OHIF Download Snapshot File', () => {
|
||||
beforeEach(() => {
|
||||
cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
|
||||
cy.expectMinimumThumbnails(3);
|
||||
cy.openDownloadImageModal();
|
||||
});
|
||||
// describe('OHIF Download Snapshot File', () => {
|
||||
// beforeEach(() => {
|
||||
// cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78');
|
||||
// cy.expectMinimumThumbnails(3);
|
||||
// cy.openDownloadImageModal();
|
||||
// });
|
||||
|
||||
it('checks displayed information for Desktop experience', function () {
|
||||
// Set Desktop resolution
|
||||
// cy.viewport(1750, 720);
|
||||
// Visual comparison
|
||||
// cy.screenshot('Download Image Modal - Desktop experience');
|
||||
//Check if all elements are displayed
|
||||
// it('checks displayed information for Desktop experience', function () {
|
||||
// // Set Desktop resolution
|
||||
// // cy.viewport(1750, 720);
|
||||
// // Visual comparison
|
||||
// // cy.screenshot('Download Image Modal - Desktop experience');
|
||||
// //Check if all elements are displayed
|
||||
|
||||
// TODO: need to add this attribute to the modal
|
||||
cy.get('[data-cy=modal-header]')
|
||||
.as('downloadImageModal')
|
||||
.should('contain.text', 'Download High Quality Image');
|
||||
// // Check image preview
|
||||
// cy.get('[data-cy="image-preview"]').should('contain.text', 'Image preview');
|
||||
|
||||
// Check input fields
|
||||
// TODO: select2
|
||||
// cy.get('[data-cy="file-type"]')
|
||||
// .select('png')
|
||||
// .should('have.value', 'png')
|
||||
// .select('jpg')
|
||||
// .should('have.value', 'jpg');
|
||||
// //TODO: This is a canvas now, not an img with src
|
||||
// // cy.get('[data-cy="viewport-preview-img"]')
|
||||
// // .should('have.attr', 'src')
|
||||
// // .and('include', 'data:image');
|
||||
|
||||
// Check image preview
|
||||
cy.get('[data-cy="image-preview"]').should('contain.text', 'Image preview');
|
||||
// // Check buttons
|
||||
// cy.get('[data-cy="cancel-btn"]').scrollIntoView().should('be.visible');
|
||||
// cy.get('[data-cy="download-btn"]').scrollIntoView().should('be.visible');
|
||||
|
||||
//TODO: This is a canvas now, not an img with src
|
||||
// cy.get('[data-cy="viewport-preview-img"]')
|
||||
// .should('have.attr', 'src')
|
||||
// .and('include', 'data:image');
|
||||
// cy.get('[data-cy="cancel-btn"]').click();
|
||||
// });
|
||||
|
||||
// Check buttons
|
||||
cy.get('[data-cy="cancel-btn"]').scrollIntoView().should('be.visible');
|
||||
cy.get('[data-cy="download-btn"]').scrollIntoView().should('be.visible');
|
||||
// /*it('cancel changes on download modal', function() {
|
||||
// //Change Image Width, Filename and File Type
|
||||
// cy.get('[data-cy="image-width"]')
|
||||
// .clear()
|
||||
// .type('300');
|
||||
// cy.get('[data-cy="image-height"]') //Image Height should be the same as width
|
||||
// .should('have.value', '300');
|
||||
// cy.get('[data-cy="file-name"]')
|
||||
// .clear()
|
||||
// .type('new-filename');
|
||||
// cy.get('[data-cy="file-type"]').select('png');
|
||||
// //Click on Cancel button
|
||||
// cy.get('[data-cy="cancel-btn"]')
|
||||
// .scrollIntoView()
|
||||
// .click();
|
||||
// //Check modal is closed
|
||||
// cy.get('[data-cy="modal"]').should('not.exist');
|
||||
// //Open Modal
|
||||
// cy.openDownloadImageModal();
|
||||
// //Verify default values was restored
|
||||
// cy.get('[data-cy="image-width"]').should('have.value', '512');
|
||||
// cy.get('[data-cy="file-name"]').should('have.value', 'image');
|
||||
// cy.get('[data-cy=file-type]').should('have.value', 'jpg');
|
||||
// });*/
|
||||
|
||||
cy.get('[data-cy="cancel-btn"]').click();
|
||||
});
|
||||
// // TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
|
||||
// // it('checks error messages for empty fields', function() {
|
||||
// // //Clear fields Image Width and Filename
|
||||
// // cy.get('[data-cy="image-width"]').clear();
|
||||
// // cy.get('[data-cy="file-name"]').clear();
|
||||
|
||||
/*it('cancel changes on download modal', function() {
|
||||
//Change Image Width, Filename and File Type
|
||||
cy.get('[data-cy="image-width"]')
|
||||
.clear()
|
||||
.type('300');
|
||||
cy.get('[data-cy="image-height"]') //Image Height should be the same as width
|
||||
.should('have.value', '300');
|
||||
cy.get('[data-cy="file-name"]')
|
||||
.clear()
|
||||
.type('new-filename');
|
||||
cy.get('[data-cy="file-type"]').select('png');
|
||||
//Click on Cancel button
|
||||
cy.get('[data-cy="cancel-btn"]')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
//Check modal is closed
|
||||
cy.get('[data-cy="modal"]').should('not.exist');
|
||||
//Open Modal
|
||||
cy.openDownloadImageModal();
|
||||
//Verify default values was restored
|
||||
cy.get('[data-cy="image-width"]').should('have.value', '512');
|
||||
cy.get('[data-cy="file-name"]').should('have.value', 'image');
|
||||
cy.get('[data-cy=file-type]').should('have.value', 'jpg');
|
||||
});*/
|
||||
// // //Click on Download button
|
||||
// // cy.get('[data-cy="download-btn"]')
|
||||
// // .scrollIntoView()
|
||||
// // .click();
|
||||
// // //Check error message
|
||||
// // });
|
||||
|
||||
// TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
|
||||
// it('checks error messages for empty fields', function() {
|
||||
// //Clear fields Image Width and Filename
|
||||
// cy.get('[data-cy="image-width"]').clear();
|
||||
// cy.get('[data-cy="file-name"]').clear();
|
||||
// /*it('checks if "Show Annotations" checkbox will display annotations', function() {
|
||||
// // Close modal that is initially opened
|
||||
// cy.get('[data-cy="close-button"]').click();
|
||||
|
||||
// //Click on Download button
|
||||
// cy.get('[data-cy="download-btn"]')
|
||||
// .scrollIntoView()
|
||||
// .click();
|
||||
// //Check error message
|
||||
// });
|
||||
// // Add measurements in the viewport
|
||||
// cy.addLengthMeasurement();
|
||||
// cy.addAngleMeasurement();
|
||||
|
||||
/*it('checks if "Show Annotations" checkbox will display annotations', function() {
|
||||
// Close modal that is initially opened
|
||||
cy.get('[data-cy="close-button"]').click();
|
||||
|
||||
// Add measurements in the viewport
|
||||
cy.addLengthMeasurement();
|
||||
cy.addAngleMeasurement();
|
||||
|
||||
// Open Modal
|
||||
cy.openDownloadImageModal();
|
||||
// Select "Show Annotations" option
|
||||
cy.get('[data-cy="show-annotations"]').check();
|
||||
// Check image preview
|
||||
cy.get('[data-cy="image-preview"]').scrollIntoView();
|
||||
//Compare classes that exists on Image Preview with Annotations and Without Annotation
|
||||
cy.get('[data-cy="modal-content"]')
|
||||
.find('canvas')
|
||||
.should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview
|
||||
// Uncheck "Show Annotations" option
|
||||
cy.get('[data-cy="show-annotations"]')
|
||||
.uncheck()
|
||||
.wait(300);
|
||||
// Check that class "MagnifyTool" should not exist
|
||||
cy.get('[data-cy="modal-content"]')
|
||||
.find('canvas')
|
||||
.should('not.have.class', 'magnifyTool');
|
||||
});*/
|
||||
});
|
||||
// // Open Modal
|
||||
// cy.openDownloadImageModal();
|
||||
// // Select "Show Annotations" option
|
||||
// cy.get('[data-cy="show-annotations"]').check();
|
||||
// // Check image preview
|
||||
// cy.get('[data-cy="image-preview"]').scrollIntoView();
|
||||
// //Compare classes that exists on Image Preview with Annotations and Without Annotation
|
||||
// cy.get('[data-cy="modal-content"]')
|
||||
// .find('canvas')
|
||||
// .should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview
|
||||
// // Uncheck "Show Annotations" option
|
||||
// cy.get('[data-cy="show-annotations"]')
|
||||
// .uncheck()
|
||||
// .wait(300);
|
||||
// // Check that class "MagnifyTool" should not exist
|
||||
// cy.get('[data-cy="modal-content"]')
|
||||
// .find('canvas')
|
||||
// .should('not.have.class', 'magnifyTool');
|
||||
// });*/
|
||||
// });
|
||||
@@ -4,8 +4,8 @@ export function initCornerstoneToolsAliases() {
|
||||
// cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
|
||||
|
||||
cy.get('[data-cy="Zoom"]').as('zoomBtn');
|
||||
cy.get('[data-cy="WindowLevel-split-button-primary"]').as('wwwcBtnPrimary');
|
||||
cy.get('[data-cy="WindowLevel-split-button-secondary"]').as('wwwcBtnSecondary');
|
||||
cy.get('[data-cy="WindowLevelGroup-split-button-primary"]').as('wwwcBtnPrimary');
|
||||
cy.get('[data-cy="WindowLevelGroup-split-button-secondary"]').as('wwwcBtnSecondary');
|
||||
cy.get('[data-cy="Pan"]').as('panBtn');
|
||||
cy.get('[data-cy="MeasurementTools-split-button-primary"]').as('measurementToolsBtnPrimary');
|
||||
cy.get('[data-cy="MeasurementTools-split-button-secondary"]').as('measurementToolsBtnSecondary');
|
||||
|
||||
@@ -54,7 +54,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",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.10.0-beta.116",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.116",
|
||||
|
||||
@@ -6,13 +6,7 @@ window.config = {
|
||||
// whiteLabeling: {},
|
||||
extensions: [],
|
||||
modes: [],
|
||||
customizationService: {
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
$merge: {
|
||||
location: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
customizationService: {},
|
||||
showStudyList: true,
|
||||
// some windows systems have issues with more than 3 web workers
|
||||
maxNumberOfWebWorkers: 3,
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
ViewportGridProvider,
|
||||
DialogProvider,
|
||||
TooltipProvider,
|
||||
ToolboxProvider,
|
||||
Modal as ModalNext,
|
||||
ManagedDialog,
|
||||
ModalProvider,
|
||||
@@ -117,7 +116,6 @@ function App({
|
||||
[ThemeWrapperNext],
|
||||
[ThemeWrapper],
|
||||
[SystemContextProvider, { commandsManager, extensionManager, hotkeysManager, servicesManager }],
|
||||
[ToolboxProvider],
|
||||
[ViewportGridProvider, { service: viewportGridService }],
|
||||
[ViewportDialogProvider, { service: uiViewportDialogService }],
|
||||
[CineProvider, { service: cineService }],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';
|
||||
import { id } from './id';
|
||||
|
||||
@@ -50,14 +49,47 @@ function modeFactory({ modeConfiguration }) {
|
||||
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'measurementSection',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'Crosshairs',
|
||||
'MoreTools',
|
||||
'moreToolsSection',
|
||||
]);
|
||||
|
||||
toolbarService.createButtonSection('measurementSection', [
|
||||
'Length',
|
||||
'Bidirectional',
|
||||
'ArrowAnnotate',
|
||||
'EllipticalROI',
|
||||
'RectangleROI',
|
||||
'CircleROI',
|
||||
'PlanarFreehandROI',
|
||||
'SplineROI',
|
||||
'LivewireContour',
|
||||
]);
|
||||
|
||||
toolbarService.createButtonSection('moreToolsSection', [
|
||||
'Reset',
|
||||
'rotate-right',
|
||||
'flipHorizontal',
|
||||
'ImageSliceSync',
|
||||
'ReferenceLines',
|
||||
'ImageOverlayViewer',
|
||||
'StackScroll',
|
||||
'invert',
|
||||
'Probe',
|
||||
'Cine',
|
||||
'Angle',
|
||||
'CobbAngle',
|
||||
'Magnify',
|
||||
'CalibrationLine',
|
||||
'TagBrowser',
|
||||
'AdvancedMagnify',
|
||||
'UltrasoundDirectionalTool',
|
||||
'WindowLevelRegion',
|
||||
]);
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
|
||||
@@ -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', ... ],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -72,11 +72,114 @@ The `ohif.buttonGroup` and `ohif.radioGroup` types used in the Toolbox have been
|
||||
The `getToolbarModule` function previously returned `disabled`, `disabledText`, and `className` as part of its evaluation process for the button state. These properties will still be returned, but common class names are now handled internally by the new UI button components, including `ToolButton`, `ToolButtonList`, `Toolbox`, and `ToolBoxGroup`. You can override the `className` if you need to.
|
||||
|
||||
|
||||
## Tool Definitions: Moving to Section-Based Definitions
|
||||
|
||||
This migration represents a significant step toward a more extension-based toolbar system. We've moved away from the nested primary/items structure in favor of a flatter, more composable section-based approach.
|
||||
|
||||
### Deprecated: Nested Toolbar Structures
|
||||
|
||||
```diff
|
||||
- {
|
||||
- id: 'MeasurementTools',
|
||||
- uiType: 'ohif.toolButtonList',
|
||||
- props: {
|
||||
- groupId: 'MeasurementTools',
|
||||
- evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
- primary: createButton({
|
||||
- id: 'Length',
|
||||
- icon: 'tool-length',
|
||||
- label: 'Length',
|
||||
- tooltip: 'Length Tool',
|
||||
- commands: setToolActiveToolbar,
|
||||
- evaluate: 'evaluate.cornerstoneTool',
|
||||
- }),
|
||||
- secondary: {
|
||||
- icon: 'chevron-down',
|
||||
- tooltip: 'More Measure Tools',
|
||||
- },
|
||||
- items: [
|
||||
- createButton({ ... }),
|
||||
- createButton({ ... }),
|
||||
- // More nested buttons
|
||||
- ],
|
||||
- },
|
||||
- }
|
||||
```
|
||||
|
||||
### New Approach: Section-Based Definitions
|
||||
|
||||
```diff
|
||||
+ // 1. Define the toolbar section container
|
||||
+ {
|
||||
+ id: 'MeasurementTools',
|
||||
+ uiType: 'ohif.toolButtonList',
|
||||
+ props: {
|
||||
+ buttonSection: 'measurementSection',
|
||||
+ groupId: 'MeasurementTools',
|
||||
+ },
|
||||
+ },
|
||||
+ // 2. Register individual buttons separately
|
||||
+ {
|
||||
+ id: 'Length',
|
||||
+ uiType: 'ohif.toolButton',
|
||||
+ props: {
|
||||
+ icon: 'tool-length',
|
||||
+ label: 'Length',
|
||||
+ tooltip: 'Length Tool',
|
||||
+ commands: setToolActiveToolbar,
|
||||
+ evaluate: 'evaluate.cornerstoneTool',
|
||||
+ },
|
||||
+ },
|
||||
+ {
|
||||
+ id: 'Bidirectional',
|
||||
+ uiType: 'ohif.toolButton',
|
||||
+ props: {
|
||||
+ icon: 'tool-bidirectional',
|
||||
+ label: 'Bidirectional',
|
||||
+ tooltip: 'Bidirectional Tool',
|
||||
+ commands: setToolActiveToolbar,
|
||||
+ evaluate: 'evaluate.cornerstoneTool',
|
||||
+ },
|
||||
+ },
|
||||
```
|
||||
|
||||
and then in your mode you can compose the section and associate buttons
|
||||
|
||||
```diff
|
||||
+ // 3. In your mode, create the section and associate buttons
|
||||
+ toolbarService.createButtonSection('primary', [
|
||||
+ 'MeasurementTools',
|
||||
+ 'Pan',
|
||||
+ 'Zoom',
|
||||
+ ]);
|
||||
+
|
||||
+
|
||||
+ toolbarService.createButtonSection('measurementSection', [
|
||||
+ 'Length',
|
||||
+ 'Bidirectional',
|
||||
+ 'ArrowAnnotate',
|
||||
+ 'EllipticalROI',
|
||||
+ ]);
|
||||
```
|
||||
|
||||
:::note
|
||||
The `measurementSection` is defined in the tool button configuration of the `MeasurementTools` button.
|
||||
:::
|
||||
|
||||
|
||||
### Group Evaluators Deprecated
|
||||
|
||||
Group evaluator functions like `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList` are now deprecated. Instead, the `uiType` component itself is responsible for grouping and displaying the buttons from a section as needed. This allows for more flexible UI implementations that aren't tied to specific evaluation logic.
|
||||
|
||||
|
||||
|
||||
## ToolBox
|
||||
|
||||
Previously, the segmentation toolbox was not using an `evaluator` property. This is now taken into account
|
||||
|
||||
|
||||
### evaluators in Toolbox
|
||||
|
||||
```js
|
||||
// old
|
||||
{
|
||||
@@ -84,7 +187,6 @@ Previously, the segmentation toolbox was not using an `evaluator` property. This
|
||||
uiType: 'ohif.buttonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
items: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +197,237 @@ Previously, the segmentation toolbox was not using an `evaluator` property. This
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Replace Toolbox imports from ui-next with extension-default
|
||||
|
||||
```diff
|
||||
- import { Toolbox } from '@ohif/ui-next';
|
||||
+ import { Toolbox } from '@ohif/extension-default';
|
||||
```
|
||||
|
||||
|
||||
Ensure you're importing the Toolbox component from the correct location:
|
||||
|
||||
```javascript
|
||||
// New import pattern
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
|
||||
// Usage remains similar
|
||||
<Toolbox
|
||||
servicesManager={servicesManager}
|
||||
buttonSectionId="segmentation"
|
||||
title="Segmentation Tools"
|
||||
/>
|
||||
```
|
||||
|
||||
### Stacked Sections in Toolbox
|
||||
|
||||
The new Toolbox component supports stacked sections, which allows for more complex UI organization. Instead of flat button groups, you can now create deep hierarchies of tool sections and subsections.
|
||||
|
||||
|
||||
Previously you were able to have something like this
|
||||
|
||||
```js
|
||||
// old
|
||||
// buttons for BrushTools were a giant group of buttons
|
||||
const buttons = {
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Threshold',
|
||||
icon: 'icon-tool-threshold',
|
||||
label: 'Threshold Tool',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Shapes',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
id: 'Shapes',
|
||||
icon: 'icon-tool-shape',
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
toolbarService.addButtons(buttons);
|
||||
|
||||
toolbarService.createButtonSection('segmentationToolbox', ['BrushTools', 'Shapes']);
|
||||
```
|
||||
|
||||
But now you should have at least one section defined in your toolbar buttons
|
||||
|
||||
|
||||
```js
|
||||
// separate flat definitions for each button and each section
|
||||
const buttons = [
|
||||
{
|
||||
id: 'Brush',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-tool-brush',
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-tool-eraser',
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Threshold',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-tool-threshold',
|
||||
label: 'Threshold Tool',
|
||||
evaluate: {
|
||||
// ...
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Shapes',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-shape',
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
disabledText: 'Create new segmentation to enable shapes tool.',
|
||||
},
|
||||
options: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
// Sections
|
||||
{
|
||||
id: 'SegmentationTools',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
groupId: 'SegmentationTools',
|
||||
buttonSection: 'segmentationToolboxToolsSection',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
buttonSection: 'brushToolsSection',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
toolbarService.addButtons(buttons);
|
||||
```
|
||||
|
||||
and then
|
||||
|
||||
```js
|
||||
// Step 2: Create the section hierarchy
|
||||
// Top level toolbox section
|
||||
toolbarService.createButtonSection('segmentationToolbox', ['SegmentationTools']);
|
||||
|
||||
// Next level - subsections within the toolbox
|
||||
toolbarService.createButtonSection('segmentationToolboxToolsSection', ['BrushTools', 'Shapes']);
|
||||
|
||||
// Lowest level - buttons within a subsection
|
||||
toolbarService.createButtonSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']);
|
||||
```
|
||||
|
||||
|
||||
### Remove ToolboxProvider from composition root
|
||||
|
||||
If you have the ToolboxProvider in your application composition, remove it:
|
||||
|
||||
```diff
|
||||
// In App.tsx or similar
|
||||
const appComposition = [
|
||||
[ThemeWrapperNext],
|
||||
[ThemeWrapper],
|
||||
[SystemContextProvider, { commandsManager, extensionManager, hotkeysManager, servicesManager }],
|
||||
- [ToolboxProvider],
|
||||
[ViewportGridProvider, { service: viewportGridService }],
|
||||
// Other providers...
|
||||
];
|
||||
```
|
||||
|
||||
we now keep the state for toolbar inside the ToolbarService itself
|
||||
|
||||
### 3. Update tool option handlers to use onChange instead of commands
|
||||
|
||||
```diff
|
||||
- <RowSegmentedControl
|
||||
- key={option.id}
|
||||
- option={option}
|
||||
- />
|
||||
|
||||
+ <RowSegmentedControl
|
||||
+ key={option.id}
|
||||
+ option={option}
|
||||
+ onChange={option.onChange}
|
||||
+ />
|
||||
```
|
||||
@@ -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