Merge branch 'feat/v2-main' into feat/OHIF-177

This commit is contained in:
Danny Brown 2020-06-25 16:17:50 -04:00 committed by GitHub
commit 0264e5998d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 513 additions and 47 deletions

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0", "cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1", "cornerstone-tools": "4.16.0",
"dcmjs": "0.14.0", "dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",

View File

@ -2,33 +2,32 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui'; import { ToolbarButton } from '@ohif/ui';
function NestedMenu({ children }) { function NestedMenu({ children, label, icon, isActive }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
useEffect(() => { const toggleNestedMenu = () => setIsOpen(!isOpen);
function closeNestedMenu() {
if (isOpen) { const closeNestedMenu = () => {
setIsOpen(false); if (isOpen) {
} setIsOpen(false);
} }
};
useEffect(() => {
window.addEventListener('click', closeNestedMenu); window.addEventListener('click', closeNestedMenu);
return () => { return () => {
window.removeEventListener('click', closeNestedMenu); window.removeEventListener('click', closeNestedMenu);
}; };
}, [isOpen]); }, [isOpen]);
const dropdownContent = isOpen ? children : undefined;
return ( return (
<ToolbarButton <ToolbarButton
id="More" id="NestedMenu"
label="More" label={label}
icon="tool-more-menu" icon={icon}
onClick={() => { onClick={toggleNestedMenu}
setIsOpen(!isOpen); dropdownContent={isOpen && children}
}} isActive={isActive || isOpen}
dropdownContent={dropdownContent}
isActive={isOpen}
type="primary" type="primary"
/> />
); );
@ -36,6 +35,13 @@ function NestedMenu({ children }) {
NestedMenu.propTypes = { NestedMenu.propTypes = {
children: PropTypes.any.isRequired, children: PropTypes.any.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
};
NestedMenu.defaultProps = {
icon: "tool-more-menu",
label: "More",
}; };
export default NestedMenu; export default NestedMenu;

View File

@ -55,7 +55,20 @@ function ViewerLayout({
}; };
}; };
const defaultTool = { icon: 'tool-more-menu', label: 'More', isActive: false };
const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); const [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
const [activeTool, setActiveTool] = useState(defaultTool);
const setActiveToolHandler = (tool, isNested) => {
setActiveTool(isNested ? tool : defaultTool);
};
const onPrimaryClickHandler = (evt, btn) => {
if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) {
const { commandName, commandOptions } = btn.props.commands[evt.value];
commandsManager.runCommand(commandName, commandOptions);
}
};
useEffect(() => { useEffect(() => {
const { unsubscribe } = ToolBarService.subscribe( const { unsubscribe } = ToolBarService.subscribe(
@ -63,8 +76,8 @@ function ViewerLayout({
() => { () => {
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
const updatedToolbars = { const updatedToolbars = {
primary: ToolBarService.getButtonSection('primary'), primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler, setActiveTool: setActiveToolHandler }),
secondary: ToolBarService.getButtonSection('secondary'), secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }),
}; };
setToolbars(updatedToolbars); setToolbars(updatedToolbars);
} }
@ -86,11 +99,10 @@ function ViewerLayout({
if (!isNested) { if (!isNested) {
const { id, Component, componentProps } = toolDef; const { id, Component, componentProps } = toolDef;
return <Component key={id} id={id} {...componentProps} />; return <Component key={id} id={id} {...componentProps} />;
} else { } else {
return ( return (
<NestedMenu> <NestedMenu isActive={activeTool.isActive} icon={activeTool.icon} label={activeTool.label}>
<div className="flex"> <div className="flex">
{toolDef.map(x => { {toolDef.map(x => {
const { id, Component, componentProps } = x; const { id, Component, componentProps } = x;

View File

@ -9,7 +9,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
{ {
name: 'ohif.divider', name: 'ohif.divider',
defaultComponent: ToolbarDivider, defaultComponent: ToolbarDivider,
clickHandler: () => {}, clickHandler: () => { },
}, },
{ {
name: 'ohif.action', name: 'ohif.action',
@ -30,7 +30,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
optionalConfig: [], optionalConfig: [],
requiredProps: [], requiredProps: [],
optionalProps: [], optionalProps: [],
clickHandler: (evt, clickedBtn, btnSectionName) => { clickHandler: (evt, clickedBtn, btnSectionName, metadata, viewerProps) => {
const { props } = clickedBtn; const { props } = clickedBtn;
const allButtons = toolbarService.getButtons(); const allButtons = toolbarService.getButtons();
@ -47,6 +47,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
clickedBtn.config.groupName === btn.config.groupName clickedBtn.config.groupName === btn.config.groupName
) { ) {
btn.props.isActive = false; btn.props.isActive = false;
if (viewerProps.setActiveTool) {
viewerProps.setActiveTool(props, metadata.isNested);
}
} }
}); });
@ -63,7 +67,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
{ {
name: 'ohif.layoutSelector', name: 'ohif.layoutSelector',
defaultComponent: ToolbarLayoutSelector, defaultComponent: ToolbarLayoutSelector,
clickHandler: (evt, clickedBtn, btnSectionName) => {}, clickHandler: (evt, clickedBtn, btnSectionName) => { },
}, },
{ {
name: 'ohif.toggle', name: 'ohif.toggle',

View File

@ -30,7 +30,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.15.1", "cornerstone-tools": "4.16.0",
"dcmjs": "0.14.0", "dcmjs": "0.14.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^0.50.0", "@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0", "cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1", "cornerstone-tools": "4.16.0",
"dcmjs": "0.14.0", "dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import CornerstoneViewport from 'react-cornerstone-viewport'; import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import { import {
@ -13,6 +14,18 @@ import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle'; import throttle from 'lodash.throttle';
import { useTrackedMeasurements } from './../getContextModule'; import { useTrackedMeasurements } from './../getContextModule';
// TODO -> Get this list from the list of tracked measurements.
const {
ArrowAnnotateTool,
BidirectionalTool,
EllipticalRoiTool,
LengthTool,
} = cornerstoneTools;
const BaseAnnotationTool = cornerstoneTools.importInternal(
'base/BaseAnnotationTool'
);
// const cine = viewportSpecificData.cine; // const cine = viewportSpecificData.cine;
// isPlaying = cine.isPlaying === true; // isPlaying = cine.isPlaying === true;
@ -27,6 +40,7 @@ function TrackedCornerstoneViewport({
viewportIndex, viewportIndex,
}) { }) {
const [trackedMeasurements] = useTrackedMeasurements(); const [trackedMeasurements] = useTrackedMeasurements();
const [ const [
{ activeViewportIndex, viewports }, { activeViewportIndex, viewports },
dispatchViewportGrid, dispatchViewportGrid,
@ -34,6 +48,8 @@ function TrackedCornerstoneViewport({
// viewportIndex, onSubmit // viewportIndex, onSubmit
const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [viewportData, setViewportData] = useState(null); const [viewportData, setViewportData] = useState(null);
const [element, setElement] = useState(null);
const [isTracked, setIsTracked] = useState(false);
// TODO: Still needed? Better way than import `OHIF` and destructure? // TODO: Still needed? Better way than import `OHIF` and destructure?
// Why is this managed by `core`? // Why is this managed by `core`?
useEffect(() => { useEffect(() => {
@ -42,6 +58,75 @@ function TrackedCornerstoneViewport({
}; };
}, []); }, []);
useEffect(() => {
if (!element) {
return;
}
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(tool => tool.element === element);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.image) {
cornerstone.updateImage(element);
}
}, [isTracked]);
const onElementEnabled = evt => {
const eventData = evt.detail;
const targetElement = eventData.element;
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(
tool => tool.element === targetElement
);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
} else if (tool instanceof BaseAnnotationTool) {
const configuration = tool.configuration;
configuration.renderDashed = true;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(targetElement);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
setElement(targetElement);
};
useEffect(() => { useEffect(() => {
const { const {
StudyInstanceUID, StudyInstanceUID,
@ -116,6 +201,7 @@ function TrackedCornerstoneViewport({
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
); );
const { trackedSeries } = trackedMeasurements.context; const { trackedSeries } = trackedMeasurements.context;
const { const {
Modality, Modality,
SeriesDate, SeriesDate,
@ -131,6 +217,11 @@ function TrackedCornerstoneViewport({
SliceThickness, SliceThickness,
} = displaySet.images[0]; } = displaySet.images[0];
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
return ( return (
<> <>
<ViewportActionBar <ViewportActionBar
@ -159,6 +250,7 @@ function TrackedCornerstoneViewport({
{/* TODO: Viewport interface to accept stack or layers of content like this? */} {/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full"> <div className="relative flex flex-row w-full h-full">
<CornerstoneViewport <CornerstoneViewport
onElementEnabled={onElementEnabled}
viewportIndex={viewportIndex} viewportIndex={viewportIndex}
imageIds={imageIds} imageIds={imageIds}
imageIdIndex={currentImageIdIndex} imageIdIndex={currentImageIdIndex}

View File

@ -55,7 +55,7 @@
}, },
"devDependencies": { "devDependencies": {
"@ohif/core": "^2.9.6", "@ohif/core": "^2.9.6",
"cornerstone-tools": "4.15.1", "cornerstone-tools": "4.16.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.3.0", "cornerstone-core": "^2.3.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",

View File

@ -1,5 +1,8 @@
// TODO: torn, can either bake this here; or have to create a whole new button type // TODO: torn, can either bake this here; or have to create a whole new button type
// Only ways that you can pass in a custom React component for render :l // Only ways that you can pass in a custom React component for render :l
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
import React from 'react';
import classnames from 'classnames';
export default [ export default [
// Divider // Divider
@ -29,13 +32,58 @@ export default [
config: { config: {
groupName: 'primaryTool', groupName: 'primaryTool',
}, },
component: ExpandableToolbarButton,
props: { props: {
isActive: true, isActive: true,
icon: 'tool-window-level', icon: 'tool-window-level',
label: 'Levels',
commandName: 'setToolActive', commandName: 'setToolActive',
commandOptions: { toolName: 'Wwwc' }, commandOptions: { toolName: 'Wwwc' },
commands: {
'windowLevelPreset1': {
commandName: 'windowLevelPreset1',
commandOptions: {},
},
'windowLevelPreset2': {
commandName: 'windowLevelPreset2',
commandOptions: {},
},
'windowLevelPreset3': {
commandName: 'windowLevelPreset3',
commandOptions: {},
},
'windowLevelPreset4': {
commandName: 'windowLevelPreset4',
commandOptions: {},
},
'windowLevelPreset5': {
commandName: 'windowLevelPreset5',
commandOptions: {},
}
},
type: 'primary', type: 'primary',
content: ListMenu,
contentProps: {
options: [
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
],
renderer: ({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)
}
}, },
}, },
{ {

View File

@ -31,7 +31,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"cornerstone-core": "^2.3.0", "cornerstone-core": "^2.3.0",
"cornerstone-tools": "^4.12.0", "cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3" "dicom-parser": "^1.8.3"
}, },

View File

@ -62,7 +62,7 @@ export default class ToolBarService {
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
} }
getButtonSection(key) { getButtonSection(key, props) {
const buttonSectionIds = this.buttonSections[key]; const buttonSectionIds = this.buttonSections[key];
const buttonsInSection = []; const buttonsInSection = [];
@ -79,7 +79,8 @@ export default class ToolBarService {
btnIds.forEach(nestedBtnId => { btnIds.forEach(nestedBtnId => {
const nestedBtn = this.buttons[nestedBtnId]; const nestedBtn = this.buttons[nestedBtnId];
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key); const metadata = { isNested: true };
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, metadata, props);
nestedButtons.push(mappedNestedBtn); nestedButtons.push(mappedNestedBtn);
}); });
@ -90,7 +91,8 @@ export default class ToolBarService {
} else { } else {
const btnId = btnIdOrArray; const btnId = btnIdOrArray;
const btn = this.buttons[btnId]; const btn = this.buttons[btnId];
const mappedBtn = this._mapButtonToDisplay(btn, key); const metadata = { isNested: false };
const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props);
buttonsInSection.push(mappedBtn); buttonsInSection.push(mappedBtn);
} }
@ -136,8 +138,8 @@ export default class ToolBarService {
* @param {*} btn * @param {*} btn
* @param {*} btnSection * @param {*} btnSection
*/ */
_mapButtonToDisplay(btn, btnSection) { _mapButtonToDisplay(btn, btnSection, metadata, props) {
const { id, type, component, props } = btn; const { id, type, component } = btn;
const buttonType = this._buttonTypes()[type]; const buttonType = this._buttonTypes()[type];
if (!buttonType) { if (!buttonType) {
@ -146,7 +148,7 @@ export default class ToolBarService {
const onClick = evt => { const onClick = evt => {
if (buttonType.clickHandler) { if (buttonType.clickHandler) {
buttonType.clickHandler(evt, btn, btnSection); buttonType.clickHandler(evt, btn, btnSection, metadata, props);
} }
if (btn.props.onClick) { if (btn.props.onClick) {
btn.onClick(evt, btn, btnSection); btn.onClick(evt, btn, btnSection);
@ -154,12 +156,15 @@ export default class ToolBarService {
if (btn.props.clickHandler) { if (btn.props.clickHandler) {
btn.clickHandler(evt, btn, btnSection); btn.clickHandler(evt, btn, btnSection);
} }
if (props && props.onClick) {
props.onClick(evt, btn, btnSection, props);
}
}; };
return { return {
id, id,
Component: component || buttonType.defaultComponent, Component: component || buttonType.defaultComponent,
componentProps: Object.assign({}, props, { onClick }), // componentProps: Object.assign({}, btn.props, { onClick }), //
}; };
} }
} }

View File

@ -32,6 +32,8 @@ export {
DateRange, DateRange,
Dialog, Dialog,
EmptyStudies, EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon, Icon,
IconButton, IconButton,
Input, Input,

View File

@ -0,0 +1,16 @@
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
content: "";
position: absolute;
bottom: -10px;
border-width: 10px 10px 0;
border-style: solid;
border-color:#5acce6 transparent;
}
.ExpandableToolbarButton .ExpandableToolbarButton__content {
display: none;
}
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
display: block;
}

View File

@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { IconButton, Icon } from '@ohif/ui';
import './ExpandableToolbarButton.css';
const ExpandableToolbarButton = ({
type,
id,
isActive,
onClick,
icon,
className,
content: Content,
contentProps
}) => {
const classes = {
type: {
primary: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
secondary: isActive
? 'text-black'
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
},
};
const onChildClickHandler = (...args) => {
onClick(...args);
if (contentProps.onClick) {
contentProps.onClick(...args);
}
};
const onClickHandler = (...args) => {
onClick(...args);
};
return (
<div key={id} className="ExpandableToolbarButton">
<IconButton
variant={isActive ? 'contained' : 'text'}
className={classnames(
'mx-1',
classes.type[type],
isActive && 'ExpandableToolbarButton__arrow'
)}
onClick={onClickHandler}
key={id}
>
<Icon name={icon} />
</IconButton>
<div className="absolute z-10 pt-4">
<div className={classnames("ExpandableToolbarButton__content w-48", className)}>
<Content {...contentProps} onClick={onChildClickHandler} />
</div>
</div>
</div>
);
};
const noop = () => { };
ExpandableToolbarButton.defaultProps = {
isActive: false,
type: 'primary',
content: null,
onClick: noop,
};
ExpandableToolbarButton.propTypes = {
/* Influences background/hover styling */
type: PropTypes.oneOf(['primary', 'secondary']),
id: PropTypes.string.isRequired,
isActive: PropTypes.bool,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
contentProps: PropTypes.object,
};
export default ExpandableToolbarButton;

View File

@ -0,0 +1,58 @@
---
name: Expandable Toolbar Button
menu: General
route: components/expandableToolbarButton
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
# Toolbar Button
Expandable Toolbar Buttons are used to populate the Toolbar.
## Import
```javascript
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
const props = {
content: ListMenu,
contentProps: {
options: [
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
],
renderer: ({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)
}
};
return (
<div className="py-10 flex justify-center">
<ExpandableToolbarButton {...props} />
</div>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />

View File

@ -0,0 +1,2 @@
import ExpandableToolbarButton from './ExpandableToolbarButton';
export default ExpandableToolbarButton;

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
const baseClasses = const baseClasses =
'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none'; 'text-center items-center justify-center outline-none font-bold focus:outline-none';
const roundedClasses = { const roundedClasses = {
none: '', none: '',
@ -113,7 +113,7 @@ const IconButton = ({
}; };
IconButton.defaultProps = { IconButton.defaultProps = {
onClick: () => {}, onClick: () => { },
color: 'default', color: 'default',
disabled: false, disabled: false,
fullWidth: false, fullWidth: false,

View File

@ -0,0 +1,65 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
const ListMenu = ({ options = [], renderer, onClick }) => {
const [selectedIndex, setSelectedIndex] = useState(null);
const ListItem = (props) => {
const flex = 'flex flex-row justify-between items-center';
const theme = 'bg-indigo-dark';
const hover = 'hover:bg-primary-dark';
const spacing = 'p-3 h-8';
return (
<div
className={classnames(
flex,
theme,
spacing,
'cursor-pointer',
!props.isActive && hover,
props.isActive && 'bg-primary-light',
)}
onClick={props.onClick}
>
{renderer && renderer(props)}
</div>
);
};
return (
<div className="flex flex-col rounded-md bg-secondary-dark pt-2 pb-2">
{options.map((option, index) => {
const onClickHandler = () => {
setSelectedIndex(index);
onClick({ ...option, index });
};
return (
<ListItem
key={`ListItem${index}`}
{...option}
index={index}
isActive={selectedIndex === index}
onClick={onClickHandler}
/>
);
})}
</div>
);
};
const noop = () => { };
ListMenu.propTypes = {
options: PropTypes.array.isRequired,
renderer: PropTypes.func.isRequired,
onClick: PropTypes.func
};
ListMenu.defaultProps = {
onClick: noop
};
export default ListMenu;

View File

@ -0,0 +1,52 @@
---
name: List Menu
menu: General
route: components/listMenu
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ListMenu } from '@ohif/ui';
# List Menu
List Menus are used to populate expandable Toolbar.
## Import
```javascript
import { ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
return (
<ToolbarButton options={[
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
]}
renderer={
({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)}
/>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />

View File

@ -0,0 +1,2 @@
import ListMenu from './ListMenu';
export default ListMenu;

View File

@ -76,7 +76,7 @@ const Select = ({
options={options} options={options}
value={selectedOptions} value={selectedOptions}
onChange={(selectedOptions, { action }) => { onChange={(selectedOptions, { action }) => {
const newSelection = selectedOptions.reduce( const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce(
(acc, curr) => acc.concat([curr.value]), (acc, curr) => acc.concat([curr.value]),
[] []
); );

View File

@ -25,6 +25,7 @@ const ToolbarButton = ({
}; };
const shouldShowDropdown = !!isActive && !!dropdownContent; const shouldShowDropdown = !!isActive && !!dropdownContent;
return ( return (
<div key={id}> <div key={id}>
<Tooltip <Tooltip

View File

@ -24,7 +24,7 @@ const arrowPositionStyle = {
}, },
}; };
const Tooltip = ({ content, isSticky, position, tight, children }) => { const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const handleMouseOver = () => { const handleMouseOver = () => {
@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => {
} }
}; };
const isOpen = isSticky || isActive; const isOpen = (isSticky || isActive) && !isDisabled;
return ( return (
<div <div
@ -84,10 +84,12 @@ Tooltip.defaultProps = {
tight: false, tight: false,
isSticky: false, isSticky: false,
position: 'bottom', position: 'bottom',
isDisabled: false
}; };
Tooltip.propTypes = { Tooltip.propTypes = {
// Allow null /** prevents tooltip from rendering despite hover/active/sticky */
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf([ position: PropTypes.oneOf([
'bottom', 'bottom',

View File

@ -39,6 +39,8 @@ import ThumbnailNoImage from './ThumbnailNoImage';
import ThumbnailTracked from './ThumbnailTracked'; import ThumbnailTracked from './ThumbnailTracked';
import ThumbnailList from './ThumbnailList'; import ThumbnailList from './ThumbnailList';
import ToolbarButton from './ToolbarButton'; import ToolbarButton from './ToolbarButton';
import ExpandableToolbarButton from './ExpandableToolbarButton';
import ListMenu from './ListMenu';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import Typography from './Typography'; import Typography from './Typography';
import Viewport from './Viewport'; import Viewport from './Viewport';
@ -52,6 +54,8 @@ export {
DateRange, DateRange,
Dialog, Dialog,
EmptyStudies, EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon, Icon,
IconButton, IconButton,
Input, Input,

View File

@ -16,6 +16,13 @@ module.exports = {
initial: 'initial', initial: 'initial',
inherit: 'inherit', inherit: 'inherit',
indigo: {
dark: '#0b1a42'
},
aqua: {
pale: '#7bb2ce'
},
primary: { primary: {
light: '#5acce6', light: '#5acce6',
main: '#0944b3', main: '#0944b3',

View File

@ -67,7 +67,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.2.1", "core-js": "^3.2.1",
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.15.1", "cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.0", "dcmjs": "0.14.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",

View File

@ -19,7 +19,9 @@ window.config = {
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
}, },
}, },
], ],
defaultDataSourceName: 'dicomweb',
}; };

View File

@ -6755,10 +6755,10 @@ cornerstone-math@^0.1.8:
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5" resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw== integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
cornerstone-tools@4.15.1: cornerstone-tools@4.16.0:
version "4.15.1" version "4.16.0"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.15.1.tgz#f0b1026da9c7758defc088bb2f1e78426a23d91e" resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.0.tgz#af3d32d13722b97bec258492642e622312280196"
integrity sha512-fJuTUJW/NDSD520jPB++tX//Kq80jPDngChHZrhx2xjj/9dCnexD9kmTX30Z0WUq0a2JEHZ6FlcOX38kSmw1hQ== integrity sha512-kUhuSb2Ixpd2hgbdem+740rnN4hmoxzcOBNaUcsizFRWjMAsgc0yUxBFwLl0mIs811mefq79JOL+juwRA8T3Tg==
dependencies: dependencies:
"@babel/runtime" "7.1.2" "@babel/runtime" "7.1.2"
cornerstone-math "0.1.7" cornerstone-math "0.1.7"