Skip to content

feat(Mode): Add mode selector#5987

Open
igoroctaviano wants to merge 8 commits into
masterfrom
feat/mode-selector
Open

feat(Mode): Add mode selector#5987
igoroctaviano wants to merge 8 commits into
masterfrom
feat/mode-selector

Conversation

@igoroctaviano
Copy link
Copy Markdown
Contributor

@igoroctaviano igoroctaviano commented Apr 28, 2026

Screenshot 2026-04-28 at 20 32 29

Context

Fixes #5188

Users needed a way to discover and switch OHIF viewer modes (workflows) from the UI instead of guessing or hacking URLs.

This change introduces a toolbar control that opens a compact popover listing available modes from appConfig.loadedModes. For each mode, mode.isValidMode is evaluated with modalities (normalized /\ to match viewer semantics) and a study envelope resolved from dataSource.query.studies.search or DicomMetadataStore, aligning with validators such as TMTV.

Valid destinations navigate with Link (/{modeRoute}[/{dataSource}]) while preserveQueryParameters keeps the viewer query string. Invalid modes remain non-navigation rows with Tooltip explanations when validity.description is present.

Changes & Results

New / updated

  • ToolbarModeSelector (Viewers/extensions/default/src/Toolbar/ToolbarModeSelector.tsx): Popover-driven mode list; LayoutSelectorTrigger pattern for the toolbar Button (h-10 w-10, size="icon", Ghost); header Modes shares popover surface (no tinted header slab); selectable rows Link, current mode styled selection chip, unavailable modes with Tooltip (z-[220] so tooltips stack above popover z-[100]).
  • Toolbar registration ohif.modeSelector in getToolbarModule.tsx; Toolbar index export optional.
  • i18n: ToolbarModeSelector strings (Browse modes, Modes, loading line, Current mode, Unable to evaluate this mode), en-US, zh, nl, fr, test-LNG and locale index.js registrations.

Modes that include the control use uiType: 'ohif.modeSelector' in their toolbar definitions (basic, segmentation, tmtv, usAnnotation, preclinical-4d, basic-test-mode, basic-dev-mode).

Before → after

Before After
Modes reachable mainly by URL/route knowledge Dedicated modes picker next to familiar toolbar tools
isValidMode only inferred from elsewhere Same validation rules as collapsed worklist UX, surfaced in-picker

(Optional: attach a short screen recording or screenshot of toolbar → modes popover, current-mode row, disabled row + tooltip.)

Testing

  1. Launch the viewer with a study that exposes multiple modes (configure loadedModes as needed).
  2. Open a viewer route that renders the primary toolbar (layout/tools row).
  3. Click the list icon (Browse modes) and confirm Modes popover opens aligned with sibling tools.
  4. Confirm valid modes Link to /{routeName} preserving query params; dataSource segment preserved when configured.
  5. Hover the invalid row: tooltip shows description above the panel (no inner scroll trap).
  6. Current mode matches the active route — row shows styled “current” chip; navigating away updates selection.

Checklist

PR

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

Suggested PR title: feat(Default): toolbar mode selector with study validation (adjust scope if reviewers prefer ToolbarModeSelector).

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.

(Consider a short docs follow-up noting the toolbar ohif.modeSelector and **Modes/isValidMode behavior.)

Tested Environment

  • OS: macOS (update as appropriate)
  • Node version:
  • Browser: Chromium-based / Firefox / Safari (update versions as appropriate)

Greptile Summary

This PR introduces a toolbar mode selector (ohif.modeSelector) that opens a popover listing available OHIF modes, evaluating each against the current study's modalities via isValidMode, and navigating via React Router Link with preserved query parameters. The implementation is well-structured and follows existing toolbar patterns.

  • New ToolbarModeSelector component renders a popover-driven mode list, correctly gating rendering on studyEnvelope presence, memoizing dataSource, and guarding against null validity results.
  • modeSelectorUtils.ts adds fetchStudyEnvelope (with correct method-call this binding), evaluateModeValidity, and usePreservedViewerSearch; the hook has a bug where preserveQueryParameters duplicates any preserveKeys already present in the URL (see inline comment).
  • Mode toolbarButtons receive a consistent ohif.modeSelector entry across all six affected modes, and all i18n locales are in sync.

Confidence Score: 4/5

Safe to merge after fixing the duplicate query parameter issue in usePreservedViewerSearch.

The usePreservedViewerSearch hook initialises next from locationSearch and then calls preserveQueryParameters(next), which appends the same preserved keys (configUrl, multimonitor, screenNumber, hangingProtocolId) a second time. In any OHIF deployment that uses ?configUrl=…, every mode-switch link will carry a doubled configUrl, producing a malformed URL on navigation. The rest of the component logic — null-validity guards, dataSource memoization, study envelope fetching, and locale files — looks correct.

extensions/default/src/utils/modeSelectorUtils.ts — specifically the usePreservedViewerSearch hook's interaction with preserveQueryParameters.

Important Files Changed

Filename Overview
extensions/default/src/Toolbar/ToolbarModeSelector.tsx New popover mode-selector component; modeMenuRows guarded well against null validity; rendering logic correctly gated on studyEnvelope presence; dataSource now memoized; buildHrefForMode defined with useCallback.
extensions/default/src/utils/modeSelectorUtils.ts New utility module; fetchStudyEnvelope correctly calls search as a method; usePreservedViewerSearch has a duplicate-param bug — preserved keys are added to a URLSearchParams that already contains them.
extensions/default/src/getToolbarModule.tsx Registers ohif.modeSelector toolbar type — straightforward addition matching existing ohif.layoutSelector pattern.
platform/i18n/src/locales/en-US/ToolbarModeSelector.json New i18n namespace with 7 keys; all translated locales (fr, nl, zh, test-LNG) contain exactly the same keys — no orphaned entries.
modes/basic/src/toolbarButtons.ts Adds ohif.modeSelector button entry before Layout — consistent pattern applied across all 6 modes.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
extensions/default/src/utils/modeSelectorUtils.ts:193-198
**Duplicate preserved query parameters on mode-switch links**

`next` is already built from `locationSearch` (which equals `window.location.search`), so `preserveQueryParameters(next)` — whose `current` defaults to `new URLSearchParams(window.location.search)` — calls `next.append(key, value)` for each preserved key that is already present, producing duplicates like `?configUrl=https://...&StudyInstanceUIDs=xxx&configUrl=https://...`. Any OHIF deployment that passes `?configUrl=…` will encounter this on every mode switch. Deleting the keys from `next` before re-adding them via `preserveQueryParameters` prevents the duplication.

```suggestion
  return useMemo(() => {
    const next = new URLSearchParams(locationSearch);
    // Remove preserved keys first so preserveQueryParameters doesn't append duplicates
    // (next was initialised from the same search string that preserveQueryParameters reads).
    for (const key of preserveKeys) {
      next.delete(key);
    }
    preserveQueryParameters(next);
    const s = next.toString();
    return s ? `?${s}` : '';
  }, [locationSearch]);
```

Reviews (8): Last reviewed commit: "Merge branch 'master' into feat/mode-sel..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 28, 2026

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit e3f14f5
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69fb942b3d147a000828f0c4
😎 Deploy Preview https://deploy-preview-5987--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.

Comment thread extensions/default/src/Toolbar/ToolbarModeSelector.tsx Outdated
Comment thread extensions/default/src/Toolbar/ToolbarModeSelector.tsx Outdated
Comment thread extensions/default/src/Toolbar/ToolbarModeSelector.tsx Outdated
Comment thread platform/i18n/src/locales/fr/ToolbarModeSelector.json Outdated
@cypress
Copy link
Copy Markdown

cypress Bot commented Apr 28, 2026

Viewers    Run #6239

Run Properties:  status check passed Passed #6239  •  git commit e3f14f5420: Merge branch 'master' into feat/mode-selector
Project Viewers
Branch Review feat/mode-selector
Run status status check passed Passed #6239
Run duration 02m 18s
Commit git commit e3f14f5420: Merge branch 'master' into feat/mode-selector
Committer Igor Octaviano
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 37
View all changes introduced in this branch ↗︎

Comment thread extensions/default/src/Toolbar/ToolbarModeSelector.tsx Outdated
Comment thread extensions/default/src/Toolbar/ToolbarModeSelector.tsx Outdated
Comment thread extensions/default/src/utils/modeSelectorUtils.ts Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This does not look like the same image/setup. Also, these are all getting replaced in another PR to be just viewport comparisons. Lets wait for that other PR to merge shortly and then you shouldn't need to do the updates here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. Thx.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@wayfarer3130, the PR you mentioned was merged? What is left here beyond updating the tests? @dan-rukas any thoughts?

@wayfarer3130
Copy link
Copy Markdown
Contributor

@dan-rukas - can you check the mode selector here? We can leave it like this, or I could provide a customization framework allowing app-config defined customizations to allow injecting the mode selector in specific configurations, or you could provide a bit of UI for where this should go. My feeling is it is ok as is, but you might want it differently.

@wayfarer3130
Copy link
Copy Markdown
Contributor

@igoroctaviano - reading this, it looks to me like this loses the measurements etc on changing modes.
What about adding a configuration flag to the on mode enter stuff that indicates this is a mode change which should preserve annotation/segmentation data? That might be a step too far right now and I'd like to see this in sooner rather than later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is wrong

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

incorrect

Copy link
Copy Markdown
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

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

Lots of tests are updated to an incorrect image.

@igoroctaviano
Copy link
Copy Markdown
Contributor Author

Lots of tests are updated to an incorrect image.

#5987 (comment)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment on lines +193 to +198
return useMemo(() => {
const next = new URLSearchParams(locationSearch);
preserveQueryParameters(next);
const s = next.toString();
return s ? `?${s}` : '';
}, [locationSearch]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Duplicate preserved query parameters on mode-switch links

next is already built from locationSearch (which equals window.location.search), so preserveQueryParameters(next) — whose current defaults to new URLSearchParams(window.location.search) — calls next.append(key, value) for each preserved key that is already present, producing duplicates like ?configUrl=https://...&StudyInstanceUIDs=xxx&configUrl=https://.... Any OHIF deployment that passes ?configUrl=… will encounter this on every mode switch. Deleting the keys from next before re-adding them via preserveQueryParameters prevents the duplication.

Suggested change
return useMemo(() => {
const next = new URLSearchParams(locationSearch);
preserveQueryParameters(next);
const s = next.toString();
return s ? `?${s}` : '';
}, [locationSearch]);
return useMemo(() => {
const next = new URLSearchParams(locationSearch);
// Remove preserved keys first so preserveQueryParameters doesn't append duplicates
// (next was initialised from the same search string that preserveQueryParameters reads).
for (const key of preserveKeys) {
next.delete(key);
}
preserveQueryParameters(next);
const s = next.toString();
return s ? `?${s}` : '';
}, [locationSearch]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/default/src/utils/modeSelectorUtils.ts
Line: 193-198

Comment:
**Duplicate preserved query parameters on mode-switch links**

`next` is already built from `locationSearch` (which equals `window.location.search`), so `preserveQueryParameters(next)` — whose `current` defaults to `new URLSearchParams(window.location.search)` — calls `next.append(key, value)` for each preserved key that is already present, producing duplicates like `?configUrl=https://...&StudyInstanceUIDs=xxx&configUrl=https://...`. Any OHIF deployment that passes `?configUrl=…` will encounter this on every mode switch. Deleting the keys from `next` before re-adding them via `preserveQueryParameters` prevents the duplication.

```suggestion
  return useMemo(() => {
    const next = new URLSearchParams(locationSearch);
    // Remove preserved keys first so preserveQueryParameters doesn't append duplicates
    // (next was initialised from the same search string that preserveQueryParameters reads).
    for (const key of preserveKeys) {
      next.delete(key);
    }
    preserveQueryParameters(next);
    const s = next.toString();
    return s ? `?${s}` : '';
  }, [locationSearch]);
```

How can I resolve this? If you propose a fix, please make it concise.

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.

[Feature Request] Improve accessibility of the available modes

3 participants