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,17 +1,24 @@
import React from 'react';
import classNames from 'classnames';
import { ToolButton } from '@ohif/ui-next';
import { useToolbar } from '@ohif/core/src/hooks/useToolbar';
/**
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
* @param props - Component props
* @returns Component
*/
export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) {
if (!items || !groupId) {
export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props }) {
const { onInteraction, toolbarButtons } = useToolbar({
buttonSection,
});
if (!groupId) {
return null;
}
const items = toolbarButtons.map(button => button.componentProps);
return (
<div className="bg-popover flex flex-row space-x-1 rounded-md px-0 py-0">
{items.map(item => (
@@ -20,16 +27,22 @@ export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...pr
key={item.id}
size="small"
className={props.disabled && 'text-primary'}
onInteraction={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
onInteraction={event => {
onInteraction?.({
event,
groupId,
commands: item.commands,
itemId: item.id,
item,
});
}}
/>
))}
</div>
);
}
export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
export function ToolBoxButtonWrapper({ onInteraction, options, ...props }) {
return (
<div className="bg-popover flex flex-row rounded-md px-0 py-0">
<ToolButton
@@ -37,7 +50,14 @@ export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
id={props.id}
size="small"
className={classNames(props.disabled && 'text-primary')}
onInteraction={() => onInteraction?.({ itemId: props.id, commands: props.commands })}
onInteraction={event => {
onInteraction?.({
event,
itemId: props.id,
commands: props.commands,
options,
});
}}
/>
</div>
);
@@ -7,23 +7,11 @@ import {
ToolButtonListItem,
ToolButtonListDivider,
} from '@ohif/ui-next';
interface ButtonItem {
id: string;
icon?: string;
label?: string;
tooltip?: string;
isActive?: boolean;
disabledText?: string;
commands?: Record<string, unknown>;
disabled?: boolean;
className?: string;
}
import { useToolbar } from '@ohif/core/src';
interface ToolButtonListWrapperProps {
groupId: string;
primary: ButtonItem;
items: ButtonItem[];
buttonSection: string;
onInteraction?: (details: {
groupId: string;
itemId: string;
@@ -39,10 +27,22 @@ interface ToolButtonListWrapperProps {
*/
export default function ToolButtonListWrapper({
groupId,
primary,
items,
onInteraction,
buttonSection,
}: ToolButtonListWrapperProps) {
const { onInteraction, toolbarButtons } = useToolbar({
buttonSection,
});
if (!toolbarButtons?.length) {
return null;
}
const primary =
toolbarButtons.find(button => button.componentProps.isActive)?.componentProps ||
toolbarButtons[0].componentProps;
const items = toolbarButtons.map(button => button.componentProps);
return (
<ToolButtonList>
<ToolButtonListDefault>
@@ -63,20 +63,22 @@ export default function ToolButtonListWrapper({
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
<div data-cy={`${groupId}-split-button-secondary`}>
<ToolButtonListDropDown>
{items.map(item => (
<ToolButtonListItem
key={item.id}
{...item}
data-cy={item.id}
data-tool={item.id}
data-active={item.isActive}
onSelect={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
>
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
</ToolButtonListItem>
))}
{items.map(item => {
return (
<ToolButtonListItem
key={item.id}
{...item}
data-cy={item.id}
data-tool={item.id}
data-active={item.isActive}
onSelect={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
>
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
</ToolButtonListItem>
);
})}
</ToolButtonListDropDown>
</div>
</ToolButtonList>
@@ -19,6 +19,7 @@ function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size })
groupId,
itemId: item.id,
commands: item.commands,
item,
});
}}
// Note: this is necessary since tooltip will add
@@ -23,7 +23,7 @@ function ToolbarSplitButtonWithServices({
onInteraction({
groupId,
itemId: item.id,
commands: item.commands,
item,
});
},
})),
@@ -16,6 +16,10 @@ import { panelGroupDefinition } from './constants/panels';
* @param width the max and min width to set on the element
*/
const setMinMaxWidth = (elem, width?) => {
if (!elem) {
return;
}
elem.style.minWidth = width === undefined ? '' : `${width}px`;
elem.style.maxWidth = elem.style.minWidth;
};
@@ -20,7 +20,9 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
dataSources?.[0]?.value ?? null
);
const handleSave = (reportName: string) => {
const [reportName, setReportName] = useState('');
const handleSave = () => {
onSave({
reportName,
dataSource: selectedDataSource,
@@ -62,7 +64,11 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
</div>
)}
<div className={showDataSourceSelect ? 'mt-1 w-2/3' : 'w-full'}>
<InputDialog>
<InputDialog
value={reportName}
onChange={setReportName}
submitOnEnter
>
<InputDialog.Field>
<InputDialog.Input placeholder="Report name" />
</InputDialog.Field>
@@ -60,27 +60,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
name: 'ohif.progressDropdown',
defaultComponent: ProgressDropdownWithService,
},
{
name: 'evaluate.group.promoteToPrimary',
evaluate: ({ viewportId, button, itemId }) => {
const { items } = button.props;
if (!itemId) {
return {
primary: button.props.primary,
items,
};
}
// other wise we can move the clicked tool to the primary button
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
return {
primary: clickedItemProps,
items,
};
},
},
{
name: 'evaluate.cine',
evaluate: () => {
+2
View File
@@ -34,6 +34,7 @@ import promptLabelAnnotation from './utils/promptLabelAnnotation';
import usePatientInfo from './hooks/usePatientInfo';
import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader';
import * as utils from './utils';
import { Toolbox } from './utils';
import MoreDropdownMenu from './Components/MoreDropdownMenu';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
const defaultExtension: Types.Extensions.Extension = {
@@ -97,6 +98,7 @@ export {
usePatientInfo,
PanelStudyBrowserHeader,
utils,
Toolbox,
MoreDropdownMenu,
requestDisplaySetCreationForStudy,
callInputDialog,
+122
View File
@@ -0,0 +1,122 @@
import React from 'react';
import { PanelSection, ToolSettings } from '@ohif/ui-next';
import { useSystem, useToolbar } from '@ohif/core';
import classnames from 'classnames';
interface ButtonProps {
isActive?: boolean;
options?: unknown;
}
/**
* 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
*/
export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; title: string }) {
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { toolbarButtons: toolboxSections, onInteraction } = useToolbar({
servicesManager,
buttonSection: buttonSectionId,
});
if (!toolboxSections.length) {
return null;
}
// Ensure we have proper button sections at the top level.
if (!toolboxSections.every(section => section.componentProps.buttonSection)) {
throw new Error(
'Toolbox accepts only button sections at the top level, not buttons. Create at least one button section.'
);
}
// Helper to check a list of buttons for an active tool.
const findActiveOptions = (buttons: any[]): unknown => {
for (const tool of buttons) {
if (tool.componentProps.isActive) {
return tool.componentProps.options;
}
if (tool.componentProps.buttonSection) {
const nestedButtons = toolbarService.getButtonPropsInButtonSection(
tool.componentProps.buttonSection
) as ButtonProps[];
const activeNested = nestedButtons.find(nested => nested.isActive);
if (activeNested) {
return activeNested.options;
}
}
}
return null;
};
// Look for active tool options across all sections.
const activeToolOptions = toolboxSections.reduce((activeOptions, section) => {
if (activeOptions) {
return activeOptions;
}
const sectionId = section.componentProps.buttonSection;
const buttons = toolbarService.getButtonSection(sectionId);
return findActiveOptions(buttons);
}, null);
// Define the interaction handler once.
const handleInteraction = ({ itemId }: { itemId: string }) => {
onInteraction?.({ itemId });
};
return (
<PanelSection>
<PanelSection.Header>
<span>{title}</span>
</PanelSection.Header>
<PanelSection.Content className="flex-shrink-0 border-none">
{toolboxSections.map(section => {
const sectionId = section.componentProps.buttonSection;
const buttons = toolbarService.getButtonSection(sectionId) as any[];
return (
<div
key={sectionId}
className="bg-muted flex flex-wrap space-x-2 py-2 px-1"
>
{buttons.map(tool => {
if (!tool) {
return null;
}
const { id, Component, componentProps } = tool;
return (
<div
key={id}
className={classnames('ml-1')}
>
<Component
{...componentProps}
id={id}
onInteraction={handleInteraction}
size="toolbox"
servicesManager={servicesManager}
/>
</div>
);
})}
</div>
);
})}
{activeToolOptions && (
<div className="bg-primary-dark h-auto px-2">
<ToolSettings options={activeToolOptions} />
</div>
)}
</PanelSection.Content>
</PanelSection>
);
}
@@ -57,6 +57,10 @@ export async function callInputDialog({
submitOnEnter = true,
}: {
uiDialogService: AppTypes.UIDialogService;
defaultValue?: string;
title?: string;
placeholder?: string;
submitOnEnter?: boolean;
}) {
const dialogId = 'dialog-enter-annotation';
+1
View File
@@ -1 +1,2 @@
export { addIcon } from './addIcon';
export * from './Toolbox';