Skip to content

Fix segmentation save dialog#6116

Open
aayandeb wants to merge 4 commits into
OHIF:masterfrom
aayandeb:fix-segmentation-save-dialog
Open

Fix segmentation save dialog#6116
aayandeb wants to merge 4 commits into
OHIF:masterfrom
aayandeb:fix-segmentation-save-dialog

Conversation

@aayandeb

@aayandeb aayandeb commented Jun 29, 2026

Copy link
Copy Markdown

Context

Fixes #6106

This change improves the segmentation save workflow described in #6106. The previous dialog mixed saving to an existing segmentation series with creating a new segmentation series, which made the save destination unclear.

This change improves the segmentation save workflow so users can more clearly understand where a saved segmentation will go.

Previously, the save flow did not clearly distinguish between saving into an existing series and creating a new series. The segmentation panel save action also used a database cylinder icon, which could read more like storage/data-source selection than a direct save action.

Changes & Results

  • Adds an explicit save button for exportable segmentations in the segmentation panel.
  • Opens the save destination dialog from the segmentation panel save action.
  • Updates the report dialog to clearly separate:
    • Save to existing series
    • Create new series
  • Adds a reusable Save icon to ui-next.
  • Replaces the database cylinder icon with the regular file save icon.

Result: the segmentation save action is easier to recognize, and the destination dialog makes the save behavior clearer before the user confirms.

Testing

Not run locally.

Suggested manual test:

  • Open a study with a segmentation.
  • Open the segmentation panel.
  • Confirm the save icon appears for an exportable segmentation.
  • Click the save icon.
  • Confirm the dialog allows choosing either an existing series or creating a new series.
  • Save and confirm the segmentation stores with the selected destination behavior.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: Windows 10
  • Node version: v24.4.1
  • Browser: Chrome 149.0.7827.200

Summary by CodeRabbit

  • New Features

    • Added a save action for exportable segmentations directly in the segmentation panel.
    • Added clearer report-saving options: save to an existing series or create a new one.
  • Bug Fixes

    • Improved report save behavior so the selected save mode and target series are handled more reliably.
    • The new save action only appears when the current segmentation can be exported.

Deb and others added 2 commits June 26, 2026 13:45
Replace the database cylinder with a file save icon so the panel action reads as saving a segmentation rather than selecting storage. Register the icon in ui-next for reuse.

Co-authored-by: Cursor <cursoragent@cursor.com>
@netlify

netlify Bot commented Jun 29, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 87e13e3
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a43e342d7b3ef000864a576
😎 Deploy Preview https://deploy-preview-6116--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new Save SVG icon component registered in the icon set. Introduces a conditional Save button in PanelSegmentation that appears when the current segmentation is exportable and dispatches storeSegmentation. Refactors ReportDialog to replace the single dropdown with explicit radio-controlled "Save to existing series" / "Create new series" modes.

Changes

Segmentation Save Button & Report Dialog Rework

Layer / File(s) Summary
Save icon definition and registration
platform/ui-next/src/components/Icons/Sources/Save.tsx, platform/ui-next/src/components/Icons/Icons.tsx
New Save SVG React component added and registered in the Icons exported object.
Save button in PanelSegmentation
extensions/cornerstone/src/panels/PanelSegmentation.tsx
Imports Icons, computes saveModality and isCurrentSegExportable from exportOptions, adds renderSaveButton that dispatches commandsManager.run('storeSegmentation'), and inserts it into both collapsed and expanded header areas.
ReportDialog two-mode save rework
extensions/default/src/customizations/reportDialogCustomization.tsx
Adds SaveMode union type; derives existingSeriesOptions from display-set cache; replaces reportName/useEffect state with saveMode, selectedSeries, newSeriesName, and priorSeriesNumber; rewrites handleSave/handleDownload to branch on mode; replaces the single dropdown UI with radio-controlled existing/new series sections. minSeriesNumber prop made optional.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • jbocce

Poem

🐰 A Save button hops into the panel with grace,
No more hiding in dropdowns — it's clear in its place.
"Existing or new?" asks the dialog with flair,
Two radio choices sit tidy right there.
The disk icon gleams like a carrot at noon —
Now saving segmentations can't happen too soon! 💾

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: fixing the segmentation save dialog.
Description check ✅ Passed The description includes Context, Changes & Results, Testing, and a complete checklist with concrete details.
Linked Issues check ✅ Passed The changes implement the direct Segmentation panel save action and the split existing/new-series dialog requested in #6106.
Out of Scope Changes check ✅ Passed The added icon, dialog refactor, and panel button are all directly tied to the segmentation save workflow.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/cornerstone/src/panels/PanelSegmentation.tsx`:
- Around line 236-239: The save modality is being derived from
segmentationRepresentationTypes[0] instead of the currently selected
segmentation, which can make the Save flow use the wrong modality for mixed
representations. Update PanelSegmentation so saveModality is computed from
selectedSegmentationIdForType and its matching representation entry, and ensure
the Save handler in the same component passes that selected modality through the
downstream store flow that builds the save dialog path and default filename.

In `@extensions/default/src/customizations/reportDialogCustomization.tsx`:
- Around line 148-239: The new save-flow text in reportDialogCustomization.tsx
is hard-coded instead of using the existing i18n setup, so localize the added
copy with react-i18next. Update the labels, placeholder, and helper text in the
save-mode UI around the Select, SelectTrigger/SelectValue, InputDialog, and the
“Series name cannot be changed here” message to use translation keys via the
same translation hook already used in this component.
- Around line 49-80: The existing series state in reportDialogCustomization is
derived from a one-time getDisplaySetCache snapshot, so existingSeriesOptions,
saveMode, and selectedSeries can become stale when display sets change after
mount. Update the logic around existingSeriesOptions, hasExistingSeries, and the
selected-series defaults to subscribe to displaySetService updates instead of
relying on useMemo over the cache snapshot. Recompute and reconcile the derived
selection whenever the service publishes changes, and ensure the subscription is
cleaned up on unmount so the “default to existing when available” behavior stays
in sync.
- Around line 213-236: The new-series input in reportDialogCustomization’s save
mode flow still only switches via the wrapper click, so keyboard interaction
cannot activate it. Update the saveMode handling around the
InputDialog/InputDialog.Input block so entering or focusing the “Series name”
field when saveMode is existing calls switchToNew, and keep the existing click
behavior as well. Use the existing symbols switchToNew, saveMode, and
InputDialog.Input to locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f1673b88-0770-442b-9032-9303a111c16d

📥 Commits

Reviewing files that changed from the base of the PR and between d4153f1 and 4f245cf.

📒 Files selected for processing (4)
  • extensions/cornerstone/src/panels/PanelSegmentation.tsx
  • extensions/default/src/customizations/reportDialogCustomization.tsx
  • platform/ui-next/src/components/Icons/Icons.tsx
  • platform/ui-next/src/components/Icons/Sources/Save.tsx

Comment on lines +236 to +239
const saveModality =
segmentationRepresentationTypes?.[0] === SegmentationRepresentations.Contour
? 'RTSTRUCT'
: 'SEG';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Derive saveModality from the selected representation, not the first configured type.

Line 236 always keys off segmentationRepresentationTypes?.[0], but the active selectedSegmentationIdForType can come from any entry in that array. In a mixed labelmap/contour panel, the Save click at Line 303 can therefore send the wrong modality for the selected segmentation, and the downstream store flow uses that field to build the save dialog path and default filename.

Suggested fix
-  const saveModality =
-    segmentationRepresentationTypes?.[0] === SegmentationRepresentations.Contour
+  const selectedRepresentationType = segmentationRepresentationTypes?.find(
+    type => selectedSegmentationsForViewportMap?.get(type) === selectedSegmentationIdForType
+  );
+
+  const saveModality =
+    selectedRepresentationType === SegmentationRepresentations.Contour
       ? 'RTSTRUCT'
       : 'SEG';

Also applies to: 303-309

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/cornerstone/src/panels/PanelSegmentation.tsx` around lines 236 -
239, The save modality is being derived from segmentationRepresentationTypes[0]
instead of the currently selected segmentation, which can make the Save flow use
the wrong modality for mixed representations. Update PanelSegmentation so
saveModality is computed from selectedSegmentationIdForType and its matching
representation entry, and ensure the Save handler in the same component passes
that selected modality through the downstream store flow that builds the save
dialog path and default filename.

Comment on lines +49 to +80
const existingSeriesOptions = useMemo(() => {
const displaySetsMap = displaySetService.getDisplaySetCache();
const displaySets = Array.from(displaySetsMap.values());
const options = displaySets
return displaySets
.filter(ds => ds.Modality === modality)
.map(ds => ({
value: ds.predecessorImageId || ds.SeriesInstanceUID,
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
description: ds.SeriesDescription,
description: ds.SeriesDescription ?? '',
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
}));
}, [displaySetService, modality, minSeriesNumber]);

return [
{
value: null,
description: null,
seriesNumber: minSeriesNumber,
label: 'Create new series',
},
...options,
];
}, [displaySetService, modality]);
const hasExistingSeries = existingSeriesOptions.length > 0;

useEffect(() => {
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
const newReportName =
selectedSeries && seriesOption?.description ? seriesOption.description : '';
setReportName(newReportName);
}, [selectedSeries, seriesOptions]);
const [saveMode, setSaveMode] = useState<SaveMode>(hasExistingSeries ? 'existing' : 'new');
const [selectedSeries, setSelectedSeries] = useState<string | null>(
predecessorImageId
? (existingSeriesOptions.find(s => s.value === predecessorImageId)?.value ??
existingSeriesOptions[0]?.value ??
null)
: (existingSeriesOptions[0]?.value ?? null)
);
const [newSeriesName, setNewSeriesName] = useState('');

const priorSeriesNumber = useMemo(
() => Math.max(minSeriesNumber, ...existingSeriesOptions.map(s => s.seriesNumber)),
[existingSeriesOptions, minSeriesNumber]
);

const existingSeriesDesc = useMemo(
() => existingSeriesOptions.find(s => s.value === selectedSeries)?.description ?? '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Subscribe to displaySetService instead of memoizing a cache snapshot.

getDisplaySetCache() is only read during render, and this memo depends on the service object rather than cache updates. That leaves existingSeriesOptions, the default saveMode, and selectedSeries stale if display sets arrive/change after mount, which breaks the “default to existing when available” flow. Drive this from a service subscription and reconcile the derived selection when the service publishes changes. As per coding guidelines, "Prioritize pub-sub by calling service subscribe over useEffects for more reliable event handling, unsubscribing in cleanup functions".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/default/src/customizations/reportDialogCustomization.tsx` around
lines 49 - 80, The existing series state in reportDialogCustomization is derived
from a one-time getDisplaySetCache snapshot, so existingSeriesOptions, saveMode,
and selectedSeries can become stale when display sets change after mount. Update
the logic around existingSeriesOptions, hasExistingSeries, and the
selected-series defaults to subscribe to displaySetService updates instead of
relying on useMemo over the cache snapshot. Recompute and reconcile the derived
selection whenever the service publishes changes, and ensure the subscription is
cleaned up on unmount so the “default to existing when available” behavior stays
in sync.

Source: Coding guidelines

Comment on lines +148 to +239
{showDataSourceSelect && (
<div>
<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>
)}

{/* Save to existing series */}
<div>
<label className="mb-2 flex cursor-pointer items-center gap-2">
<input
type="radio"
name="save-mode"
checked={saveMode === 'existing'}
onChange={() => setSaveMode('existing')}
disabled={!hasExistingSeries}
className="cursor-pointer"
/>
<span className={!hasExistingSeries ? 'text-muted-foreground' : ''}>
Save to existing series
</span>
</label>
<div className="pl-6">
<Select
value={selectedSeries}
onValueChange={setSelectedSeries}
disabled={saveMode !== 'existing' || !hasExistingSeries}
>
<SelectTrigger>
<SelectValue
placeholder={hasExistingSeries ? 'Select a series' : 'No existing series'}
/>
</SelectTrigger>
<SelectContent>
{existingSeriesOptions.map(series => (
<SelectItem
key={series.value}
value={series.value}
>
{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={selectedSeries}
onValueChange={setSelectedSeries}
>
<SelectTrigger>
<SelectValue placeholder="Select a series" />
</SelectTrigger>
<SelectContent>
{seriesOptions.map(series => (
<SelectItem
key={series.value}
value={series.value}
>
{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>

{/* Create new series — entire section clickable to switch mode */}
<div onClick={saveMode === 'existing' ? switchToNew : undefined}>
<label className="mb-2 flex cursor-pointer items-center gap-2">
<input
type="radio"
name="save-mode"
checked={saveMode === 'new'}
onChange={switchToNew}
className="cursor-pointer"
/>
<span>Create new series</span>
</label>
<div className="pl-6">
<InputDialog
value={saveMode === 'existing' ? existingSeriesDesc : newSeriesName}
onChange={saveMode === 'new' ? setNewSeriesName : undefined}
submitOnEnter
>
<InputDialog.Field className="mb-0">
<InputDialog.Input
placeholder="Series name"
disabled={saveMode === 'existing'}
/>
</InputDialog.Field>
</InputDialog>
{saveMode === 'existing' && (
<p className="text-muted-foreground mt-1 pl-1 text-xs">
Series name cannot be changed here

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize the new dialog copy.

The new labels, placeholders, and helper text here are hard-coded even though this component already uses react-i18next. That makes the new save flow English-only while the rest of the dialog is localized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/default/src/customizations/reportDialogCustomization.tsx` around
lines 148 - 239, The new save-flow text in reportDialogCustomization.tsx is
hard-coded instead of using the existing i18n setup, so localize the added copy
with react-i18next. Update the labels, placeholder, and helper text in the
save-mode UI around the Select, SelectTrigger/SelectValue, InputDialog, and the
“Series name cannot be changed here” message to use translation keys via the
same translation hook already used in this component.

Comment on lines +213 to +236
<div onClick={saveMode === 'existing' ? switchToNew : undefined}>
<label className="mb-2 flex cursor-pointer items-center gap-2">
<input
type="radio"
name="save-mode"
checked={saveMode === 'new'}
onChange={switchToNew}
className="cursor-pointer"
/>
<span>Create new series</span>
</label>
<div className="pl-6">
<InputDialog
value={saveMode === 'existing' ? existingSeriesDesc : newSeriesName}
onChange={saveMode === 'new' ? setNewSeriesName : undefined}
submitOnEnter
>
<InputDialog.Field className="mb-0">
<InputDialog.Input
placeholder="Series name"
disabled={saveMode === 'existing'}
/>
</InputDialog.Field>
</InputDialog>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Typing into the new-series section still cannot switch modes.

Only the wrapper click calls switchToNew. While saveMode === 'existing', the field is disabled, so keyboard users cannot focus it and start typing to auto-select “Create new series,” which leaves the linked requirement unmet. Trigger the mode switch on first field interaction, not only on pointer clicks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/default/src/customizations/reportDialogCustomization.tsx` around
lines 213 - 236, The new-series input in reportDialogCustomization’s save mode
flow still only switches via the wrapper click, so keyboard interaction cannot
activate it. Update the saveMode handling around the
InputDialog/InputDialog.Input block so entering or focusing the “Series name”
field when saveMode is existing calls switchToNew, and keep the existing click
behavior as well. Use the existing symbols switchToNew, saveMode, and
InputDialog.Input to locate the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export Dialog for Segmentation Changes

1 participant