feat(segmentation mode): Add create, and export SEG with Brushes (#3632)
This commit is contained in:
1 parent
896866e9d0
commit
48bbd6281a
186 files changed
+6801
-2883
No files matched your search
@@ -4,49 +4,31 @@ import { DicomMetadataStore } from '@ohif/core';
|
||||
/**
|
||||
*
|
||||
* @param {*} servicesManager
|
||||
* @param {*} dataSource
|
||||
* @param {*} measurements
|
||||
* @param {*} options
|
||||
* @returns {string[]} displaySetInstanceUIDs
|
||||
*/
|
||||
async function createReportAsync(
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
dataSource,
|
||||
measurements,
|
||||
options
|
||||
) {
|
||||
async function createReportAsync({ servicesManager, getReport, reportType = 'measurement' }) {
|
||||
const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services;
|
||||
const loadingDialogId = uiDialogService.create({
|
||||
showOverlay: true,
|
||||
isDraggable: false,
|
||||
centralize: true,
|
||||
// TODO: Create a loading indicator component + zeplin design?
|
||||
content: Loading,
|
||||
});
|
||||
|
||||
try {
|
||||
const naturalizedReport = await commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: measurements,
|
||||
dataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options,
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
const naturalizedReport = await getReport();
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
const displaySetInstanceUID = displaySetService.getMostRecentDisplaySet();
|
||||
const displaySet = displaySetService.getMostRecentDisplaySet();
|
||||
|
||||
const displaySetInstanceUID = displaySet.displaySetInstanceUID;
|
||||
|
||||
uiNotificationService.show({
|
||||
title: 'Create Report',
|
||||
message: 'Measurements saved successfully',
|
||||
message: `${reportType} saved successfully`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
@@ -54,7 +36,7 @@ async function createReportAsync(
|
||||
} catch (error) {
|
||||
uiNotificationService.show({
|
||||
title: 'Create Report',
|
||||
message: error.message || 'Failed to store measurements',
|
||||
message: error.message || `Failed to store ${reportType}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -119,18 +119,18 @@ function createDicomJSONApi(dicomJsonConfig) {
|
||||
});
|
||||
},
|
||||
processResults: () => {
|
||||
console.debug(' DICOMJson QUERY processResults');
|
||||
console.warn(' DICOMJson QUERY processResults not implemented');
|
||||
},
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: () => {
|
||||
console.debug(' DICOMJson QUERY SERIES SEARCH');
|
||||
console.warn(' DICOMJson QUERY SERIES SEARCH not implemented');
|
||||
},
|
||||
},
|
||||
instances: {
|
||||
search: () => {
|
||||
console.debug(' DICOMJson QUERY instances SEARCH');
|
||||
console.warn(' DICOMJson QUERY instances SEARCH not implemented');
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -211,7 +211,7 @@ function createDicomJSONApi(dicomJsonConfig) {
|
||||
},
|
||||
store: {
|
||||
dicom: () => {
|
||||
console.debug(' DICOMJson store dicom');
|
||||
console.warn(' DICOMJson store dicom not implemented');
|
||||
},
|
||||
},
|
||||
getImageIdsForDisplaySet(displaySet) {
|
||||
|
||||
@@ -85,7 +85,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
});
|
||||
},
|
||||
processResults: () => {
|
||||
console.debug(' DICOMLocal QUERY processResults');
|
||||
console.warn(' DICOMLocal QUERY processResults not implemented');
|
||||
},
|
||||
},
|
||||
series: {
|
||||
@@ -107,7 +107,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
},
|
||||
instances: {
|
||||
search: () => {
|
||||
console.debug(' DICOMLocal QUERY instances SEARCH');
|
||||
console.warn(' DICOMLocal QUERY instances SEARCH not implemented');
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -221,7 +221,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
} else {
|
||||
const meta = {
|
||||
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
|
||||
FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||
@@ -295,6 +295,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
});
|
||||
|
||||
instance.imageId = imageId;
|
||||
instance.wadoRoot = dicomWebConfig.wadoRoot;
|
||||
instance.wadoUri = dicomWebConfig.wadoUri;
|
||||
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
|
||||
@@ -2,18 +2,18 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LegacyButton, ButtonGroup } from '@ohif/ui';
|
||||
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
|
||||
|
||||
function ActionButtons({ onExportClick, onCreateReportClick }) {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ButtonGroup
|
||||
<LegacyButtonGroup
|
||||
color="black"
|
||||
size="inherit"
|
||||
>
|
||||
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
|
||||
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
|
||||
<LegacyButton
|
||||
className="px-2 py-2 text-base"
|
||||
onClick={onExportClick}
|
||||
@@ -26,7 +26,7 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
|
||||
>
|
||||
{t('Create Report')}
|
||||
</LegacyButton>
|
||||
</ButtonGroup>
|
||||
</LegacyButtonGroup>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,13 +103,20 @@ export default function PanelMeasurementTable({
|
||||
// creating too many series instances.
|
||||
const options = findSRWithSameSeriesDescription(SeriesDescription, displaySetService);
|
||||
|
||||
return createReportAsync(
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
dataSource,
|
||||
trackedMeasurements,
|
||||
options
|
||||
);
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
'storeMeasurements',
|
||||
{
|
||||
measurementData: trackedMeasurements,
|
||||
dataSource,
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options,
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
);
|
||||
};
|
||||
|
||||
return createReportAsync({ servicesManager, getReport });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,8 +103,7 @@ function PanelStudyBrowser({
|
||||
}
|
||||
|
||||
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [StudyInstanceUIDs, getStudiesForPatientByMRN]);
|
||||
}, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]);
|
||||
|
||||
// // ~~ Initial Thumbnails
|
||||
useEffect(() => {
|
||||
@@ -116,21 +115,23 @@ function PanelStudyBrowser({
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
||||
if (imageId && !displaySet?.unsupported) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
|
||||
if (isMounted.current) {
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
}
|
||||
if (!imageId || displaySet?.unsupported) {
|
||||
return;
|
||||
}
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
|
||||
if (!isMounted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]);
|
||||
|
||||
// ~~ displaySets
|
||||
useEffect(() => {
|
||||
@@ -140,8 +141,7 @@ function PanelStudyBrowser({
|
||||
sortStudyInstances(mappedDisplaySets);
|
||||
|
||||
setDisplaySets(mappedDisplaySets);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [thumbnailImageSrcMap]);
|
||||
}, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
|
||||
|
||||
// ~~ subscriptions --> displaySets
|
||||
useEffect(() => {
|
||||
@@ -149,32 +149,40 @@ function PanelStudyBrowser({
|
||||
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
data => {
|
||||
const { displaySetsAdded } = data;
|
||||
const { displaySetsAdded, options } = data;
|
||||
displaySetsAdded.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||
if (!displaySet?.unsupported) {
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (imageId) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId,
|
||||
dSet.initialViewport
|
||||
);
|
||||
if (isMounted.current) {
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
}
|
||||
}
|
||||
if (displaySet?.unsupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId,
|
||||
dSet.initialViewport
|
||||
);
|
||||
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
SubscriptionDisplaySetsAdded.unsubscribe();
|
||||
};
|
||||
}, [getImageSrc, dataSource, displaySetService]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Will this always hold _all_ the displaySets we care about?
|
||||
// DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
|
||||
const SubscriptionDisplaySetsChanged = displaySetService.subscribe(
|
||||
@@ -198,12 +206,10 @@ function PanelStudyBrowser({
|
||||
);
|
||||
|
||||
return () => {
|
||||
SubscriptionDisplaySetsAdded.unsubscribe();
|
||||
SubscriptionDisplaySetsChanged.unsubscribe();
|
||||
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
|
||||
|
||||
const tabs = _createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets);
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable';
|
||||
import createReportDialogPrompt from './createReportDialogPrompt';
|
||||
|
||||
export { PanelStudyBrowser, WrappedPanelStudyBrowser, PanelMeasurementTable };
|
||||
export {
|
||||
PanelStudyBrowser,
|
||||
WrappedPanelStudyBrowser,
|
||||
PanelMeasurementTable,
|
||||
createReportDialogPrompt,
|
||||
};
|
||||
@@ -1,51 +1,29 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default function Toolbar({ servicesManager }) {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
const [buttonState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
// Could track buttons and state separately...?
|
||||
useEffect(() => {
|
||||
const { unsubscribe: unsub1 } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
() => setToolbarButtons(toolbarService.getButtonSection('primary'))
|
||||
);
|
||||
const { unsubscribe: unsub2 } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => setButtonState({ ...toolbarService.state })
|
||||
const { unsubscribe } = toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, () =>
|
||||
setToolbarButtons(toolbarService.getButtonSection('primary'))
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsub1();
|
||||
unsub2();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const onInteraction = useCallback(
|
||||
args => toolbarService.recordInteraction(args),
|
||||
[toolbarService]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map(toolDef => {
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
// TODO: ...
|
||||
|
||||
// isActive if:
|
||||
// - id is primary?
|
||||
// - id is in list of "toggled on"?
|
||||
let isActive;
|
||||
if (componentProps.type === 'toggle') {
|
||||
isActive = buttonState.toggles[id];
|
||||
}
|
||||
// Also need... to filter list for splitButton, and set primary based on most recently clicked
|
||||
// Also need to kill the radioGroup button's magic logic
|
||||
// Everything should be reactive off these props, so commands can inform ToolbarService
|
||||
|
||||
// These can... Trigger toolbar events based on updates?
|
||||
// Then sync using useEffect, or simply modify the state here?
|
||||
return (
|
||||
// The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item.
|
||||
// This allows for the individual items to be included in other UI components where perhaps alternative margins are desired.
|
||||
@@ -56,9 +34,7 @@ export default function Toolbar({ servicesManager }) {
|
||||
<Component
|
||||
id={id}
|
||||
{...componentProps}
|
||||
bState={buttonState}
|
||||
isActive={isActive}
|
||||
onInteraction={args => toolbarService.recordInteraction(args)}
|
||||
onInteraction={onInteraction}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ToolbarButtonWithServices({
|
||||
id,
|
||||
type,
|
||||
commands,
|
||||
onInteraction,
|
||||
servicesManager,
|
||||
...props
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services || {};
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
const { primaryToolId } = buttonsState;
|
||||
|
||||
const isActive =
|
||||
(type === 'tool' && id === primaryToolId) ||
|
||||
(type === 'toggle' && buttonsState.toggles[id] === true);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
commands={commands}
|
||||
id={id}
|
||||
type={type}
|
||||
isActive={isActive}
|
||||
onInteraction={onInteraction}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarButtonWithServices.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
commands: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string.isRequired,
|
||||
context: PropTypes.string,
|
||||
})
|
||||
),
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
toolbarService: PropTypes.shape({
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
state: PropTypes.shape({
|
||||
primaryToolId: PropTypes.string,
|
||||
toggles: PropTypes.objectOf(PropTypes.bool),
|
||||
groups: PropTypes.objectOf(PropTypes.object),
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ToolbarButtonWithServices;
|
||||
@@ -1,13 +1,37 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
function LayoutSelector({ rows, columns, className, servicesManager, ...rest }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
const { hangingProtocolService, toolbarService } = (servicesManager as ServicesManager).services;
|
||||
const onSelection = useCallback(
|
||||
props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
[toolbarService]
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutSelector
|
||||
{...props}
|
||||
onSelection={onSelection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutSelector({ rows, columns, className, onSelection, ...rest }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const closeOnOutsideClick = () => {
|
||||
if (isOpen) {
|
||||
@@ -15,19 +39,6 @@ function LayoutSelector({ rows, columns, className, servicesManager, ...rest })
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
evt => {
|
||||
const { protocol } = evt;
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [hangingProtocolService]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', closeOnOutsideClick);
|
||||
return () => {
|
||||
@@ -38,19 +49,6 @@ function LayoutSelector({ rows, columns, className, servicesManager, ...rest })
|
||||
const onInteractionHandler = () => setIsOpen(!isOpen);
|
||||
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
||||
|
||||
const onSelectionHandler = props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
id="Layout"
|
||||
@@ -64,7 +62,7 @@ function LayoutSelector({ rows, columns, className, servicesManager, ...rest })
|
||||
<DropdownContent
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onSelection={onSelectionHandler}
|
||||
onSelection={onSelection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -87,4 +85,4 @@ LayoutSelector.defaultProps = {
|
||||
onLayoutChange: () => {},
|
||||
};
|
||||
|
||||
export default LayoutSelector;
|
||||
export default ToolbarLayoutSelectorWithServices;
|
||||
@@ -1,3 +0,0 @@
|
||||
import { SplitButton } from '@ohif/ui';
|
||||
|
||||
export default SplitButton;
|
||||
@@ -0,0 +1,186 @@
|
||||
import { SplitButton, Icon, ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
function ToolbarSplitButtonWithServices({
|
||||
isRadio,
|
||||
isAction,
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
items,
|
||||
renderer,
|
||||
onInteraction,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services;
|
||||
|
||||
const handleItemClick = (item, index) => {
|
||||
const { id, type, commands } = item;
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: id,
|
||||
interactionType: type,
|
||||
commands,
|
||||
});
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
primary: !isAction && isRadio ? { ...item, index } : state.primary,
|
||||
isExpanded: false,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.index !== index : true
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
/* Bubbles up individual item clicks */
|
||||
const getSplitButtonItems = items =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
onClick: () => handleItemClick(item, index),
|
||||
}));
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
primary,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.id !== primary.id : true
|
||||
),
|
||||
});
|
||||
|
||||
const { primaryToolId, toggles } = buttonsState;
|
||||
|
||||
const isPrimaryToggle = state.primary.type === 'toggle';
|
||||
|
||||
const isPrimaryActive =
|
||||
(state.primary.type === 'tool' && primaryToolId === state.primary.id) ||
|
||||
(isPrimaryToggle && toggles[state.primary.id] === true);
|
||||
|
||||
const PrimaryButtonComponent =
|
||||
toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton;
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const updatedItems = state.items.map(item => {
|
||||
const isActive = item.type === 'tool' && primaryToolId === item.id;
|
||||
|
||||
// We could have added the
|
||||
// item.type === 'toggle' && toggles[item.id] === true
|
||||
// too but that makes the button active when the toggle is active under it
|
||||
// which feels weird
|
||||
return {
|
||||
...item,
|
||||
isActive,
|
||||
};
|
||||
});
|
||||
|
||||
const DefaultListItemRenderer = ({ type, icon, label, t, id }) => {
|
||||
const isActive = type === 'toggle' && toggles[id] === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'hover:bg-primary-dark flex h-8 w-full flex-row items-center p-3',
|
||||
'whitespace-pre text-base',
|
||||
isActive && 'bg-primary-dark',
|
||||
isActive
|
||||
? 'text-[#348CFD]'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<span className="mr-4">
|
||||
<Icon
|
||||
name={icon}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="mr-5">{t(label)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const listItemRenderer = renderer || DefaultListItemRenderer;
|
||||
|
||||
return (
|
||||
<SplitButton
|
||||
isRadio={isRadio}
|
||||
isAction={isAction}
|
||||
primary={state.primary}
|
||||
secondary={secondary}
|
||||
items={updatedItems}
|
||||
groupId={groupId}
|
||||
renderer={listItemRenderer}
|
||||
isActive={isPrimaryActive || updatedItems.some(item => item.isActive)}
|
||||
isToggle={isPrimaryToggle}
|
||||
onInteraction={onInteraction}
|
||||
Component={props => (
|
||||
<PrimaryButtonComponent
|
||||
{...props}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarSplitButtonWithServices.propTypes = {
|
||||
isRadio: PropTypes.bool,
|
||||
isAction: PropTypes.bool,
|
||||
groupId: PropTypes.string,
|
||||
primary: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
uiType: PropTypes.string,
|
||||
}),
|
||||
secondary: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
}),
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
icon: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
})
|
||||
),
|
||||
renderer: PropTypes.func,
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
toolbarService: PropTypes.object,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
ToolbarSplitButtonWithServices.defaultProps = {
|
||||
isRadio: false,
|
||||
isAction: false,
|
||||
};
|
||||
|
||||
export default ToolbarSplitButtonWithServices;
|
||||
@@ -44,7 +44,7 @@ function ViewerLayout({
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`${id} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||
`${id} is not valid for an extension module. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file)`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider.tsx';
|
||||
import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.tsx';
|
||||
import ToolbarSplitButton from './Toolbar/ToolbarSplitButton.tsx';
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider';
|
||||
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
|
||||
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
||||
import ToolbarButtonWithServices from './Toolbar/ToolbarButtonWithServices';
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
return [
|
||||
@@ -12,27 +12,27 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
},
|
||||
{
|
||||
name: 'ohif.action',
|
||||
defaultComponent: ToolbarButton,
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButton,
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.splitButton',
|
||||
defaultComponent: ToolbarSplitButton,
|
||||
defaultComponent: ToolbarSplitButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.layoutSelector',
|
||||
defaultComponent: ToolbarLayoutSelector,
|
||||
defaultComponent: ToolbarLayoutSelectorWithServices,
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.toggle',
|
||||
defaultComponent: ToolbarButton,
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -13,6 +13,8 @@ import { id } from './id.js';
|
||||
import preRegistration from './init';
|
||||
import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu';
|
||||
import * as dicomWebUtils from './DicomWebDataSource/utils';
|
||||
import { createReportDialogPrompt } from './Panels';
|
||||
import createReportAsync from './Actions/createReportAsync';
|
||||
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
/**
|
||||
@@ -48,4 +50,6 @@ export {
|
||||
CustomizableContextMenuTypes,
|
||||
getStudiesForPatientByMRN,
|
||||
dicomWebUtils,
|
||||
createReportDialogPrompt,
|
||||
createReportAsync,
|
||||
};
|
||||
Reference in new issue
Block a user