feat(studies-panel): New OHIF study panel - under experimental flag (#4254)
This commit is contained in:
parent
4432b6b420
commit
7a96406a11
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import { Toolbox as NewToolbox } from '@ohif/ui-next';
|
||||
import { Toolbox as OldToolbox } from '@ohif/ui';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
|
||||
const getPanelModule = ({
|
||||
@ -13,7 +14,27 @@ const getPanelModule = ({
|
||||
}: withAppTypes) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
const wrappedPanelSegmentation = ({ configuration, renderHeader, getCloseIcon, tab }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
@ -30,9 +51,16 @@ const getPanelModule = ({
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const wrappedPanelSegmentationWithTools = ({
|
||||
configuration,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@ -44,6 +72,9 @@ const getPanelModule = ({
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
@ -74,6 +105,13 @@ const getPanelModule = ({
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationWithTools,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationNoHeader',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationNoHeader,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { SegmentationPanelMode } from '../types/segmentation';
|
||||
import callInputDialog from './callInputDialog';
|
||||
import callColorPickerDialog from './colorPickerDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const components = {
|
||||
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
||||
@ -17,6 +18,9 @@ export default function PanelSegmentation({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
segmentationService,
|
||||
@ -319,52 +323,76 @@ export default function PanelSegmentation({
|
||||
: onSegmentationAdd;
|
||||
|
||||
return (
|
||||
<SegmentationGroupTableComponent
|
||||
title={t('Segmentations')}
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
showDeleteSegment={true}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SegmentationGroupTableComponent
|
||||
title={t('Segmentations')}
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
showDeleteSegment={true}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import { Toolbox as NewToolbox } from '@ohif/ui-next';
|
||||
import { Toolbox as OldToolbox } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
const wrappedDynamicDataPanel = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = () => {
|
||||
const wrappedDynamicToolbox = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@ -23,6 +31,9 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,17 +1,44 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager }) {
|
||||
function DynamicDataPanel({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { SidePanel } from '@ohif/ui';
|
||||
import { SidePanel as NewSidePanel } from '@ohif/ui-next';
|
||||
import { SidePanel as OldSidePanel } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
export type SidePanelWithServicesProps = {
|
||||
@ -26,6 +28,7 @@ const SidePanelWithServices = ({
|
||||
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const handleSidePanelOpen = useCallback(() => {
|
||||
setHasBeenOpened(true);
|
||||
@ -75,6 +78,8 @@ const SidePanelWithServices = ({
|
||||
};
|
||||
}, [tabs, hasBeenOpened, panelService]);
|
||||
|
||||
const SidePanel = appConfig?.useExperimentalUI ? NewSidePanel : OldSidePanel;
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
{...props}
|
||||
|
||||
@ -17,6 +17,7 @@ import createReportDialogPrompt, {
|
||||
} from './createReportDialogPrompt';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
|
||||
@ -24,6 +25,9 @@ export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes): React.FunctionComponent {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
|
||||
@ -212,6 +216,24 @@ export default function PanelMeasurementTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="ohif-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
data-cy={'measurements-panel'}
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { StudyBrowser as NewStudyBrowser } from '@ohif/ui-next';
|
||||
import { StudyBrowser as OldStudyBrowser } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
|
||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||
|
||||
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
|
||||
|
||||
@ -16,10 +22,14 @@ function PanelStudyBrowser({
|
||||
getStudiesForPatientByMRN,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, uiNotificationService } =
|
||||
const { hangingProtocolService, displaySetService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
@ -36,6 +46,31 @@ function PanelStudyBrowser({
|
||||
const [displaySets, setDisplaySets] = useState([]);
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
|
||||
const [viewPresets, setViewPresets] = useState(
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
|
||||
);
|
||||
|
||||
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
|
||||
|
||||
// multiple can be true or false
|
||||
const updateActionIconValue = actionIcon => {
|
||||
actionIcon.value = !actionIcon.value;
|
||||
const newActionIcons = [...actionIcons];
|
||||
setActionIcons(newActionIcons);
|
||||
};
|
||||
|
||||
// only one is true at a time
|
||||
const updateViewPresetValue = viewPreset => {
|
||||
if (!viewPreset) {
|
||||
return;
|
||||
}
|
||||
const newViewPresets = viewPresets.map(preset => {
|
||||
preset.selected = preset.id === viewPreset.id;
|
||||
return preset;
|
||||
});
|
||||
setViewPresets(newViewPresets);
|
||||
};
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
let updatedViewports = [];
|
||||
const viewportId = activeViewportId;
|
||||
@ -164,9 +199,10 @@ function PanelStudyBrowser({
|
||||
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
data => {
|
||||
if (!hasLoadedViewports) {
|
||||
return;
|
||||
}
|
||||
// for some reason this breaks thumbnail loading
|
||||
// if (!hasLoadedViewports) {
|
||||
// return;
|
||||
// }
|
||||
const { displaySetsAdded, options } = data;
|
||||
displaySetsAdded.forEach(async dSet => {
|
||||
const newImageSrcEntry = {};
|
||||
@ -236,7 +272,7 @@ function PanelStudyBrowser({
|
||||
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
|
||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||
? // eslint-disable-next-line prettier/prettier
|
||||
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
|
||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||
|
||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||
@ -249,19 +285,42 @@ function PanelStudyBrowser({
|
||||
|
||||
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
||||
|
||||
const StudyBrowser = appConfig?.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser;
|
||||
|
||||
return (
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<PanelStudyBrowserHeader
|
||||
tab={tab}
|
||||
getCloseIcon={getCloseIcon}
|
||||
viewPresets={viewPresets}
|
||||
updateViewPresetValue={updateViewPresetValue}
|
||||
actionIcons={actionIcons}
|
||||
updateActionIconValue={updateActionIconValue}
|
||||
/>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
||||
viewPresets={viewPresets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { actionIcon, viewPreset } from './types';
|
||||
|
||||
function PanelStudyBrowserHeader({
|
||||
tab,
|
||||
getCloseIcon,
|
||||
viewPresets,
|
||||
updateViewPresetValue,
|
||||
actionIcons,
|
||||
updateActionIconValue,
|
||||
}: {
|
||||
tab: any;
|
||||
getCloseIcon: () => JSX.Element;
|
||||
viewPresets: viewPreset[];
|
||||
updateViewPresetValue: (viewPreset: viewPreset) => void;
|
||||
actionIcons: actionIcon[];
|
||||
updateActionIconValue: (actionIcon: actionIcon) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-muted flex h-[40px] select-none rounded-t p-2">
|
||||
<div className={'flex h-[24px] w-full select-none justify-center self-center text-[14px]'}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={viewPresets.filter(preset => preset.selected)[0].id}
|
||||
onValueChange={value => {
|
||||
const selectedViewPreset = viewPresets.find(preset => preset.id === value);
|
||||
updateViewPresetValue(selectedViewPreset);
|
||||
}}
|
||||
>
|
||||
{viewPresets.map((viewPreset: viewPreset, index) => (
|
||||
<ToggleGroupItem
|
||||
key={index}
|
||||
aria-label={viewPreset.id}
|
||||
value={viewPreset.id}
|
||||
className="text-actions-primary"
|
||||
>
|
||||
{React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex items-center justify-center">
|
||||
{' '}
|
||||
<span>{tab.label}</span>{' '}
|
||||
</div>
|
||||
|
||||
<div className="mr-[30px] flex items-center justify-center">
|
||||
<div className="flex items-center space-x-1">
|
||||
{actionIcons.map((icon: actionIcon, index) =>
|
||||
React.createElement(Icons[icon.iconName] || Icons.MissingIcon, {
|
||||
key: index,
|
||||
onClick: () => updateActionIconValue(icon),
|
||||
className: `cursor-pointer`,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { PanelStudyBrowserHeader };
|
||||
@ -0,0 +1,11 @@
|
||||
import type { actionIcon } from '../types/actionsIcon';
|
||||
|
||||
const defaultActionIcons = [
|
||||
{
|
||||
id: 'settings',
|
||||
iconName: 'Settings',
|
||||
value: false,
|
||||
},
|
||||
] as actionIcon[];
|
||||
|
||||
export { defaultActionIcons };
|
||||
@ -0,0 +1,4 @@
|
||||
import { defaultActionIcons } from './actionIcons';
|
||||
import { defaultViewPresets } from './viewPresets';
|
||||
|
||||
export { defaultActionIcons, defaultViewPresets };
|
||||
@ -0,0 +1,16 @@
|
||||
import type { viewPreset } from '../types/viewPreset';
|
||||
|
||||
const defaultViewPresets = [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
] as viewPreset[];
|
||||
|
||||
export { defaultViewPresets };
|
||||
@ -0,0 +1,7 @@
|
||||
type actionIcon = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
export type { actionIcon };
|
||||
@ -0,0 +1,4 @@
|
||||
import type { actionIcon } from './actionIcon';
|
||||
import type { viewPreset } from './viewPreset';
|
||||
|
||||
export type { actionIcon, viewPreset };
|
||||
@ -0,0 +1,7 @@
|
||||
type viewPreset = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type { viewPreset };
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||
import getStudiesForPatientByMRN from './getStudiesForPatientByMRN';
|
||||
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
|
||||
@ -13,7 +13,14 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu
|
||||
* @param {object} commandsManager
|
||||
* @param {object} extensionManager
|
||||
*/
|
||||
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
|
||||
function WrappedPanelStudyBrowser({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}) {
|
||||
// TODO: This should be made available a different way; route should have
|
||||
// already determined our datasource
|
||||
const dataSource = extensionManager.getDataSources()[0];
|
||||
@ -34,6 +41,9 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
|
||||
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable';
|
||||
import createReportDialogPrompt from './createReportDialogPrompt';
|
||||
|
||||
@ -184,6 +184,22 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.viewPresets',
|
||||
// change your default selected preset here
|
||||
value: [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -8,12 +8,15 @@ import i18n from 'i18next';
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedMeasurementPanel = () => {
|
||||
const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
return (
|
||||
<PanelMeasurementTable
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -24,11 +27,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
iconName: 'tab-studies',
|
||||
iconLabel: 'Studies',
|
||||
label: i18n.t('SidePanel:Studies'),
|
||||
component: WrappedPanelStudyBrowser.bind(null, {
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}),
|
||||
component: props => (
|
||||
<WrappedPanelStudyBrowser
|
||||
{...props}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'measurements',
|
||||
|
||||
@ -8,6 +8,7 @@ import dcmjs from 'dcmjs';
|
||||
import callInputDialog from '../../utils/callInputDialog';
|
||||
import constructSR from '../../utils/constructSR';
|
||||
import { saveByteArray } from '../../utils/saveByteArray';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
let saving = false;
|
||||
const { datasetToBuffer } = dcmjs.data;
|
||||
@ -56,6 +57,9 @@ interface IMicroscopyPanelProps extends WithTranslation {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
extensionManager: ExtensionManager;
|
||||
commandsManager: CommandsManager;
|
||||
renderHeader?: boolean;
|
||||
getCloseIcon?: PropTypes.func;
|
||||
tab: PropTypes.object;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,6 +332,20 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{props.tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{props.getCloseIcon()}
|
||||
</div>
|
||||
<Separator orientation="horizontal" className="bg-black" thickness="2px" />
|
||||
</>
|
||||
)}{' '}
|
||||
<div
|
||||
className="ohif-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
data-cy={'measurements-panel'}
|
||||
|
||||
@ -13,7 +13,7 @@ export default function getPanelModule({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams) {
|
||||
const wrappedMeasurementPanel = () => {
|
||||
const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
const [{ activeViewportId, viewports }] = useViewportGrid();
|
||||
|
||||
return (
|
||||
@ -25,6 +25,9 @@ export default function getPanelModule({
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { Types } from '@ohif/core';
|
||||
import { PanelMeasurementTableTracking, PanelStudyBrowserTracking } from './panels';
|
||||
import i18n from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
// TODO:
|
||||
// - No loading UI exists yet
|
||||
// - cancel promises when component is destroyed
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }): Types.Panel[] {
|
||||
return [
|
||||
{
|
||||
@ -13,11 +15,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
|
||||
iconName: 'tab-studies',
|
||||
iconLabel: 'Studies',
|
||||
label: i18n.t('SidePanel:Studies'),
|
||||
component: PanelStudyBrowserTracking.bind(null, {
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}),
|
||||
component: props => (
|
||||
<PanelStudyBrowserTracking
|
||||
{...props}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@ -25,11 +30,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }):
|
||||
iconName: 'tab-linear',
|
||||
iconLabel: 'Measure',
|
||||
label: i18n.t('SidePanel:Measurements'),
|
||||
component: PanelMeasurementTableTracking.bind(null, {
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}),
|
||||
component: props => (
|
||||
<PanelMeasurementTableTracking
|
||||
{...props}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { useAppConfig } from '@state';
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
const { formatDate } = utils;
|
||||
@ -18,7 +19,13 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
||||
description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
|
||||
};
|
||||
|
||||
function PanelMeasurementTableTracking({ servicesManager, extensionManager }: withAppTypes) {
|
||||
function PanelMeasurementTableTracking({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
|
||||
@ -176,6 +183,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
|
||||
ref={measurementsPanelRef}
|
||||
|
||||
@ -3,8 +3,15 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import { StudyBrowser, useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { StudyBrowser as NewStudyBrowser } from '@ohif/ui-next';
|
||||
import { StudyBrowser as OldStudyBrowser } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
import { PanelStudyBrowserTrackingHeader } from './PanelStudyBrowserTrackingHeader';
|
||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||
|
||||
const { formatDate, createStudyBrowserTabs } = utils;
|
||||
|
||||
@ -18,6 +25,9 @@ function PanelStudyBrowserTracking({
|
||||
getStudiesForPatientByMRN,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
displaySetService,
|
||||
@ -26,10 +36,12 @@ function PanelStudyBrowserTracking({
|
||||
uiNotificationService,
|
||||
measurementService,
|
||||
studyPrefetcherService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
@ -49,6 +61,29 @@ function PanelStudyBrowserTracking({
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
|
||||
|
||||
const [viewPresets, setViewPresets] = useState(
|
||||
customizationService.getCustomization('studyBrowser.viewPresets')?.value || defaultViewPresets
|
||||
);
|
||||
|
||||
const [actionIcons, setActionIcons] = useState(defaultActionIcons);
|
||||
|
||||
const updateActionIconValue = actionIcon => {
|
||||
actionIcon.value = !actionIcon.value;
|
||||
const newActionIcons = [...actionIcons];
|
||||
setActionIcons(newActionIcons);
|
||||
};
|
||||
|
||||
const updateViewPresetValue = viewPreset => {
|
||||
if (!viewPreset) {
|
||||
return;
|
||||
}
|
||||
const newViewPresets = viewPresets.map(preset => {
|
||||
preset.selected = preset.id === viewPreset.id;
|
||||
return preset;
|
||||
});
|
||||
setViewPresets(newViewPresets);
|
||||
};
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
let updatedViewports = [];
|
||||
const viewportId = activeViewportId;
|
||||
@ -431,23 +466,46 @@ function PanelStudyBrowserTracking({
|
||||
});
|
||||
};
|
||||
|
||||
const StudyBrowser = appConfig.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser;
|
||||
|
||||
return (
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
onClickUntrack={displaySetInstanceUID => {
|
||||
onClickUntrack(displaySetInstanceUID);
|
||||
}}
|
||||
onClickThumbnail={() => {}}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
|
||||
/>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<PanelStudyBrowserTrackingHeader
|
||||
tab={tab}
|
||||
getCloseIcon={getCloseIcon}
|
||||
viewPresets={viewPresets}
|
||||
updateViewPresetValue={updateViewPresetValue}
|
||||
actionIcons={actionIcons}
|
||||
updateActionIconValue={updateActionIconValue}
|
||||
/>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
servicesManager={servicesManager}
|
||||
activeTabName={activeTabName}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
setActiveTabName(clickedTabName);
|
||||
}}
|
||||
onClickUntrack={displaySetInstanceUID => {
|
||||
onClickUntrack(displaySetInstanceUID);
|
||||
}}
|
||||
onClickThumbnail={() => {}}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
|
||||
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
||||
viewPresets={viewPresets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { actionIcon, viewPreset } from './types';
|
||||
|
||||
function PanelStudyBrowserTrackingHeader({
|
||||
tab,
|
||||
getCloseIcon,
|
||||
viewPresets,
|
||||
updateViewPresetValue,
|
||||
actionIcons,
|
||||
updateActionIconValue,
|
||||
}: {
|
||||
tab: any;
|
||||
getCloseIcon: () => JSX.Element;
|
||||
viewPresets: viewPreset[];
|
||||
updateViewPresetValue: (viewPreset: viewPreset) => void;
|
||||
actionIcons: actionIcon[];
|
||||
updateActionIconValue: (actionIcon: actionIcon) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-muted flex h-[40px] select-none rounded-t p-2">
|
||||
<div className={'flex h-[24px] w-full select-none justify-center self-center text-[14px]'}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={viewPresets.filter(preset => preset.selected)[0].id}
|
||||
onValueChange={value => {
|
||||
const selectedViewPreset = viewPresets.find(preset => preset.id === value);
|
||||
updateViewPresetValue(selectedViewPreset);
|
||||
}}
|
||||
>
|
||||
{viewPresets.map((viewPreset: viewPreset, index) => (
|
||||
<ToggleGroupItem
|
||||
key={index}
|
||||
aria-label={viewPreset.id}
|
||||
value={viewPreset.id}
|
||||
className="text-actions-primary"
|
||||
>
|
||||
{React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex items-center justify-center">
|
||||
{' '}
|
||||
<span>{tab.label}</span>{' '}
|
||||
</div>
|
||||
|
||||
<div className="mr-[30px] flex items-center justify-center">
|
||||
<div className="flex items-center space-x-1">
|
||||
{actionIcons.map((icon: actionIcon, index) =>
|
||||
React.createElement(Icons[icon.iconName] || Icons.MissingIcon, {
|
||||
key: index,
|
||||
onClick: () => updateActionIconValue(icon),
|
||||
className: `cursor-pointer`,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { PanelStudyBrowserTrackingHeader };
|
||||
@ -0,0 +1,11 @@
|
||||
import type { actionIcon } from '../PanelStudyBrowserTracking/types/actionsIcon';
|
||||
|
||||
const defaultActionIcons = [
|
||||
{
|
||||
id: 'settings',
|
||||
iconName: 'Settings',
|
||||
value: false,
|
||||
},
|
||||
] as actionIcon[];
|
||||
|
||||
export { defaultActionIcons };
|
||||
@ -0,0 +1,4 @@
|
||||
import { defaultActionIcons } from './actionIcons';
|
||||
import { defaultViewPresets } from './viewPresets';
|
||||
|
||||
export { defaultActionIcons, defaultViewPresets };
|
||||
@ -0,0 +1,16 @@
|
||||
import type { viewPreset } from '../PanelStudyBrowserTracking/types/viewPreset';
|
||||
|
||||
const defaultViewPresets = [
|
||||
{
|
||||
id: 'list',
|
||||
iconName: 'ListView',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
iconName: 'ThumbnailView',
|
||||
selected: true,
|
||||
},
|
||||
] as viewPreset[];
|
||||
|
||||
export { defaultViewPresets };
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
|
||||
@ -25,6 +25,9 @@ function WrappedPanelStudyBrowserTracking({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
|
||||
@ -46,6 +49,9 @@ function WrappedPanelStudyBrowserTracking({
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
|
||||
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
type actionIcon = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
export type { actionIcon };
|
||||
@ -0,0 +1,4 @@
|
||||
import type { actionIcon } from './actionIcon';
|
||||
import type { viewPreset } from './viewPreset';
|
||||
|
||||
export type { actionIcon, viewPreset };
|
||||
@ -0,0 +1,7 @@
|
||||
type viewPreset = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type { viewPreset };
|
||||
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { PanelSection, Input, Button } from '@ohif/ui';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const DEFAULT_MEATADATA = {
|
||||
PatientWeight: null,
|
||||
@ -22,7 +23,13 @@ const DEFAULT_MEATADATA = {
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export default function PanelPetSUV({ servicesManager, commandsManager }: withAppTypes) {
|
||||
export default function PanelPetSUV({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const { t } = useTranslation('PanelSUV');
|
||||
const { displaySetService, toolGroupService, toolbarService, hangingProtocolService } =
|
||||
servicesManager.services;
|
||||
@ -125,104 +132,127 @@ export default function PanelPetSUV({ servicesManager, commandsManager }: withAp
|
||||
}, 0);
|
||||
}
|
||||
return (
|
||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]">
|
||||
<PanelSection title={t('Patient Information')}>
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-primary-dark flex flex-col gap-4 p-2">
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Patient Sex')}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.PatientSex || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientSex: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Weight')}
|
||||
labelChildren={<span className="text-aqua-pale"> kg</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.PatientWeight || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientWeight: e.target.value,
|
||||
});
|
||||
}}
|
||||
id="weight-input"
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Total Dose')}
|
||||
labelChildren={<span className="text-aqua-pale"> bq</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideTotalDose: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Half Life')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideHalfLife: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Injection Time')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || ''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadiopharmaceuticalStartTime: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Acquisition Time')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.SeriesTime || ''}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Button
|
||||
className="!h-[26px] !w-[115px] self-end !p-0"
|
||||
onClick={updateMetadata}
|
||||
>
|
||||
Reload Data
|
||||
</Button>
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
</PanelSection>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]">
|
||||
<PanelSection title={t('Patient Information')}>
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-primary-dark flex flex-col gap-4 p-2">
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Patient Sex')}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.PatientSex || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientSex: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Weight')}
|
||||
labelChildren={<span className="text-aqua-pale"> kg</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.PatientWeight || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientWeight: e.target.value,
|
||||
});
|
||||
}}
|
||||
id="weight-input"
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Total Dose')}
|
||||
labelChildren={<span className="text-aqua-pale"> bq</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideTotalDose: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Half Life')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideHalfLife: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Injection Time')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime ||
|
||||
''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadiopharmaceuticalStartTime: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
containerClassName={'!flex-row !justify-between items-center'}
|
||||
label={t('Acquisition Time')}
|
||||
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||
labelClassName="text-[13px] font-inter text-white"
|
||||
className="!m-0 !h-[26px] !w-[117px]"
|
||||
value={metadata.SeriesTime || ''}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Button
|
||||
className="!h-[26px] !w-[115px] self-end !p-0"
|
||||
onClick={updateMetadata}
|
||||
>
|
||||
Reload Data
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import { Toolbox as NewToolbox } from '@ohif/ui-next';
|
||||
import { Toolbox as OldToolbox } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
// TODO:
|
||||
// - No loading UI exists yet
|
||||
@ -8,16 +10,24 @@ import { Toolbox } from '@ohif/ui';
|
||||
// - show errors in UI for thumbnails if promise fails
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedPanelPetSuv = () => {
|
||||
const wrappedPanelPetSuv = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
return (
|
||||
<PanelPetSUV
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdToolbox = () => {
|
||||
const wrappedROIThresholdToolbox = ({ renderHeader, getCloseIcon, tab }: withAppTypes) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@ -26,6 +36,9 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -7,7 +7,7 @@ const dynamicVolume = {
|
||||
};
|
||||
|
||||
const cornerstone = {
|
||||
segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||
segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationNoHeader',
|
||||
activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel',
|
||||
};
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ const ohif = {
|
||||
|
||||
const cs3d = {
|
||||
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||
segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationNoHeader',
|
||||
};
|
||||
|
||||
const tmtv = {
|
||||
|
||||
@ -16,6 +16,7 @@ window.config = {
|
||||
experimentalStudyBrowserSort: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
groupEnabledModesFirst: true,
|
||||
useExperimentalUI: false,
|
||||
maxNumRequests: {
|
||||
interaction: 100,
|
||||
thumbnail: 75,
|
||||
|
||||
@ -8,6 +8,7 @@ window.config = {
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
showCPUFallbackMessage: true,
|
||||
useExperimentalUI: false,
|
||||
showLoadingIndicator: true,
|
||||
experimentalStudyBrowserSort: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
|
||||
@ -105,10 +105,13 @@ export default class PanelService extends PubSubService {
|
||||
entry = panelsData[0].entry;
|
||||
|
||||
// stack the content of the panels in one react component
|
||||
content = () => (
|
||||
content = props => (
|
||||
<>
|
||||
{panelsData.map(({ content: PanelContent }, index) => (
|
||||
<PanelContent key={index} />
|
||||
<PanelContent
|
||||
key={index}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
||||
57
platform/ui-next/src/components/Accordion/Accordion.tsx
Normal file
57
platform/ui-next/src/components/Accordion/Accordion.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-[8px] px-[8px] text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-90',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn(className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
3
platform/ui-next/src/components/Accordion/index.ts
Normal file
3
platform/ui-next/src/components/Accordion/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion';
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icons } from '../Icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../Tooltip';
|
||||
|
||||
/**
|
||||
* Displays a tooltip with a list of messages of a displaySet
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => {
|
||||
const { t } = useTranslation('Messages');
|
||||
if (messages?.size()) {
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger id={id}>
|
||||
<Icons.StatusWarning className="h-[20px] w-[20px]" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="max-w-64 text-left text-base text-white">
|
||||
<div
|
||||
className="break-normal text-base font-bold text-blue-300"
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
{t('Display Set Messages')}
|
||||
</div>
|
||||
<ol
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
marginRight: '12px',
|
||||
}}
|
||||
>
|
||||
{messages.messages.map((message, index) => (
|
||||
<li
|
||||
style={{
|
||||
marginTop: '6px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{index + 1}. {t(message.id)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
DisplaySetMessageListTooltip.propTypes = {
|
||||
messages: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DisplaySetMessageListTooltip;
|
||||
@ -0,0 +1,3 @@
|
||||
import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
|
||||
|
||||
export default DisplaySetMessageListTooltip;
|
||||
1242
platform/ui-next/src/components/Icons/Icons.tsx
Normal file
1242
platform/ui-next/src/components/Icons/Icons.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="8.88756485px" height="5.15088921px" viewBox="0 0 8.88756485 5.15088921" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-disclosure-close</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-115.6464, -67.6464)">
|
||||
<g id="icon-disclosure-close" transform="translate(110, 60)">
|
||||
<polyline id="Path-2" stroke="#348CFD" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 737 B |
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="5.15088921px" height="8.88756485px" viewBox="0 0 5.15088921 8.88756485" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-disclosure-open</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-152.338, -65.6013)">
|
||||
<g id="icon-disclosure-open" transform="translate(145, 60)">
|
||||
<polyline id="Path-2" stroke="#348CFD" transform="translate(10.0902, 10.0451) rotate(90) translate(-10.0902, -10.0451)" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 815 B |
3
platform/ui-next/src/components/Icons/index.ts
Normal file
3
platform/ui-next/src/components/Icons/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Icons } from './Icons';
|
||||
|
||||
export { Icons };
|
||||
@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PanelSection = ({ title, children, actionIcons = [], childrenClassName }) => {
|
||||
const [areChildrenVisible, setChildrenVisible] = useState(true);
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
setChildrenVisible(!areChildrenVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="bg-secondary-dark flex h-7 cursor-pointer select-none items-center justify-between rounded-[4px] pl-2.5 text-[13px]"
|
||||
onClick={handleHeaderClick}
|
||||
>
|
||||
<div className="text-aqua-pale">{title}</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{actionIcons.map((icon, index) => {
|
||||
const Icon = Icons[icon.name];
|
||||
return (
|
||||
<Icon
|
||||
key={index}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!areChildrenVisible) {
|
||||
setChildrenVisible(true);
|
||||
}
|
||||
icon.onClick();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="grid h-[28px] w-[28px] place-items-center">
|
||||
{areChildrenVisible ? <Icons.ChevronOpen /> : <Icons.ChevronClosed />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{areChildrenVisible && (
|
||||
<>
|
||||
<div className="h-[2px] bg-black"></div>
|
||||
<div
|
||||
className={`bg-primary-dark flex flex-col overflow-hidden rounded-b-[4px] ${childrenClassName}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PanelSection.propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
childrenClassName: PropTypes.string,
|
||||
actionIcons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default PanelSection;
|
||||
3
platform/ui-next/src/components/PanelSection/index.ts
Normal file
3
platform/ui-next/src/components/PanelSection/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PanelSection from './PanelSection';
|
||||
|
||||
export default PanelSection;
|
||||
@ -1,24 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
type SeparatorProps = React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> & {
|
||||
thickness?: string;
|
||||
};
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-primary/25 shrink-0',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
SeparatorProps
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, thickness = "1px", ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? `h-[${thickness}] w-full` : `h-full w-[${thickness}]`,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator };
|
||||
export { Separator }
|
||||
3
platform/ui-next/src/components/Separator/index.ts
Normal file
3
platform/ui-next/src/components/Separator/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Separator } from "./Separator";
|
||||
|
||||
export default Separator;
|
||||
422
platform/ui-next/src/components/SidePanel/SidePanel.tsx
Normal file
422
platform/ui-next/src/components/SidePanel/SidePanel.tsx
Normal file
@ -0,0 +1,422 @@
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
|
||||
import Separator from '../Separator';
|
||||
|
||||
type StyleMap = {
|
||||
open: {
|
||||
left: { marginLeft: string };
|
||||
right: { marginRight: string };
|
||||
};
|
||||
closed: {
|
||||
left: { marginLeft: string };
|
||||
right: { marginRight: string };
|
||||
};
|
||||
};
|
||||
const borderSize = 4;
|
||||
const collapsedWidth = 25;
|
||||
const closeIconWidth = 30;
|
||||
const gridHorizontalPadding = 10;
|
||||
const tabSpacerWidth = 2;
|
||||
|
||||
const baseClasses =
|
||||
'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col';
|
||||
|
||||
const classesMap = {
|
||||
open: {
|
||||
left: `mr-1`,
|
||||
right: `ml-1`,
|
||||
},
|
||||
closed: {
|
||||
left: `mr-2 items-end`,
|
||||
right: `ml-2 items-start`,
|
||||
},
|
||||
};
|
||||
|
||||
const openStateIconName = {
|
||||
left: 'SidePanelCloseLeft',
|
||||
right: 'SidePanelCloseRight',
|
||||
};
|
||||
|
||||
const getTabWidth = (numTabs: number) => {
|
||||
if (numTabs < 3) {
|
||||
return 68;
|
||||
} else {
|
||||
return 40;
|
||||
}
|
||||
};
|
||||
|
||||
const getGridWidth = (numTabs: number, gridAvailableWidth: number) => {
|
||||
const spacersWidth = (numTabs - 1) * tabSpacerWidth;
|
||||
const tabsWidth = getTabWidth(numTabs) * numTabs;
|
||||
|
||||
if (gridAvailableWidth > tabsWidth + spacersWidth) {
|
||||
return tabsWidth + spacersWidth;
|
||||
}
|
||||
|
||||
return gridAvailableWidth;
|
||||
};
|
||||
|
||||
const getNumGridColumns = (numTabs: number, gridWidth: number) => {
|
||||
if (numTabs === 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Start by calculating the number of tabs assuming each tab was accompanied by a spacer.
|
||||
const tabWidth = getTabWidth(numTabs);
|
||||
const numTabsWithOneSpacerEach = Math.floor(gridWidth / (tabWidth + tabSpacerWidth));
|
||||
|
||||
// But there is always one less spacer than tabs, so now check if an extra tab with one less spacer fits.
|
||||
if (
|
||||
(numTabsWithOneSpacerEach + 1) * tabWidth + numTabsWithOneSpacerEach * tabSpacerWidth <=
|
||||
gridWidth
|
||||
) {
|
||||
return numTabsWithOneSpacerEach + 1;
|
||||
}
|
||||
|
||||
return numTabsWithOneSpacerEach;
|
||||
};
|
||||
|
||||
const getTabClassNames = (
|
||||
numColumns: number,
|
||||
numTabs: number,
|
||||
tabIndex: number,
|
||||
isActiveTab: boolean,
|
||||
isTabDisabled: boolean
|
||||
) =>
|
||||
classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', {
|
||||
'hover:text-primary-active': !isActiveTab && !isTabDisabled,
|
||||
'rounded-l': tabIndex % numColumns === 0,
|
||||
'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1,
|
||||
});
|
||||
|
||||
const getTabStyle = (numTabs: number) => {
|
||||
return {
|
||||
width: `${getTabWidth(numTabs)}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => {
|
||||
return classnames('h-full w-full flex items-center justify-center', {
|
||||
'bg-customblue-40': isActiveTab,
|
||||
rounded: isActiveTab,
|
||||
});
|
||||
};
|
||||
const createStyleMap = (
|
||||
expandedWidth: number,
|
||||
borderSize: number,
|
||||
collapsedWidth: number
|
||||
): StyleMap => {
|
||||
const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
|
||||
|
||||
return {
|
||||
open: {
|
||||
left: { marginLeft: '0px' },
|
||||
right: { marginRight: '0px' },
|
||||
},
|
||||
closed: {
|
||||
left: { marginLeft: `-${collapsedHideWidth}px` },
|
||||
right: { marginRight: `-${collapsedHideWidth}px` },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getToolTipContent = (label: string, disabled: boolean) => {
|
||||
return (
|
||||
<>
|
||||
<div>{label}</div>
|
||||
{disabled && <div className="text-white">{'Not available based on current context'}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const createBaseStyle = (expandedWidth: number) => {
|
||||
return {
|
||||
maxWidth: `${expandedWidth}px`,
|
||||
width: `${expandedWidth}px`,
|
||||
// To align the top of the side panel with the top of the viewport grid, use position relative and offset the
|
||||
// top by the same top offset as the viewport grid. Also adjust the height so that there is no overflow.
|
||||
position: 'relative',
|
||||
top: '0.2%',
|
||||
height: '99.8%',
|
||||
};
|
||||
};
|
||||
const SidePanel = ({
|
||||
side,
|
||||
className,
|
||||
activeTabIndex: activeTabIndexProp = null,
|
||||
tabs,
|
||||
onOpen,
|
||||
expandedWidth = 280,
|
||||
onActiveTabIndexChange,
|
||||
}) => {
|
||||
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
|
||||
const [renderHeader, setRenderHeader] = useState(false);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
|
||||
const baseStyle = createBaseStyle(expandedWidth);
|
||||
const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
|
||||
const gridWidth = getGridWidth(tabs.length, gridAvailableWidth);
|
||||
const openStatus = panelOpen ? 'open' : 'closed';
|
||||
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
|
||||
|
||||
const updatePanelOpen = useCallback(
|
||||
(panelOpen: boolean) => {
|
||||
setPanelOpen(panelOpen);
|
||||
if (panelOpen && onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const updateActiveTabIndex = useCallback(
|
||||
(activeTabIndex: number) => {
|
||||
if (activeTabIndex === null) {
|
||||
updatePanelOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
updatePanelOpen(true);
|
||||
|
||||
if (onActiveTabIndexChange) {
|
||||
onActiveTabIndexChange({ activeTabIndex });
|
||||
}
|
||||
},
|
||||
[onActiveTabIndexChange, updatePanelOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderHeader(tabs.length === 1);
|
||||
}, [tabs]);
|
||||
useEffect(() => {
|
||||
updateActiveTabIndex(activeTabIndexProp);
|
||||
}, [activeTabIndexProp, updateActiveTabIndex]);
|
||||
|
||||
const getCloseStateComponent = () => {
|
||||
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classnames(
|
||||
'bg-secondary-dark flex h-[28px] w-full cursor-pointer items-center rounded-md',
|
||||
side === 'left' ? 'justify-end pr-2' : 'justify-start pl-2'
|
||||
)}
|
||||
onClick={() => {
|
||||
updatePanelOpen(!panelOpen);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
<Icons.NavigationPanelReveal
|
||||
className={classnames('text-primary-active', side === 'left' && 'rotate-180 transform')}
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames('mt-3 flex flex-col space-y-3')}>
|
||||
<TooltipProvider>
|
||||
{_childComponents.map((childComponent, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
id={`${childComponent.name}-btn`}
|
||||
data-cy={`${childComponent.name}-btn`}
|
||||
className="text-primary-active hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
return childComponent.disabled ? null : updateActiveTabIndex(index);
|
||||
}}
|
||||
>
|
||||
{React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': childComponent.disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side === 'left' ? 'right' : 'left'}>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center',
|
||||
side === 'left' ? 'justify-end' : 'justify-start'
|
||||
)}
|
||||
>
|
||||
{getToolTipContent(childComponent.label, childComponent.disabled)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getCloseIcon = () => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute flex h-[24px] cursor-pointer items-center justify-center',
|
||||
side === 'left' ? 'right-0' : 'left-0'
|
||||
)}
|
||||
style={{ width: `${closeIconWidth}px` }}
|
||||
onClick={() => {
|
||||
updatePanelOpen(!panelOpen);
|
||||
}}
|
||||
data-cy={`side-panel-header-${side}`}
|
||||
>
|
||||
{React.createElement(Icons[openStateIconName[side]] || Icons.MissingIcon, {
|
||||
className: 'text-primary-active',
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getTabGridComponent = () => {
|
||||
const numCols = getNumGridColumns(tabs.length, gridWidth);
|
||||
|
||||
return (
|
||||
<>
|
||||
{getCloseIcon()}
|
||||
|
||||
<div className={classnames('flex grow justify-center')}>
|
||||
<div className={classnames('bg-primary-dark text-primary-active flex flex-wrap')}>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
const { disabled } = tab;
|
||||
return (
|
||||
<React.Fragment key={tabIndex}>
|
||||
{tabIndex % numCols !== 0 && (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex h-[28px] w-[2px] items-center bg-black',
|
||||
tabSpacerWidth
|
||||
)}
|
||||
>
|
||||
<div className="bg-primary-dark h-[20px] w-full"></div>
|
||||
</div>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip key={tabIndex}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className={getTabClassNames(
|
||||
numCols,
|
||||
tabs.length,
|
||||
tabIndex,
|
||||
tabIndex === activeTabIndex,
|
||||
disabled
|
||||
)}
|
||||
style={getTabStyle(tabs.length)}
|
||||
onClick={() => {
|
||||
return disabled ? null : updateActiveTabIndex(tabIndex);
|
||||
}}
|
||||
data-cy={`${tab.name}-btn`}
|
||||
>
|
||||
<div
|
||||
className={getTabIconClassNames(
|
||||
tabs.length,
|
||||
tabIndex === activeTabIndex
|
||||
)}
|
||||
>
|
||||
{React.createElement(Icons[tab.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{getToolTipContent(tab.label, disabled)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getOpenStateComponent = () => {
|
||||
if (tabs.length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-bkg-med flex h-[40px] select-none rounded-t p-2">
|
||||
{getTabGridComponent()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, baseClasses, classesMap[openStatus][side])}
|
||||
style={style}
|
||||
>
|
||||
{panelOpen ? (
|
||||
<>
|
||||
{getOpenStateComponent()}
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
if (tabIndex === activeTabIndex) {
|
||||
return (
|
||||
<tab.content
|
||||
key={tabIndex}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
renderHeader={renderHeader}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SidePanel.propTypes = {
|
||||
side: PropTypes.oneOf(['left', 'right']).isRequired,
|
||||
className: PropTypes.string,
|
||||
activeTabIndex: PropTypes.number,
|
||||
tabs: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
iconName: PropTypes.string.isRequired,
|
||||
iconLabel: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
|
||||
})
|
||||
),
|
||||
]),
|
||||
onOpen: PropTypes.func,
|
||||
onActiveTabIndexChange: PropTypes.func,
|
||||
expandedWidth: PropTypes.number,
|
||||
};
|
||||
|
||||
export default SidePanel;
|
||||
2
platform/ui-next/src/components/SidePanel/index.ts
Normal file
2
platform/ui-next/src/components/SidePanel/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import SidePanel from './SidePanel';
|
||||
export default SidePanel;
|
||||
143
platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx
Normal file
143
platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import StudyItem from '../StudyItem';
|
||||
import StudyBrowserSort from '../StudyBrowserSort';
|
||||
import StudyBrowserViewOptions from '../StudyBrowserViewOptions';
|
||||
|
||||
const getTrackedSeries = displaySets => {
|
||||
let trackedSeries = 0;
|
||||
displaySets.forEach(displaySet => {
|
||||
if (displaySet.isTracked) {
|
||||
trackedSeries++;
|
||||
}
|
||||
});
|
||||
|
||||
return trackedSeries;
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const StudyBrowser = ({
|
||||
tabs,
|
||||
activeTabName,
|
||||
expandedStudyInstanceUIDs,
|
||||
onClickTab = noop,
|
||||
onClickStudy = noop,
|
||||
onClickThumbnail = noop,
|
||||
onDoubleClickThumbnail = noop,
|
||||
onClickUntrack = noop,
|
||||
activeDisplaySetInstanceUIDs,
|
||||
servicesManager,
|
||||
showSettings,
|
||||
viewPresets,
|
||||
}: withAppTypes) => {
|
||||
const getTabContent = () => {
|
||||
const tabData = tabs.find(tab => tab.name === activeTabName);
|
||||
const viewPreset = viewPresets
|
||||
? viewPresets.filter(preset => preset.selected)[0]?.id
|
||||
: 'thumbnails';
|
||||
return tabData.studies.map(
|
||||
({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => {
|
||||
const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid);
|
||||
return (
|
||||
<React.Fragment key={studyInstanceUid}>
|
||||
<StudyItem
|
||||
date={date}
|
||||
description={description}
|
||||
numInstances={numInstances}
|
||||
isExpanded={isExpanded}
|
||||
displaySets={displaySets}
|
||||
modalities={modalities}
|
||||
trackedSeries={getTrackedSeries(displaySets)}
|
||||
isActive={isExpanded}
|
||||
onClick={() => {
|
||||
onClickStudy(studyInstanceUid);
|
||||
}}
|
||||
onClickThumbnail={onClickThumbnail}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnail}
|
||||
onClickUntrack={onClickUntrack}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
data-cy="thumbnail-list"
|
||||
viewPreset={viewPreset}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showSettings && (
|
||||
<div
|
||||
className="w-100 bg-bkg-low flex h-[48px] items-center justify-center gap-[10px] px-[8px] py-[10px]"
|
||||
data-cy={'studyBrowser-panel'}
|
||||
>
|
||||
<StudyBrowserViewOptions
|
||||
tabs={tabs}
|
||||
onSelectTab={onClickTab}
|
||||
activeTabName={activeTabName}
|
||||
/>
|
||||
<StudyBrowserSort servicesManager={servicesManager} />
|
||||
</div>
|
||||
)}
|
||||
<div className="ohif-scrollbar invisible-scrollbar bg-bkg-low flex flex-1 flex-col gap-[4px] overflow-auto">
|
||||
{getTabContent()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
StudyBrowser.propTypes = {
|
||||
onClickTab: PropTypes.func.isRequired,
|
||||
onClickStudy: PropTypes.func,
|
||||
onClickThumbnail: PropTypes.func,
|
||||
onDoubleClickThumbnail: PropTypes.func,
|
||||
onClickUntrack: PropTypes.func,
|
||||
activeTabName: PropTypes.string.isRequired,
|
||||
expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
tabs: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
studies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
studyInstanceUid: PropTypes.string.isRequired,
|
||||
date: PropTypes.string,
|
||||
numInstances: PropTypes.number,
|
||||
modalities: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
displaySets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string,
|
||||
imageAltText: PropTypes.string,
|
||||
seriesDate: PropTypes.string,
|
||||
seriesNumber: PropTypes.any,
|
||||
numInstances: PropTypes.number,
|
||||
description: PropTypes.string,
|
||||
componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage'])
|
||||
.isRequired,
|
||||
isTracked: PropTypes.bool,
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
* `dragData.type` to identify which targets can receive this draggable item.
|
||||
* If this is not set, drag-n-drop will be disabled for this thumbnail.
|
||||
*
|
||||
* Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members
|
||||
*/
|
||||
dragData: PropTypes.shape({
|
||||
/** Must match the "type" a dropTarget expects */
|
||||
type: PropTypes.string.isRequired,
|
||||
}),
|
||||
})
|
||||
),
|
||||
})
|
||||
).isRequired,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default StudyBrowser;
|
||||
2
platform/ui-next/src/components/StudyBrowser/index.ts
Normal file
2
platform/ui-next/src/components/StudyBrowser/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import StudyBrowser from './StudyBrowser';
|
||||
export default StudyBrowser;
|
||||
@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
|
||||
export default function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
||||
const { customizationService, displaySetService } = servicesManager.services;
|
||||
const { values: sortFunctions } = customizationService.get('studyBrowser.sortFunctions');
|
||||
|
||||
const [selectedSort, setSelectedSort] = useState(sortFunctions[0]);
|
||||
const [sortDirection, setSortDirection] = useState('ascending');
|
||||
|
||||
const handleSortChange = event => {
|
||||
const selectedSortFunction = sortFunctions.find(sort => sort.label === event.target.value);
|
||||
setSelectedSort(selectedSortFunction);
|
||||
};
|
||||
|
||||
const toggleSortDirection = e => {
|
||||
e.stopPropagation();
|
||||
setSortDirection(prevDirection => (prevDirection === 'ascending' ? 'descending' : 'ascending'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection);
|
||||
}, [displaySetService, selectedSort, sortDirection]);
|
||||
|
||||
useEffect(() => {
|
||||
const SubscriptionDisplaySetsChanged = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
||||
() => {
|
||||
displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true);
|
||||
}
|
||||
);
|
||||
const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||
() => {
|
||||
displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
SubscriptionDisplaySetsChanged.unsubscribe();
|
||||
SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
|
||||
};
|
||||
}, [displaySetService, selectedSort, sortDirection]);
|
||||
|
||||
return (
|
||||
<div className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-center rounded border bg-black p-2">
|
||||
<select
|
||||
onChange={handleSortChange}
|
||||
value={selectedSort.label}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="w-full appearance-none bg-transparent text-sm leading-tight text-white shadow transition duration-300 focus:outline-none"
|
||||
>
|
||||
{sortFunctions.map(sort => (
|
||||
<option
|
||||
className="appearance-none bg-black text-white"
|
||||
value={sort.label}
|
||||
key={sort.label}
|
||||
>
|
||||
{sort.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={toggleSortDirection}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
{sortDirection === 'ascending' ? (
|
||||
<Icons.SortingAscending className="text-primary-main w-2" />
|
||||
) : (
|
||||
<Icons.SortingDescending className="text-primary-main w-2" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import StudyBrowserSort from './StudyBrowserSort';
|
||||
|
||||
export default StudyBrowserSort;
|
||||
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function StudyBrowserViewOptions({
|
||||
tabs,
|
||||
onSelectTab,
|
||||
activeTabName,
|
||||
}: withAppTypes) {
|
||||
const handleTabChange = (tabName: string) => {
|
||||
onSelectTab(tabName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-center rounded border bg-black p-2">
|
||||
<select
|
||||
onChange={e => handleTabChange(e.target.value)}
|
||||
value={activeTabName}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="w-full appearance-none bg-transparent text-sm leading-tight text-white shadow transition duration-300 focus:outline-none"
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
const { name, label, studies } = tab;
|
||||
const isActive = activeTabName === name;
|
||||
const isDisabled = !studies.length;
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<option
|
||||
className={`appearance-none bg-black text-white ${isActive ? 'font-bold' : ''}`}
|
||||
value={name}
|
||||
key={name}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import StudyBrowserViewOptions from './StudyBrowserViewOptions';
|
||||
|
||||
export default StudyBrowserViewOptions;
|
||||
88
platform/ui-next/src/components/StudyItem/StudyItem.tsx
Normal file
88
platform/ui-next/src/components/StudyItem/StudyItem.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ThumbnailList from '../ThumbnailList';
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../Accordion';
|
||||
|
||||
const StudyItem = ({
|
||||
date,
|
||||
description,
|
||||
numInstances,
|
||||
modalities,
|
||||
isActive,
|
||||
onClick,
|
||||
isExpanded,
|
||||
displaySets,
|
||||
activeDisplaySetInstanceUIDs,
|
||||
onClickThumbnail,
|
||||
onDoubleClickThumbnail,
|
||||
onClickUntrack,
|
||||
viewPreset = 'thumbnails',
|
||||
}) => {
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
defaultValue={isActive ? 'study-item' : undefined}
|
||||
>
|
||||
<AccordionItem value="study-item">
|
||||
<AccordionTrigger className={classnames('hover:bg-accent bg-popover rounded')}>
|
||||
<div className="flex h-[40px] flex-1 flex-row">
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-col items-start text-[13px]">
|
||||
<div className="text-white">{date}</div>
|
||||
<div className="text-muted-foreground max-w-[160px] overflow-hidden truncate whitespace-nowrap">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground mr-2 flex flex-col items-end text-[12px]">
|
||||
<div>{modalities}</div>
|
||||
<div>{numInstances}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isExpanded && displaySets && (
|
||||
<ThumbnailList
|
||||
thumbnails={displaySets}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
onThumbnailClick={onClickThumbnail}
|
||||
onThumbnailDoubleClick={onDoubleClickThumbnail}
|
||||
onClickUntrack={onClickUntrack}
|
||||
viewPreset={viewPreset}
|
||||
/>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
StudyItem.propTypes = {
|
||||
date: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
modalities: PropTypes.string.isRequired,
|
||||
numInstances: PropTypes.number.isRequired,
|
||||
trackedSeries: PropTypes.number,
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isExpanded: PropTypes.bool,
|
||||
displaySets: PropTypes.array,
|
||||
activeDisplaySetInstanceUIDs: PropTypes.array,
|
||||
onClickThumbnail: PropTypes.func,
|
||||
onDoubleClickThumbnail: PropTypes.func,
|
||||
onClickUntrack: PropTypes.func,
|
||||
viewPreset: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StudyItem;
|
||||
2
platform/ui-next/src/components/StudyItem/index.ts
Normal file
2
platform/ui-next/src/components/StudyItem/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import StudyItem from './StudyItem';
|
||||
export default StudyItem;
|
||||
3
platform/ui-next/src/components/Tabs/index.ts
Normal file
3
platform/ui-next/src/components/Tabs/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs"
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
294
platform/ui-next/src/components/Thumbnail/Thumbnail.tsx
Normal file
294
platform/ui-next/src/components/Thumbnail/Thumbnail.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { Icons } from '../Icons';
|
||||
import DisplaySetMessageListTooltip from '../DisplaySetMessageListTooltip';
|
||||
import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
|
||||
|
||||
/**
|
||||
* Display a thumbnail for a display set.
|
||||
*/
|
||||
const Thumbnail = ({
|
||||
displaySetInstanceUID,
|
||||
className,
|
||||
imageSrc,
|
||||
imageAltText,
|
||||
description,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
countIcon,
|
||||
messages,
|
||||
dragData = {},
|
||||
isActive,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
viewPreset = 'thumbnails',
|
||||
modality,
|
||||
isHydratedForDerivedDisplaySet = false,
|
||||
canReject = false,
|
||||
onReject = () => {},
|
||||
isTracked = false,
|
||||
onClickUntrack = () => {},
|
||||
}): React.ReactNode => {
|
||||
// TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as
|
||||
// this will still allow for "drag", even if there is no drop target for the
|
||||
// specified item.
|
||||
const [collectedProps, drag, dragPreview] = useDrag({
|
||||
type: 'displayset',
|
||||
item: { ...dragData },
|
||||
canDrag: function (monitor) {
|
||||
return Object.keys(dragData).length !== 0;
|
||||
},
|
||||
});
|
||||
|
||||
const [lastTap, setLastTap] = useState(0);
|
||||
|
||||
const handleTouchEnd = e => {
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - lastTap;
|
||||
if (tapLength < 300 && tapLength > 0) {
|
||||
onDoubleClick(e);
|
||||
} else {
|
||||
onClick(e);
|
||||
}
|
||||
setLastTap(currentTime);
|
||||
};
|
||||
|
||||
const renderThumbnailPreset = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-[2px] p-[4px]">
|
||||
<div className="h-[114px] w-[128px]">
|
||||
<div className="relative">
|
||||
{imageSrc ? (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageAltText}
|
||||
className="h-[114px] w-[128px] rounded"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-background h-[114px] w-[128px] rounded"></div>
|
||||
)}
|
||||
|
||||
{/* bottom left */}
|
||||
<div className="bg-muted absolute bottom-0 left-0 flex h-[14px] items-center gap-[4px] p-[4px]">
|
||||
<div
|
||||
className={classnames(
|
||||
'h-[10px] w-[10px] rounded-[2px]',
|
||||
isActive || isHydratedForDerivedDisplaySet ? 'bg-highlight' : 'bg-primary/65',
|
||||
loadingProgress && loadingProgress < 1 && 'bg-primary/25'
|
||||
)}
|
||||
></div>
|
||||
<div className="text-[11px] text-white">{modality}</div>
|
||||
</div>
|
||||
|
||||
{/* top right */}
|
||||
<div className="absolute top-0 right-0 flex items-center gap-[4px]">
|
||||
<DisplaySetMessageListTooltip
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
{canReject && (
|
||||
<Icons.Trash
|
||||
className="h-[20px] w-[20px] text-red-500"
|
||||
onClick={onReject}
|
||||
/>
|
||||
)}
|
||||
{isTracked && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[52px] w-[128px] flex-col">
|
||||
<div className="text-[12px] text-white">{description}</div>
|
||||
<div className="flex h-[12px] items-center gap-[7px] overflow-hidden">
|
||||
<div className="text-muted-foreground text-[12px]"> S:{seriesNumber}</div>
|
||||
<div className="text-muted-foreground text-[12px]">
|
||||
<div className="flex items-center gap-[4px]">
|
||||
{' '}
|
||||
{countIcon ? (
|
||||
React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' })
|
||||
) : (
|
||||
<Icons.InfoSeries className="w-3" />
|
||||
)}
|
||||
<div>{numInstances}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderListPreset = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-between pr-[8px] pl-[8px] pt-[4px] pb-[4px]">
|
||||
<div className="relative flex h-[32px] items-center gap-[8px]">
|
||||
<div
|
||||
className={classnames(
|
||||
'h-[32px] w-[4px] rounded-[2px]',
|
||||
isActive || isHydratedForDerivedDisplaySet ? 'bg-highlight' : 'bg-primary/65',
|
||||
loadingProgress && loadingProgress < 1 && 'bg-primary/25'
|
||||
)}
|
||||
></div>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-[7px]">
|
||||
<div className="text-[13px] text-white">{modality}</div>
|
||||
|
||||
<div className="max-w-[160px] overflow-hidden overflow-ellipsis whitespace-nowrap text-[13px] text-white">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[12px] items-center gap-[7px] overflow-hidden">
|
||||
<div className="text-muted-foreground text-[12px]"> S:{seriesNumber}</div>
|
||||
<div className="text-muted-foreground text-[12px]">
|
||||
<div className="flex items-center gap-[4px]">
|
||||
{' '}
|
||||
{countIcon ? (
|
||||
React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' })
|
||||
) : (
|
||||
<Icons.InfoSeries className="w-3" />
|
||||
)}
|
||||
<div>{numInstances}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full items-center gap-[4px]">
|
||||
<DisplaySetMessageListTooltip
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
{canReject && (
|
||||
<Icons.Trash
|
||||
className="h-[20px] w-[20px] text-red-500"
|
||||
onClick={onReject}
|
||||
/>
|
||||
)}
|
||||
{isTracked && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'bg-muted hover:bg-primary/30 flex cursor-pointer select-none flex-col outline-none',
|
||||
viewPreset === 'thumbnails' && 'h-[170px] w-[135px]',
|
||||
viewPreset === 'list' && 'h-[40px] w-[275px]'
|
||||
)}
|
||||
id={`thumbnail-${displaySetInstanceUID}`}
|
||||
data-cy={`study-browser-thumbnail`}
|
||||
data-series={seriesNumber}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
ref={drag}
|
||||
className="h-full w-full"
|
||||
>
|
||||
{viewPreset === 'thumbnails' && renderThumbnailPreset()}
|
||||
{viewPreset === 'list' && renderListPreset()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Thumbnail.propTypes = {
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
imageSrc: PropTypes.string,
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
* `dragData.type` to identify which targets can receive this draggable item.
|
||||
* If this is not set, drag-n-drop will be disabled for this thumbnail.
|
||||
*
|
||||
* Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members
|
||||
*/
|
||||
dragData: PropTypes.shape({
|
||||
/** Must match the "type" a dropTarget expects */
|
||||
type: PropTypes.string.isRequired,
|
||||
}),
|
||||
imageAltText: PropTypes.string,
|
||||
description: PropTypes.string.isRequired,
|
||||
seriesNumber: PropTypes.any,
|
||||
numInstances: PropTypes.number.isRequired,
|
||||
loadingProgress: PropTypes.number,
|
||||
messages: PropTypes.object,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onDoubleClick: PropTypes.func.isRequired,
|
||||
viewPreset: PropTypes.string,
|
||||
modality: PropTypes.string,
|
||||
isHydratedForDerivedDisplaySet: PropTypes.bool,
|
||||
canReject: PropTypes.bool,
|
||||
onReject: PropTypes.func,
|
||||
isTracked: PropTypes.bool,
|
||||
onClickUntrack: PropTypes.func,
|
||||
countIcon: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Thumbnail;
|
||||
2
platform/ui-next/src/components/Thumbnail/index.ts
Normal file
2
platform/ui-next/src/components/Thumbnail/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import Thumbnail from './Thumbnail';
|
||||
export default Thumbnail;
|
||||
162
platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx
Normal file
162
platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Thumbnail from '../Thumbnail';
|
||||
|
||||
const ThumbnailList = ({
|
||||
thumbnails,
|
||||
onThumbnailClick,
|
||||
onThumbnailDoubleClick,
|
||||
onClickUntrack,
|
||||
activeDisplaySetInstanceUIDs = [],
|
||||
viewPreset,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
id="ohif-thumbnail-list"
|
||||
className={`ohif-scrollbar bg-bkg-low grid place-items-center overflow-y-hidden pt-[4px] pr-[2.5px] pl-[2.5px] ${viewPreset === 'thumbnails' ? 'grid-cols-2 gap-[4px] pb-[12px]' : 'grid-cols-1 gap-[2px]'}`}
|
||||
>
|
||||
{thumbnails.map(
|
||||
({
|
||||
displaySetInstanceUID,
|
||||
description,
|
||||
dragData,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
loadingProgress,
|
||||
modality,
|
||||
componentType,
|
||||
seriesDate,
|
||||
countIcon,
|
||||
isTracked,
|
||||
canReject,
|
||||
onReject,
|
||||
imageSrc,
|
||||
messages,
|
||||
imageAltText,
|
||||
isHydratedForDerivedDisplaySet,
|
||||
}) => {
|
||||
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
|
||||
switch (componentType) {
|
||||
case 'thumbnail':
|
||||
return (
|
||||
<Thumbnail
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances || 1}
|
||||
countIcon={countIcon}
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
messages={messages}
|
||||
isActive={isActive}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
|
||||
viewPreset={viewPreset}
|
||||
modality={modality}
|
||||
/>
|
||||
);
|
||||
case 'thumbnailTracked':
|
||||
return (
|
||||
<Thumbnail
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances}
|
||||
loadingProgress={loadingProgress}
|
||||
countIcon={countIcon}
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
messages={messages}
|
||||
isTracked={isTracked}
|
||||
isActive={isActive}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
|
||||
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
|
||||
viewPreset={viewPreset}
|
||||
modality={modality}
|
||||
/>
|
||||
);
|
||||
case 'thumbnailNoImage':
|
||||
return (
|
||||
<Thumbnail
|
||||
isActive={isActive}
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
modality={modality}
|
||||
messages={messages}
|
||||
description={description}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
|
||||
viewPreset={viewPreset}
|
||||
countIcon={countIcon}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances || 1}
|
||||
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
|
||||
canReject={canReject}
|
||||
onReject={onReject}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ThumbnailList.propTypes = {
|
||||
thumbnails: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string,
|
||||
imageAltText: PropTypes.string,
|
||||
seriesDate: PropTypes.string,
|
||||
seriesNumber: PropTypes.any,
|
||||
numInstances: PropTypes.number,
|
||||
description: PropTypes.string,
|
||||
componentType: PropTypes.any,
|
||||
isTracked: PropTypes.bool,
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
* `dragData.type` to identify which targets can receive this draggable item.
|
||||
* If this is not set, drag-n-drop will be disabled for this thumbnail.
|
||||
*
|
||||
* Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members
|
||||
*/
|
||||
dragData: PropTypes.shape({
|
||||
/** Must match the "type" a dropTarget expects */
|
||||
type: PropTypes.string.isRequired,
|
||||
}),
|
||||
})
|
||||
),
|
||||
activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
onThumbnailClick: PropTypes.func.isRequired,
|
||||
onThumbnailDoubleClick: PropTypes.func.isRequired,
|
||||
onClickUntrack: PropTypes.func.isRequired,
|
||||
viewPreset: PropTypes.string,
|
||||
};
|
||||
|
||||
// TODO: Support "Viewport Identificator"?
|
||||
function _getModalityTooltip(modality) {
|
||||
if (_modalityTooltips.hasOwnProperty(modality)) {
|
||||
return _modalityTooltips[modality];
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const _modalityTooltips = {
|
||||
SR: 'Structured Report',
|
||||
SEG: 'Segmentation',
|
||||
OT: 'Other',
|
||||
RTSTRUCT: 'RT Structure Set',
|
||||
};
|
||||
|
||||
export default ThumbnailList;
|
||||
2
platform/ui-next/src/components/ThumbnailList/index.ts
Normal file
2
platform/ui-next/src/components/ThumbnailList/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
export default ThumbnailList;
|
||||
@ -9,12 +9,12 @@ const toggleVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
default: 'bg-transparent hover:text-primary',
|
||||
outline:
|
||||
'border border-input bg-transparent shadow-sm hover:bg-primary/20 hover:text-primary-foreground',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-3 mx-0.5',
|
||||
default: 'h-[24px] w-[28px]',
|
||||
sm: 'h-8 px-2',
|
||||
lg: 'h-10 px-3',
|
||||
},
|
||||
|
||||
3
platform/ui-next/src/components/Toggle/index.ts
Normal file
3
platform/ui-next/src/components/Toggle/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Toggle, toggleVariants } from "./Toggle";
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
55
platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx
Normal file
55
platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
|
||||
import { VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { toggleVariants } from '../Toggle';
|
||||
|
||||
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('bg-primary/10 flex items-center justify-center rounded-md', className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
3
platform/ui-next/src/components/ToggleGroup/index.ts
Normal file
3
platform/ui-next/src/components/ToggleGroup/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from "./ToggleGroup";
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
183
platform/ui-next/src/components/Toolbox/Toolbox.tsx
Normal file
183
platform/ui-next/src/components/Toolbox/Toolbox.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
import { ToolboxUI } from './';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { useToolbox } from '@ohif/ui';
|
||||
import Separator from '../Separator';
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
* custom control panels to users. This component is a generic UI component that
|
||||
* interacts with services and commands in a generic fashion. While it might
|
||||
* seem unconventional to import it from the UI and integrate it into the JSX,
|
||||
* it belongs in the UI components as there isn't anything in this component that
|
||||
* couldn't be used for a completely different type of app. It plays a crucial
|
||||
* role in enhancing the app with a toolbox by providing a way to integrate
|
||||
* and display various tools and their corresponding options
|
||||
*/
|
||||
function Toolbox({
|
||||
servicesManager,
|
||||
buttonSectionId,
|
||||
commandsManager,
|
||||
title,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
...props
|
||||
}: withAppTypes) {
|
||||
const { state: toolboxState, api } = useToolbox(buttonSectionId);
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
const prevButtonIdsRef = useRef('');
|
||||
const prevToolboxStateRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
const currentButtonIdsStr = JSON.stringify(
|
||||
toolbarButtons.map(button => {
|
||||
const { id, componentProps } = button;
|
||||
if (componentProps.items?.length) {
|
||||
return componentProps.items.map(item => `${item.id}-${item.disabled}`);
|
||||
}
|
||||
return `${id}-${componentProps.disabled}`;
|
||||
})
|
||||
);
|
||||
|
||||
const currentToolBoxStateStr = JSON.stringify(
|
||||
Object.keys(toolboxState.toolOptions).map(tool => {
|
||||
const options = toolboxState.toolOptions[tool];
|
||||
if (Array.isArray(options)) {
|
||||
return options?.map(option => `${option.id}-${option.value}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
prevButtonIdsRef.current === currentButtonIdsStr &&
|
||||
prevToolboxStateRef.current === currentToolBoxStateStr
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevButtonIdsRef.current = currentButtonIdsStr;
|
||||
prevToolboxStateRef.current = currentToolBoxStateStr;
|
||||
|
||||
const initializeOptionsWithEnhancements = toolbarButtons.reduce(
|
||||
(accumulator, toolbarButton) => {
|
||||
const { id: buttonId, componentProps } = toolbarButton;
|
||||
|
||||
const createEnhancedOptions = (options, parentId) => {
|
||||
const optionsToUse = Array.isArray(options) ? options : [options];
|
||||
|
||||
return optionsToUse.map(option => {
|
||||
if (typeof option.optionComponent === 'function') {
|
||||
return option;
|
||||
}
|
||||
|
||||
const value =
|
||||
toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ??
|
||||
option.value;
|
||||
|
||||
const updatedOptions = toolboxState.toolOptions?.[parentId];
|
||||
|
||||
return {
|
||||
...option,
|
||||
value,
|
||||
commands: value => {
|
||||
api.handleToolOptionChange(parentId, option.id, value);
|
||||
|
||||
const { isArray } = Array;
|
||||
const cmds = isArray(option.commands) ? option.commands : [option.commands];
|
||||
|
||||
cmds.forEach(command => {
|
||||
const isString = typeof command === 'string';
|
||||
const isObject = typeof command === 'object';
|
||||
const isFunction = typeof command === 'function';
|
||||
|
||||
if (isString) {
|
||||
commandsManager.run(command, { value });
|
||||
} else if (isObject) {
|
||||
commandsManager.run({
|
||||
...command,
|
||||
commandOptions: {
|
||||
...command.commandOptions,
|
||||
...option,
|
||||
value,
|
||||
options: updatedOptions,
|
||||
},
|
||||
});
|
||||
} else if (isFunction) {
|
||||
command({ value, commandsManager, servicesManager, options: updatedOptions });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const { items, options } = componentProps;
|
||||
|
||||
if (items?.length) {
|
||||
items.forEach(({ options, id }) => {
|
||||
accumulator[id] = createEnhancedOptions(options, id);
|
||||
});
|
||||
} else if (options?.length) {
|
||||
accumulator[buttonId] = createEnhancedOptions(options, buttonId);
|
||||
} else if (options?.optionComponent) {
|
||||
accumulator[buttonId] = options.optionComponent;
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
api.initializeToolOptions(initializeOptionsWithEnhancements);
|
||||
}, [toolbarButtons, api, toolboxState, commandsManager, servicesManager]);
|
||||
|
||||
const handleToolOptionChange = (toolName, optionName, newValue) => {
|
||||
api.handleToolOptionChange(toolName, optionName, newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
api.handleToolSelect(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toolbox;
|
||||
126
platform/ui-next/src/components/Toolbox/ToolboxUI.tsx
Normal file
126
platform/ui-next/src/components/Toolbox/ToolboxUI.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { PanelSection } from '../../components';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { ToolSettings } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const ItemsPerRow = 4;
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just refactoring from the toolbox component to make it more readable
|
||||
*/
|
||||
function ToolboxUI(props: withAppTypes) {
|
||||
const {
|
||||
toolbarButtons,
|
||||
handleToolSelect,
|
||||
toolboxState,
|
||||
numRows,
|
||||
servicesManager,
|
||||
title,
|
||||
useCollapsedPanel = true,
|
||||
} = props;
|
||||
|
||||
const { activeTool, toolOptions, selectedEvent } = toolboxState;
|
||||
const activeToolOptions = toolOptions?.[activeTool];
|
||||
|
||||
const prevToolOptions = usePrevious(activeToolOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeToolOptions || Array.isArray(activeToolOptions) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeToolOptions.forEach((option, index) => {
|
||||
const prevOption = prevToolOptions ? prevToolOptions[index] : undefined;
|
||||
if (!prevOption || option.value !== prevOption.value || selectedEvent) {
|
||||
const isOptionValid = option.condition
|
||||
? option.condition({ options: activeToolOptions })
|
||||
: true;
|
||||
if (isOptionValid) {
|
||||
const { commands } = option;
|
||||
commands(option.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [activeToolOptions, selectedEvent]);
|
||||
|
||||
const render = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-black">
|
||||
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
const isLastRow = Math.floor(index / ItemsPerRow) + 1 === numRows;
|
||||
|
||||
const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`;
|
||||
|
||||
const onInteraction = ({ itemId, id, commands }) => {
|
||||
const idToUse = itemId || id;
|
||||
handleToolSelect(idToUse);
|
||||
props.onInteraction({
|
||||
itemId,
|
||||
commands,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames({
|
||||
[toolClasses]: true,
|
||||
'border-secondary-light flex flex-col items-center justify-center rounded-md border':
|
||||
true,
|
||||
})}
|
||||
>
|
||||
<div className="flex rounded-md bg-black">
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-primary-dark h-auto px-2">
|
||||
{activeToolOptions && <ToolSettings options={activeToolOptions} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{useCollapsedPanel ? (
|
||||
<PanelSection
|
||||
childrenClassName="flex-shrink-0"
|
||||
title={title}
|
||||
>
|
||||
{render()}
|
||||
</PanelSection>
|
||||
) : (
|
||||
render()
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToolboxUI };
|
||||
3
platform/ui-next/src/components/Toolbox/index.ts
Normal file
3
platform/ui-next/src/components/Toolbox/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { ToolboxUI } from './ToolboxUI';
|
||||
import Toolbox from './Toolbox';
|
||||
export { ToolboxUI, Toolbox };
|
||||
@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'bg-primary-dark border-secondary-light text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-xs',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -6,15 +6,28 @@ import Combobox from './Combobox';
|
||||
import Popover from './Popover';
|
||||
import Calendar from './Calendar';
|
||||
import DatePickerWithRange from './DateRange';
|
||||
import Separator from './Separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
|
||||
import { Toggle, toggleVariants } from './Toggle';
|
||||
import { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
|
||||
import { Input } from './Input';
|
||||
import { Label } from './Label';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
|
||||
import { Separator } from './Separator';
|
||||
import { Switch } from './Switch';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { Toggle, toggleVariants } from './Toggle';
|
||||
import { Slider } from './Slider';
|
||||
import { ScrollArea, ScrollBar } from './ScrollArea';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion';
|
||||
import { Icons } from './Icons';
|
||||
import SidePanel from './SidePanel';
|
||||
import StudyItem from './StudyItem';
|
||||
import StudyBrowser from './StudyBrowser';
|
||||
import StudyBrowserSort from './StudyBrowserSort';
|
||||
import StudyBrowserViewOptions from './StudyBrowserViewOptions';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
import PanelSection from './PanelSection';
|
||||
import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
|
||||
import { Toolbox, ToolboxUI } from './Toolbox';
|
||||
|
||||
export {
|
||||
Button,
|
||||
@ -39,4 +52,23 @@ export {
|
||||
toggleVariants,
|
||||
Slider,
|
||||
ScrollArea,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
ScrollBar,
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Icons,
|
||||
SidePanel,
|
||||
StudyItem,
|
||||
StudyBrowser,
|
||||
StudyBrowserSort,
|
||||
StudyBrowserViewOptions,
|
||||
Thumbnail,
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
};
|
||||
|
||||
@ -8,9 +8,34 @@ import {
|
||||
Combobox,
|
||||
Calendar,
|
||||
DatePickerWithRange,
|
||||
Separator,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Toggle,
|
||||
toggleVariants,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Icons,
|
||||
SidePanel,
|
||||
StudyItem,
|
||||
StudyBrowser,
|
||||
StudyBrowserSort,
|
||||
StudyBrowserViewOptions,
|
||||
Thumbnail,
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
} from './components';
|
||||
|
||||
import { useNotification, NotificationProvider } from './contextProviders';
|
||||
import { Toggle, toggleVariants } from './Toggle';
|
||||
|
||||
export {
|
||||
// components
|
||||
@ -26,4 +51,29 @@ export {
|
||||
// contextProviders
|
||||
NotificationProvider,
|
||||
useNotification,
|
||||
Separator,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Toggle,
|
||||
toggleVariants,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Icons,
|
||||
SidePanel,
|
||||
StudyItem,
|
||||
StudyBrowser,
|
||||
StudyBrowserSort,
|
||||
StudyBrowserViewOptions,
|
||||
Thumbnail,
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
};
|
||||
|
||||
@ -90,6 +90,20 @@ module.exports = {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
bkg: {
|
||||
low: '#050615',
|
||||
med: '#090C29',
|
||||
full: '#041C4A',
|
||||
},
|
||||
info: {
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#7BB2CE',
|
||||
},
|
||||
actions: {
|
||||
primary: '#348CFD',
|
||||
highlight: '#5ACCE6',
|
||||
hover: 'rgba(52, 140, 253, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@ui/*": ["./src/*"]
|
||||
"@ui/*": ["./src/*"],
|
||||
"@ohif/core": ["../core/src"],
|
||||
"@ohif/ui": ["../ui/src"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
@ -59,7 +59,20 @@ module.exports = {
|
||||
dark: '#726f7e',
|
||||
active: '#2c3074',
|
||||
},
|
||||
|
||||
bkg: {
|
||||
low: '#050615',
|
||||
med: '#090C29',
|
||||
full: '#041C4A',
|
||||
},
|
||||
info: {
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#7BB2CE',
|
||||
},
|
||||
actions: {
|
||||
primary: '#348CFD',
|
||||
highlight: '#5ACCE6',
|
||||
hover: 'rgba(52, 140, 253, 0.2)',
|
||||
},
|
||||
customgreen: {
|
||||
100: '#05D97C',
|
||||
200: '#0FD97C',
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@ -3527,9 +3527,9 @@
|
||||
graceful-fs "4.2.10"
|
||||
|
||||
"@pnpm/npm-conf@^2.1.0":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0"
|
||||
integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz#bb375a571a0bd63ab0a23bece33033c683e9b6b0"
|
||||
integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==
|
||||
dependencies:
|
||||
"@pnpm/config.env-replace" "^1.1.0"
|
||||
"@pnpm/network.ca-file" "^1.0.1"
|
||||
@ -3564,6 +3564,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
|
||||
|
||||
"@radix-ui/react-accordion@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz#aed0770fcb16285db992d81873ccd7a014c7f17d"
|
||||
integrity sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-collapsible" "1.1.0"
|
||||
"@radix-ui/react-collection" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-arrow@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
|
||||
@ -3593,6 +3608,20 @@
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collapsible@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
|
||||
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user