feat(sort): custom series sort in study panel (#4214)
This commit is contained in:
parent
6f93449149
commit
a433d406e2
@ -505,13 +505,8 @@ const commandsModule = ({
|
||||
}: UpdateViewportDisplaySetParams) => {
|
||||
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
|
||||
// Sort the display sets as per the hanging protocol service viewport/display set scoring system.
|
||||
// The thumbnail list uses the same sorting.
|
||||
const dsSortFn = hangingProtocolService.getDisplaySetSortFunction();
|
||||
const currentDisplaySets = [...displaySetService.activeDisplaySets];
|
||||
|
||||
currentDisplaySets.sort(dsSortFn);
|
||||
|
||||
const { activeViewportId, viewports, isHangingProtocolLayout } =
|
||||
viewportGridService.getState();
|
||||
|
||||
|
||||
@ -4,6 +4,9 @@ import DataSourceSelector from './Panels/DataSourceSelector';
|
||||
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
|
||||
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
||||
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const formatDate = utils.formatDate;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -162,6 +165,26 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
id: 'progressDropdownWithServiceComponent',
|
||||
component: ProgressDropdownWithService,
|
||||
},
|
||||
{
|
||||
id: 'studyBrowser.sortFunctions',
|
||||
merge: 'Append',
|
||||
values: [
|
||||
{
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesNumber - b?.SeriesNumber;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Series Date',
|
||||
sortFunction: (a, b) => {
|
||||
const dateA = new Date(formatDate(a?.SeriesDate));
|
||||
const dateB = new Date(formatDate(b?.SeriesDate));
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -272,11 +272,7 @@ function PanelStudyBrowserTracking({
|
||||
};
|
||||
}, [thumbnailImageSrcMap, trackedSeries, viewports, dataSource, displaySetService]);
|
||||
|
||||
const tabs = createStudyBrowserTabs(
|
||||
StudyInstanceUIDs,
|
||||
studyDisplayList,
|
||||
displaySets,
|
||||
);
|
||||
const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets);
|
||||
|
||||
// TODO: Should not fire this on "close"
|
||||
function _handleStudyClick(StudyInstanceUID) {
|
||||
|
||||
@ -90,6 +90,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
// },
|
||||
// ]);
|
||||
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/** @type {AppTypes.Config} */
|
||||
|
||||
const config = {
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
// whiteLabeling: {},
|
||||
extensions: [],
|
||||
@ -13,6 +13,7 @@ const config = {
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
showCPUFallbackMessage: true,
|
||||
showLoadingIndicator: true,
|
||||
experimentalStudyBrowserSort: true,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
groupEnabledModesFirst: true,
|
||||
maxNumRequests: {
|
||||
@ -289,5 +290,3 @@ const config = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
window.config = config;
|
||||
|
||||
@ -9,6 +9,7 @@ window.config = {
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
showCPUFallbackMessage: true,
|
||||
showLoadingIndicator: true,
|
||||
experimentalStudyBrowserSort: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
groupEnabledModesFirst: true,
|
||||
// filterQueryParam: false,
|
||||
|
||||
@ -177,9 +177,10 @@ export default class CustomizationService extends PubSubService {
|
||||
defaultCustomization ||
|
||||
{};
|
||||
|
||||
// use the source merge type if not provided then fallback to merge
|
||||
this.modeCustomizations.set(
|
||||
customizationId,
|
||||
this.mergeValue(sourceCustomization, customization, merge)
|
||||
this.mergeValue(sourceCustomization, customization, sourceCustomization.merge ?? merge)
|
||||
);
|
||||
this.transformedCustomizations.clear();
|
||||
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
|
||||
|
||||
@ -424,4 +424,24 @@ export default class DisplaySetService extends PubSubService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param sortFn function to sort the display sets
|
||||
* @param direction direction to sort the display sets
|
||||
* @returns void
|
||||
*/
|
||||
public sortDisplaySets(
|
||||
sortFn: (a: DisplaySet, b: DisplaySet) => number,
|
||||
direction: string,
|
||||
suppressEvent = false
|
||||
): void {
|
||||
this.activeDisplaySets.sort(sortFn);
|
||||
if (direction === 'descending') {
|
||||
this.activeDisplaySets.reverse();
|
||||
}
|
||||
if (!suppressEvent) {
|
||||
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ declare global {
|
||||
customizationService?: any;
|
||||
extensions?: string[];
|
||||
modes?: string[];
|
||||
experimentalStudyBrowserSort?: boolean;
|
||||
defaultDataSourceName?: string;
|
||||
hotkeys?: Record<string, Hotkey> | Hotkey[];
|
||||
useSharedArrayBuffer?: 'AUTO' | 'FALSE' | 'TRUE';
|
||||
|
||||
@ -124,6 +124,7 @@ Here are a list of some options available:
|
||||
- `acceptHeader` : accept header to request specific dicom transfer syntax ex : [ 'multipart/related; type=image/jls; q=1', 'multipart/related; type=application/octet-stream; q=0.1' ]
|
||||
- `investigationalUseDialog`: This should contain an object with `option` value, it can be either `always` which always shows the dialog once per session, `never` which never shows the dialog, or `configure` which shows the dialog once and won't show it again until a set number of days defined by the user, if it's set to configure, you are required to add an additional property `days` which is the number of days to wait before showing the dialog again.
|
||||
- `groupEnabledModesFirst`: boolean, if set to true, all valid modes for the study get grouped together first, then the rest of the modes. If false, all modes are shown in the order they are defined in the configuration.
|
||||
- `experimentalStudyBrowserSort`: boolean, if set to true, you will get the experimental StudyBrowserSort component in the UI, which displays a list of sort functions that the displaySets can be sorted by, the sort reflects in all part of the app including the thumbnail/study panel. These sort functions are defined in the customizationModule and can be expanded by users.
|
||||
- `disableConfirmationPrompts`: boolean, if set to true, it skips confirmation prompts for measurement tracking and hydration.
|
||||
- `showPatientInfo`: string, if set to 'visible', the patient info header will be shown and its initial state is expanded. If set to 'visibleCollapsed', the patient info header will be shown but it's initial state is collapsed. If set to 'disabled', the patient info header will never be shown, and if set to 'visibleReadOnly', the patient info header will be shown and always expanded.
|
||||
- `requestTransferSyntaxUID` : Request a specific Transfer syntax from dicom web server ex: 1.2.840.10008.1.2.4.80 (applied only if acceptHeader is not set)
|
||||
|
||||
BIN
platform/docs/docs/faq/study-sorting.png
Normal file
BIN
platform/docs/docs/faq/study-sorting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@ -272,3 +272,78 @@ to
|
||||
which then it will look like
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## How do I sort the series in the study panel by a specific value
|
||||
|
||||
You need to enable the experimental StudyBrowserSort component by setting the `experimentalStudyBrowserSort` to true in your config file. This will add a dropdown in the study panel to sort the series by a specific value. This component is experimental
|
||||
since we are re-deigning the study panel and it might change in the future, but the functionality will remain the same.
|
||||
|
||||
```js
|
||||
{
|
||||
experimentalStudyBrowserSort: true,
|
||||
}
|
||||
```
|
||||
The component will appear in the study panel and will allow you to sort the series by a specific value. It comes with 3 default sorting functions, Series Number, Series Image Count, and Series Date.
|
||||
|
||||
You can sort the series in the study panel by a specific value by adding a custom sorting function in the customizationModule, you can use the existing customizationModule in `extensions/default/src/getCustomizationModule.tsx` or create your own in your extension.
|
||||
|
||||
The value to be used for the entry is `studyBrowser.sortFunctions` and should be under the `default` key.
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
export default function getCustomizationModule({ servicesManager, extensionManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
|
||||
{
|
||||
id: 'studyBrowser.sortFunctions',
|
||||
values: [
|
||||
{
|
||||
label: 'Series Number',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.SeriesNumber - b?.SeriesNumber;
|
||||
},
|
||||
},
|
||||
// Add more sort functions as needed
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Explanation
|
||||
This function will be retrieved by the StudyBrowserSort component and will be used to sort all displaySets, it will reflect in all parts of the app since it works at the displaySetService level, which means the thumbnails in the study panel will also be sorted by the desired value.
|
||||
You can define multiple functions and pick which sort to use via the dropdown in the StudyBrowserSort component that appears in the study panel.
|
||||
|
||||
|
||||
## How can i change the sorting of the thumbnail / study panel / study browser
|
||||
We are currently redesigning the study panel and the study browser. During this process, you can enable our undesigned component via the `experimentalStudyBrowserSort` flag. This will look like:
|
||||
|
||||

|
||||
|
||||
You can also add your own sorting functions by utilizing the `customizationService` and adding the `studyBrowser.sortFunctions` key, as shown below:
|
||||
|
||||
```
|
||||
customizationService.addModeCustomizations([
|
||||
{
|
||||
id: 'studyBrowser.sortFunctions',
|
||||
values: [{
|
||||
label: 'Series Images',
|
||||
sortFunction: (a, b) => {
|
||||
return a?.numImageFrames - b?.numImageFrames;
|
||||
},
|
||||
}],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
:::note
|
||||
Notice the arrays and objects, the values are arrays
|
||||
:::
|
||||
|
||||
@ -7,6 +7,7 @@ import LegacyButtonGroup from '../LegacyButtonGroup';
|
||||
import LegacyButton from '../LegacyButton';
|
||||
import ThumbnailList from '../ThumbnailList';
|
||||
import { StringNumber } from '../../types';
|
||||
import StudyBrowserSort from '../StudyBrowserSort';
|
||||
|
||||
const getTrackedSeries = displaySets => {
|
||||
let trackedSeries = 0;
|
||||
@ -73,7 +74,7 @@ const StudyBrowser = ({
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="w-100 border-secondary-light bg-primary-dark flex h-16 flex-row items-center justify-center border-b p-4"
|
||||
className="w-100 border-secondary-light bg-primary-dark flex h-20 flex-col items-center justify-center gap-2 border-b p-4"
|
||||
data-cy={'studyBrowser-panel'}
|
||||
>
|
||||
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
|
||||
@ -111,6 +112,9 @@ const StudyBrowser = ({
|
||||
);
|
||||
})}
|
||||
</LegacyButtonGroup>
|
||||
{window.config.experimentalStudyBrowserSort && (
|
||||
<StudyBrowserSort servicesManager={servicesManager} />
|
||||
)}
|
||||
</div>
|
||||
<div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto">
|
||||
{getTabContent()}
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Icon from '../Icon';
|
||||
|
||||
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="flex gap-2">
|
||||
<select
|
||||
onChange={handleSortChange}
|
||||
value={selectedSort.label}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="border-inputfield-main focus:border-inputfield-main w-full appearance-none rounded border bg-black py-2 px-3 text-sm leading-tight text-white shadow transition duration-300 focus:outline-none"
|
||||
>
|
||||
{sortFunctions.map(sort => (
|
||||
<option
|
||||
value={sort.label}
|
||||
key={sort.label}
|
||||
>
|
||||
{sort.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={toggleSortDirection}
|
||||
className="border-inputfield-main flex items-center justify-center rounded border bg-black"
|
||||
>
|
||||
<Icon
|
||||
name={sortDirection === 'ascending' ? 'sorting-active-up' : 'sorting-active-down'}
|
||||
className="text-primary-main mx-2 w-2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
platform/ui/src/components/StudyBrowserSort/index.js
Normal file
3
platform/ui/src/components/StudyBrowserSort/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import StudyBrowserSort from './StudyBrowserSort';
|
||||
|
||||
export default StudyBrowserSort;
|
||||
@ -95,6 +95,7 @@ import InvestigationalUseDialog from './InvestigationalUseDialog';
|
||||
import MeasurementItem from './MeasurementTable/MeasurementItem';
|
||||
import LayoutPreset from './LayoutPreset';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import StudyBrowserSort from './StudyBrowserSort';
|
||||
|
||||
export {
|
||||
ActionButtons,
|
||||
@ -198,4 +199,5 @@ export {
|
||||
ToolSettings,
|
||||
Toolbox,
|
||||
InvestigationalUseDialog,
|
||||
StudyBrowserSort,
|
||||
};
|
||||
|
||||
@ -132,6 +132,7 @@ export {
|
||||
Toolbox,
|
||||
InvestigationalUseDialog,
|
||||
LayoutPreset,
|
||||
StudyBrowserSort,
|
||||
} from './components';
|
||||
|
||||
export { useSessionStorage } from './hooks';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user