feat: Group findings table values with custom components/grouping functions (#4712)
This commit is contained in:
parent
0522d58d9b
commit
df8efba82c
@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||||
debugger;
|
|
||||||
const { segmentationService } = servicesManager.services;
|
const { segmentationService } = servicesManager.services;
|
||||||
const segmentations = segmentationService.getSegmentations();
|
const segmentations = segmentationService.getSegmentations();
|
||||||
const { seriesMetadata, instance } =
|
const { seriesMetadata, instance } =
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
Types as CoreTypes,
|
Types as CoreTypes,
|
||||||
cache,
|
cache,
|
||||||
BaseVolumeViewport,
|
BaseVolumeViewport,
|
||||||
|
triggerEvent,
|
||||||
|
eventTarget,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
ToolGroupManager,
|
ToolGroupManager,
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
utilities as cstUtils,
|
utilities as cstUtils,
|
||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
annotation,
|
annotation,
|
||||||
|
Types as ToolTypes,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
|
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
|
||||||
@ -35,6 +38,7 @@ import { toolNames } from './initCornerstoneTools';
|
|||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
|
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
|
||||||
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
|
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
|
||||||
|
|
||||||
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
||||||
const toggleSyncFunctions = {
|
const toggleSyncFunctions = {
|
||||||
imageSlice: toggleImageSliceSync,
|
imageSlice: toggleImageSliceSync,
|
||||||
@ -436,25 +440,26 @@ function commandsModule({
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeMeasurement: ({ uid }) => {
|
removeMeasurement: ({ uid }) => {
|
||||||
measurementService.remove(uid);
|
if (Array.isArray(uid)) {
|
||||||
|
measurementService.removeMany(uid);
|
||||||
|
} else {
|
||||||
|
measurementService.remove(uid);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleLockMeasurement: ({ uid }) => {
|
toggleLockMeasurement: ({ uid }) => {
|
||||||
measurementService.toggleLockMeasurement(uid);
|
measurementService.toggleLockMeasurement(uid);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleVisibilityMeasurement: ({ uid }) => {
|
toggleVisibilityMeasurement: ({ uid, items, visibility }) => {
|
||||||
measurementService.toggleVisibilityMeasurement(uid);
|
if (visibility === undefined && items?.length) {
|
||||||
},
|
visibility = !items[0].isVisible;
|
||||||
|
}
|
||||||
/**
|
if (Array.isArray(uid)) {
|
||||||
* Clear the measurements
|
measurementService.toggleVisibilityMeasurementMany(uid, visibility);
|
||||||
*/
|
} else {
|
||||||
clearMeasurements: options => {
|
measurementService.toggleVisibilityMeasurement(uid, visibility);
|
||||||
const { measurementFilter } = options;
|
}
|
||||||
measurementService.clearMeasurements(
|
|
||||||
measurementFilter ? measurementFilter.bind(options) : null
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -514,16 +519,18 @@ function commandsModule({
|
|||||||
|
|
||||||
viewportGridService.setActiveViewportId(viewportId);
|
viewportGridService.setActiveViewportId(viewportId);
|
||||||
},
|
},
|
||||||
arrowTextCallback: ({ callback }) => {
|
arrowTextCallback: async ({ callback }) => {
|
||||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||||
|
|
||||||
callInputDialogAutoComplete({
|
const value = await callInputDialogAutoComplete({
|
||||||
uiDialogService,
|
uiDialogService,
|
||||||
labelConfig,
|
labelConfig,
|
||||||
renderContent,
|
renderContent,
|
||||||
});
|
});
|
||||||
|
callback?.(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleCine: () => {
|
toggleCine: () => {
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
const { isCineEnabled } = cineService.getState();
|
const { isCineEnabled } = cineService.getState();
|
||||||
@ -879,7 +886,7 @@ function commandsModule({
|
|||||||
const options = { imageIndex: jumpIndex };
|
const options = { imageIndex: jumpIndex };
|
||||||
csUtils.jumpToSlice(viewport.element, options);
|
csUtils.jumpToSlice(viewport.element, options);
|
||||||
},
|
},
|
||||||
scroll: ({ direction }) => {
|
scroll: (options: ToolTypes.ScrollOptions) => {
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
if (!enabledElement) {
|
if (!enabledElement) {
|
||||||
@ -887,7 +894,6 @@ function commandsModule({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
const options = { delta: direction };
|
|
||||||
|
|
||||||
csUtils.scroll(viewport, options);
|
csUtils.scroll(viewport, options);
|
||||||
},
|
},
|
||||||
@ -1490,6 +1496,7 @@ function commandsModule({
|
|||||||
viewportGridService.getActiveViewportId()
|
viewportGridService.getActiveViewportId()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteActiveAnnotation: () => {
|
deleteActiveAnnotation: () => {
|
||||||
const activeAnnotationsUID = cornerstoneTools.annotation.selection.getAnnotationsSelected();
|
const activeAnnotationsUID = cornerstoneTools.annotation.selection.getAnnotationsSelected();
|
||||||
activeAnnotationsUID.forEach(activeAnnotationUID => {
|
activeAnnotationsUID.forEach(activeAnnotationUID => {
|
||||||
@ -1577,9 +1584,6 @@ function commandsModule({
|
|||||||
updateMeasurement: {
|
updateMeasurement: {
|
||||||
commandFn: actions.updateMeasurement,
|
commandFn: actions.updateMeasurement,
|
||||||
},
|
},
|
||||||
clearMeasurements: {
|
|
||||||
commandFn: actions.clearMeasurements,
|
|
||||||
},
|
|
||||||
jumpToMeasurement: {
|
jumpToMeasurement: {
|
||||||
commandFn: actions.jumpToMeasurement,
|
commandFn: actions.jumpToMeasurement,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,192 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
|
||||||
|
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
export type AccordionGrouping = {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccordionGroupProps = {
|
||||||
|
grouping: AccordionGrouping;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for the required type from the provided allChildren list and
|
||||||
|
* renders them.
|
||||||
|
*/
|
||||||
|
export const CloneChildren = props => {
|
||||||
|
const { group, allChildren, children, childType, defaultTypes } = props;
|
||||||
|
|
||||||
|
const subType = group?.subType;
|
||||||
|
|
||||||
|
for (const child of allChildren) {
|
||||||
|
if (subType && child.props?.subType !== subType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (childType && child.type === childType) {
|
||||||
|
return React.cloneElement(child, { ...props, children: child.props.children });
|
||||||
|
}
|
||||||
|
if (defaultTypes?.indexOf(child.type) === -1) {
|
||||||
|
return childType({ ...props, children: child });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
throw new Error(`No children defined for ${props.name} CloneChildren in group ${group?.name}`);
|
||||||
|
}
|
||||||
|
return React.cloneElement(children, props);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Used to exclude defaults */
|
||||||
|
const DEFAULT_TYPES = [GroupAccordion, Content, Trigger];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AccordionGroup is a component that splits a set of items into different
|
||||||
|
* groups according to a set of grouping rules. It then puts the groups
|
||||||
|
* into a set of accordion folds selected from the body of the accordion group,
|
||||||
|
* looking for matching trigger/content sections according to the type definition
|
||||||
|
* in the group with first one found being used.
|
||||||
|
*
|
||||||
|
* This design allows for easy customization of the component by declaring grouping
|
||||||
|
* functions with default grouping setups and then only overriding the specific
|
||||||
|
* children needing to be changed. See the PanelMeasurement for some example
|
||||||
|
* possibilities of how to modify the default grouping, or the test-extension
|
||||||
|
* measurements panel for a practical, working example.
|
||||||
|
*/
|
||||||
|
export function AccordionGroup(props) {
|
||||||
|
const { grouping, items, children, sourceChildren, type } = props;
|
||||||
|
const childProps = useSystem();
|
||||||
|
let defaultValue = props.defaultValue;
|
||||||
|
const groups = grouping.groupingFunction(items, grouping, childProps);
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
const defaultGroup = groups.values().find(group => group.isSelected);
|
||||||
|
defaultValue = defaultGroup?.key || defaultGroup?.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueArr =
|
||||||
|
(Array.isArray(defaultValue) && defaultValue) || (defaultValue && [defaultValue]) || [];
|
||||||
|
const sourceChildrenArr = sourceChildren ? React.Children.toArray(sourceChildren) : [];
|
||||||
|
const childrenArr = children ? React.Children.toArray(children) : [];
|
||||||
|
const allChildren = sourceChildrenArr.concat(childrenArr);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CloneChildren
|
||||||
|
allChildren={allChildren}
|
||||||
|
groups={groups}
|
||||||
|
childType={GroupAccordion}
|
||||||
|
grouping={grouping}
|
||||||
|
defaultValue={valueArr}
|
||||||
|
name={'grouping ' + grouping.name}
|
||||||
|
>
|
||||||
|
<DefaultAccordion name="DefaultAccordion" />
|
||||||
|
</CloneChildren>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultAccordion(props) {
|
||||||
|
const { groups, defaultValue, grouping, allChildren, asChild } = props;
|
||||||
|
if (!allChildren || !groups) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean(asChild)) {
|
||||||
|
return React.cloneElement(props.children, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
type={grouping.type || 'multiple'}
|
||||||
|
className="text-white"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
>
|
||||||
|
{[...groups.entries()].map(([key, group]) => {
|
||||||
|
return (
|
||||||
|
<AccordionItem
|
||||||
|
key={group.key + '-i'}
|
||||||
|
value={group.key}
|
||||||
|
>
|
||||||
|
<CloneChildren
|
||||||
|
allChildren={allChildren}
|
||||||
|
group={group}
|
||||||
|
childType={Trigger}
|
||||||
|
name="AccordionGroup.Trigger"
|
||||||
|
/>
|
||||||
|
<CloneChildren
|
||||||
|
allChildren={allChildren}
|
||||||
|
group={group}
|
||||||
|
childType={Content}
|
||||||
|
defaultTypes={DEFAULT_TYPES}
|
||||||
|
name="AccordionGroup.Content"
|
||||||
|
/>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GroupAccordion(props) {
|
||||||
|
const { groups, children } = props;
|
||||||
|
if (!groups) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [...groups.values()].map(group =>
|
||||||
|
React.cloneElement(children, {
|
||||||
|
...props,
|
||||||
|
children: children.props.children,
|
||||||
|
group,
|
||||||
|
...group,
|
||||||
|
key: group.title,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content(props) {
|
||||||
|
const { children, asChild, ...childProps } = props;
|
||||||
|
const { group } = props;
|
||||||
|
Object.assign(childProps, group);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (asChild) {
|
||||||
|
return React.cloneElement(children, { ...group, ...props, children: children.props.children });
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AccordionContent>
|
||||||
|
{React.cloneElement(children, { ...group, ...props, children: children.props.children })}
|
||||||
|
</AccordionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trigger(props) {
|
||||||
|
const { children, asChild, ...childProps } = props;
|
||||||
|
const { group } = props;
|
||||||
|
Object.assign(childProps, group);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (asChild) {
|
||||||
|
return React.cloneElement(children, childProps);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AccordionTrigger
|
||||||
|
value={group.value}
|
||||||
|
asChild={true}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{React.cloneElement(children, childProps)}
|
||||||
|
<ChevronDownIcon key="chevronDown" className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccordionGroup.Content = Content;
|
||||||
|
AccordionGroup.Trigger = Trigger;
|
||||||
|
AccordionGroup.Accordion = GroupAccordion;
|
||||||
|
|
||||||
|
export default AccordionGroup;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from './AccordionGroup';
|
||||||
|
import AccordionGroup from './AccordionGroup';
|
||||||
|
|
||||||
|
export default AccordionGroup;
|
||||||
70
extensions/cornerstone/src/components/MeasurementItems.tsx
Normal file
70
extensions/cornerstone/src/components/MeasurementItems.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem } from '@ohif/ui-next';
|
||||||
|
|
||||||
|
import PanelAccordionTrigger from './PanelAccordionTrigger';
|
||||||
|
import MeasurementsMenu from './MeasurementsMenu';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
|
export function MeasurementItem(props) {
|
||||||
|
const { index, item } = props;
|
||||||
|
return (
|
||||||
|
<PanelAccordionTrigger
|
||||||
|
count={index + 1}
|
||||||
|
text={item.toolName || item.label || item.title}
|
||||||
|
colorHex="#f00"
|
||||||
|
isActive={item.isSelected}
|
||||||
|
menu={MeasurementsMenu}
|
||||||
|
group={{ items: [item], onClick: props.onClick }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MeasurementAccordion(props) {
|
||||||
|
const { items } = props;
|
||||||
|
const system = useSystem();
|
||||||
|
|
||||||
|
const onClick = (e, group) => {
|
||||||
|
const { items } = group;
|
||||||
|
// Just jump to the first measurement in the set, and mark that one as active
|
||||||
|
// with the set of items.
|
||||||
|
system.commandsManager.run('jumpToMeasurement', {
|
||||||
|
uid: items[0].uid,
|
||||||
|
displayMeasurements: items,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
className="flex-shrink-0 overflow-hidden"
|
||||||
|
>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const { displayText: details = {} } = item;
|
||||||
|
return (
|
||||||
|
<AccordionItem
|
||||||
|
key={`measurementAccordion:${item.uid}`}
|
||||||
|
value={item.uid}
|
||||||
|
>
|
||||||
|
<MeasurementItem
|
||||||
|
item={item}
|
||||||
|
key={`measurementItem:${item.uid}`}
|
||||||
|
index={index}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
<AccordionContent key={`measurementContent:${item.uid}`}>
|
||||||
|
<div className="ml-7 px-2 py-2">
|
||||||
|
<div className="text-secondary-foreground flex items-center gap-1 text-base leading-normal">
|
||||||
|
{details.primary?.length > 0 &&
|
||||||
|
details.primary.map((detail, index) => (
|
||||||
|
<span key={`details:${item.uid}:${index}`}>{detail}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MeasurementTable } from '@ohif/ui-next';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a measurement table that is designed to be nested inside
|
||||||
|
* the accordion groups.
|
||||||
|
*/
|
||||||
|
export default function MeasurementTableNested(props) {
|
||||||
|
const { title, items, group, customHeader } = props;
|
||||||
|
const { commandsManager } = useSystem();
|
||||||
|
const onAction = (e, command, uid) => {
|
||||||
|
commandsManager.run(command, { uid, annotationUID: uid, displayMeasurements: items });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MeasurementTable
|
||||||
|
title={title ? title : `Measurements`}
|
||||||
|
data={items}
|
||||||
|
onAction={onAction}
|
||||||
|
{...group}
|
||||||
|
key={group.key}
|
||||||
|
>
|
||||||
|
<MeasurementTable.Header key="measurementTableHeader">
|
||||||
|
{customHeader && group.isFirst && customHeader({ ...props, items: props.allItems })}
|
||||||
|
</MeasurementTable.Header>
|
||||||
|
<MeasurementTable.Body key="measurementTableBody" />
|
||||||
|
</MeasurementTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
extensions/cornerstone/src/components/MeasurementsMenu.tsx
Normal file
81
extensions/cornerstone/src/components/MeasurementsMenu.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
Icons,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
|
|
||||||
|
export function MeasumentsMenu(props) {
|
||||||
|
const { group, classNames } = props;
|
||||||
|
if (!group.items?.length) {
|
||||||
|
console.log('No items to iterate', group.items);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { items } = group;
|
||||||
|
const [item] = items;
|
||||||
|
const { isSelected, isVisible } = item;
|
||||||
|
|
||||||
|
const system = useSystem();
|
||||||
|
|
||||||
|
const onAction = (event, command, args?) => {
|
||||||
|
const uid = items.map(item => item.uid);
|
||||||
|
// Some commands use displayMeasurements and some use items
|
||||||
|
system.commandsManager.run(command, {
|
||||||
|
...args,
|
||||||
|
uid,
|
||||||
|
displayMeasurements: items,
|
||||||
|
items,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ml-2 inline-flex items-center space-x-1 ${classNames}`}>
|
||||||
|
{/* Visibility Toggle Icon */}
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className={`h-6 w-6 transition-opacity ${
|
||||||
|
isSelected || !isVisible ? 'opacity-100' : 'opacity-50 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
aria-label={isVisible ? 'Hide' : 'Show'}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAction(e, ['jumpToMeasurement', 'toggleVisibilityMeasurement']);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
|
||||||
|
</Button>
|
||||||
|
{/* Actions Dropdown Menu */}
|
||||||
|
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className={`h-6 w-6 transition-opacity ${
|
||||||
|
isSelected || isDropdownOpen ? 'opacity-100' : 'opacity-50 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
aria-label="Actions"
|
||||||
|
onClick={e => e.stopPropagation()} // Prevent row selection on button click
|
||||||
|
>
|
||||||
|
<Icons.More className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={e => onAction(e, 'removeMeasurement')}>
|
||||||
|
<Icons.Delete className="text-foreground" />
|
||||||
|
<span className="pl-2">Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasumentsMenu;
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AccordionGroup from './AccordionGroup';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
import MeasurementTableNested from './MeasurementTableNested';
|
||||||
|
|
||||||
|
const { filterNot, filterAdditionalFindings } = utils.MeasurementFilters;
|
||||||
|
|
||||||
|
export const MeasurementOrAdditionalFindingSets = [
|
||||||
|
{
|
||||||
|
title: 'Measurements',
|
||||||
|
filter: filterNot(filterAdditionalFindings),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Additional Findings',
|
||||||
|
filter: filterAdditionalFindings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups measurements by study in order to allow display and saving by study
|
||||||
|
* @param {Object} servicesManager
|
||||||
|
*/
|
||||||
|
export const groupByNamedSets = (items, grouping) => {
|
||||||
|
const groups = new Map();
|
||||||
|
const { namedSets } = grouping;
|
||||||
|
|
||||||
|
for (const namedSet of namedSets) {
|
||||||
|
const name = namedSet.id || namedSet.title;
|
||||||
|
groups.set(name, {
|
||||||
|
...grouping,
|
||||||
|
...namedSet,
|
||||||
|
items: [],
|
||||||
|
isFirst: groups.size === 0,
|
||||||
|
title: name,
|
||||||
|
key: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.forEach(item => {
|
||||||
|
for (const namedSet of namedSets) {
|
||||||
|
if (namedSet.filter(item)) {
|
||||||
|
const name = namedSet.id || namedSet.title;
|
||||||
|
groups.get(name).items.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const namedSet of namedSets) {
|
||||||
|
const name = namedSet.id || namedSet.title;
|
||||||
|
if (!groups.get(name).items.length) {
|
||||||
|
groups.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
|
||||||
|
const { items, children, grouping = {}, customHeader, group } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionGroup
|
||||||
|
grouping={{
|
||||||
|
groupingFunction: groupByNamedSets,
|
||||||
|
name: 'measurementsOrAdditional',
|
||||||
|
namedSets: MeasurementOrAdditionalFindingSets,
|
||||||
|
StudyInstanceUID: group?.StudyInstanceUID,
|
||||||
|
...grouping,
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
|
sourceChildren={children}
|
||||||
|
>
|
||||||
|
<AccordionGroup.Accordion noWrapper="true">
|
||||||
|
<MeasurementTableNested
|
||||||
|
customHeader={customHeader}
|
||||||
|
allItems={items}
|
||||||
|
/>
|
||||||
|
</AccordionGroup.Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasurementsOrAdditionalFindings;
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AccordionTrigger, ColorCircle } from '@ohif/ui-next';
|
||||||
|
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
function onClickDefault(e) {
|
||||||
|
const { group, onClick = group?.onClick } = this;
|
||||||
|
if (!onClick) {
|
||||||
|
console.log('No onClick function', group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('onClickDefault');
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
onClick(e, group);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PanelAccordionTrigger(props) {
|
||||||
|
const { marginLeft = 8, isActive = false, colorHex, count, text, menu: Menu = null } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionTrigger
|
||||||
|
style={{ marginLeft: `${marginLeft}px`, padding: 0 }}
|
||||||
|
asChild={true}
|
||||||
|
>
|
||||||
|
<div className={`inline-flex text-base ${isActive ? 'bg-popover' : 'bg-muted'} flex-grow`}>
|
||||||
|
<button onClick={onClickDefault.bind(props)}>
|
||||||
|
<span
|
||||||
|
className={`inline-flex rounded-l border-r border-black ${isActive ? 'bg-highlight' : 'bg-muted'}`}
|
||||||
|
>
|
||||||
|
{count !== undefined ? <span className="px-2">{count}</span> : null}
|
||||||
|
{colorHex && <ColorCircle colorHex={colorHex} />}
|
||||||
|
</span>
|
||||||
|
<span>{text}</span>
|
||||||
|
</button>
|
||||||
|
{Menu && (
|
||||||
|
<Menu
|
||||||
|
{...props}
|
||||||
|
classNames="justify-end flex-grow"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
extensions/cornerstone/src/components/SeriesMeasurements.tsx
Normal file
90
extensions/cornerstone/src/components/SeriesMeasurements.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useActiveViewportDisplaySets, useSystem } from '@ohif/core';
|
||||||
|
|
||||||
|
import AccordionGroup from './AccordionGroup';
|
||||||
|
import PanelAccordionTrigger from './PanelAccordionTrigger';
|
||||||
|
import MeasurementItems from './MeasurementItems';
|
||||||
|
import MeasurementsMenu from './MeasurementsMenu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups measurements by study in order to allow display and saving by study
|
||||||
|
* @param {Object} servicesManager
|
||||||
|
*/
|
||||||
|
export const groupByDisplaySet = (items, grouping, childProps) => {
|
||||||
|
const groups = new Map();
|
||||||
|
const { displaySetService } = childProps.servicesManager.services;
|
||||||
|
const { activeDisplaySetInstanceUID } = grouping;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const { displaySetInstanceUID } = item;
|
||||||
|
|
||||||
|
if (!groups.has(displaySetInstanceUID)) {
|
||||||
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
groups.set(displaySetInstanceUID, {
|
||||||
|
header: null,
|
||||||
|
isSelected: displaySetInstanceUID == activeDisplaySetInstanceUID,
|
||||||
|
...grouping,
|
||||||
|
items: [],
|
||||||
|
key: displaySetInstanceUID,
|
||||||
|
title: 'Series Measurements',
|
||||||
|
displaySet,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.get(displaySetInstanceUID).items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SeriesMeasurementTrigger(props) {
|
||||||
|
const { group, isSelected, displaySet, menu } = props;
|
||||||
|
const { SeriesNumber = 1, SeriesDescription } = displaySet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelAccordionTrigger
|
||||||
|
text={`Series #${SeriesNumber} ${SeriesDescription}`}
|
||||||
|
count={group.items.length}
|
||||||
|
isActive={isSelected}
|
||||||
|
group={group}
|
||||||
|
menu={menu}
|
||||||
|
marginLeft="0"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SeriesMeasurements(props): React.ReactNode {
|
||||||
|
const { items, grouping = {}, children } = props;
|
||||||
|
const system = useSystem();
|
||||||
|
const activeDisplaySets = useActiveViewportDisplaySets(system);
|
||||||
|
const activeDisplaySetInstanceUID = activeDisplaySets?.[0]?.displaySetInstanceUID;
|
||||||
|
const onClick = (_e, group) => {
|
||||||
|
const { items } = group;
|
||||||
|
system.commandsManager.run('jumpToMeasurement', {
|
||||||
|
uid: items[0].uid,
|
||||||
|
displayMeasurements: items,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The content of the accordion group will default to the children of the
|
||||||
|
// parent declaration if present, otherwise to MeasurementItems
|
||||||
|
return (
|
||||||
|
<AccordionGroup
|
||||||
|
grouping={{
|
||||||
|
groupingFunction: groupByDisplaySet,
|
||||||
|
activeDisplaySetInstanceUID,
|
||||||
|
...grouping,
|
||||||
|
onClick,
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
|
sourceChildren={children}
|
||||||
|
>
|
||||||
|
<AccordionGroup.Trigger asChild={true}>
|
||||||
|
<SeriesMeasurementTrigger menu={MeasurementsMenu} />
|
||||||
|
</AccordionGroup.Trigger>
|
||||||
|
<MeasurementItems />
|
||||||
|
</AccordionGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeriesMeasurements;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
const { formatDate } = utils;
|
||||||
|
|
||||||
|
export function SeriesSummaryFromDisplaySet({ displaySet }) {
|
||||||
|
if (!displaySet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { SeriesDate, SeriesDescription, SeriesNumber = 1 } = displaySet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-2 my-0">
|
||||||
|
<div className="text-foreground pb-1 text-sm">
|
||||||
|
Series #{SeriesNumber} {SeriesDescription}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">{formatDate(SeriesDate)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
extensions/cornerstone/src/components/StudyMeasurements.tsx
Normal file
80
extensions/cornerstone/src/components/StudyMeasurements.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useActiveViewportDisplaySets, useSystem } from '@ohif/core';
|
||||||
|
// import { AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
|
||||||
|
|
||||||
|
import { AccordionGroup } from './AccordionGroup';
|
||||||
|
import MeasurementsOrAdditionalFindings from './MeasurementsOrAdditionalFindings';
|
||||||
|
import StudySummaryWithActions from './StudySummaryWithActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups measurements by study in order to allow display and saving by study
|
||||||
|
* @param {Object} servicesManager
|
||||||
|
*/
|
||||||
|
export const groupByStudy = (items, grouping, childProps) => {
|
||||||
|
const groups = new Map();
|
||||||
|
const { activeStudyUID } = grouping;
|
||||||
|
const { displaySetService } = childProps.servicesManager.services;
|
||||||
|
|
||||||
|
const getItemStudyInstanceUID = item => {
|
||||||
|
const displaySet = displaySetService.getDisplaySetByUID(item.displaySetInstanceUID);
|
||||||
|
return displaySet.instances[0].StudyInstanceUID;
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstSelected, firstGroup;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const studyUID = getItemStudyInstanceUID(item);
|
||||||
|
if (!groups.has(studyUID)) {
|
||||||
|
const items = [];
|
||||||
|
const group = {
|
||||||
|
...grouping,
|
||||||
|
items,
|
||||||
|
displayMeasurements: items,
|
||||||
|
key: studyUID,
|
||||||
|
isSelected: studyUID === activeStudyUID,
|
||||||
|
StudyInstanceUID: activeStudyUID,
|
||||||
|
};
|
||||||
|
if (group.isSelected && !firstSelected) {
|
||||||
|
firstSelected = group;
|
||||||
|
}
|
||||||
|
firstGroup ||= group;
|
||||||
|
groups.set(studyUID, group);
|
||||||
|
}
|
||||||
|
if (!firstSelected && firstGroup) {
|
||||||
|
firstGroup.isSelected = true;
|
||||||
|
}
|
||||||
|
const group = groups.get(studyUID);
|
||||||
|
group.items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StudyMeasurements(props): React.ReactNode {
|
||||||
|
const { items, grouping = {}, children } = props;
|
||||||
|
|
||||||
|
const system = useSystem();
|
||||||
|
const activeDisplaySets = useActiveViewportDisplaySets(system);
|
||||||
|
const activeStudyUID = activeDisplaySets?.[0]?.StudyInstanceUID;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionGroup
|
||||||
|
grouping={{
|
||||||
|
name: 'groupByStudy',
|
||||||
|
groupingFunction: groupByStudy,
|
||||||
|
activeStudyUID,
|
||||||
|
...grouping,
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
|
value={[activeStudyUID]}
|
||||||
|
sourceChildren={children}
|
||||||
|
>
|
||||||
|
<AccordionGroup.Trigger>
|
||||||
|
<StudySummaryWithActions />
|
||||||
|
</AccordionGroup.Trigger>
|
||||||
|
<MeasurementsOrAdditionalFindings activeStudyUID={activeStudyUID} />
|
||||||
|
</AccordionGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StudyMeasurements;
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Icons } from '@ohif/ui-next';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
|
export function StudyMeasurementsActions({ items, StudyInstanceUID, measurementFilter, actions }) {
|
||||||
|
const { commandsManager } = useSystem();
|
||||||
|
const disabled = !items?.length;
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-background flex h-9 w-full items-center rounded pr-0.5">
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="pl-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
commandsManager.runCommand('downloadCSVMeasurementsReport', {
|
||||||
|
StudyInstanceUID,
|
||||||
|
measurementFilter,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Download className="h-5 w-5" />
|
||||||
|
<span className="pl-1">CSV</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="pl-0.5"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (actions?.createSR) {
|
||||||
|
actions.createSR({ StudyInstanceUID, measurementFilter });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commandsManager.run('promptSaveReport', {
|
||||||
|
StudyInstanceUID,
|
||||||
|
measurementFilter,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Add />
|
||||||
|
Create SR
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="pl-0.5"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
commandsManager.runCommand('clearMeasurements', {
|
||||||
|
measurementFilter,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Delete />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StudyMeasurementsActions;
|
||||||
@ -4,7 +4,8 @@ import { StudySummary } from '@ohif/ui-next';
|
|||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
export function StudySummaryFromMetadata({ StudyInstanceUID }) {
|
export function StudySummaryFromMetadata(props) {
|
||||||
|
const { StudyInstanceUID } = props;
|
||||||
if (!StudyInstanceUID) {
|
if (!StudyInstanceUID) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -20,6 +21,6 @@ export function StudySummaryFromMetadata({ StudyInstanceUID }) {
|
|||||||
<StudySummary
|
<StudySummary
|
||||||
date={formatDate(StudyDate)}
|
date={formatDate(StudyDate)}
|
||||||
description={StudyDescription}
|
description={StudyDescription}
|
||||||
></StudySummary>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StudySummaryFromMetadata } from './StudySummaryFromMetadata';
|
||||||
|
import StudyMeasurementsActions from './StudyMeasurementsActions';
|
||||||
|
|
||||||
|
export function StudySummaryWithActions(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StudySummaryFromMetadata {...props} />
|
||||||
|
<StudyMeasurementsActions {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StudySummaryWithActions;
|
||||||
10
extensions/cornerstone/src/components/index.ts
Normal file
10
extensions/cornerstone/src/components/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export * from './AccordionGroup';
|
||||||
|
export * from './MeasurementTableNested';
|
||||||
|
export * from './StudyMeasurements';
|
||||||
|
export * from './MeasurementsMenu';
|
||||||
|
export * from './SeriesMeasurements';
|
||||||
|
export * from './StudyMeasurementsActions';
|
||||||
|
export * from './MeasurementsOrAdditionalFindings';
|
||||||
|
import DicomUpload from './DicomUpload/DicomUpload';
|
||||||
|
|
||||||
|
export { DicomUpload };
|
||||||
@ -51,19 +51,6 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedPanelMeasurement = ({ configuration }) => {
|
|
||||||
return (
|
|
||||||
<PanelMeasurement
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
extensionManager={extensionManager}
|
|
||||||
configuration={{
|
|
||||||
...configuration,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'activeViewportWindowLevel',
|
name: 'activeViewportWindowLevel',
|
||||||
@ -76,7 +63,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
iconName: 'tab-linear',
|
iconName: 'tab-linear',
|
||||||
iconLabel: 'Measure',
|
iconLabel: 'Measure',
|
||||||
label: 'Measurement',
|
label: 'Measurement',
|
||||||
component: wrappedPanelMeasurement,
|
component: PanelMeasurement,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'panelSegmentation',
|
name: 'panelSegmentation',
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledEle
|
|||||||
|
|
||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||||
|
import PlanarFreehandROI from './utils/measurementServiceMappings/PlanarFreehandROI';
|
||||||
|
import RectangleROI from './utils/measurementServiceMappings/RectangleROI';
|
||||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
@ -51,11 +53,11 @@ import { useMeasurements } from './hooks/useMeasurements';
|
|||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
import PanelMeasurement from './panels/PanelMeasurement';
|
import PanelMeasurement from './panels/PanelMeasurement';
|
||||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
|
||||||
import { useSegmentations } from './hooks/useSegmentations';
|
import { useSegmentations } from './hooks/useSegmentations';
|
||||||
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
export * from './components';
|
||||||
|
|
||||||
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
||||||
|
|
||||||
@ -234,6 +236,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
export type { PublicViewportOptions };
|
export type { PublicViewportOptions };
|
||||||
export {
|
export {
|
||||||
measurementMappingUtils,
|
measurementMappingUtils,
|
||||||
|
PlanarFreehandROI,
|
||||||
|
RectangleROI,
|
||||||
CornerstoneExtensionTypes as Types,
|
CornerstoneExtensionTypes as Types,
|
||||||
toolNames,
|
toolNames,
|
||||||
getActiveViewportEnabledElement,
|
getActiveViewportEnabledElement,
|
||||||
@ -254,7 +258,6 @@ export {
|
|||||||
useSegmentations,
|
useSegmentations,
|
||||||
PanelSegmentation,
|
PanelSegmentation,
|
||||||
PanelMeasurement,
|
PanelMeasurement,
|
||||||
DicomUpload,
|
|
||||||
StudySummaryFromMetadata,
|
StudySummaryFromMetadata,
|
||||||
CornerstoneViewportDownloadForm,
|
CornerstoneViewportDownloadForm,
|
||||||
utils,
|
utils,
|
||||||
|
|||||||
@ -1,107 +1,103 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React from 'react';
|
||||||
import { utils } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
import { MeasurementTable } from '@ohif/ui-next';
|
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import { useMeasurements } from '../hooks/useMeasurements';
|
import { useMeasurements } from '../hooks/useMeasurements';
|
||||||
|
import StudyMeasurements from '../components/StudyMeasurements';
|
||||||
|
/**
|
||||||
|
* The PanelMeasurement is a fairly simple wrapper that gets the filtered
|
||||||
|
* measurements and then passes it on to the children component, default to
|
||||||
|
* the StudyMeasurements sub-component if no children are specified.
|
||||||
|
* Some example customizations that could work:
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Creates a default study measurements panel with default children:
|
||||||
|
* ```
|
||||||
|
* <PanelMEasurement>
|
||||||
|
* <StudyMeasurements />
|
||||||
|
* </PanelMeasurement>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* A study measurements with body replacement
|
||||||
|
* ```
|
||||||
|
* <StudyMeasurements>
|
||||||
|
* <SeriesMeasurements />
|
||||||
|
* </StudyMeasurements>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* A study measurements replacing just the trigger, leaving the default body
|
||||||
|
* ```
|
||||||
|
* <StudyMeasurements>
|
||||||
|
* <AccordionGroup.Trigger>
|
||||||
|
* This is a new custom trigger
|
||||||
|
* </AccordionGroup.Trigger>
|
||||||
|
*</StudyMeasurements>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* A study measurements with the trigger and body replaced
|
||||||
|
* ```
|
||||||
|
* <StudyMeasurements>
|
||||||
|
* <AccordionGroup.Trigger>
|
||||||
|
* This is a new custom trigger
|
||||||
|
* </AccordionGroup.Trigger>
|
||||||
|
* <SeriesMeasurements />
|
||||||
|
* </StudyMeasurements>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* A study measurements with a custom header for the additional findings
|
||||||
|
* ```
|
||||||
|
* <StudyMeasurements>
|
||||||
|
* <MeasurementOrAdditionalFindings>
|
||||||
|
* <AccordionGroup.Trigger groupName="additionalFindings">
|
||||||
|
* <CustomAdditionalFindingsHeader />
|
||||||
|
* </AccordionGroup.Trigger>
|
||||||
|
* <AccordionGroup.Trigger groupName="measurements">
|
||||||
|
* <CustomMeasurementsHeader />
|
||||||
|
* </AccordionGroup.Trigger>
|
||||||
|
* </MeasurementOrAdditionalFindings>
|
||||||
|
* </StudyMeasurements>
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
export default function PanelMeasurement(props): React.ReactNode {
|
||||||
|
const {
|
||||||
|
measurementFilter,
|
||||||
|
emptyComponent: EmptyComponent,
|
||||||
|
key = 'PanelMeasurement',
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const { filterAdditionalFindings: filterAdditionalFinding, filterAny } = utils.MeasurementFilters;
|
const system = useSystem();
|
||||||
|
const displayMeasurements = useMeasurements(system.servicesManager, {
|
||||||
export type withAppAndFilters = withAppTypes & {
|
|
||||||
measurementFilter: (item) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PanelMeasurement({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
customHeader,
|
|
||||||
measurementFilter = filterAny,
|
|
||||||
}: withAppAndFilters): React.ReactNode {
|
|
||||||
const measurementsPanelRef = useRef(null);
|
|
||||||
|
|
||||||
const { measurementService } = servicesManager.services;
|
|
||||||
|
|
||||||
const displayMeasurements = useMeasurements(servicesManager, {
|
|
||||||
measurementFilter,
|
measurementFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (!displayMeasurements.length) {
|
||||||
if (displayMeasurements.length > 0) {
|
return EmptyComponent ? (
|
||||||
debounce(() => {
|
<EmptyComponent
|
||||||
measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
|
key={key}
|
||||||
}, 300)();
|
data-cy={key}
|
||||||
}
|
items={displayMeasurements}
|
||||||
}, [displayMeasurements.length]);
|
/>
|
||||||
|
) : (
|
||||||
const bindCommand = (name: string | string[], options?) => {
|
<span className="text-white">No Measurements</span>
|
||||||
return (uid: string) => {
|
);
|
||||||
commandsManager.run(name, { ...options, uid });
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements });
|
|
||||||
const removeMeasurement = bindCommand('removeMeasurement');
|
|
||||||
const renameMeasurement = bindCommand(['jumpToMeasurement', 'renameMeasurement'], {
|
|
||||||
displayMeasurements,
|
|
||||||
});
|
|
||||||
const toggleLockMeasurement = bindCommand('toggleLockMeasurement');
|
|
||||||
const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement');
|
|
||||||
|
|
||||||
const additionalFilter = filterAdditionalFinding(measurementService);
|
|
||||||
|
|
||||||
const measurements = displayMeasurements.filter(
|
|
||||||
item => !additionalFilter(item) && measurementFilter(item)
|
|
||||||
);
|
|
||||||
const additionalFindings = displayMeasurements.filter(
|
|
||||||
item => additionalFilter(item) && measurementFilter(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onArgs = {
|
|
||||||
onClick: jumpToImage,
|
|
||||||
onDelete: removeMeasurement,
|
|
||||||
onToggleVisibility: toggleVisibilityMeasurement,
|
|
||||||
onToggleLocked: toggleLockMeasurement,
|
|
||||||
onRename: renameMeasurement,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
const cloned = React.Children.map(children, child =>
|
||||||
|
React.cloneElement(child, {
|
||||||
|
items: displayMeasurements,
|
||||||
|
filter: measurementFilter,
|
||||||
|
'data-cy': key,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
// Need to merge defaults on the content props to ensure they get passed to children
|
||||||
return (
|
return (
|
||||||
<>
|
<StudyMeasurements
|
||||||
<div
|
key={key}
|
||||||
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
|
data-cy={key}
|
||||||
ref={measurementsPanelRef}
|
items={displayMeasurements}
|
||||||
data-cy={'trackedMeasurements-panel'}
|
/>
|
||||||
>
|
|
||||||
<MeasurementTable
|
|
||||||
key="tracked"
|
|
||||||
title="Measurements"
|
|
||||||
data={measurements}
|
|
||||||
{...onArgs}
|
|
||||||
// onColor={changeColorMeasurement}
|
|
||||||
>
|
|
||||||
<MeasurementTable.Header>
|
|
||||||
{customHeader && (
|
|
||||||
<>
|
|
||||||
{typeof customHeader === 'function'
|
|
||||||
? customHeader({
|
|
||||||
additionalFindings,
|
|
||||||
measurements,
|
|
||||||
})
|
|
||||||
: customHeader}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MeasurementTable.Header>
|
|
||||||
<MeasurementTable.Body />
|
|
||||||
</MeasurementTable>
|
|
||||||
{additionalFindings.length > 0 && (
|
|
||||||
<MeasurementTable
|
|
||||||
key="additional"
|
|
||||||
data={additionalFindings}
|
|
||||||
title="Additional Findings"
|
|
||||||
{...onArgs}
|
|
||||||
>
|
|
||||||
<MeasurementTable.Body />
|
|
||||||
</MeasurementTable>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as CornerstoneCacheService from './CornerstoneCacheService';
|
import * as CornerstoneCacheService from './CornerstoneCacheService';
|
||||||
import CornerstoneServices from './CornerstoneServices';
|
import type CornerstoneServices from './CornerstoneServices';
|
||||||
|
export { type SyncGroup } from '../services/SyncGroupService';
|
||||||
|
|
||||||
export type { CornerstoneCacheService, CornerstoneServices };
|
export type { CornerstoneCacheService, CornerstoneServices };
|
||||||
|
|||||||
@ -16,7 +16,6 @@ const SegmentBidirectional = {
|
|||||||
) => {
|
) => {
|
||||||
const { annotation } = csToolsEventDetail;
|
const { annotation } = csToolsEventDetail;
|
||||||
const { metadata, data, annotationUID } = annotation;
|
const { metadata, data, annotationUID } = annotation;
|
||||||
debugger;
|
|
||||||
const isLocked = getIsLocked(annotationUID);
|
const isLocked = getIsLocked(annotationUID);
|
||||||
const isVisible = getIsVisible(annotationUID);
|
const isVisible = getIsVisible(annotationUID);
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ function DataSourceConfigurationComponent({
|
|||||||
const dataSourceChangedCallback = async () => {
|
const dataSourceChangedCallback = async () => {
|
||||||
const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition();
|
const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition();
|
||||||
|
|
||||||
if (!activeDataSourceDef.configuration.configurationAPI) {
|
if (!activeDataSourceDef?.configuration?.configurationAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export default function CreateReportDialogPrompt({
|
|||||||
dataSourceName: undefined,
|
dataSourceName: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
defaultValue: title,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocol
|
|||||||
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
||||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||||
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||||
|
import promptSaveReport from './utils/promptSaveReport';
|
||||||
|
|
||||||
export type HangingProtocolParams = {
|
export type HangingProtocolParams = {
|
||||||
protocolId?: string;
|
protocolId?: string;
|
||||||
@ -75,6 +76,14 @@ const commandsModule = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Displays a prompt and then save the report if relevant */
|
||||||
|
promptSaveReport: props => {
|
||||||
|
const { StudyInstanceUID } = props;
|
||||||
|
promptSaveReport({ servicesManager, commandsManager, extensionManager }, props, {
|
||||||
|
data: { StudyInstanceUID },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that the specified study is available for display
|
* Ensures that the specified study is available for display
|
||||||
* Then, if commands is specified, runs the given commands list/instance
|
* Then, if commands is specified, runs the given commands list/instance
|
||||||
@ -140,8 +149,9 @@ const commandsModule = ({
|
|||||||
type: type,
|
type: type,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clearMeasurements: () => {
|
|
||||||
measurementService.clearMeasurements();
|
clearMeasurements: options => {
|
||||||
|
measurementService.clearMeasurements(options.measurementFilter);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -340,7 +350,6 @@ const commandsModule = ({
|
|||||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
||||||
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
||||||
console.log('setViewportGridLayout running', onLayoutChange, numRows, numCols);
|
|
||||||
// Don't apply the layout if the run command returns false
|
// Don't apply the layout if the run command returns false
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -621,6 +630,7 @@ const commandsModule = ({
|
|||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
multimonitor: actions.multimonitor,
|
multimonitor: actions.multimonitor,
|
||||||
|
promptSaveReport: actions.promptSaveReport,
|
||||||
loadStudy: actions.loadStudy,
|
loadStudy: actions.loadStudy,
|
||||||
showContextMenu: actions.showContextMenu,
|
showContextMenu: actions.showContextMenu,
|
||||||
closeContextMenu: actions.closeContextMenu,
|
closeContextMenu: actions.closeContextMenu,
|
||||||
|
|||||||
@ -94,12 +94,12 @@ export async function callInputDialogAutoComplete({
|
|||||||
const dropDownItems = labelConfig ? labelConfig.items : [];
|
const dropDownItems = labelConfig ? labelConfig.items : [];
|
||||||
|
|
||||||
const value = await new Promise<Map<string, string>>((resolve, reject) => {
|
const value = await new Promise<Map<string, string>>((resolve, reject) => {
|
||||||
const labellingDoneCallback = value => {
|
const labellingDoneCallback = newValue => {
|
||||||
uiDialogService.hide('select-annotation');
|
uiDialogService.hide('select-annotation');
|
||||||
if (typeof value === 'string') {
|
if (measurement && typeof newValue === 'string') {
|
||||||
measurement.label = value;
|
measurement.label = newValue;
|
||||||
}
|
}
|
||||||
resolve(measurement);
|
resolve(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
uiDialogService.show({
|
uiDialogService.show({
|
||||||
|
|||||||
@ -1,58 +1,76 @@
|
|||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
import createReportAsync from '../Actions/createReportAsync';
|
import createReportAsync from '../Actions/createReportAsync';
|
||||||
|
import { createReportDialogPrompt } from '../Panels';
|
||||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||||
import createReportDialogPrompt from '../Panels/createReportDialogPrompt';
|
|
||||||
|
|
||||||
/**
|
const {
|
||||||
* Prompts the user to save a report and handles the report creation process
|
filterAnd,
|
||||||
* @param services - Object containing required services and managers
|
filterMeasurementsByStudyUID,
|
||||||
* @param ctx - The current context containing tracked study and series information
|
filterMeasurementsBySeriesUID,
|
||||||
* @param evt - The event object containing viewport and save-related data
|
filterPlanarMeasurement,
|
||||||
*/
|
} = utils.MeasurementFilters;
|
||||||
async function promptSaveReport(services, ctx, evt) {
|
|
||||||
const { servicesManager, extensionManager, commandsManager } = services;
|
|
||||||
|
|
||||||
|
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
|
||||||
const { measurementService, displaySetService } = servicesManager.services;
|
const { measurementService, displaySetService } = servicesManager.services;
|
||||||
|
const viewportId = evt.viewportId === undefined ? evt.data.viewportId : evt.viewportId;
|
||||||
const viewportId = evt.viewportId ?? evt.data?.viewportId;
|
const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
|
||||||
const isBackupSave = evt.isBackupSave ?? evt.data?.isBackupSave;
|
const StudyInstanceUID = evt?.data?.StudyInstanceUID || ctx.trackedStudy;
|
||||||
const { StudyInstanceUID, SeriesInstanceUID } = evt?.data ?? {};
|
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
|
||||||
|
|
||||||
const { trackedStudy, trackedSeries } = ctx;
|
|
||||||
const dataSources = extensionManager.getDataSources();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value: reportName,
|
trackedSeries,
|
||||||
dataSourceName: dataSource,
|
measurementFilter = filterAnd(
|
||||||
action,
|
filterMeasurementsByStudyUID(StudyInstanceUID),
|
||||||
} = await createReportDialogPrompt({
|
filterMeasurementsBySeriesUID(trackedSeries),
|
||||||
servicesManager,
|
filterPlanarMeasurement
|
||||||
extensionManager,
|
),
|
||||||
});
|
defaultSaveTitle = 'Research Derived Series',
|
||||||
|
} = ctx;
|
||||||
let displaySetInstanceUIDs;
|
let displaySetInstanceUIDs;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
|
const promptResult = await createReportDialogPrompt({
|
||||||
const selectedDataSource = dataSource ?? dataSources[0];
|
title: defaultSaveTitle,
|
||||||
const trackedMeasurements = getTrackedMeasurements(
|
extensionManager,
|
||||||
measurementService,
|
servicesManager,
|
||||||
trackedStudy,
|
});
|
||||||
trackedSeries
|
|
||||||
);
|
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||||
|
const dataSources = extensionManager.getDataSources();
|
||||||
|
const dataSource = dataSources[0];
|
||||||
|
const measurementData = measurementService.getMeasurements(measurementFilter);
|
||||||
|
|
||||||
|
const SeriesDescription = promptResult.value || defaultSaveTitle;
|
||||||
|
|
||||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||||
displaySetInstanceUIDs = await handleReportCreation({
|
|
||||||
|
const getReport = async () => {
|
||||||
|
return commandsManager.runCommand(
|
||||||
|
'storeMeasurements',
|
||||||
|
{
|
||||||
|
measurementData,
|
||||||
|
dataSource,
|
||||||
|
additionalFindingTypes: ['ArrowAnnotate'],
|
||||||
|
options: {
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'CORNERSTONE_STRUCTURED_REPORT'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
displaySetInstanceUIDs = await createReportAsync({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
getReport,
|
||||||
trackedMeasurements,
|
|
||||||
selectedDataSource,
|
|
||||||
reportName,
|
|
||||||
SeriesNumber,
|
|
||||||
});
|
});
|
||||||
|
} else if (promptResult.action === RESPONSE.CANCEL) {
|
||||||
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userResponse: action,
|
userResponse: promptResult.action,
|
||||||
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
@ -60,50 +78,9 @@ async function promptSaveReport(services, ctx, evt) {
|
|||||||
isBackupSave,
|
isBackupSave,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn('Unable to save report', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets tracked measurements based on study and series criteria
|
|
||||||
*/
|
|
||||||
function getTrackedMeasurements(measurementService, trackedStudy, trackedSeries) {
|
|
||||||
return measurementService
|
|
||||||
.getMeasurements()
|
|
||||||
.filter(
|
|
||||||
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
|
|
||||||
)
|
|
||||||
.filter(m => m.referencedImageId != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the creation of the report using the measurement service
|
|
||||||
*/
|
|
||||||
async function handleReportCreation({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
trackedMeasurements,
|
|
||||||
selectedDataSource,
|
|
||||||
reportName,
|
|
||||||
SeriesNumber,
|
|
||||||
}) {
|
|
||||||
return createReportAsync({
|
|
||||||
servicesManager,
|
|
||||||
getReport: () =>
|
|
||||||
commandsManager.runCommand(
|
|
||||||
'storeMeasurements',
|
|
||||||
{
|
|
||||||
measurementData: trackedMeasurements,
|
|
||||||
dataSource: selectedDataSource,
|
|
||||||
additionalFindingTypes: ['ArrowAnnotate'],
|
|
||||||
options: {
|
|
||||||
SeriesDescription: reportName,
|
|
||||||
SeriesNumber,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'CORNERSTONE_STRUCTURED_REPORT'
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default promptSaveReport;
|
export default promptSaveReport;
|
||||||
|
|||||||
@ -25,6 +25,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
|
|||||||
component: props => (
|
component: props => (
|
||||||
<PanelMeasurementTableTracking
|
<PanelMeasurementTableTracking
|
||||||
{...props}
|
{...props}
|
||||||
|
key="trackedMeasurements-panel"
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
|
|||||||
@ -1,90 +1,81 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { AccordionTrigger, MeasurementTable, useViewportGrid } from '@ohif/ui-next';
|
||||||
import { Button, Icons } from '@ohif/ui-next';
|
import {
|
||||||
import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone';
|
PanelMeasurement,
|
||||||
|
StudyMeasurements,
|
||||||
|
StudyMeasurementsActions,
|
||||||
|
StudySummaryFromMetadata,
|
||||||
|
AccordionGroup,
|
||||||
|
MeasurementsOrAdditionalFindings,
|
||||||
|
} from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
import { useTrackedMeasurements } from '../getContextModule';
|
import { useTrackedMeasurements } from '../getContextModule';
|
||||||
|
|
||||||
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
|
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
|
||||||
utils.MeasurementFilters;
|
utils.MeasurementFilters;
|
||||||
|
|
||||||
function PanelMeasurementTableTracking({
|
function PanelMeasurementTableTracking(props) {
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
commandsManager,
|
|
||||||
}: withAppTypes) {
|
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
const { customizationService } = servicesManager.services;
|
|
||||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||||
const measurementFilter = trackedStudy
|
const measurementFilter = trackedStudy
|
||||||
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
|
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
|
||||||
: filterPlanarMeasurement;
|
: filterPlanarMeasurement;
|
||||||
|
|
||||||
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
|
const EmptyComponent = () => (
|
||||||
|
<MeasurementTable
|
||||||
|
title="Measurements"
|
||||||
|
isExpanded={false}
|
||||||
|
>
|
||||||
|
<MeasurementTable.Body />
|
||||||
|
</MeasurementTable>
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
createSR: ({ StudyInstanceUID }) => {
|
||||||
|
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
||||||
|
viewportId: viewportGrid.activeViewportId,
|
||||||
|
isBackupSave: true,
|
||||||
|
StudyInstanceUID,
|
||||||
|
measurementFilter,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = props => (
|
||||||
|
<AccordionTrigger
|
||||||
|
asChild={true}
|
||||||
|
className="px-0"
|
||||||
|
>
|
||||||
|
<div data-cy="TrackingHeader">
|
||||||
|
<StudySummaryFromMetadata {...props} />
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PanelMeasurement
|
||||||
<StudySummaryFromMetadata StudyInstanceUID={trackedStudy} />
|
measurementFilter={measurementFilter}
|
||||||
<PanelMeasurement
|
emptyComponent={EmptyComponent}
|
||||||
servicesManager={servicesManager}
|
sourceChildren={props.children}
|
||||||
extensionManager={extensionManager}
|
>
|
||||||
commandsManager={commandsManager}
|
<StudyMeasurements grouping={props.grouping}>
|
||||||
measurementFilter={measurementFilter}
|
<AccordionGroup.Trigger
|
||||||
customHeader={({ additionalFindings, measurements }) => {
|
key="trackingMeasurementsHeader"
|
||||||
const disabled = additionalFindings.length === 0 && measurements.length === 0;
|
asChild={true}
|
||||||
|
>
|
||||||
if (disableEditing || disabled) {
|
<Header key="trackingHeadChild" />
|
||||||
return null;
|
</AccordionGroup.Trigger>
|
||||||
}
|
<MeasurementsOrAdditionalFindings
|
||||||
|
key="measurementsOrAdditionalFindings"
|
||||||
return (
|
activeStudyUID={trackedStudy}
|
||||||
<div className="bg-background flex h-9 w-full items-center rounded pr-0.5">
|
customHeader={StudyMeasurementsActions}
|
||||||
<div className="flex space-x-1">
|
measurementFilter={measurementFilter}
|
||||||
<Button
|
actions={actions}
|
||||||
size="sm"
|
/>
|
||||||
variant="ghost"
|
</StudyMeasurements>
|
||||||
className="pl-1.5"
|
</PanelMeasurement>
|
||||||
onClick={() => {
|
|
||||||
commandsManager.runCommand('downloadCSVMeasurementsReport', {
|
|
||||||
measurementFilter,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.Download className="h-5 w-5" />
|
|
||||||
<span className="pl-1">CSV</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="pl-0.5"
|
|
||||||
onClick={() => {
|
|
||||||
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
|
||||||
viewportId: viewportGrid.activeViewportId,
|
|
||||||
isBackupSave: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.Add />
|
|
||||||
Create SR
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="pl-0.5"
|
|
||||||
onClick={() => {
|
|
||||||
commandsManager.runCommand('clearMeasurements', { measurementFilter });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.Delete />
|
|
||||||
Delete All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
></PanelMeasurement>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
extensions/test-extension/src/getPanelModule.tsx
Normal file
36
extensions/test-extension/src/getPanelModule.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
PanelMeasurement,
|
||||||
|
SeriesMeasurements,
|
||||||
|
StudyMeasurements,
|
||||||
|
} from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
|
export default function getPanelModule({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
}: withAppTypes) {
|
||||||
|
const childProps = {
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
};
|
||||||
|
const wrappedPanelMeasurementSeries = () => {
|
||||||
|
return (
|
||||||
|
<PanelMeasurement {...childProps}>
|
||||||
|
<StudyMeasurements>
|
||||||
|
<SeriesMeasurements />
|
||||||
|
</StudyMeasurements>
|
||||||
|
</PanelMeasurement>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'panelMeasurementSeries',
|
||||||
|
iconName: 'tool-freehand-roi',
|
||||||
|
iconLabel: 'Measure Series',
|
||||||
|
label: 'Measurement Series',
|
||||||
|
component: wrappedPanelMeasurementSeries,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import getCustomizationModule from './getCustomizationModule';
|
|||||||
import sameAs from './custom-attribute/sameAs';
|
import sameAs from './custom-attribute/sameAs';
|
||||||
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
|
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
|
||||||
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
|
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
|
||||||
|
import getPanelModule from './getPanelModule';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test extension provides additional behavior for testing various
|
* The test extension provides additional behavior for testing various
|
||||||
@ -50,6 +51,8 @@ const testExtension: Types.Extensions.Extension = {
|
|||||||
/** Registers some customizations */
|
/** Registers some customizations */
|
||||||
getCustomizationModule,
|
getCustomizationModule,
|
||||||
|
|
||||||
|
getPanelModule,
|
||||||
|
|
||||||
getHangingProtocolModule: () => {
|
getHangingProtocolModule: () => {
|
||||||
return [
|
return [
|
||||||
// Create a MxN hanging protocol available by default
|
// Create a MxN hanging protocol available by default
|
||||||
|
|||||||
@ -15,6 +15,10 @@ const ohif = {
|
|||||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testExtension = {
|
||||||
|
measurements: '@ohif/extension-test.panelModule.panelMeasurementSeries',
|
||||||
|
};
|
||||||
|
|
||||||
const tracked = {
|
const tracked = {
|
||||||
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
|
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
|
||||||
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
|
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
|
||||||
@ -199,7 +203,7 @@ function modeFactory() {
|
|||||||
props: {
|
props: {
|
||||||
leftPanels: [tracked.thumbnailList],
|
leftPanels: [tracked.thumbnailList],
|
||||||
leftPanelResizable: true,
|
leftPanelResizable: true,
|
||||||
rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements],
|
rightPanels: [cornerstone.panel, tracked.measurements, testExtension.measurements],
|
||||||
rightPanelResizable: true,
|
rightPanelResizable: true,
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@ window.config = {
|
|||||||
name: 'config/kheops.js',
|
name: 'config/kheops.js',
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
extensions: [],
|
extensions: [],
|
||||||
modes: [],
|
modes: ['@ohif/mode-test'],
|
||||||
customizationService: {},
|
customizationService: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
// some windows systems have issues with more than 3 web workers
|
// some windows systems have issues with more than 3 web workers
|
||||||
@ -26,7 +26,7 @@ window.config = {
|
|||||||
// filterQueryParam: false,
|
// filterQueryParam: false,
|
||||||
// Uses the ohif datasource as the default - this requires that KHEOPS be
|
// Uses the ohif datasource as the default - this requires that KHEOPS be
|
||||||
// configured with an OHIF path to .../viewer/dicomwebproxy
|
// configured with an OHIF path to .../viewer/dicomwebproxy
|
||||||
defaultDataSourceName: 'ohif3',
|
defaultDataSourceName: 'dicomweb',
|
||||||
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
|
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
|
||||||
// dangerouslyUseDynamicConfig: {
|
// dangerouslyUseDynamicConfig: {
|
||||||
// enabled: true,
|
// enabled: true,
|
||||||
|
|||||||
@ -9,9 +9,7 @@ import { DisplaySet } from '../types';
|
|||||||
* @returns Array of display sets for the active viewport
|
* @returns Array of display sets for the active viewport
|
||||||
*/
|
*/
|
||||||
const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
|
const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
|
||||||
const [displaySets, setDisplaySets] = useState<DisplaySet[]>([]);
|
|
||||||
const { displaySetService, viewportGridService } = servicesManager.services;
|
const { displaySetService, viewportGridService } = servicesManager.services;
|
||||||
|
|
||||||
// Move this function outside useEffect and memoize it
|
// Move this function outside useEffect and memoize it
|
||||||
const getDisplaySetsForViewport = useCallback(
|
const getDisplaySetsForViewport = useCallback(
|
||||||
(viewportId: string) => {
|
(viewportId: string) => {
|
||||||
@ -21,19 +19,22 @@ const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
|
|||||||
[displaySetService, viewportGridService]
|
[displaySetService, viewportGridService]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Get initial state
|
||||||
// Get initial state
|
const viewportId = viewportGridService.getActiveViewportId();
|
||||||
const viewportId = viewportGridService.getActiveViewportId();
|
const displaySetsNew = getDisplaySetsForViewport(viewportId) || [];
|
||||||
setDisplaySets(getDisplaySetsForViewport(viewportId));
|
const [displaySets, setDisplaySets] = useState<DisplaySet[]>(displaySetsNew);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const handleViewportChange = ({ viewportId }) => {
|
const handleViewportChange = ({ viewportId }) => {
|
||||||
setDisplaySets(getDisplaySetsForViewport(viewportId));
|
const displaySetsNew = getDisplaySetsForViewport(viewportId);
|
||||||
|
setDisplaySets(displaySetsNew);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGridStateChange = ({ state }) => {
|
const handleGridStateChange = ({ state }) => {
|
||||||
const activeViewportId = state.activeViewportId;
|
const activeViewportId = state.activeViewportId;
|
||||||
if (activeViewportId) {
|
if (activeViewportId) {
|
||||||
setDisplaySets(getDisplaySetsForViewport(activeViewportId));
|
const displaySetsNew = getDisplaySetsForViewport(activeViewportId);
|
||||||
|
setDisplaySets(displaySetsNew);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -405,11 +405,14 @@ class MeasurementService extends PubSubService {
|
|||||||
let measurement = {};
|
let measurement = {};
|
||||||
try {
|
try {
|
||||||
measurement = toMeasurementSchema(data);
|
measurement = toMeasurementSchema(data);
|
||||||
|
if (!measurement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
measurement.source = source;
|
measurement.source = source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}:`,
|
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}:`,
|
||||||
error.message
|
error
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -589,6 +592,30 @@ class MeasurementService extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove multiple measurements at once.
|
||||||
|
*/
|
||||||
|
removeMany(measurementUIDs: string[]): void {
|
||||||
|
const measurements = [];
|
||||||
|
for (const measurementUID of measurementUIDs) {
|
||||||
|
const measurement =
|
||||||
|
this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
|
||||||
|
|
||||||
|
if (!measurementUID || !measurement) {
|
||||||
|
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unmappedMeasurements.delete(measurementUID);
|
||||||
|
this.measurements.delete(measurementUID);
|
||||||
|
measurements.push(measurement);
|
||||||
|
}
|
||||||
|
if (!measurements.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears measurements that match the filter, defaulting to all of them.
|
* Clears measurements that match the filter, defaulting to all of them.
|
||||||
* That allows, for example, clearing all of a single studies measurements
|
* That allows, for example, clearing all of a single studies measurements
|
||||||
@ -769,7 +796,7 @@ class MeasurementService extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleVisibilityMeasurement(measurementUID: string): void {
|
public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement = this.measurements.get(measurementUID);
|
||||||
|
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
@ -777,7 +804,10 @@ class MeasurementService extends PubSubService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
measurement.isVisible = !measurement.isVisible;
|
if (measurement.isVisible === visibility && visibility !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
measurement.isVisible = visibility !== undefined ? visibility : !measurement.isVisible;
|
||||||
|
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
source: measurement.source,
|
source: measurement.source,
|
||||||
@ -786,6 +816,10 @@ class MeasurementService extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toggleVisibilityMeasurementMany(measurementUIDs: string[], visibility?: boolean): void {
|
||||||
|
return measurementUIDs.forEach(uid => this.toggleVisibilityMeasurement(uid, visibility));
|
||||||
|
}
|
||||||
|
|
||||||
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement = this.measurements.get(measurementUID);
|
||||||
|
|
||||||
|
|||||||
@ -130,6 +130,9 @@ class ViewportGridService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setActiveViewportId(id: string) {
|
public setActiveViewportId(id: string) {
|
||||||
|
if (id === this.getActiveViewportId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.serviceImplementation._setActiveViewport(id);
|
this.serviceImplementation._setActiveViewport(id);
|
||||||
|
|
||||||
// Use queueMicrotask to delay the event broadcast
|
// Use queueMicrotask to delay the event broadcast
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { InstanceMetadata } from './StudyMetadata';
|
|||||||
export type DisplaySet = {
|
export type DisplaySet = {
|
||||||
displaySetInstanceUID: string;
|
displaySetInstanceUID: string;
|
||||||
instances: InstanceMetadata[];
|
instances: InstanceMetadata[];
|
||||||
|
isReconstructable?: boolean;
|
||||||
StudyInstanceUID: string;
|
StudyInstanceUID: string;
|
||||||
SeriesInstanceUID?: string;
|
SeriesInstanceUID?: string;
|
||||||
SeriesNumber?: number;
|
SeriesNumber?: number;
|
||||||
|
|||||||
@ -1,11 +1,19 @@
|
|||||||
|
import MeasurementService from '../services/MeasurementService';
|
||||||
/**
|
/**
|
||||||
* Returns a filter function which filters for measurements belonging to both
|
* Returns a filter function which filters for measurements belonging to both
|
||||||
* the study and series.
|
* the study and series.
|
||||||
*/
|
*/
|
||||||
export function filterMeasurementsBySeriesUID(selectedSeries: string[]) {
|
export function filterMeasurementsBySeriesUID(selectedSeries: string[]) {
|
||||||
|
if (!selectedSeries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
|
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterMeasurementsByStudyUID(studyUID) {
|
||||||
|
return measurement => measurement.referenceStudyUID == studyUID;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true for measurements include referencedImageId (coplanar with an image)
|
* @returns true for measurements include referencedImageId (coplanar with an image)
|
||||||
*/
|
*/
|
||||||
@ -13,6 +21,10 @@ export function filterPlanarMeasurement(measurement) {
|
|||||||
return measurement?.referencedImageId;
|
return measurement?.referencedImageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterTool(toolName: string) {
|
||||||
|
return annotation => annotation.metadata?.toolName === toolName;
|
||||||
|
}
|
||||||
|
|
||||||
/** A filter that always returns true */
|
/** A filter that always returns true */
|
||||||
export function filterAny(_measurement) {
|
export function filterAny(_measurement) {
|
||||||
return true;
|
return true;
|
||||||
@ -30,6 +42,10 @@ export function filterNone(_measurement) {
|
|||||||
export function filterOr(...filters) {
|
export function filterOr(...filters) {
|
||||||
return function (item) {
|
return function (item) {
|
||||||
for (let filter of filters) {
|
for (let filter of filters) {
|
||||||
|
if (!filter) {
|
||||||
|
// Un undefined filter means all, so return true for the or
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (typeof filter === 'string') {
|
if (typeof filter === 'string') {
|
||||||
filter = this[filter];
|
filter = this[filter];
|
||||||
}
|
}
|
||||||
@ -44,13 +60,14 @@ export function filterOr(...filters) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { POINT } = MeasurementService.VALUE_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters for additional findings, that is, measurements with
|
* Filters for additional findings, that is, measurements with
|
||||||
* a value of type point, and having a referenced image
|
* a value of type point, and having a referenced image
|
||||||
*/
|
*/
|
||||||
export function filterAdditionalFindings(measurementService) {
|
export function filterAdditionalFindings(dm) {
|
||||||
const { POINT } = measurementService.VALUE_TYPES;
|
return dm.type === POINT && dm.referencedImageId;
|
||||||
return dm => dm.type === POINT && dm.referencedImageId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,8 +87,15 @@ const isString = s => typeof s === 'string' || s instanceof String;
|
|||||||
* called on the final filter call.
|
* called on the final filter call.
|
||||||
*/
|
*/
|
||||||
export function filterAnd(...filters) {
|
export function filterAnd(...filters) {
|
||||||
|
const nonNullFilters = filters.filter(filter => !!filter);
|
||||||
|
if (!nonNullFilters.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nonNullFilters.length === 1 && typeof nonNullFilters[0] === 'function') {
|
||||||
|
return nonNullFilters[0];
|
||||||
|
}
|
||||||
return function (item) {
|
return function (item) {
|
||||||
for (const filter of filters) {
|
for (const filter of nonNullFilters) {
|
||||||
if (isString(filter)) {
|
if (isString(filter)) {
|
||||||
if (!this[filter](item)) {
|
if (!this[filter](item)) {
|
||||||
return false;
|
return false;
|
||||||
@ -98,7 +122,7 @@ export function filterAnd(...filters) {
|
|||||||
*/
|
*/
|
||||||
export function filterNot(...filters) {
|
export function filterNot(...filters) {
|
||||||
if (filters.length !== 1) {
|
if (filters.length !== 1) {
|
||||||
return filterAnd.apply(null, filters.map(filterNot));
|
return filterAnd(...filters.map(filter => filterNot(filter)));
|
||||||
}
|
}
|
||||||
const [filter] = filters;
|
const [filter] = filters;
|
||||||
if (isString(filter)) {
|
if (isString(filter)) {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||||
import { Button } from '../../../../ui-next/src/components/Button';
|
|
||||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
|
||||||
|
|
||||||
// Mock data to demonstrate DataRow usage
|
// Mock data to demonstrate DataRow usage
|
||||||
const mockData = [
|
const mockData = [
|
||||||
|
|||||||
@ -35,8 +35,14 @@ const AccordionTrigger = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{props.asChild ? (
|
||||||
<ChevronDownIcon className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
|
children
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
));
|
));
|
||||||
|
|||||||
12
platform/ui-next/src/components/ColorCircle.tsx
Normal file
12
platform/ui-next/src/components/ColorCircle.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function ColorCircle({ colorHex, className = 'inline-flex' }) {
|
||||||
|
return (
|
||||||
|
<div className={`h-5 w-5 ${className} items-center justify-center`}>
|
||||||
|
<span
|
||||||
|
className="ml-2 h-2 w-2 rounded-full"
|
||||||
|
style={{ backgroundColor: colorHex, marginLeft: 0 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -63,25 +63,25 @@ interface DataRowProps {
|
|||||||
details?: { primary: string[]; secondary: string[] };
|
details?: { primary: string[]; secondary: string[] };
|
||||||
//
|
//
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelect?: () => void;
|
onSelect?: (e) => void;
|
||||||
//
|
//
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onToggleVisibility: () => void;
|
onToggleVisibility: (e) => void;
|
||||||
//
|
//
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
onToggleLocked: () => void;
|
onToggleLocked: (e) => void;
|
||||||
//
|
//
|
||||||
title: string;
|
title: string;
|
||||||
onRename: () => void;
|
onRename: (e) => void;
|
||||||
//
|
//
|
||||||
onDelete: () => void;
|
onDelete: (e) => void;
|
||||||
//
|
//
|
||||||
colorHex?: string;
|
colorHex?: string;
|
||||||
onColor: () => void;
|
onColor: (e) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataRow: React.FC<DataRowProps> = ({
|
export const DataRow: React.FC<DataRowProps> = ({
|
||||||
number,
|
number,
|
||||||
title,
|
title,
|
||||||
colorHex,
|
colorHex,
|
||||||
@ -114,16 +114,16 @@ const DataRow: React.FC<DataRowProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'Rename':
|
case 'Rename':
|
||||||
onRename();
|
onRename(e);
|
||||||
break;
|
break;
|
||||||
case 'Lock':
|
case 'Lock':
|
||||||
onToggleLocked();
|
onToggleLocked(e);
|
||||||
break;
|
break;
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
onDelete();
|
onDelete(e);
|
||||||
break;
|
break;
|
||||||
case 'Color':
|
case 'Color':
|
||||||
onColor();
|
onColor(e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -269,7 +269,7 @@ const DataRow: React.FC<DataRowProps> = ({
|
|||||||
aria-label={isVisible ? 'Hide' : 'Show'}
|
aria-label={isVisible ? 'Hide' : 'Show'}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleVisibility();
|
onToggleVisibility(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
|
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
import DataRow from './DataRow';
|
export * from './DataRow';
|
||||||
|
|
||||||
export { DataRow };
|
|
||||||
|
|||||||
@ -5,13 +5,9 @@ import { createContext } from '../../lib/createContext';
|
|||||||
|
|
||||||
interface MeasurementTableContext {
|
interface MeasurementTableContext {
|
||||||
data?: any[];
|
data?: any[];
|
||||||
onClick?: (uid: string) => void;
|
onAction?: (e, command: string | string[], uid: string) => void;
|
||||||
onDelete?: (uid: string) => void;
|
|
||||||
onToggleVisibility?: (uid: string) => void;
|
|
||||||
onToggleLocked?: (uid: string) => void;
|
|
||||||
onRename?: (uid: string) => void;
|
|
||||||
onColor?: (uid: string) => void;
|
|
||||||
disableEditing?: boolean;
|
disableEditing?: boolean;
|
||||||
|
isExpanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [MeasurementTableProvider, useMeasurementTableContext] =
|
const [MeasurementTableProvider, useMeasurementTableContext] =
|
||||||
@ -24,12 +20,8 @@ interface MeasurementDataProps extends MeasurementTableContext {
|
|||||||
|
|
||||||
const MeasurementTable = ({
|
const MeasurementTable = ({
|
||||||
data = [],
|
data = [],
|
||||||
onClick,
|
onAction,
|
||||||
onDelete,
|
isExpanded = true,
|
||||||
onToggleVisibility,
|
|
||||||
onToggleLocked,
|
|
||||||
onRename,
|
|
||||||
onColor,
|
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
disableEditing = false,
|
disableEditing = false,
|
||||||
@ -40,19 +32,18 @@ const MeasurementTable = ({
|
|||||||
return (
|
return (
|
||||||
<MeasurementTableProvider
|
<MeasurementTableProvider
|
||||||
data={data}
|
data={data}
|
||||||
onClick={onClick}
|
onAction={onAction}
|
||||||
onDelete={onDelete}
|
isExpanded={isExpanded}
|
||||||
onToggleVisibility={onToggleVisibility}
|
|
||||||
onToggleLocked={onToggleLocked}
|
|
||||||
onRename={onRename}
|
|
||||||
onColor={onColor}
|
|
||||||
disableEditing={disableEditing}
|
disableEditing={disableEditing}
|
||||||
>
|
>
|
||||||
<PanelSection defaultOpen={true}>
|
<PanelSection defaultOpen={true}>
|
||||||
<PanelSection.Header className="bg-secondary-dark">
|
<PanelSection.Header
|
||||||
|
key="measurementTableHeader"
|
||||||
|
className="bg-secondary-dark"
|
||||||
|
>
|
||||||
<span>{`${t(title)} (${amount})`}</span>
|
<span>{`${t(title)} (${amount})`}</span>
|
||||||
</PanelSection.Header>
|
</PanelSection.Header>
|
||||||
<PanelSection.Content>{children}</PanelSection.Content>
|
<PanelSection.Content key="measurementTableContent">{children}</PanelSection.Content>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
</MeasurementTableProvider>
|
</MeasurementTableProvider>
|
||||||
);
|
);
|
||||||
@ -99,6 +90,7 @@ interface MeasurementItem {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
|
isExpanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
@ -107,16 +99,10 @@ interface RowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({ item, index }: RowProps) => {
|
const Row = ({ item, index }: RowProps) => {
|
||||||
const {
|
const { onAction, isExpanded, disableEditing } =
|
||||||
onClick,
|
useMeasurementTableContext('MeasurementTable.Row');
|
||||||
onDelete,
|
|
||||||
onToggleVisibility,
|
|
||||||
onToggleLocked,
|
|
||||||
onRename,
|
|
||||||
onColor,
|
|
||||||
disableEditing,
|
|
||||||
} = useMeasurementTableContext('MeasurementTable.Row');
|
|
||||||
|
|
||||||
|
const { uid } = item;
|
||||||
return (
|
return (
|
||||||
<DataRow
|
<DataRow
|
||||||
key={item.uid}
|
key={item.uid}
|
||||||
@ -126,15 +112,15 @@ const Row = ({ item, index }: RowProps) => {
|
|||||||
colorHex={item.colorHex}
|
colorHex={item.colorHex}
|
||||||
isSelected={item.isSelected}
|
isSelected={item.isSelected}
|
||||||
details={item.displayText}
|
details={item.displayText}
|
||||||
onSelect={() => onClick(item.uid)}
|
onDelete={e => onAction(e, 'removeMeasurement', uid)}
|
||||||
onDelete={() => onDelete(item.uid)}
|
onSelect={e => onAction(e, 'jumpToMeasurement', uid)}
|
||||||
|
onRename={e => onAction(e, 'renameMeasurement', uid)}
|
||||||
|
onToggleVisibility={e => onAction(e, 'toggleVisibilityMeasurement', uid)}
|
||||||
|
onToggleLocked={e => onAction(e, 'toggleLockMeasurement', uid)}
|
||||||
disableEditing={disableEditing}
|
disableEditing={disableEditing}
|
||||||
|
isExpanded={isExpanded}
|
||||||
isVisible={item.isVisible}
|
isVisible={item.isVisible}
|
||||||
isLocked={item.isLocked}
|
isLocked={item.isLocked}
|
||||||
onToggleVisibility={() => onToggleVisibility(item.uid)}
|
|
||||||
onToggleLocked={() => onToggleLocked(item.uid)}
|
|
||||||
onRename={() => onRename(item.uid)}
|
|
||||||
// onColor={() => onColor(item.uid)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,10 @@ interface StudySummaryProps {
|
|||||||
*/
|
*/
|
||||||
const StudySummary: React.FC<StudySummaryProps> = ({ date, description }) => {
|
const StudySummary: React.FC<StudySummaryProps> = ({ date, description }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mx-2 my-0">
|
<div
|
||||||
|
className="mx-2 my-0"
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
>
|
||||||
<div className="text-foreground text-sm">{date}</div>
|
<div className="text-foreground text-sm">{date}</div>
|
||||||
<div className="text-muted-foreground pb-1 text-sm">{description}</div>
|
<div className="text-muted-foreground pb-1 text-sm">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -79,8 +79,8 @@ import {
|
|||||||
} from './DropdownMenu';
|
} from './DropdownMenu';
|
||||||
import { Onboarding } from './Onboarding';
|
import { Onboarding } from './Onboarding';
|
||||||
import { DoubleSlider } from './DoubleSlider';
|
import { DoubleSlider } from './DoubleSlider';
|
||||||
import { DataRow } from './DataRow';
|
export { DataRow } from './DataRow';
|
||||||
import { MeasurementTable } from './MeasurementTable';
|
export { MeasurementTable } from './MeasurementTable';
|
||||||
import {
|
import {
|
||||||
SegmentationTable,
|
SegmentationTable,
|
||||||
useSegmentationTableContext,
|
useSegmentationTableContext,
|
||||||
@ -90,6 +90,7 @@ import {
|
|||||||
import { Toaster, toast } from './Sonner';
|
import { Toaster, toast } from './Sonner';
|
||||||
import { StudySummary } from './StudySummary';
|
import { StudySummary } from './StudySummary';
|
||||||
import { ErrorBoundary } from './Errorboundary';
|
import { ErrorBoundary } from './Errorboundary';
|
||||||
|
export * from './ColorCircle';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
|
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
|
||||||
import {
|
import {
|
||||||
@ -208,8 +209,6 @@ export {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
DataRow,
|
|
||||||
MeasurementTable,
|
|
||||||
Toaster,
|
Toaster,
|
||||||
toast,
|
toast,
|
||||||
SegmentationTable,
|
SegmentationTable,
|
||||||
|
|||||||
@ -1,254 +1,6 @@
|
|||||||
import {
|
export * from './components';
|
||||||
Button,
|
export * from './contextProviders';
|
||||||
buttonVariants,
|
|
||||||
ThemeWrapper,
|
|
||||||
Dialog,
|
|
||||||
Command,
|
|
||||||
Popover,
|
|
||||||
Combobox,
|
|
||||||
Calendar,
|
|
||||||
DatePickerWithRange,
|
|
||||||
Separator,
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
Clipboard,
|
|
||||||
TabsTrigger,
|
|
||||||
Toggle,
|
|
||||||
toggleVariants,
|
|
||||||
ToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Icons,
|
|
||||||
SidePanel,
|
|
||||||
StudyItem,
|
|
||||||
StudyBrowser,
|
|
||||||
StudyBrowserSort,
|
|
||||||
StudyBrowserViewOptions,
|
|
||||||
Thumbnail,
|
|
||||||
ThumbnailList,
|
|
||||||
PanelSection,
|
|
||||||
DisplaySetMessageListTooltip,
|
|
||||||
ToolboxUI,
|
|
||||||
ToolSettings,
|
|
||||||
DoubleSlider,
|
|
||||||
Label,
|
|
||||||
Slider,
|
|
||||||
Input,
|
|
||||||
Switch,
|
|
||||||
Checkbox,
|
|
||||||
Onboarding,
|
|
||||||
PopoverAnchor,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizableHandle,
|
|
||||||
Select,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectValue,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
ScrollArea,
|
|
||||||
MeasurementTable,
|
|
||||||
SegmentationTable,
|
|
||||||
useSegmentationTableContext,
|
|
||||||
useSegmentationExpanded,
|
|
||||||
useSegmentStatistics,
|
|
||||||
TooltipProvider,
|
|
||||||
Tooltip,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
StudySummary,
|
|
||||||
ErrorBoundary,
|
|
||||||
Header,
|
|
||||||
ViewportActionButton,
|
|
||||||
PatientInfo,
|
|
||||||
ViewportActionBar,
|
|
||||||
ViewportActionArrows,
|
|
||||||
ViewportPane,
|
|
||||||
ViewportActionCorners,
|
|
||||||
ViewportActionCornersLocations,
|
|
||||||
ViewportOverlay,
|
|
||||||
ViewportGrid,
|
|
||||||
ToolButton,
|
|
||||||
ToolButtonList,
|
|
||||||
ToolButtonListDefault,
|
|
||||||
ToolButtonListDropDown,
|
|
||||||
ToolButtonListItem,
|
|
||||||
ToolButtonListDivider,
|
|
||||||
Numeric,
|
|
||||||
InputDialog,
|
|
||||||
PresetDialog,
|
|
||||||
Modal,
|
|
||||||
AboutModal,
|
|
||||||
ImageModal,
|
|
||||||
UserPreferencesModal,
|
|
||||||
FooterAction,
|
|
||||||
InputFilter,
|
|
||||||
} from './components';
|
|
||||||
import { DataRow } from './components/DataRow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
useNotification,
|
|
||||||
NotificationProvider,
|
|
||||||
useModal,
|
|
||||||
ModalProvider,
|
|
||||||
DialogProvider,
|
|
||||||
useDialog,
|
|
||||||
ManagedDialog,
|
|
||||||
} from './contextProviders';
|
|
||||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
|
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
|
|
||||||
export {
|
export { utils };
|
||||||
ErrorBoundary,
|
|
||||||
// components
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
Command,
|
|
||||||
Popover,
|
|
||||||
Combobox,
|
|
||||||
Checkbox,
|
|
||||||
DoubleSlider,
|
|
||||||
buttonVariants,
|
|
||||||
ThemeWrapper,
|
|
||||||
Calendar,
|
|
||||||
DatePickerWithRange,
|
|
||||||
Clipboard,
|
|
||||||
// contextProviders
|
|
||||||
NotificationProvider,
|
|
||||||
useNotification,
|
|
||||||
ViewportGridContext,
|
|
||||||
ViewportGridProvider,
|
|
||||||
useViewportGrid,
|
|
||||||
Separator,
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
Toggle,
|
|
||||||
toggleVariants,
|
|
||||||
ToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Icons,
|
|
||||||
SidePanel,
|
|
||||||
StudyItem,
|
|
||||||
StudyBrowser,
|
|
||||||
StudyBrowserSort,
|
|
||||||
StudyBrowserViewOptions,
|
|
||||||
Thumbnail,
|
|
||||||
ThumbnailList,
|
|
||||||
PanelSection,
|
|
||||||
DisplaySetMessageListTooltip,
|
|
||||||
ToolboxUI,
|
|
||||||
Label,
|
|
||||||
Slider,
|
|
||||||
Input,
|
|
||||||
Switch,
|
|
||||||
Onboarding,
|
|
||||||
PopoverAnchor,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizableHandle,
|
|
||||||
Select,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectValue,
|
|
||||||
DataRow,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
ScrollArea,
|
|
||||||
MeasurementTable,
|
|
||||||
SegmentationTable,
|
|
||||||
useSegmentationTableContext,
|
|
||||||
useSegmentationExpanded,
|
|
||||||
TooltipProvider,
|
|
||||||
Tooltip,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
StudySummary,
|
|
||||||
Header,
|
|
||||||
ViewportActionButton,
|
|
||||||
PatientInfo,
|
|
||||||
ViewportActionBar,
|
|
||||||
ViewportActionArrows,
|
|
||||||
ViewportPane,
|
|
||||||
ViewportActionCorners,
|
|
||||||
ViewportActionCornersLocations,
|
|
||||||
ViewportOverlay,
|
|
||||||
ViewportGrid,
|
|
||||||
ToolButton,
|
|
||||||
ToolButtonList,
|
|
||||||
ToolButtonListDefault,
|
|
||||||
ToolButtonListDropDown,
|
|
||||||
ToolButtonListItem,
|
|
||||||
ToolButtonListDivider,
|
|
||||||
utils,
|
|
||||||
Numeric,
|
|
||||||
AboutModal,
|
|
||||||
ImageModal,
|
|
||||||
UserPreferencesModal,
|
|
||||||
InputDialog,
|
|
||||||
PresetDialog,
|
|
||||||
Modal,
|
|
||||||
useModal,
|
|
||||||
ModalProvider,
|
|
||||||
FooterAction,
|
|
||||||
DialogProvider,
|
|
||||||
useDialog,
|
|
||||||
ManagedDialog,
|
|
||||||
ToolSettings,
|
|
||||||
useSegmentStatistics,
|
|
||||||
InputFilter,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ class LabellingFlow extends Component<PropType> {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { label } = props.measurementData;
|
const label = props.measurementData?.label;
|
||||||
const className = props.componentClassName;
|
const className = props.componentClassName;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|||||||
@ -33,8 +33,13 @@ const MeasurementTable = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-secondary-main flex justify-between px-2 py-1">
|
<div className="bg-secondary-main flex justify-between px-2 py-1">
|
||||||
<span className="text-base font-bold uppercase tracking-widest text-white">{t(title)}</span>
|
<div>
|
||||||
<span className="text-base font-bold text-white">{amount}</span>
|
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||||
|
{t(title)}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-bold text-white"> {amount}</span>
|
||||||
|
</div>
|
||||||
|
<CustomizedToolbar servicesManager={servicesManager} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ohif-scrollbar max-h-112 overflow-hidden">
|
<div className="ohif-scrollbar max-h-112 overflow-hidden">
|
||||||
{data.length !== 0 &&
|
{data.length !== 0 &&
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user