ohif-viewer/extensions/default/src/customizations/reportDialogCustomization.tsx
Bill Wallace dbb8b1526e
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]

* feat: load DICOM SEG images via imageLoader

* feat: load DICOM SEG images via imageLoader

* PR review fixes

* Test fragment of compressed multiframes

* fix: Removed dependencies incorrectly

* Update frame as a targetted change to avoid stripping remaining params

* lock

* Update test to agree with the missing representation branch

* test(contour): contour color change coverage (#6042)

* test(contour): contour interactions delete segment (#6069)

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]

* feat(testing): OHIF Test Agent Skills (#5993)

* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]

* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)

* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]

* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)

* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)

* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]

* Debug fixes

* perf(seg): pass explicit frame decode concurrency (16) to SEG loader

Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal

Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.

First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pre-cache the image data using a full part10 file for performance

* Add customizations to specify type of segmentation save

* Add clearcache of the cacheData

* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base

libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix seg OOM

* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader

- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
  dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
  group (download / clipboard / local wadouri blob / local store); keep the
  hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
  local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
  NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
  project dependency; the rule only serves the gitignored cs3d-link
  workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PR review comment fixes

* Update versions

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-09 20:54:23 -04:00

269 lines
8.4 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InputDialog } from '@ohif/ui-next';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
type DataSource = {
value: string;
label: string;
placeHolder: string;
};
/** Radix Select item value for "Create new series" (state remains null). */
const NEW_SERIES_SELECT_VALUE = '__new_series_id__';
type SeriesOption = {
optionKey: string;
selectValue: string;
value: string | null;
seriesNumber: number;
description: string | null;
label: string;
};
type ReportDialogProps = {
dataSources: DataSource[];
modality?: string;
predecessorImageId?: string;
hide: () => void;
onSave: (data: {
reportName: string;
dataSource: string | null;
series: string | null;
priorSeriesNumber: number;
}) => void;
onCancel: () => void;
enableDownload?: boolean;
};
function ReportDialog({
dataSources,
modality = 'SR',
predecessorImageId,
minSeriesNumber = 3000,
hide,
onSave,
onCancel,
enableDownload = false,
}: ReportDialogProps) {
const { t } = useTranslation('Buttons');
const { servicesManager } = useSystem();
const actionTakenRef = useRef(false);
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
dataSources?.[0]?.value ?? null
);
const { displaySetService } = servicesManager.services;
const [selectedSeries, setSelectedSeries] = useState<string | null>(predecessorImageId || null);
const [reportName, setReportName] = useState('');
const seriesOptions = useMemo((): SeriesOption[] => {
const displaySetsMap = displaySetService.getDisplaySetCache();
const displaySets = Array.from(displaySetsMap.values());
const options = displaySets
.filter(ds => ds.Modality === modality)
.map(ds => {
const value = ds.predecessorImageId || ds.SeriesInstanceUID;
const selectValue = value || ds.displaySetInstanceUID;
return {
optionKey: `series-${ds.displaySetInstanceUID}`,
selectValue,
value: value || null,
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
description: ds.SeriesDescription,
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
};
})
.filter(
option =>
option.selectValue && option.selectValue !== NEW_SERIES_SELECT_VALUE
);
return [
{
optionKey: NEW_SERIES_SELECT_VALUE,
selectValue: NEW_SERIES_SELECT_VALUE,
value: null,
description: null,
seriesNumber: minSeriesNumber,
label: 'Create new series',
},
...options,
];
}, [displaySetService, modality, minSeriesNumber]);
const handleSeriesChange = useCallback(
(selectValue: string) => {
const option = seriesOptions.find(o => o.selectValue === selectValue);
setSelectedSeries(
selectValue === NEW_SERIES_SELECT_VALUE ? null : (option?.value ?? selectValue)
);
},
[seriesOptions]
);
useEffect(() => {
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
const newReportName =
selectedSeries && seriesOption?.description ? seriesOption.description : '';
setReportName(newReportName);
}, [selectedSeries, seriesOptions]);
const handleSave = useCallback(() => {
actionTakenRef.current = true;
onSave({
reportName,
dataSource: selectedDataSource,
priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)),
series: selectedSeries,
});
hide();
}, [selectedDataSource, selectedSeries, reportName, hide, onSave]);
const handleCancel = useCallback(() => {
actionTakenRef.current = true;
onCancel();
hide();
}, [onCancel, hide]);
const handleDownload = useCallback(() => {
actionTakenRef.current = true;
onSave({
reportName,
dataSource: 'download',
priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)),
series: selectedSeries,
});
hide();
}, [selectedDataSource, selectedSeries, reportName, hide, onSave]);
// Handles the close dialog button/external close as a cancel
useEffect(() => {
return () => {
if (!actionTakenRef.current) {
onCancel();
}
};
}, [onCancel]);
const showDataSourceSelect = dataSources?.length > 1;
const showDownloadButton = enableDownload;
const selectedSeriesSelectValue =
selectedSeries == null
? NEW_SERIES_SELECT_VALUE
: (seriesOptions.find(o => o.value === selectedSeries)?.selectValue ??
selectedSeries);
return (
<div className="text-foreground flex min-w-[400px] max-w-md flex-col">
<div className="flex flex-col gap-4">
<div className="flex gap-4">
{showDataSourceSelect && (
<>
<div className="mt-1 w-1/2">
<div className="mb-1 pl-1 text-base">Data source</div>
<Select
value={selectedDataSource}
onValueChange={setSelectedDataSource}
>
<SelectTrigger>
<SelectValue placeholder="Select a data source" />
</SelectTrigger>
<SelectContent>
{dataSources.map(source => (
<SelectItem
key={source.value}
value={source.value}
>
{source.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className={showDataSourceSelect ? 'mt-1 w-1/2' : 'mt-1 w-full'}>
<div className="mb-1 pl-1 text-base">Series</div>
<Select
value={selectedSeriesSelectValue}
onValueChange={handleSeriesChange}
>
<SelectTrigger>
<SelectValue placeholder="Select a series" />
</SelectTrigger>
<SelectContent>
{seriesOptions.map(series => (
<SelectItem
key={series.optionKey}
value={series.selectValue}
>
{series.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)}
</div>
<div className="flex items-end gap-4">
{!showDataSourceSelect && (
<div className="w-1/3">
<div className="mb-1 pl-1 text-base">Series</div>
<Select
value={selectedSeriesSelectValue}
onValueChange={handleSeriesChange}
>
<SelectTrigger>
<SelectValue placeholder="Select a series" />
</SelectTrigger>
<SelectContent>
{seriesOptions.map(series => (
<SelectItem
key={series.optionKey}
value={series.selectValue}
>
{series.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<InputDialog
value={reportName}
onChange={setReportName}
submitOnEnter
className="flex-1"
>
<InputDialog.Field className="mb-0">
<InputDialog.Input
placeholder="Report name"
disabled={!!selectedSeries}
/>
</InputDialog.Field>
</InputDialog>
</div>
<div className="flex justify-end gap-2">
<InputDialog>
<InputDialog.Actions>
{showDownloadButton && (
<InputDialog.ActionsSecondary onClick={handleDownload}>
{t('Download')}
</InputDialog.ActionsSecondary>
)}
<InputDialog.ActionsPrimary onClick={handleSave}>Save</InputDialog.ActionsPrimary>
</InputDialog.Actions>
</InputDialog>
</div>
</div>
</div>
);
}
export { ReportDialog };
export default {
'ohif.createReportDialog': ReportDialog,
};