feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)
No files matched your search
@@ -85,6 +85,14 @@ const config: StorybookConfig = {
|
||||
include: path.resolve(__dirname, '../'),
|
||||
});
|
||||
|
||||
// ignore the file @icr/polyseg-wasm during the build as it is a wasm file and
|
||||
// we don't need that for ui
|
||||
config.module.rules.push({
|
||||
test: /@icr\/polyseg-wasm/,
|
||||
type: 'javascript/auto',
|
||||
loader: 'file-loader',
|
||||
});
|
||||
|
||||
// Return the altered config
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-tool-brush</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-40, -145)">
|
||||
<g id="icon-tool-brush" transform="translate(40, 145)">
|
||||
<path d="M3.24640621,22.3286833 C3.09173375,22.2551472 2.99513748,22.0971513 3.00018895,21.9259625 C3.00524042,21.7547737 3.11098486,21.6027485 3.26972426,21.5384606 C5.3260304,20.7059201 4.66362518,18.3620247 5.27421252,17.1088957 C6.02197747,15.6026514 7.84114758,14.9766383 9.35746132,15.7037675 C13.9485253,17.9422999 8.48346644,24.8211232 3.24640621,22.3286833 Z" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M21.5968442,3.302022 C21.1256527,2.8826075 20.4094461,2.90228142 19.9619901,3.34693083 L9.69255027,13.5887345 C11.0536437,14.0051578 12.1843437,14.9616637 12.8206229,16.2349008 L21.7410706,4.93687606 C22.1347378,4.44008272 22.0714081,3.72222021 21.5968442,3.302022 Z" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>tool-seg-brush</title>
|
||||
<g id="tool-seg-brush" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
<path d="M3.24640621,21.8286833 C3.09173375,21.7551472 2.99513748,21.5971513 3.00018895,21.4259625 C3.00524042,21.2547737 3.11098486,21.1027485 3.26972426,21.0384606 C5.3260304,20.2059201 4.66362518,17.8620247 5.27421252,16.6088957 C6.02197747,15.1026514 7.84114758,14.4766383 9.35746132,15.2037675 C13.9485253,17.4422999 8.48346644,24.3211232 3.24640621,21.8286833 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M21.5968442,2.302022 C21.1256527,1.8826075 20.4094461,1.90228142 19.9619901,2.34693083 L9.69255027,12.5887345 C11.0536437,13.0051578 12.1843437,13.9616637 12.8206229,15.2349008 L21.7410706,3.93687606 C22.1347378,3.44008272 22.0714081,2.72222021 21.5968442,2.302022 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,14 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-tool-eraser</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-83, -145)">
|
||||
<g id="icon-tool-eraser" transform="translate(83, 145)">
|
||||
<path d="M8.39130435,20.50359 C7.23520605,20.4411204 6.135127,19.9856264 5.27326087,19.2125465 L3.29104348,17.2285031 C2.58057562,16.5140665 2.58057562,15.3599396 3.29104348,14.6455031 L14.4036957,3.53285089 C15.1181322,2.82238304 16.2722591,2.82238304 16.9866957,3.53285089 L21.7080435,8.25419872 C22.4185113,8.96863525 22.4185113,10.1227622 21.7080435,10.8371987 L13.3345217,19.2107205 C12.4731497,19.9842722 11.3733983,20.4404081 10.2173913,20.50359 L8.39130435,20.50359 Z" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<line x1="10.2173913" y1="7.71915524" x2="17.5217391" y2="15.0235031" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
<line x1="2" y1="22.3278509" x2="23" y2="22.3278509" id="Path" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>tool-seg-eraser</title>
|
||||
<g id="tool-seg-eraser" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
<path d="M8.47826087,18.0030772 C7.48731947,17.9495317 6.54439458,17.5591084 5.80565217,16.8964685 L4.1066087,15.1958598 C3.49763625,14.5834856 3.49763625,13.5942339 4.1066087,12.9818598 L13.6317391,3.45672934 C14.2441133,2.84775689 15.233365,2.84775689 15.8457391,3.45672934 L19.8926087,7.5035989 C20.5015811,8.11597307 20.5015811,9.10522473 19.8926087,9.7175989 L12.7153043,16.8949033 C11.9769855,17.5579476 11.0343414,17.9489213 10.0434783,18.0030772 L8.47826087,18.0030772 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<line x1="10.0434783" y1="7.04499021" x2="16.3043478" y2="13.3058598" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
<line x1="3" y1="20.5667293" x2="21" y2="20.5667293" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-tool-shape</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-170, -193)">
|
||||
<g id="icon-tool-shape" transform="translate(170, 193)">
|
||||
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
|
||||
<rect id="Rectangle" stroke="currentColor" stroke-width="1.5" x="3" y="3" width="13" height="13" rx="1"></rect>
|
||||
<circle id="Oval" stroke="currentColor" stroke-width="1.5" cx="16.5" cy="16.5" r="6.5"></circle>
|
||||
</g>
|
||||
</g>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>tool-seg-shape</title>
|
||||
<g id="tool-seg-shape" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
<circle id="Oval" stroke="currentColor" stroke-width="1.5" cx="16" cy="15" r="6"></circle>
|
||||
<rect id="Rectangle" stroke="currentColor" stroke-width="1.5" x="3" y="3" width="13" height="13" rx="2"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 615 B |
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-tool-threshold</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-127, -145)">
|
||||
<g id="icon-tool-threshold" transform="translate(127, 145)">
|
||||
<line x1="19.4565217" y1="16.8695652" x2="22.5" y2="16.8695652" id="Line-2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"></line>
|
||||
<line x1="2.5" y1="16.8695652" x2="14.673913" y2="16.8695652" id="Line-2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"></line>
|
||||
<circle id="Oval" stroke="currentColor" stroke-width="1.25" cx="16.8478261" cy="16.8695652" r="2.17391304"></circle>
|
||||
<line x1="11.6304348" y1="8.17391304" x2="22.0652174" y2="8.17391304" id="Line-2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"></line>
|
||||
<line x1="2.5" y1="8.17391304" x2="7.2826087" y2="8.17391304" id="Line-2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"></line>
|
||||
<circle id="Oval" stroke="currentColor" stroke-width="1.25" cx="9.45652174" cy="8.17391304" r="2.17391304"></circle>
|
||||
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
|
||||
</g>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>tool-seg-threshold</title>
|
||||
<g id="tool-seg-threshold" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
<g id="Group" transform="translate(2.4184, 14.3673)" stroke="currentColor" stroke-width="1.5">
|
||||
<line x1="16.5816327" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line>
|
||||
<line x1="0.0816326531" y1="2.13265306" x2="12.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line>
|
||||
<circle id="Oval" cx="14.2244898" cy="2.09183673" r="2.09183673"></circle>
|
||||
</g>
|
||||
<g id="Group" transform="translate(11.7092, 7.4592) scale(-1, 1) translate(-11.7092, -7.4592)translate(1.9184, 5.3673)" stroke="currentColor" stroke-width="1.5">
|
||||
<line x1="16" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line>
|
||||
<line x1="0.0816326531" y1="2.13265306" x2="11" y2="2.13265306" id="Line-2" stroke-linecap="round"></line>
|
||||
<circle id="Oval" cx="13.2244898" cy="2.09183673" r="2.09183673"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -3,6 +3,10 @@ import classnames from 'classnames';
|
||||
import { PanelSection, Icon, Tooltip } from '../../components';
|
||||
import ToolSettings from './ToolSettings';
|
||||
|
||||
/**
|
||||
* Use Toolbox component instead of this although it doesn't have "Advanced" in its name
|
||||
* it is better to use it instead of this one
|
||||
*/
|
||||
const AdvancedToolbox = ({ title, items }) => {
|
||||
const [activeItemName, setActiveItemName] = useState(null);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { ButtonGroup, InputRange, ButtonEnums } from '../../components';
|
||||
import { ButtonGroup, InputDoubleRange, InputRange } from '../../components';
|
||||
|
||||
const SETTING_TYPES = {
|
||||
RANGE: 'range',
|
||||
RADIO: 'radio',
|
||||
CUSTOM: 'custom',
|
||||
DOUBLE_RANGE: 'double-range',
|
||||
};
|
||||
|
||||
function ToolSettings({ options }) {
|
||||
@@ -12,73 +13,111 @@ function ToolSettings({ options }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getButtons = option => {
|
||||
const buttons = [];
|
||||
|
||||
option.values?.map(({ label, value: optionValue }, index) => {
|
||||
buttons.push({
|
||||
children: label,
|
||||
onClick: () => option.onChange(optionValue),
|
||||
key: `button-${option.id}-${index}`, // A unique key
|
||||
});
|
||||
});
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 py-2 text-white">
|
||||
{options?.map(option => {
|
||||
if (option.type === SETTING_TYPES.RANGE) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={option.id}
|
||||
>
|
||||
<div className="w-1/3 text-[13px]">{option.name}</div>
|
||||
<div className="w-2/3">
|
||||
<InputRange
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
value={option.value}
|
||||
onChange={e => option.onChange(e)}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
inputClassName="ml-1 w-4/5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (option.condition && option.condition?.({ options }) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (option.type === SETTING_TYPES.RADIO) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between text-[13px]"
|
||||
key={option.id}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
<div className="max-w-1/2">
|
||||
<ButtonGroup
|
||||
buttons={getButtons(option)}
|
||||
defaultActiveIndex={option.defaultActiveIndex}
|
||||
size={ButtonEnums.size.small}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (option.type === SETTING_TYPES.CUSTOM) {
|
||||
return (
|
||||
<div key={option.id}>
|
||||
{typeof option.children === 'function' ? option.children() : option.children}
|
||||
</div>
|
||||
);
|
||||
switch (option.type) {
|
||||
case SETTING_TYPES.RANGE:
|
||||
return renderRangeSetting(option);
|
||||
case SETTING_TYPES.RADIO:
|
||||
return renderRadioSetting(option);
|
||||
case SETTING_TYPES.DOUBLE_RANGE:
|
||||
return renderDoubleRangeSetting(option);
|
||||
case SETTING_TYPES.CUSTOM:
|
||||
return renderCustomSetting(option);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderRangeSetting = option => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={option.id}
|
||||
>
|
||||
<div className="w-1/3 text-[13px]">{option.name}</div>
|
||||
<div className="w-2/3">
|
||||
<InputRange
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
value={option.value}
|
||||
onChange={value => option.commands?.(value)}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
inputClassName="ml-1 w-4/5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRadioSetting = option => {
|
||||
const renderButtons = option => {
|
||||
return option.values?.map(({ label, value: optionValue }, index) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
option.commands?.(optionValue);
|
||||
}}
|
||||
key={`button-${option.id}-${index}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between text-[13px]"
|
||||
key={option.id}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
<div className="max-w-1/2">
|
||||
<ButtonGroup
|
||||
activeIndex={option.values.findIndex(({ value }) => value === option.value) || 0}
|
||||
>
|
||||
{renderButtons(option)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDoubleRangeSetting = option => {
|
||||
return (
|
||||
<div
|
||||
className="flex w-full items-center"
|
||||
key={option.id}
|
||||
>
|
||||
<InputDoubleRange
|
||||
values={option.values}
|
||||
onChange={option.commands}
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomSetting = option => {
|
||||
return (
|
||||
<div key={option.id}>
|
||||
{typeof option.children === 'function' ? option.children() : option.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSettings;
|
||||
@@ -1,2 +1,4 @@
|
||||
import AdvancedToolbox from './AdvancedToolbox';
|
||||
import ToolSettings from './ToolSettings';
|
||||
export default AdvancedToolbox;
|
||||
export { ToolSettings };
|
||||
@@ -67,9 +67,12 @@ const Button = ({
|
||||
name,
|
||||
className,
|
||||
onClick,
|
||||
dataCY,
|
||||
startIconTooltip = null,
|
||||
endIconTooltip = null,
|
||||
}) => {
|
||||
dataCY = dataCY || `${name}-btn`;
|
||||
|
||||
const startIcon = startIconProp && (
|
||||
<>
|
||||
{React.cloneElement(startIconProp, {
|
||||
@@ -109,7 +112,7 @@ const Button = ({
|
||||
disabled={disabled}
|
||||
ref={buttonElement}
|
||||
onClick={handleOnClick}
|
||||
data-cy={`${name}-btn`}
|
||||
data-cy={dataCY}
|
||||
>
|
||||
{startIconTooltip ? <Tooltip content={startIconTooltip}>{startIcon}</Tooltip> : startIcon}
|
||||
{children}
|
||||
@@ -148,6 +151,8 @@ Button.propTypes = {
|
||||
startIconTooltip: PropTypes.node,
|
||||
/** Tooltip for the end icon */
|
||||
endIconTooltip: PropTypes.node,
|
||||
/** Data attribute for testing */
|
||||
dataCY: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -1,21 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, cloneElement, Children } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ButtonEnums } from '../../components';
|
||||
|
||||
const ButtonGroup = ({
|
||||
buttons,
|
||||
onActiveIndexChange,
|
||||
children,
|
||||
className,
|
||||
orientation = ButtonEnums.orientation.horizontal,
|
||||
defaultActiveIndex = 0,
|
||||
activeIndex: defaultActiveIndex = 0,
|
||||
onActiveIndexChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
|
||||
|
||||
const handleButtonClick = (e, index) => {
|
||||
useEffect(() => {
|
||||
setActiveIndex(defaultActiveIndex);
|
||||
}, [defaultActiveIndex]);
|
||||
|
||||
const handleButtonClick = index => {
|
||||
setActiveIndex(index);
|
||||
onActiveIndexChange && onActiveIndexChange(index);
|
||||
buttons[index].onClick && buttons[index].onClick(e);
|
||||
};
|
||||
|
||||
const orientationClasses = {
|
||||
@@ -29,35 +33,40 @@ const ButtonGroup = ({
|
||||
<div
|
||||
className={classnames(
|
||||
wrapperClasses,
|
||||
'border-secondary-light rounded-[5px] border bg-black text-[13px] '
|
||||
'border-secondary-light rounded-[5px] border bg-black text-[13px]'
|
||||
)}
|
||||
>
|
||||
{buttons.map((buttonProps, index) => {
|
||||
const isActive = index === activeIndex;
|
||||
return (
|
||||
<button
|
||||
{...buttonProps}
|
||||
key={index}
|
||||
className={classnames(
|
||||
{Children.map(children, (child, index) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return cloneElement(child, {
|
||||
key: index,
|
||||
className: classnames(
|
||||
'rounded-[4px] px-2 py-1',
|
||||
isActive ? 'bg-customblue-40 text-white' : 'text-primary-active bg-black'
|
||||
)}
|
||||
onClick={e => handleButtonClick(e, index)}
|
||||
/>
|
||||
);
|
||||
index === activeIndex
|
||||
? 'bg-customblue-40 text-white'
|
||||
: 'text-primary-active bg-black',
|
||||
child.props.className,
|
||||
disabled ? 'ohif-disabled' : ''
|
||||
),
|
||||
onClick: e => {
|
||||
child.props.onClick && child.props.onClick(e);
|
||||
handleButtonClick(index);
|
||||
},
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonGroup.propTypes = {
|
||||
buttons: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
orientation: PropTypes.oneOf(Object.values(ButtonEnums.orientation)),
|
||||
type: PropTypes.oneOf(Object.values(ButtonEnums.type)),
|
||||
size: PropTypes.oneOf(Object.values(ButtonEnums.size)),
|
||||
defaultActiveIndex: PropTypes.number,
|
||||
activeIndex: PropTypes.number,
|
||||
onActiveIndexChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
||||
@@ -16,10 +16,6 @@ export const argTypes = {
|
||||
component={ButtonGroup}
|
||||
/>
|
||||
|
||||
export const buttonGroupTemplate = args => {
|
||||
return <ButtonGroup {...args} />;
|
||||
};
|
||||
|
||||
<Heading
|
||||
title="ButtonGroup"
|
||||
componentRelativePath="ButtonGroup/ButtonGroup.tsx"
|
||||
@@ -35,31 +31,16 @@ export const buttonGroupTemplate = args => {
|
||||
ButtonGroup is a container for a group of buttons.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
type: ButtonEnums.type.primary,
|
||||
size: ButtonEnums.size.medium,
|
||||
orientation: ButtonEnums.orientation.horizontal,
|
||||
buttons: [
|
||||
{
|
||||
children: 'Button 1',
|
||||
onClick: () => console.log('Button 1 clicked'),
|
||||
},
|
||||
{
|
||||
children: 'Button 2',
|
||||
onClick: () => console.log('Button 2 clicked'),
|
||||
},
|
||||
{
|
||||
children: 'Button 3',
|
||||
onClick: () => console.log('Button 3 clicked'),
|
||||
},
|
||||
],
|
||||
activeIndex: 0,
|
||||
onActiveIndexChange: index => console.log(index),
|
||||
}}
|
||||
>
|
||||
{buttonGroupTemplate.bind({})}
|
||||
<Story name="Overview">
|
||||
<ButtonGroup
|
||||
orientation={ButtonEnums.orientation.horizontal}
|
||||
activeIndex={0}
|
||||
onActiveIndexChange={index => console.log(index)}
|
||||
>
|
||||
<button onClick={() => console.log('Button 1111 clicked')}>Button 1</button>
|
||||
<button onClick={() => console.log('Button 222 clicked')}>Button 2</button>
|
||||
<button onClick={() => console.log('Button 33 clicked')}>Button 3</button>
|
||||
</ButtonGroup>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
|
||||
@@ -30,13 +30,11 @@ function Header({
|
||||
|
||||
return (
|
||||
<NavBar
|
||||
className="justify-between border-b-4 border-black"
|
||||
isSticky={isSticky}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex items-center">
|
||||
{/* // TODO: Should preserve filter/sort
|
||||
// Either injected service? Or context (like react router's `useLocation`?) */}
|
||||
<div className="relative h-[48px] items-center ">
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 ">
|
||||
<div
|
||||
className={classNames(
|
||||
'mr-3 inline-flex items-center',
|
||||
@@ -56,8 +54,11 @@ function Header({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center">
|
||||
{/* <div className="absolute top-1/2 left-[211px] h-8 w-20 -translate-y-1/2">{future left component}</div> */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<div className="flex items-center justify-center space-x-2">{children}</div>
|
||||
</div>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 ">
|
||||
<Dropdown
|
||||
id="options"
|
||||
showDropdownIcon={false}
|
||||
|
||||
@@ -94,7 +94,7 @@ import toolMove from './../../assets/icons/tool-move.svg';
|
||||
import toolWindow from './../../assets/icons/tool-window-level.svg';
|
||||
import toolAnnotate from './../../assets/icons/tool-annotate.svg';
|
||||
import toolBidirectional from './../../assets/icons/tool-bidirectional.svg';
|
||||
import toolElipse from './../../assets/icons/tool-elipse.svg';
|
||||
import toolElipse from './../../assets/icons/tool-ellipse.svg';
|
||||
import toolCircle from './../../assets/icons/tool-circle.svg';
|
||||
import toolLength from './../../assets/icons/tool-length.svg';
|
||||
import toolStackScroll from './../../assets/icons/tool-stack-scroll.svg';
|
||||
@@ -235,7 +235,7 @@ const ICONS = {
|
||||
'tool-window-level': toolWindow,
|
||||
'tool-annotate': toolAnnotate,
|
||||
'tool-bidirectional': toolBidirectional,
|
||||
'tool-elipse': toolElipse,
|
||||
'tool-ellipse': toolElipse,
|
||||
'tool-circle': toolCircle,
|
||||
'tool-length': toolLength,
|
||||
'tool-stack-scroll': toolStackScroll,
|
||||
|
||||
@@ -14,7 +14,7 @@ const roundedClasses = {
|
||||
};
|
||||
|
||||
const disabledClasses = {
|
||||
true: 'cursor-not-allowed',
|
||||
true: 'ohif-disabled',
|
||||
false: '',
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ const variantClasses = {
|
||||
'border border-primary-main text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black',
|
||||
},
|
||||
contained: {
|
||||
default: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
default: 'text-common-bright hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
primary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
secondary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
white: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
@@ -51,30 +51,6 @@ const variantClasses = {
|
||||
},
|
||||
};
|
||||
|
||||
const backgroundClasses = {
|
||||
text: {
|
||||
default: '',
|
||||
primary: '',
|
||||
secondary: '',
|
||||
white: '',
|
||||
black: '',
|
||||
},
|
||||
outlined: {
|
||||
default: 'bg-transparent',
|
||||
primary: 'bg-transparent',
|
||||
secondary: 'bg-transparent',
|
||||
white: 'bg-transparent',
|
||||
black: 'bg-black',
|
||||
},
|
||||
contained: {
|
||||
default: 'bg-primary-light',
|
||||
primary: 'bg-primary-main',
|
||||
secondary: 'bg-secondary-light',
|
||||
white: 'bg-white',
|
||||
black: 'bg-black',
|
||||
},
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'py-2 px-2 text-base',
|
||||
medium: 'py-3 px-3 text-lg',
|
||||
@@ -87,7 +63,7 @@ const iconSizeClasses = {
|
||||
small: 'w-4 h-4',
|
||||
medium: 'w-5 h-5',
|
||||
large: 'w-6 h-6',
|
||||
toolbar: 'w-5 h-5',
|
||||
toolbar: 'w-[24px] h-[24px]',
|
||||
};
|
||||
|
||||
const fullWidthClasses = {
|
||||
@@ -106,9 +82,7 @@ const IconButton = ({
|
||||
fullWidth,
|
||||
onClick,
|
||||
className,
|
||||
name,
|
||||
id,
|
||||
bgColor,
|
||||
...rest
|
||||
}) => {
|
||||
const buttonElement = useRef(null);
|
||||
@@ -118,7 +92,7 @@ const IconButton = ({
|
||||
onClick(e);
|
||||
};
|
||||
|
||||
const bgColorToUse = bgColor ? bgColor : backgroundClasses[variant][color];
|
||||
const padding = size === 'toolbar' ? '8px' : size === 'toolbox' ? '4px' : null;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -129,11 +103,10 @@ const IconButton = ({
|
||||
sizeClasses[size],
|
||||
fullWidthClasses[fullWidth],
|
||||
disabledClasses[disabled],
|
||||
bgColorToUse,
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
padding: size === 'toolbar' ? '10px' : null,
|
||||
padding,
|
||||
}}
|
||||
ref={buttonElement}
|
||||
onClick={handleOnClick}
|
||||
@@ -161,7 +134,7 @@ IconButton.defaultProps = {
|
||||
|
||||
IconButton.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
size: PropTypes.oneOf(['small', 'medium', 'large', 'initial', 'toolbar']),
|
||||
size: PropTypes.oneOf(['small', 'medium', 'large', 'initial', 'toolbar', 'toolbox']),
|
||||
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
|
||||
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
|
||||
color: PropTypes.oneOf(['default', 'primary', 'secondary', 'white', 'black', 'inherit']),
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Icon from '../Icon';
|
||||
import Tooltip from '../Tooltip';
|
||||
import ListMenu from '../ListMenu';
|
||||
|
||||
const baseClasses = {
|
||||
Button: 'flex items-center rounded-md border-transparent cursor-pointer group/button',
|
||||
Primary:
|
||||
// By default border on left, top and bottom for hover effect and only rounded on left side.
|
||||
// Extra padding on right to compensate for no right border.
|
||||
'h-full border-l-2 border-t-2 border-b-2 rounded-tl-md rounded-bl-md group/primary !pl-2 !py-2',
|
||||
Secondary:
|
||||
'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 border-2 border-transparent group/secondary',
|
||||
SecondaryIcon: 'w-4 h-full stroke-1',
|
||||
Separator: 'border-l py-2.5',
|
||||
Content: 'absolute z-10 top-0 mt-12',
|
||||
};
|
||||
|
||||
const classes = {
|
||||
Button: ({ isExpanded, primary }) =>
|
||||
classNames(
|
||||
baseClasses.Button,
|
||||
!isExpanded && !primary.isActive && 'hover:!bg-primary-dark hover:border-primary-dark'
|
||||
),
|
||||
Interface: 'h-full flex flex-row items-center',
|
||||
Primary: ({ isActive, isExpanded }) =>
|
||||
classNames(
|
||||
baseClasses.Primary,
|
||||
isActive
|
||||
? isExpanded
|
||||
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light'
|
||||
: 'border-primary-light bg-primary-light border-2 rounded-md !p-2'
|
||||
: `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light
|
||||
${
|
||||
isExpanded
|
||||
? 'border-primary-dark bg-primary-dark !text-primary-light'
|
||||
: 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'
|
||||
}
|
||||
`
|
||||
),
|
||||
Secondary: ({ isExpanded, primary }) =>
|
||||
classNames(
|
||||
baseClasses.Secondary,
|
||||
isExpanded
|
||||
? 'bg-primary-light !rounded-tr-md !rounded-br-md'
|
||||
: primary.isActive
|
||||
? 'bg-secondary-dark'
|
||||
: 'hover:bg-primary-dark bg-secondary-dark group-hover/button:border-primary-dark'
|
||||
),
|
||||
SecondaryIcon: ({ isExpanded }) =>
|
||||
classNames(
|
||||
baseClasses.SecondaryIcon,
|
||||
isExpanded
|
||||
? 'text-primary-dark'
|
||||
: 'text-primary-active group-hover/secondary:text-primary-light'
|
||||
),
|
||||
Separator: ({ primary, isExpanded, isHovering }) =>
|
||||
classNames(
|
||||
baseClasses.Separator,
|
||||
isHovering || isExpanded || primary.isActive ? 'border-transparent' : 'border-primary-active'
|
||||
),
|
||||
Content: ({ isExpanded }) => classNames(baseClasses.Content, isExpanded ? 'block' : 'hidden'),
|
||||
};
|
||||
|
||||
const SplitButton = ({
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
items,
|
||||
renderer,
|
||||
onInteraction,
|
||||
Component,
|
||||
isActive,
|
||||
}) => {
|
||||
const { t } = useTranslation('Buttons');
|
||||
const [state, setState] = useState({ isHovering: false, isExpanded: false });
|
||||
|
||||
const toggleExpanded = () => setState({ ...state, isExpanded: !state.isExpanded });
|
||||
const setHover = hovering => setState({ ...state, isHovering: hovering });
|
||||
const collapse = () => setState({ ...state, isExpanded: false });
|
||||
|
||||
const renderPrimaryButton = () => (
|
||||
<Component
|
||||
key={primary.id}
|
||||
{...primary}
|
||||
isActive={isActive}
|
||||
onInteraction={onInteraction}
|
||||
rounded="none"
|
||||
className={classes.Primary({ isActive, isExpanded: state.isExpanded })}
|
||||
data-tool={primary.id}
|
||||
data-cy={`${groupId}-split-button-primary`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<OutsideClickHandler
|
||||
onOutsideClick={collapse}
|
||||
disabled={!state.isExpanded}
|
||||
>
|
||||
<div
|
||||
id="SplitButton"
|
||||
className="relative"
|
||||
>
|
||||
<div
|
||||
className={classes.Button({ ...state, primary: { isActive } })}
|
||||
style={{ height: '40px' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<div className={classes.Interface}>
|
||||
<div onClick={collapse}>{renderPrimaryButton()}</div>
|
||||
<div className={classes.Separator({ ...state, primary: { isActive } })}></div>
|
||||
<div
|
||||
className={classes.Secondary({ ...state, primary: { isActive } })}
|
||||
onClick={toggleExpanded}
|
||||
data-cy={`${groupId}-split-button-secondary`}
|
||||
>
|
||||
<Tooltip
|
||||
isDisabled={state.isExpanded || !secondary.tooltip}
|
||||
content={secondary.tooltip}
|
||||
className="h-full"
|
||||
>
|
||||
<Icon
|
||||
name={secondary.icon}
|
||||
className={classes.SecondaryIcon({ ...state })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classes.Content({ ...state })}
|
||||
data-cy={`${groupId}-list-menu`}
|
||||
>
|
||||
<ListMenu
|
||||
items={items}
|
||||
onClick={collapse}
|
||||
renderer={args => renderer({ ...args, t })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OutsideClickHandler>
|
||||
);
|
||||
};
|
||||
|
||||
SplitButton.propTypes = {
|
||||
isToggle: PropTypes.bool,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
primary: PropTypes.object.isRequired,
|
||||
secondary: PropTypes.object.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
renderer: PropTypes.func,
|
||||
isActive: PropTypes.bool,
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
Component: PropTypes.elementType,
|
||||
};
|
||||
|
||||
SplitButton.defaultProps = {
|
||||
isToggle: false,
|
||||
renderer: null,
|
||||
isActive: false,
|
||||
Component: null,
|
||||
};
|
||||
|
||||
export default SplitButton;
|
||||
@@ -0,0 +1,2 @@
|
||||
import LegacySplitButton from './LegacySplitButton';
|
||||
export default LegacySplitButton;
|
||||
@@ -2,24 +2,25 @@ import React, { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const flex = 'flex flex-row justify-between items-center';
|
||||
const theme = 'bg-indigo-dark text-white';
|
||||
|
||||
const ListMenu = ({ items = [], renderer, onClick }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
|
||||
const ListItem = ({ item, index, isSelected }) => {
|
||||
const flex = 'flex flex-row justify-between items-center';
|
||||
const theme = 'bg-indigo-dark';
|
||||
|
||||
const onClickHandler = () => {
|
||||
setSelectedIndex(index);
|
||||
onClick({ item, selectedIndex: index });
|
||||
if (item.onClick) {
|
||||
item.onClick({ ...item, index, isSelected });
|
||||
}
|
||||
item.onClick?.({ ...item, index, isSelected });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(flex, theme, 'cursor-pointer')}
|
||||
className={classnames(flex, theme, {
|
||||
'cursor-pointer': !item.disabled,
|
||||
'ohif-disabled': item.disabled,
|
||||
})}
|
||||
onClick={onClickHandler}
|
||||
data-cy={item.id}
|
||||
>
|
||||
|
||||
@@ -9,12 +9,11 @@ const NavBar = ({ className, children, isSticky }) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'bg-secondary-dark z-20 flex flex-row items-center border-b-4 border-black px-1',
|
||||
'bg-secondary-dark z-20 border-black px-1',
|
||||
isSticky && stickyClasses,
|
||||
!isSticky && notStickyClasses,
|
||||
className
|
||||
)}
|
||||
style={{ paddingTop: '4px', paddingBottom: '4px', minHeight: '52px' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ const SegmentItem = ({
|
||||
>
|
||||
<div
|
||||
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
|
||||
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
|
||||
'!bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
|
||||
'border-primary-dark border': !isActive,
|
||||
})}
|
||||
onMouseEnter={() => setIsNumberBoxHovering(true)}
|
||||
|
||||
@@ -98,10 +98,11 @@ const getTabClassNames = (
|
||||
numColumns: number,
|
||||
numTabs: number,
|
||||
tabIndex: number,
|
||||
isActiveTab: boolean
|
||||
isActiveTab: boolean,
|
||||
isTabDisabled: boolean
|
||||
) =>
|
||||
classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', {
|
||||
'hover:text-primary-active': !isActiveTab,
|
||||
'hover:text-primary-active': !isActiveTab && !isTabDisabled,
|
||||
'rounded-l': tabIndex % numColumns === 0,
|
||||
'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1,
|
||||
});
|
||||
@@ -137,6 +138,15 @@ const createStyleMap = (
|
||||
};
|
||||
};
|
||||
|
||||
const getToolTipContent = (label: string, disabled: boolean) => {
|
||||
return (
|
||||
<>
|
||||
<div>{label}</div>
|
||||
{disabled && <div className="text-white">{'Not available based on current context'}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const createBaseStyle = (expandedWidth: number) => {
|
||||
return {
|
||||
maxWidth: `${expandedWidth}px`,
|
||||
@@ -218,7 +228,7 @@ const SidePanel = ({
|
||||
<Tooltip
|
||||
position={side === 'left' ? 'right' : 'left'}
|
||||
key={index}
|
||||
content={`${childComponent.label}`}
|
||||
content={getToolTipContent(childComponent.label, childComponent.disabled)}
|
||||
className={classnames(
|
||||
'flex items-center',
|
||||
side === 'left' ? 'justify-end ' : 'justify-start '
|
||||
@@ -229,12 +239,15 @@ const SidePanel = ({
|
||||
data-cy={`${childComponent.name}-btn`}
|
||||
className="text-primary-active hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
updateActiveTabIndex(index);
|
||||
return childComponent.disabled ? null : updateActiveTabIndex(index);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={childComponent.iconName}
|
||||
className="text-primary-active"
|
||||
className={classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': childComponent.disabled,
|
||||
})}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
@@ -279,6 +292,7 @@ const SidePanel = ({
|
||||
style={getGridStyle(side, tabs.length, gridWidth, expandedWidth)}
|
||||
>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
const { disabled } = tab;
|
||||
return (
|
||||
<React.Fragment key={tabIndex}>
|
||||
{tabIndex % numCols !== 0 && (
|
||||
@@ -294,22 +308,26 @@ const SidePanel = ({
|
||||
<Tooltip
|
||||
position={'bottom'}
|
||||
key={tabIndex}
|
||||
content={`${tab.label}`}
|
||||
content={getToolTipContent(tab.label, disabled)}
|
||||
>
|
||||
<div
|
||||
className={getTabClassNames(
|
||||
numCols,
|
||||
tabs.length,
|
||||
tabIndex,
|
||||
tabIndex === activeTabIndex
|
||||
tabIndex === activeTabIndex,
|
||||
disabled
|
||||
)}
|
||||
style={getTabStyle(tabs.length)}
|
||||
onClick={() => updateActiveTabIndex(tabIndex)}
|
||||
onClick={() => {
|
||||
return disabled ? null : updateActiveTabIndex(tabIndex);
|
||||
}}
|
||||
data-cy={`${tab.name}-btn`}
|
||||
>
|
||||
<div className={getTabIconClassNames(tabs.length, tabIndex === activeTabIndex)}>
|
||||
<Icon
|
||||
name={tab.iconName}
|
||||
className={`${tab.disabled && 'ohif-disabled'}`}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
|
||||
@@ -11,42 +11,29 @@ import ListMenu from '../ListMenu';
|
||||
const baseClasses = {
|
||||
Button: 'flex items-center rounded-md border-transparent cursor-pointer group/button',
|
||||
Primary:
|
||||
// By default border on left, top and bottom for hover effect and only rounded on left side.
|
||||
// Extra padding on right to compensate for no right border.
|
||||
'h-full border-l-2 border-t-2 border-b-2 rounded-tl-md rounded-bl-md group/primary !pl-2 !py-2',
|
||||
Secondary:
|
||||
'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 border-2 border-transparent group/secondary',
|
||||
SecondaryIcon: 'w-4 h-full stroke-1',
|
||||
Separator: 'border-l py-2.5',
|
||||
Separator: 'border-l py-3 ml-0.5',
|
||||
Content: 'absolute z-10 top-0 mt-12',
|
||||
};
|
||||
|
||||
const classes = {
|
||||
Button: ({ isExpanded, primary }) =>
|
||||
Button: ({ isExpanded }) =>
|
||||
classNames(
|
||||
baseClasses.Button,
|
||||
!isExpanded && !primary.isActive && 'hover:!bg-primary-dark hover:border-primary-dark'
|
||||
!isExpanded && 'hover:!bg-primary-dark hover:border-primary-dark'
|
||||
),
|
||||
Interface: 'h-full flex flex-row items-center',
|
||||
Primary: ({ primary, isExpanded }) =>
|
||||
Primary: ({ isExpanded, isActive }) =>
|
||||
classNames(
|
||||
baseClasses.Primary,
|
||||
primary.isActive
|
||||
isActive
|
||||
? isExpanded
|
||||
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light'
|
||||
: `${
|
||||
primary.isToggle
|
||||
? 'border-secondary-dark bg-secondary-light'
|
||||
: 'border-primary-light bg-primary-light'
|
||||
}
|
||||
border-2 rounded-md !p-2` // Full, rounded border with less right padding when active.
|
||||
: `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light
|
||||
${
|
||||
isExpanded
|
||||
? 'border-primary-dark bg-primary-dark !text-primary-light'
|
||||
: 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'
|
||||
}
|
||||
`
|
||||
: 'border-primary-light bg-primary-light border-2 rounded-md !p-2'
|
||||
: `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light ${isExpanded ? 'border-primary-dark bg-primary-dark !text-primary-light' : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'}`
|
||||
),
|
||||
Secondary: ({ isExpanded, primary }) =>
|
||||
classNames(
|
||||
@@ -54,13 +41,15 @@ const classes = {
|
||||
isExpanded
|
||||
? 'bg-primary-light !rounded-tr-md !rounded-br-md'
|
||||
: primary.isActive
|
||||
? 'bg-secondary-dark'
|
||||
: 'hover:bg-primary-dark bg-secondary-dark group-hover/button:border-primary-dark'
|
||||
? 'bg-secondary-dark'
|
||||
: 'hover:bg-primary-dark bg-secondary-dark group-hover/button:border-primary-dark'
|
||||
),
|
||||
SecondaryIcon: ({ isExpanded }) =>
|
||||
classNames(
|
||||
baseClasses.SecondaryIcon,
|
||||
isExpanded ? 'text-primary-dark' : 'text-[#348cfd] group-hover/secondary:text-primary-light'
|
||||
isExpanded
|
||||
? 'text-primary-dark'
|
||||
: 'text-primary-active group-hover/secondary:text-primary-light'
|
||||
),
|
||||
Separator: ({ primary, isExpanded, isHovering }) =>
|
||||
classNames(
|
||||
@@ -70,14 +59,40 @@ const classes = {
|
||||
Content: ({ isExpanded }) => classNames(baseClasses.Content, isExpanded ? 'block' : 'hidden'),
|
||||
};
|
||||
|
||||
const DefaultListItemRenderer = props => {
|
||||
const { t, icon, label, className, isActive } = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex h-8 w-full flex-row items-center p-3',
|
||||
'whitespace-pre text-base',
|
||||
className,
|
||||
`${isActive ? 'hover:opacity-80' : 'hover:bg-primary-dark '}`
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<span className="mr-4">
|
||||
<Icon
|
||||
name={icon}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="mr-5">{t?.(label)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a more generic version of SplitButton without the isActive
|
||||
* and other interaction props
|
||||
*/
|
||||
const SplitButton = ({
|
||||
isToggle,
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
items,
|
||||
renderer,
|
||||
isActive,
|
||||
onInteraction,
|
||||
Component,
|
||||
}) => {
|
||||
@@ -88,19 +103,14 @@ const SplitButton = ({
|
||||
const setHover = hovering => setState({ ...state, isHovering: hovering });
|
||||
const collapse = () => setState({ ...state, isExpanded: false });
|
||||
|
||||
const renderPrimaryButton = () => (
|
||||
<Component
|
||||
key={primary.id}
|
||||
{...primary}
|
||||
isActive={isActive}
|
||||
onInteraction={onInteraction}
|
||||
rounded="none"
|
||||
className={classes.Primary({ ...state, primary: { isActive, isToggle } })}
|
||||
data-tool={primary.id}
|
||||
data-cy={`${groupId}-split-button-primary`}
|
||||
/>
|
||||
const listItemRenderer = renderer || DefaultListItemRenderer;
|
||||
const primaryClassNames = classNames(
|
||||
classes.Primary({
|
||||
isExpanded: state.isExpanded,
|
||||
isActive: primary.isActive,
|
||||
}),
|
||||
primary.className
|
||||
);
|
||||
|
||||
return (
|
||||
<OutsideClickHandler
|
||||
onOutsideClick={collapse}
|
||||
@@ -111,16 +121,26 @@ const SplitButton = ({
|
||||
className="relative"
|
||||
>
|
||||
<div
|
||||
className={classes.Button({ ...state, primary: { isActive } })}
|
||||
className={classes.Button({ ...state })}
|
||||
style={{ height: '40px' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<div className={classes.Interface}>
|
||||
<div onClick={collapse}>{renderPrimaryButton()}</div>
|
||||
<div className={classes.Separator({ ...state, primary: { isActive } })}></div>
|
||||
<div onClick={collapse}>
|
||||
<Component
|
||||
key={primary.id}
|
||||
{...primary}
|
||||
onInteraction={onInteraction}
|
||||
rounded="none"
|
||||
className={primaryClassNames}
|
||||
data-tool={primary.id}
|
||||
data-cy={`${groupId}-split-button-primary`}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.Separator({ ...state, primary })}></div>
|
||||
<div
|
||||
className={classes.Secondary({ ...state, primary: { isActive } })}
|
||||
className={classes.Secondary({ ...state, primary })}
|
||||
onClick={toggleExpanded}
|
||||
data-cy={`${groupId}-split-button-secondary`}
|
||||
>
|
||||
@@ -131,7 +151,7 @@ const SplitButton = ({
|
||||
>
|
||||
<Icon
|
||||
name={secondary.icon}
|
||||
className={classes.SecondaryIcon({ ...state, primary: { isActive } })}
|
||||
className={classes.SecondaryIcon({ ...state })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -144,7 +164,7 @@ const SplitButton = ({
|
||||
<ListMenu
|
||||
items={items}
|
||||
onClick={collapse}
|
||||
renderer={args => renderer({ ...args, t })}
|
||||
renderer={args => listItemRenderer({ ...args, t })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,6 +182,7 @@ SplitButton.propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
Component: PropTypes.elementType,
|
||||
interactionType: PropTypes.oneOf(['action', 'tool', 'toggle']),
|
||||
};
|
||||
|
||||
SplitButton.defaultProps = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import SplitButton from '../SplitButton';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
import ToolbarButton from '../../ToolbarButton';
|
||||
|
||||
export const argTypes = {
|
||||
component: SplitButton,
|
||||
@@ -30,7 +31,7 @@ export const SplitButtonTemplate = args => (
|
||||
|
||||
## Overview
|
||||
|
||||
SplitButton is a component that renders the SplitButtons.
|
||||
SplitButton is a component that renders the SplitButtons
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
@@ -63,6 +64,7 @@ SplitButton is a component that renders the SplitButtons.
|
||||
tooltip: 'Bidirectional',
|
||||
},
|
||||
],
|
||||
Component: ToolbarButton,
|
||||
}}
|
||||
>
|
||||
{SplitButtonTemplate.bind({})}
|
||||
|
||||
@@ -64,7 +64,7 @@ const Thumbnail = ({
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageAltText}
|
||||
className="min-h-32 object-none"
|
||||
className="min-h-32 object-contain"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -8,7 +8,9 @@ import Tooltip from '../Tooltip';
|
||||
import { StringNumber } from '../../types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ThumbnailTracked ({
|
||||
// Todo: This class to me feels like it belongs in an extension, not in platform/ui
|
||||
// because it is dealing with mode specific components/information
|
||||
function ThumbnailTracked({
|
||||
displaySetInstanceUID,
|
||||
className,
|
||||
imageSrc,
|
||||
@@ -87,12 +89,12 @@ function ThumbnailTracked ({
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">{isTracked ? t('Series is tracked') : t('Series is untracked')}</span>
|
||||
<span className="text-white">
|
||||
{isTracked ? t('Series is tracked') : t('Series is untracked')}
|
||||
</span>
|
||||
</span>
|
||||
{!!viewportIdentificator.length && (
|
||||
<span>
|
||||
{`${t('Viewport')}: ${viewportIdentificatorLabel}`}
|
||||
</span>
|
||||
<span>{`${t('Viewport')}: ${viewportIdentificatorLabel}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,7 +138,7 @@ function ThumbnailTracked ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ThumbnailTracked.propTypes = {
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import IconButton from '../IconButton';
|
||||
import Icon from '../Icon';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
const ToolbarButton = ({
|
||||
type = 'tool',
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
@@ -15,47 +13,37 @@ const ToolbarButton = ({
|
||||
onInteraction,
|
||||
dropdownContent,
|
||||
//
|
||||
isActive,
|
||||
className,
|
||||
size,
|
||||
toolTipClassName,
|
||||
...rest
|
||||
//
|
||||
}) => {
|
||||
const classes = {
|
||||
tool: isActive
|
||||
? 'text-black'
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
toggle: isActive
|
||||
? '!text-[#348CFD]'
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
action: isActive
|
||||
? 'text-black'
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
|
||||
const bgClasses = {
|
||||
toggle: isActive && 'bg-transparent',
|
||||
};
|
||||
|
||||
const activeClass = isActive ? 'active' : '';
|
||||
const shouldShowDropdown = !!isActive && !!dropdownContent;
|
||||
const shouldShowDropdown = !!dropdownContent;
|
||||
const iconEl = icon ? <Icon name={icon} /> : <div>{label || 'Missing icon and label'}</div>;
|
||||
|
||||
const sizeToUse = size ?? 'toolbar';
|
||||
const toolTipClassNameToUse =
|
||||
toolTipClassName !== undefined
|
||||
? toolTipClassName
|
||||
: sizeToUse === 'toolbar'
|
||||
? 'w-[40px] h-[40px]'
|
||||
: 'w-[32px] h-[32px]';
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
<Tooltip
|
||||
isSticky={shouldShowDropdown}
|
||||
content={shouldShowDropdown ? dropdownContent : label}
|
||||
tight={shouldShowDropdown}
|
||||
className={toolTipClassNameToUse}
|
||||
>
|
||||
<IconButton
|
||||
variant={isActive ? 'contained' : 'text'}
|
||||
bgColor={bgClasses[type]}
|
||||
size="toolbar"
|
||||
className={classnames(activeClass, classes[type], className)}
|
||||
size={sizeToUse}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
onInteraction({
|
||||
itemId: id,
|
||||
interactionType: type,
|
||||
commands,
|
||||
});
|
||||
}}
|
||||
@@ -73,27 +61,20 @@ const ToolbarButton = ({
|
||||
|
||||
ToolbarButton.defaultProps = {
|
||||
dropdownContent: null,
|
||||
isActive: false,
|
||||
type: 'action',
|
||||
};
|
||||
|
||||
ToolbarButton.propTypes = {
|
||||
/* Influences background/hover styling */
|
||||
type: PropTypes.oneOf(['action', 'toggle', 'tool']),
|
||||
id: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
commands: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string.isRequired,
|
||||
commandOptions: PropTypes.object,
|
||||
})
|
||||
),
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
commands: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
|
||||
onInteraction: PropTypes.func,
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
|
||||
dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
size: PropTypes.string,
|
||||
toolTipClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
import { ToolboxUI } from './';
|
||||
import { useToolbox } from '../../contextProviders';
|
||||
|
||||
/**
|
||||
* 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 }) {
|
||||
const { state: toolboxState, api } = useToolbox(buttonSectionId);
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
const prevButtonIdsRef = 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}`;
|
||||
})
|
||||
);
|
||||
|
||||
if (prevButtonIdsRef.current === currentButtonIdsStr) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevButtonIdsRef.current = currentButtonIdsStr;
|
||||
|
||||
const initializeOptionsWithEnhancements = toolbarButtons.reduce(
|
||||
(accumulator, toolbarButton) => {
|
||||
const { id: buttonId, componentProps } = toolbarButton;
|
||||
|
||||
const createEnhancedOptions = (options, parentId) =>
|
||||
options.map(option => {
|
||||
return {
|
||||
...option,
|
||||
value:
|
||||
toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ??
|
||||
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';
|
||||
|
||||
if (isString) {
|
||||
commandsManager.run(command, { value });
|
||||
} else if (isObject) {
|
||||
commandsManager.run({
|
||||
...command,
|
||||
commandOptions: { ...command.commandOptions, ...option, value },
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (componentProps.items?.length) {
|
||||
componentProps.items.forEach(item => {
|
||||
accumulator[item.id] = createEnhancedOptions(item.options, item.id);
|
||||
});
|
||||
} else if (componentProps.options?.length) {
|
||||
accumulator[buttonId] = createEnhancedOptions(componentProps.options, buttonId);
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
api.initializeToolOptions(initializeOptionsWithEnhancements);
|
||||
}, [toolbarButtons, api]);
|
||||
|
||||
const handleToolOptionChange = (toolName, optionName, newValue) => {
|
||||
api.handleToolOptionChange(toolName, optionName, newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
activeToolOptions={toolboxState.toolOptions?.[toolboxState.activeTool]}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toolbox;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { PanelSection, ToolSettings, Tooltip } from '../../components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const ItemsPerRow = 4;
|
||||
|
||||
/**
|
||||
* Just refactoring from the toolbox component to make it more readable
|
||||
*/
|
||||
function ToolboxUI(props) {
|
||||
const {
|
||||
toolbarButtons,
|
||||
handleToolSelect,
|
||||
activeToolOptions,
|
||||
numRows,
|
||||
servicesManager,
|
||||
title,
|
||||
useCollapsedPanel = true,
|
||||
} = props;
|
||||
|
||||
const render = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-black">
|
||||
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
const isLastRow = Math.floor(index / ItemsPerRow) + 1 === numRows;
|
||||
|
||||
const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`;
|
||||
|
||||
const onInteraction = ({ itemId, id, commands }) => {
|
||||
handleToolSelect(itemId || id);
|
||||
props.onInteraction({
|
||||
itemId,
|
||||
commands,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames({
|
||||
[toolClasses]: true,
|
||||
'flex flex-col items-center justify-center': true,
|
||||
})}
|
||||
>
|
||||
{componentProps.disabled ? (
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
content={componentProps.label}
|
||||
secondaryContent={'Not available on the current viewport'}
|
||||
>
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-primary-dark h-auto px-2">
|
||||
{activeToolOptions && <ToolSettings options={activeToolOptions} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return useCollapsedPanel ? <PanelSection title={title}>{render()}</PanelSection> : render();
|
||||
}
|
||||
|
||||
export { ToolboxUI };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ToolboxUI } from './ToolboxUI';
|
||||
import Toolbox from './Toolbox';
|
||||
export { ToolboxUI, Toolbox };
|
||||
@@ -30,7 +30,16 @@ const arrowPositionStyle = {
|
||||
},
|
||||
};
|
||||
|
||||
const Tooltip = ({ content, isSticky, position, className, tight, children, isDisabled }) => {
|
||||
const Tooltip = ({
|
||||
content,
|
||||
secondaryContent = null,
|
||||
isSticky,
|
||||
position,
|
||||
className,
|
||||
tight,
|
||||
children,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const { t } = useTranslation('Buttons');
|
||||
|
||||
@@ -68,11 +77,14 @@ const Tooltip = ({ content, isSticky, position, className, tight, children, isDi
|
||||
className={classnames(
|
||||
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
|
||||
{
|
||||
'py-1 px-4': !tight,
|
||||
'py-[6px] px-[8px]': !tight,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
<div>{typeof content === 'string' ? t(content) : content}</div>
|
||||
<div className="text-aqua-pale">
|
||||
{typeof secondaryContent === 'string' ? t(secondaryContent) : secondaryContent}
|
||||
</div>
|
||||
<svg
|
||||
className="text-primary-dark stroke-secondary-light absolute h-4"
|
||||
style={arrowPositionStyle[position]}
|
||||
|
||||
@@ -10,10 +10,24 @@ const classes = {
|
||||
bottomLeft: 'bottom-viewport left-viewport',
|
||||
};
|
||||
|
||||
const ViewportOverlay = ({ topLeft, topRight, bottomRight, bottomLeft, color }) => {
|
||||
export type ViewportOverlayProps = {
|
||||
topLeft: React.ReactNode;
|
||||
topRight: React.ReactNode;
|
||||
bottomRight: React.ReactNode;
|
||||
bottomLeft: React.ReactNode;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const ViewportOverlay = ({
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomRight,
|
||||
bottomLeft,
|
||||
color = 'text-primary-light',
|
||||
}: ViewportOverlayProps) => {
|
||||
const overlay = 'absolute pointer-events-none viewport-overlay';
|
||||
return (
|
||||
<div className={classnames(color ? color : 'text-primary-light')}>
|
||||
<div className={color}>
|
||||
<div
|
||||
data-cy={'viewport-overlay-top-left'}
|
||||
className={classnames(overlay, classes.topLeft)}
|
||||
|
||||
@@ -79,6 +79,9 @@ import PanelSection from './PanelSection';
|
||||
import AdvancedToolbox from './AdvancedToolbox';
|
||||
import InputDoubleRange from './InputDoubleRange';
|
||||
import LegacyButtonGroup from './LegacyButtonGroup';
|
||||
import LegacySplitButton from './LegacySplitButton';
|
||||
import { ToolSettings } from './AdvancedToolbox';
|
||||
import { Toolbox } from './Toolbox';
|
||||
import InvestigationalUseDialog from './InvestigationalUseDialog';
|
||||
import MeasurementItem from './MeasurementTable/MeasurementItem';
|
||||
|
||||
@@ -165,6 +168,9 @@ export {
|
||||
ViewportPane,
|
||||
ViewportOverlay,
|
||||
WindowLevelMenuItem,
|
||||
LegacySplitButton,
|
||||
ToolSettings,
|
||||
Toolbox,
|
||||
InvestigationalUseDialog,
|
||||
MeasurementItem,
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export default function CineProvider({ children, service }) {
|
||||
const api = {
|
||||
getState,
|
||||
setCine,
|
||||
setIsCineEnabled,
|
||||
setIsCineEnabled: isCineEnabled => service.setIsCineEnabled(isCineEnabled),
|
||||
playClip: (element, playClipOptions) => service.playClip(element, playClipOptions),
|
||||
stopClip: element => service.stopClip(element),
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo } from 'react';
|
||||
import React, { ReactNode, createContext, useContext, useMemo } from 'react';
|
||||
|
||||
// export const IMAGE_VIEWER_DEFAULT_VALUE = {
|
||||
// StudyInstanceUIDs: [],
|
||||
// setImageViewer: () => {},
|
||||
// };
|
||||
interface ImageViewerProviderProps {
|
||||
StudyInstanceUIDs: string[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ImageViewerContext = createContext();
|
||||
export const ImageViewerContext = createContext(null);
|
||||
export const useImageViewer = () => useContext(ImageViewerContext);
|
||||
|
||||
export function ImageViewerProvider({ StudyInstanceUIDs, reducer, initialState, children }) {
|
||||
export function ImageViewerProvider({ StudyInstanceUIDs, children }: ImageViewerProviderProps) {
|
||||
const value = useMemo(() => {
|
||||
return { StudyInstanceUIDs };
|
||||
}, [StudyInstanceUIDs]);
|
||||
|
||||
return <ImageViewerContext.Provider value={value}>{children}</ImageViewerContext.Provider>;
|
||||
}
|
||||
|
||||
export const useImageViewer = () => useContext(ImageViewerContext);
|
||||
@@ -0,0 +1,106 @@
|
||||
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: {} };
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
activeTool: action.payload.activeTool,
|
||||
},
|
||||
};
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
const { toolName, optionName, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
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,
|
||||
[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),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
export function toolboxReducer(state, action) {
|
||||
let newState = { ...state };
|
||||
|
||||
const { payload } = action;
|
||||
const { stateSyncService, buttonSectionId } = payload || {};
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
newState = {
|
||||
...state,
|
||||
activeTool: payload.activeTool,
|
||||
};
|
||||
break;
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
newState = {
|
||||
...state,
|
||||
toolOptions: {
|
||||
...state.toolOptions,
|
||||
[payload.toolName]: state.toolOptions[payload.toolName].map(option =>
|
||||
option.id === payload.optionName ? { ...option, value: payload.value } : option
|
||||
),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'INITIALIZE_TOOL_OPTIONS':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const newToolOptions = Object.keys(payload?.toolOptions || {}).reduce((acc, toolId) => {
|
||||
const tool = payload.toolOptions[toolId];
|
||||
if (state.toolOptions[toolId]) {
|
||||
// Preserve existing options, potentially merging with new ones if necessary
|
||||
acc[toolId] = state.toolOptions[toolId].map(existingOption => {
|
||||
const initialOption = tool.find(option => option.id === existingOption.id);
|
||||
return initialOption
|
||||
? { ...initialOption, value: existingOption.value }
|
||||
: existingOption;
|
||||
});
|
||||
} else {
|
||||
acc[toolId] = tool;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
toolOptions: newToolOptions,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// store the state in the stateSyncService
|
||||
if (payload?.stateSyncService) {
|
||||
const prevState = stateSyncService.getState()?.['uiStateStore']?.[buttonSectionId];
|
||||
stateSyncService.store({
|
||||
uiStateStore: {
|
||||
...stateSyncService.getState()?.['uiStateStore'],
|
||||
[buttonSectionId]: {
|
||||
...prevState,
|
||||
...newState,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ interface Viewport {
|
||||
width: number;
|
||||
height: number;
|
||||
viewportLabel: any;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
@@ -45,6 +46,7 @@ const DEFAULT_STATE: DefaultState = {
|
||||
default: {
|
||||
viewportId: 'default',
|
||||
displaySetInstanceUIDs: [],
|
||||
isReady: false,
|
||||
viewportOptions: {
|
||||
viewportId: 'default',
|
||||
},
|
||||
@@ -252,6 +254,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
});
|
||||
|
||||
viewport.viewportLabel = getViewportLabel(viewports, viewport.viewportId);
|
||||
viewport.isReady = false;
|
||||
|
||||
if (!viewport.viewportOptions.presentationIds) {
|
||||
viewport.viewportOptions.presentationIds = ViewportGridService.getPresentationIds(
|
||||
@@ -289,6 +292,25 @@ export function ViewportGridProvider({ children, service }) {
|
||||
};
|
||||
}
|
||||
|
||||
case 'VIEWPORT_IS_READY': {
|
||||
const { viewportId, isReady } = action.payload;
|
||||
const viewports = new Map(state.viewports);
|
||||
const viewport = viewports.get(viewportId);
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewports.set(viewportId, {
|
||||
...viewport,
|
||||
isReady,
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
viewports,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return action.payload;
|
||||
}
|
||||
@@ -319,6 +341,25 @@ export function ViewportGridProvider({ children, service }) {
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setViewportIsReady = useCallback(
|
||||
(viewportId, isReady) => {
|
||||
dispatch({
|
||||
type: 'VIEWPORT_IS_READY',
|
||||
payload: {
|
||||
viewportId,
|
||||
isReady,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, viewportGridState]
|
||||
);
|
||||
|
||||
const getGridViewportsReady = useCallback(() => {
|
||||
const { viewports } = viewportGridState;
|
||||
const readyViewports = Array.from(viewports.values()).filter(viewport => viewport.isReady);
|
||||
return readyViewports.length === viewports.size;
|
||||
}, [viewportGridState]);
|
||||
|
||||
const setLayout = useCallback(
|
||||
({
|
||||
layoutType,
|
||||
@@ -382,6 +423,8 @@ export function ViewportGridProvider({ children, service }) {
|
||||
onModeExit: reset,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
@@ -393,6 +436,8 @@ export function ViewportGridProvider({ children, service }) {
|
||||
reset,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
]);
|
||||
|
||||
// run many of the calls through the service itself since we want to publish events
|
||||
@@ -405,8 +450,11 @@ export function ViewportGridProvider({ children, service }) {
|
||||
reset: () => service.reset(),
|
||||
set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
getActiveViewportOptionByKey,
|
||||
setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props),
|
||||
publishViewportsReady: () => service.publishViewportsReady(),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,6 +14,8 @@ export { default as ViewportDialogProvider, useViewportDialog } from './Viewport
|
||||
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
|
||||
export { useToolbox, ToolboxProvider } from './Toolbox/ToolboxContext';
|
||||
|
||||
export {
|
||||
UserAuthenticationContext,
|
||||
UserAuthenticationProvider,
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/** UTILS */
|
||||
//import utils from './utils';
|
||||
//export { utils };
|
||||
|
||||
/** CONTEXT/HOOKS */
|
||||
// Export types - need to do as two lines due to a bug in babel
|
||||
import * as Types from './types';
|
||||
|
||||
export {
|
||||
@@ -31,6 +25,8 @@ export {
|
||||
UserAuthenticationContext,
|
||||
UserAuthenticationProvider,
|
||||
useUserAuthentication,
|
||||
useToolbox,
|
||||
ToolboxProvider,
|
||||
} from './contextProviders';
|
||||
|
||||
/** COMPONENTS */
|
||||
@@ -86,6 +82,7 @@ export {
|
||||
SegmentationGroupTable,
|
||||
SidePanel,
|
||||
SplitButton,
|
||||
LegacySplitButton,
|
||||
StudyBrowser,
|
||||
StudyItem,
|
||||
StudyListExpandedRow,
|
||||
@@ -117,6 +114,8 @@ export {
|
||||
WindowLevelMenuItem,
|
||||
ImageScrollbar,
|
||||
ViewportOverlay,
|
||||
ToolSettings,
|
||||
Toolbox,
|
||||
InvestigationalUseDialog,
|
||||
MeasurementItem,
|
||||
} from './components';
|
||||
|
||||
@@ -25,4 +25,10 @@ body {
|
||||
height: 2.1rem;
|
||||
}
|
||||
|
||||
|
||||
/* disabled class - used for icons and divs based on context */
|
||||
.ohif-disabled {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
opacity: 0.35;
|
||||
}
|
||||