* 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>
269 lines
8.4 KiB
TypeScript
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,
|
|
};
|