feat(segmentation): Enhanced segmentation panel design for TMTV (#3988)

Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
IbrahimandAlireza authored and GitHub committed 2024-04-02 23:33:04 -04:00
1 parent e8e209291c
commit 9f3235ff09
51 files changed
+2001 -572

No files matched your search

+2 -4
View File
@@ -1,9 +1,7 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
scrollbar-color: #041c4a transparent;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: #173239 transparent;
overflow-y: auto;
}
.study-min-height {
@@ -13,6 +13,10 @@ function ToolSettings({ options }) {
return null;
}
if (typeof options === 'function') {
return options();
}
return (
<div className="space-y-2 py-2 text-white">
{options?.map(option => {
@@ -27,15 +27,14 @@ const ButtonGroup = ({
vertical: 'flex-col',
};
const wrapperClasses = classnames('inline-flex', orientationClasses[orientation], className);
const wrapperClasses = classnames(
'items-stretch inline-flex',
orientationClasses[orientation],
className
);
return (
<div
className={classnames(
wrapperClasses,
'border-secondary-light rounded-[5px] border bg-black text-[13px]'
)}
>
<div className={classnames(wrapperClasses, 'text-[13px]')}>
{Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return cloneElement(child, {
@@ -1,6 +1,7 @@
import React, { useEffect, useCallback, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ReactDOM from 'react-dom';
import Icon from '../Icon';
import Typography from '../Typography';
@@ -21,7 +22,9 @@ const Dropdown = ({
maxCharactersPerLine,
}) => {
const [open, setOpen] = useState(false);
const element = useRef(null);
const elementRef = useRef(null);
const dropdownRef = useRef(null);
const [coords, setCoords] = useState({ x: 0, y: 0 });
// choose the max characters per line based on the longest title
const longestTitle = list.reduce((acc, item) => {
@@ -107,23 +110,54 @@ const Dropdown = ({
};
const handleClick = e => {
if (element.current && !element.current.contains(e.target)) {
if (elementRef.current && !elementRef.current.contains(e.target)) {
setOpen(false);
}
};
useEffect(() => {
if (elementRef.current && dropdownRef.current) {
const triggerRect = elementRef.current.getBoundingClientRect();
const dropdownRect = dropdownRef.current.getBoundingClientRect();
let x, y;
switch (alignment) {
case 'right':
x = triggerRect.right + window.scrollX - dropdownRect.width;
y = triggerRect.bottom + window.scrollY;
break;
case 'left':
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
default:
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
}
setCoords({ x, y });
}
}, [open, alignment, elementRef.current, dropdownRef.current]);
const renderList = () => {
return (
const portalElement = document.getElementById('react-portal');
const listElement = (
<div
className={classnames(
'top-100 border-secondary-main absolute z-10 mt-2 transform rounded border bg-black shadow transition duration-300',
'top-100 border-secondary-main w-max-content absolute mt-2 transform rounded border bg-black shadow transition duration-300',
{
'right-0 origin-top-right': alignment === 'right',
'left-0 origin-top-left': alignment === 'left',
'scale-0': !open,
'scale-100': open,
}
)}
ref={dropdownRef}
style={{
position: 'absolute',
top: `${coords.y}px`,
left: open ? `${coords.x}px` : -999999,
zIndex: 9999,
}}
data-cy={`${id}-dropdown`}
>
{list.map((item, idx) => (
@@ -137,6 +171,7 @@ const Dropdown = ({
))}
</div>
);
return ReactDOM.createPortal(listElement, portalElement);
};
useEffect(() => {
@@ -150,7 +185,7 @@ const Dropdown = ({
return (
<div
data-cy="dropdown"
ref={element}
ref={elementRef}
className="relative"
>
<div
@@ -33,6 +33,7 @@ const Input = ({
onKeyDown,
readOnly,
disabled,
labelChildren,
...otherProps
}) => {
return (
@@ -40,6 +41,7 @@ const Input = ({
<Label
className={labelClassName}
text={label}
children={labelChildren}
></Label>
<input
data-cy={`input-${id}`}
@@ -83,6 +85,7 @@ Input.propTypes = {
onKeyPress: PropTypes.func,
onKeyDown: PropTypes.func,
disabled: PropTypes.bool,
labelChildren: PropTypes.node,
};
export default Input;
@@ -37,7 +37,6 @@ const PanelSection = ({ title, children, actionIcons = [] }) => {
</div>
{areChildrenVisible && (
<>
<div className="h-[2px] bg-black"></div>
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
</>
)}
@@ -2,15 +2,14 @@ import React from 'react';
import Icon from '../Icon';
import { useTranslation } from 'react-i18next';
function AddSegmentRow({ onClick }) {
function AddSegmentRow({ onClick, onToggleSegmentationVisibility = null, segmentation = null }) {
const { t } = useTranslation('SegmentationTable');
return (
<div
className="flex hover:cursor-pointer"
onClick={onClick}
>
<div className="h-[28px] w-[28px]"></div>
<div className="group ml-2.5 mt-1">
<div className="flex justify-between bg-black pl-[34px] hover:cursor-pointer">
<div
className="group pt-[5px] pb-[5px]"
onClick={onClick}
>
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
@@ -18,6 +17,26 @@ function AddSegmentRow({ onClick }) {
<span className="text-[13px]">{t('Add segment')}</span>
</div>
</div>
{segmentation && (
<div className="flex items-center">
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(segmentation.id)}
>
{segmentation.isVisible ? (
<Icon
name="row-shown"
className="text-primary-active"
/>
) : (
<Icon
name="row-hidden"
className="text-primary-active"
/>
)}
</div>
</div>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import { Select, Icon, Dropdown, Tooltip } from '../../components';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@@ -31,7 +31,7 @@ function SegmentationDropDownRow({
}
return (
<div className="group mx-0.5 mt-[8px] flex items-center">
<div className="group mx-0.5 mt-[8px] flex items-center pb-[10px]">
<div
onClick={e => {
e.stopPropagation();
@@ -97,7 +97,7 @@ function SegmentationDropDownRow({
],
]}
>
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
@@ -122,8 +122,22 @@ function SegmentationDropDownRow({
/>
)}
<div className="flex items-center">
<Tooltip
position="bottom-right"
content={
<div className="flex flex-col">
<div className="text-[13px] text-white">Series:</div>
<div className="text-aqua-pale text-[13px]">{activeSegmentation.description}</div>
</div>
}
>
<Icon
name="info-action"
className="text-primary-active"
/>
</Tooltip>
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
className="hover:bg-secondary-dark mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
>
{activeSegmentation.isVisible ? (
@@ -20,6 +20,7 @@ const SegmentItem = ({
onColor,
onToggleVisibility,
onToggleLocked,
displayText,
}) => {
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
@@ -27,7 +28,9 @@ const SegmentItem = ({
return (
<div
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
className={classnames('text-aqua-pale group/row bg-primary-dark flex min-h-[28px] flex-col', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
})}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
@@ -35,114 +38,140 @@ const SegmentItem = ({
tabIndex={0}
data-cy={'segment-item'}
>
<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,
'border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames('relative flex w-full', {
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
'border border-l-0 border-transparent': !isActive,
})}
>
<div className="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
<div className="flex min-h-[28px]">
<div
className={classnames('group/number grid w-[28px] place-items-center', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'bg-primary-dark border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onColor(segmentationId, segmentIndex);
onDelete(segmentationId, segmentIndex);
}}
/>
</div>
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
) : (
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
className=" h-[30px] bg-black"
style={{ width: '3px' }}
></div>
<div
className={classnames('text-aqua-pale relative flex w-full', {
'border border-l-0 border-transparent': !isActive,
})}
style={{
width: 'calc(100% - 40px)',
}}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
<div className="bg-primary-dark flex h-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
onColor(segmentationId, segmentIndex);
}}
/>
)}
</div>
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
</div>
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
name="row-lock"
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
)}
</div>
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
name="row-lock"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
}}
/>
)}
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
disableEditing={disableEditing}
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
/>
)}
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
disableEditing={disableEditing}
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
</div>
</div>
</div>
</div>
{Array.isArray(displayText) ? (
<div className="flex flex-col py-[5px] pl-[43px]">
{displayText.map(text => (
<div
key={text}
className="text-aqua-pale flex h-full items-center bg-black text-[11px]"
>
{text}
</div>
))}
</div>
) : (
displayText && (
<div className="text-aqua-pale flex h-full items-center bg-black px-2 py-[5px] pl-[45px] text-[11px]">
{displayText}
</div>
)
)}
</div>
);
};
@@ -205,6 +234,7 @@ SegmentItem.propTypes = {
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
displayText: PropTypes.string,
};
SegmentItem.defaultProps = {
@@ -100,7 +100,7 @@ const SegmentationGroupTable = ({
)}
<div className="bg-primary-dark">
{segmentations?.length === 0 ? (
<div className="select-none rounded-[4px]">
<div className="select-none bg-black pt-[5px] pb-[5px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
@@ -127,7 +127,7 @@ const SegmentationGroupTable = ({
)}
</div>
{activeSegmentation && (
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
<div className="ohif-scrollbar flex h-fit max-h-[500px] min-h-0 flex-col overflow-auto bg-black">
{activeSegmentation?.segments?.map(segment => {
if (!segment) {
return null;
@@ -0,0 +1,211 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { PanelSection } from '../../components';
import SegmentationConfig from './SegmentationConfig';
import NoSegmentationRow from './NoSegmentationRow';
import { useTranslation } from 'react-i18next';
import SegmentationItem from './SegmentationItem';
const SegmentationGroupTableExpanded = ({
segmentations,
// segmentation initial config
segmentationConfig,
// UI show/hide
disableEditing,
showAddSegmentation,
showAddSegment,
showDeleteSegment,
// segmentation/segment handlers
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
onSegmentationDownload,
onSegmentationDownloadRTSS,
storeSegmentation,
// segment handlers
onSegmentClick,
onSegmentAdd,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentColorClick,
// segmentation config handlers
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
useEffect(() => {
// find the first active segmentation to set
let activeSegmentationIdToSet = segmentations?.find(segmentation => segmentation.isActive)?.id;
// If there is no active segmentation, set the first one to be active
if (!activeSegmentationIdToSet && segmentations?.length > 0) {
activeSegmentationIdToSet = segmentations[0].id;
}
// If there is no segmentation, set the active segmentation to null
if (segmentations?.length === 0) {
activeSegmentationIdToSet = null;
}
setActiveSegmentationId(activeSegmentationIdToSet);
}, [segmentations]);
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
const { t } = useTranslation('SegmentationTable');
return (
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title={t('Segmentation')}
actionIcons={
activeSegmentation && [
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
},
]
}
>
{isConfigOpen && (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
)}
<div className="bg-primary-dark">
<div className="select-none bg-black pt-[5px] pb-[5px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
</div>
{segmentations?.length > 0 && (
<div className="ohif-scrollbar flex max-h-[500px] select-none flex-col gap-[5px] overflow-auto bg-black">
{segmentations?.map(segmentation => {
return (
<div key={segmentation.id}>
<SegmentationItem
key={segmentation.id}
segmentation={segmentation}
disableEditing={disableEditing}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
showAddSegment={showAddSegment}
onSegmentAdd={onSegmentAdd}
onSegmentClick={onSegmentClick}
onSegmentDelete={onSegmentDelete}
onSegmentEdit={onSegmentEdit}
showDeleteSegment={showDeleteSegment}
onSegmentColorClick={onSegmentColorClick}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
activeSegmentationId={activeSegmentationId}
/>
</div>
);
})}
</div>
)}
</div>
</PanelSection>
</div>
);
};
SegmentationGroupTableExpanded.propTypes = {
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segmentIndex: PropTypes.number.isRequired,
color: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
})
),
})
),
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
showAddSegment: PropTypes.bool,
showDeleteSegment: PropTypes.bool,
onSegmentationAdd: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentationDownload: PropTypes.func.isRequired,
onSegmentationDownloadRTSS: PropTypes.func,
storeSegmentation: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
setFillAlpha: PropTypes.func.isRequired,
setFillAlphaInactive: PropTypes.func.isRequired,
setOutlineWidthActive: PropTypes.func.isRequired,
setOutlineOpacityActive: PropTypes.func.isRequired,
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
};
SegmentationGroupTableExpanded.defaultProps = {
segmentations: [],
disableEditing: false,
showAddSegmentation: true,
showAddSegment: true,
showDeleteSegment: true,
onSegmentationAdd: () => {},
onSegmentationEdit: () => {},
onSegmentationClick: () => {},
onSegmentationDelete: () => {},
onSegmentationDownload: () => {},
onSemgnetationDownloadRTSS: () => {},
storeSegmentation: () => {},
onSegmentClick: () => {},
onSegmentAdd: () => {},
onSegmentDelete: () => {},
onSegmentEdit: () => {},
onToggleSegmentationVisibility: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentLock: () => {},
onSegmentColorClick: () => {},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
setOutlineOpacityActive: () => {},
setRenderFill: () => {},
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTableExpanded;
@@ -0,0 +1,211 @@
import React, { useState } from 'react';
import { Icon, Dropdown } from '../../components';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import { Tooltip } from '../../components';
function SegmentationItem({
segmentation,
disableEditing,
onSegmentationEdit,
onSegmentationDownload,
onSegmentationDownloadRTSS,
storeSegmentation,
onSegmentationDelete,
showAddSegment,
onToggleSegmentationVisibility,
onSegmentAdd,
onSegmentClick,
onSegmentDelete,
onSegmentEdit,
showDeleteSegment,
onSegmentColorClick,
onToggleSegmentVisibility,
onToggleSegmentLock,
activeSegmentationId,
}) {
const { t } = useTranslation('SegmentationTable');
const [areChildrenVisible, setChildrenVisible] = useState(true);
const handleHeaderClick = () => {
setChildrenVisible(!areChildrenVisible);
};
return (
<>
<div className="bg-secondary-dark group relative flex items-center justify-start gap-1">
<div
onClick={e => {
e.stopPropagation();
}}
className="flex"
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
alignment="left"
itemsClassName="text-primary-active"
showBorders={false}
maxCharactersPerLine={30}
list={[
...(!disableEditing
? [
{
title: t('Rename'),
onClick: () => {
onSegmentationEdit(segmentation.id);
},
},
]
: []),
{
title: t('Delete'),
onClick: () => {
onSegmentationDelete(segmentation.id);
},
},
...(!disableEditing
? [
{
title: t('Export DICOM SEG'),
onClick: () => {
storeSegmentation(segmentation.id);
},
},
]
: []),
...[
{
title: t('Download DICOM SEG'),
onClick: () => {
onSegmentationDownload(segmentation.id);
},
},
{
title: t('Download DICOM RTSTRUCT'),
onClick: () => {
onSegmentationDownloadRTSS(segmentation.id);
},
},
],
]}
>
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
<div
className=" h-[28px] bg-black"
style={{ width: '3px' }}
></div>
</div>
<div
className="flex h-full w-full cursor-pointer items-center justify-between pr-[8px]"
onClick={handleHeaderClick}
>
<div className="font-inter text-aqua-pale text-[13px]">{segmentation.label}</div>
<div className="flex h-[28px] items-center justify-center gap-2">
<Tooltip
position="bottom-right"
content={
<div className="flex flex-col">
<div className="text-[13px] text-white">Series:</div>
<div className="text-aqua-pale text-[13px]">{segmentation.description}</div>
</div>
}
>
<Icon
name="info-action"
className="text-primary-active"
/>
</Tooltip>
<div className={areChildrenVisible ? '' : 'mr-[4px]'}>
<Icon name={areChildrenVisible ? 'chevron-down-new' : 'chevron-left-new'} />
</div>
</div>
</div>
</div>
{areChildrenVisible && (
<>
{!disableEditing && showAddSegment && (
<AddSegmentRow
onClick={() => onSegmentAdd(segmentation.id)}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
segmentation={segmentation}
/>
)}
<div
className={classNames('ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden', {
'mt-1': disableEditing || !showAddSegment,
})}
>
{segmentation?.segments?.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked, displayText } = segment;
return (
<div key={segmentIndex}>
<SegmentationGroupSegment
segmentationId={segmentation.id}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={
segmentation.activeSegmentIndex === segmentIndex &&
segmentation.id === activeSegmentationId
}
disableEditing={disableEditing}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showDelete={showDeleteSegment}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
displayText={displayText}
/>
</div>
);
})}
</div>
</>
)}
</>
);
}
SegmentationItem.propTypes = {
segmentation: PropTypes.object,
disableEditing: PropTypes.bool,
onToggleSegmentationVisibility: PropTypes.func,
onSegmentationEdit: PropTypes.func,
onSegmentationDownload: PropTypes.func,
onSegmentationDownloadRTSS: PropTypes.func,
storeSegmentation: PropTypes.func,
onSegmentationDelete: PropTypes.func,
showAddSegment: PropTypes.bool,
onSegmentAdd: PropTypes.func,
onSegmentClick: PropTypes.func,
onSegmentDelete: PropTypes.func,
onSegmentEdit: PropTypes.func,
showDeleteSegment: PropTypes.bool,
onSegmentColorClick: PropTypes.func,
onToggleSegmentVisibility: PropTypes.func,
onToggleSegmentLock: PropTypes.func,
activeSegmentationId: PropTypes.string,
};
SegmentationItem.defaultProps = {
segmentation: null,
disableEditing: false,
};
export default SegmentationItem;
@@ -1,3 +1,4 @@
import SegmentationGroupTable from './SegmentationGroupTable';
import SegmentationGroupTableExpanded from './SegmentationGroupTableExpanded';
export default SegmentationGroupTable;
export { SegmentationGroupTable, SegmentationGroupTableExpanded };
@@ -16,6 +16,7 @@ const ToolbarButton = ({
//
className,
disabled,
disabledText,
size,
toolTipClassName,
disableToolTip = false,
@@ -38,7 +39,7 @@ const ToolbarButton = ({
<Tooltip
isSticky={shouldShowDropdown}
content={shouldShowDropdown ? dropdownContent : label}
secondaryContent={disabled ? 'Not available on the current viewport' : null}
secondaryContent={disabledText}
tight={shouldShowDropdown}
className={toolTipClassNameToUse}
isDisabled={disableToolTip}
+18 -7
View File
@@ -43,8 +43,14 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
(accumulator, toolbarButton) => {
const { id: buttonId, componentProps } = toolbarButton;
const createEnhancedOptions = (options, parentId) =>
options.map(option => {
const createEnhancedOptions = (options, parentId) => {
const optionsToUse = Array.isArray(options) ? options : [options];
return optionsToUse.map(option => {
if (typeof option.optionComponent === 'function') {
return option;
}
return {
...option,
value:
@@ -72,13 +78,18 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
},
};
});
};
if (componentProps.items?.length) {
componentProps.items.forEach(item => {
accumulator[item.id] = createEnhancedOptions(item.options, item.id);
const { items, options } = componentProps;
if (items?.length) {
items.forEach(({ options, id }) => {
accumulator[id] = createEnhancedOptions(options, id);
});
} else if (componentProps.options?.length) {
accumulator[buttonId] = createEnhancedOptions(componentProps.options, buttonId);
} else if (options?.length) {
accumulator[buttonId] = createEnhancedOptions(options, buttonId);
} else if (options?.optionComponent) {
accumulator[buttonId] = options.optionComponent;
}
return accumulator;
@@ -53,8 +53,21 @@ function ToolboxUI(props) {
<Tooltip
position="bottom"
content={componentProps.label}
secondaryContent={'Not available on the current viewport'}
secondaryContent={componentProps.disabledText}
>
<div className="border-secondary-light rounded border bg-black">
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
</Tooltip>
) : (
<div className="border-secondary-light rounded border bg-black">
<Component
{...componentProps}
{...props}
@@ -63,16 +76,7 @@ function ToolboxUI(props) {
onInteraction={onInteraction}
size="toolbox"
/>
</Tooltip>
) : (
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
)}
</div>
);
+89 -33
View File
@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom';
import './tooltip.css';
@@ -42,6 +43,10 @@ const Tooltip = ({
}) => {
const [isActive, setIsActive] = useState(false);
const { t } = useTranslation('Buttons');
const tooltipContainer = document.getElementById('react-portal');
const [coords, setCoords] = useState({ x: 999999, y: 999999 });
const parentRef = useRef(null);
const tooltipRef = useRef(null);
const handleMouseOver = () => {
if (!isActive) {
@@ -57,8 +62,90 @@ const Tooltip = ({
const isOpen = (isSticky || isActive) && !isDisabled;
useEffect(() => {
if (parentRef.current && tooltipRef.current) {
const parentRect = parentRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const parentWidth = parentRect.width;
const parentHeight = parentRect.height;
const tooltipWidth = tooltipRect.width;
let newX = 0;
let newY = 0;
switch (position) {
case 'bottom':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top + parentHeight;
break;
case 'top':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top - parentHeight * 2;
break;
case 'right':
newX = parentRect.left + parentWidth;
newY = parentRect.top + parentHeight / 2;
break;
case 'left':
newX = parentRect.left - tooltipWidth - 10;
newY = parentRect.top + parentHeight / 2;
break;
case 'bottom-left':
newX = parentRect.left;
newY = parentRect.top + parentHeight;
break;
case 'bottom-right':
newX = parentRect.left - tooltipWidth + parentWidth;
newY = parentRect.top + parentHeight;
break;
default:
break;
}
setCoords({ x: newX, y: newY });
}
}, [isOpen, position, parentRef.current, tooltipRef.current]);
const tooltipContent = (
<div
className={classnames(`tooltip tooltip-${position} block`, 'z-50')}
style={{
position: 'fixed',
top: coords.y,
left: isOpen ? coords.x : 999999,
}}
>
<div
ref={tooltipRef}
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-[6px] px-[8px]': !tight,
}
)}
>
<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]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 22l-12-20l-12 20"
/>
</svg>
</div>
</div>
);
return (
<div
ref={parentRef}
className={classnames('relative', className)}
onMouseOver={handleMouseOver}
onFocus={handleMouseOver}
@@ -67,37 +154,7 @@ const Tooltip = ({
role="tooltip"
>
{children}
<div
className={classnames(`tooltip tooltip-${position}`, {
block: isOpen,
hidden: !isOpen,
})}
>
<div
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-[6px] px-[8px]': !tight,
}
)}
>
<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]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 22l-12-20l-12 20"
/>
</svg>
</div>
</div>
{tooltipContainer && ReactDOM.createPortal(tooltipContent, tooltipContainer)}
</div>
);
};
@@ -110,7 +167,6 @@ Tooltip.defaultProps = {
};
Tooltip.propTypes = {
/** prevents tooltip from rendering despite hover/active/sticky */
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
+2 -1
View File
@@ -29,7 +29,7 @@ import NavBar from './NavBar';
import Notification from './Notification';
import Select from './Select';
import SegmentationTable from './SegmentationTable';
import SegmentationGroupTable from './SegmentationGroupTable';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from './SegmentationGroupTable';
import SidePanel from './SidePanel';
import SplitButton from './SplitButton';
import StudyBrowser from './StudyBrowser';
@@ -144,6 +144,7 @@ export {
Select,
SegmentationTable,
SegmentationGroupTable,
SegmentationGroupTableExpanded,
SidePanel,
SplitButton,
StudyBrowser,
+1
View File
@@ -81,6 +81,7 @@ export {
Select,
SegmentationTable,
SegmentationGroupTable,
SegmentationGroupTableExpanded,
SidePanel,
SplitButton,
LegacySplitButton,