Skip to content

feat(WorkListUINext): New Study List (WorkListUINext)#6005

Open
jbocce wants to merge 164 commits into
OHIF:masterfrom
dan-rukas:feat/OHIF-1534-studylist
Open

feat(WorkListUINext): New Study List (WorkListUINext)#6005
jbocce wants to merge 164 commits into
OHIF:masterfrom
dan-rukas:feat/OHIF-1534-studylist

Conversation

@jbocce
Copy link
Copy Markdown
Collaborator

@jbocce jbocce commented May 8, 2026

Context

This PR replaces the legacy WorkList study-list route with a new WorkListUINext route, built on a fresh ui-next study-list system (StudyList, DataTable, PreviewContainer, PreviewPatientSummary, etc.). The legacy WorkList remains importable for fallback, but / now resolves to WorkListUINext by default. The change set spans 138 commits and ~5,600 lines added.

Key drivers:

  • New design for the study list (filters, pagination, sorting, preview panel with patient summary + thumbnails/list toggle, settings popover, workflow buttons, white-labelled logo, OIDC logout, investigational-use dialog).
  • Table state (sorting, pagination, filters) synced to URL and sessionStorage via the new useStudyListStateSync hook.
  • Refetch behavior in DataSourceWrapper simplified to client-side pagination — one server query per filter change instead of per page change.
  • DICOMWeb data source extended with a thumbnailRequestStrategy (bulkDataRetrieve default, or fetch) and a series-level QIDO fallback for thumbnail fetching.
  • New workListUINext.previewSeriesView customization ('all' | 'thumbnails' | 'list') with documentation.

Changes & Results

Basics

image

Workflows/modes

image

Preview side panel

image

1. Routing (platform/app/src/routes/index.tsx)

  • The default / route now renders WorkListUINext via DataSourceWrapper.
  • The legacy WorkList import is retained and commented as a swap-in alternative.

2. DataSourceWrapper (platform/app/src/routes/DataSourceWrapper.tsx)

  • Refetch logic is simplified to client-side pagination — server queries fire once per filter change (with a single bulk query up to appConfig.queryLimit ?? 101) instead of refetching on every page/offset change.
  • Pagination, sorting, and offsets are handled in-memory by WorkListUINext via useStudyListStateSync.
  • A new hasFetchedOnce flag is plumbed through to the layout so the worklist can suppress the empty state until the first query resolves; it resets on data-source change and on onRefresh.
  • The legacy pageNumber/resultsPerPage/offset/location shape is dropped from internal state, along with the areLocationsTheSame heuristic, replaced by a queryFilterValues deep-equality check (areQueryFiltersEqual) that handles array filters like modalitiesInStudy.
  • defaultDataSourceName now reads from useAppConfig() rather than window.config directly.

3. WorkListUINext route (platform/app/src/routes/WorkListUINext/)

  • New WorkListUINext.tsx and useWorkListToolbarActions.tsx.
  • A loading indicator is shown until the first fetch completes and during filter changes.
  • The datasources query param is preserved across navigation.
  • The investigational-use dialog and white-label logo are integrated here.

4. Study list state sync (platform/app/src/hooks/useStudyListStateSync.ts)

  • URL is the source of truth.
  • sessionStorage is the fallback.
  • Component state drives reactivity.
  • Restores correctly on filter/sort/pagination combinations after a known regression in earlier commits.

5. ui-next StudyList system (platform/ui-next/src/components/StudyList/)

  • New components: Layout, Table, PreviewContainer, PreviewContent, PreviewPatientSummary, PreviewSeriesList, PreviewToggleButton, SettingsPopover, WorkflowsProvider, WorkflowMenu.
  • New hook: useDefaultWorkflow.
  • New utilities and column definitions: defaultColumns, formatStudyDate, tokenizeModalities.

6. ui-next primitives

  • New DataTable compound component (ColumnHeader, FilterRow, Pagination, Toolbar, ViewOptions, ActionOverlayCell).
  • New InputMultiSelect, Badge, and generic Table components.
  • DateRange is expanded with placeholder editing and is sized to match other filters.
  • Thumbnail is refined with blob-URL revoke on error and a viewport query param for quality.
  • Several new Icons are added: OHIFLogoHorizontal, PanelRight, PatientStudyList, SeriesPlaceholder, SettingsStudyList, SortingNew/Ascending/Descending, CloudSettings.

7. DICOMWeb data source (extensions/default/src/DicomWebDataSource/index.ts)

  • Adds thumbnailRequestStrategy (bulkDataRetrieve default, or fetch).
  • getGetThumbnailSrc now accepts a ThumbnailContext and falls back to a single QIDO instances?limit=1 query when a series-level fetch fails.
  • Documented in dicom-web.md.

8. Customization

  • workListUINext.previewSeriesView ('all' | 'thumbnails' | 'list') is registered via getCustomizationModule.tsx.
  • The preview is forced to 'list' when the data source uses wadors/thumbnailDirect rendering or bulkDataRetrieve retrieval.
  • Documented in WorkListUINext.md.

9. Misc

  • The data source configuration UI is rebuilt (DataSourceConfigurationComponent.tsx).
  • An OIDC logout menu item is added.
  • The investigational-use dialog is wired into the new worklist.
  • i18n entries for DataTable are added.

10. Removed

  • The legacy Cypress suite OHIFStudyList.spec.js is removed.
  • The obsolete tests/Worklist.spec.ts is removed (no longer applicable to the new study list).

Testing

1. Smoke test

  • Run the deploy created for this PR and confirm / loads the new WorkListUINext against the default data source.

2. State sync

  • Apply a sort, page, and a couple of filters (text + modality multi-select + date range).
  • Reload the page; the URL state should be restored.
  • Open a new tab to / (no params); sessionStorage state should restore until tab close.

3. DataSourceWrapper refetch behavior

  • With the network panel open, page forward/back through results and confirm no new query fires.
  • Change a filter (modality, date range, MRN) and confirm exactly one new query fires.
  • Switch data sources and confirm the wrapper resets and re-queries.
  • Set appConfig.queryLimit to a small value (e.g. 25) and confirm only that many studies come back.

4. Preview panel

  • Toggle the panel open/closed.
  • Switch between thumbnails and list views.
  • Verify the workListUINext.previewSeriesView customization values ('all', 'thumbnails', 'list') hide/lock the toggle as documented.

5. Thumbnail strategies

  • Smoke-test against a wadors source (e.g. local_orthanc, local_dcm4chee) and confirm the preview is forced to list view.
  • Against a thumbnail/rendered source verify both bulkDataRetrieve and fetch strategies render thumbnails.
  • Confirm that a deliberately-broken series falls back to the QIDO limit=1 lookup without crashing.

6. Workflow buttons & default mode

  • Select a study, change default workflow via the settings popover, double-click a row — confirm it launches the active workflow.
  • Verify long workflow labels truncate with ellipsis.

7. Toolbar/extras

  • White-label logo renders when configured.
  • OIDC logout menu item appears under an OIDC config.
  • Investigational-use dialog appears on first load and dismisses correctly.

8. Pagination persistence

  • Change rows-per-page, page forward, then sort and filter — the persistence restore should not double-restore or reset to page 1 (the regression fixed in a292d59f7a).

9. Legacy fallback

  • Temporarily swap the commented block in routes/index.tsx back to WorkList and confirm the legacy view still renders.

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

System:

OS: Windows 11 10.0.26200
CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700H
Memory: 5.27 GB / 31.68 GB

Binaries:

Node: 20.9.0 - C:\Users\joebo\AppData\Local\fnm_multishells\53780_1778175901087\node.EXE
Yarn: 1.22.22 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 10.1.0 - C:\Users\joebo\AppData\Local\fnm_multishells\53780_1778175901087\npm.CMD
bun: 1.2.23 - C:\Users\joebo\.bun\bin\bun.EXE

Browsers:

Chrome: 147.0.7727.138
Edge: Chromium (146.0.3856.84)
Internet Explorer: 11.0.26100.8115

Greptile Summary

This PR replaces the legacy WorkList study-list route with a new WorkList built on the ui-next component system, adding a resizable preview panel, URL+sessionStorage state sync, client-side pagination, per-data-source thumbnail strategies with abort support, and a new workList.* customization namespace. The legacy route is preserved as LegacyWorkList and can be re-activated with workList.variant: 'legacy'.

  • All three P1 issues from earlier review rounds have been resolved: the loading-state-stuck-on-error bug is fixed via a finally block; the patientName URL key mismatch is eliminated by the new studyListFilterContract.ts; and DEFAULT_DATA is now correctly defined at module scope.
  • DataSourceWrapper now fires exactly one server query per distinct filter change, compares filters with shallowEqualIgnoringArrayOrder, and surfaces a Retry button on failure.
  • The thumbnailRequestStrategy: 'fetch' path in DicomWebDataSource wires AbortSignal through to native fetch, with a QIDO limit=1 fallback when a series-level thumbnail request fails.

Confidence Score: 5/5

Safe to merge — the three blocking issues from prior reviews are all addressed and no new blocking issues were found.

The error-handling, URL key, and DEFAULT_DATA fixes are all correct and well-structured. The new studyListFilterContract.ts ensures both the URL writer and reader stay in sync. The only findings in the current diff are minor: a duplicate-value edge case in shallowEqualIgnoringArrayOrder and a UX discoverability note about thumbnail view being off by default for bulkDataRetrieve data sources.

No files require special attention for merge safety. Integrators enabling the preview panel with an existing thumbnailRendering: thumbnail config should note they also need thumbnailRequestStrategy: fetch to see thumbnail view.

Important Files Changed

Filename Overview
platform/app/src/routes/DataSourceWrapper.tsx Refactored to client-side pagination with a queryLimit bulk query; error handling now has a finally block that always resets isLoading and sets hasFetchedOnce; DEFAULT_DATA moved to module scope.
platform/app/src/hooks/useStudyListStateSync.ts New hook syncing sorting/pagination/filters across URL, sessionStorage, and component state via centralized studyListFilterContract.ts keys.
platform/app/src/routes/WorkList/WorkList.tsx Rebuilt study list using ui-next StudyList compound component; integrates useStudyListStateSync, preview panel, settings popover, and toolbar actions.
platform/app/src/routes/WorkList/SidePanelPreview.tsx New side panel managing series fetch with AbortController cancellation and pooled thumbnail worker; forceListView disables thumbnails for wadors/thumbnailDirect/bulkDataRetrieve data sources.
extensions/default/src/DicomWebDataSource/index.ts Adds thumbnailRequestStrategy; refactors getGetThumbnailSrc to accept ThumbnailContext; adds fetchThumbnailWithQidoFallbackForSeries for series-level QIDO fallback.
platform/app/src/utils/shallowEqualIgnoringArrayOrder.ts Set-based unordered array comparison; correctly handles length mismatches but treats arrays with different element multiplicities as equal once deduplicated.
platform/app/src/utils/studyListFilterContract.ts Single source of truth for URL filter keys shared by both the URL serializer and parser, eliminating the key-mismatch bug from earlier iterations.
platform/ui-next/src/components/DataTable/DataTable.tsx New compound DataTable built on TanStack Table; auto-resets pageIndex on filter/sort change unless pagination itself also changed in the same render cycle.
platform/ui-next/src/components/Thumbnail/Thumbnail.tsx Adds isDraggable, onImageLoadError, and children props; extracts drag logic into DraggableThumbnailContent/StaticThumbnailContent.
platform/app/src/routes/LegacyWorkList/LegacyWorkList.tsx Legacy WorkList preserved for opt-out via workList.variant: legacy customization; hardcodes STUDIES_LIMIT to 101.

Comments Outside Diff (2)

  1. platform/app/src/routes/DataSourceWrapper.tsx, line 141-155 (link)

    P1 Loading state stuck after query failure

    getData() sets isLoading = true on entry but only calls setIsLoading(false) on the happy-path completion. When dataSource.query.studies.search() throws, the .catch() handler logs the error or shows a modal but never resets isLoading. Because isDataInvalid is guarded by !isLoading, every subsequent location/filter change will be ignored — no further queries fire until the page is reloaded.

    The new !hasFetchedOnce condition in showStudyListLoading makes this worse: after a failed first fetch both the loading spinner and the empty-state are suppressed indefinitely, leaving users with a blank, unresponsive worklist.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: platform/app/src/routes/DataSourceWrapper.tsx
    Line: 141-155
    
    Comment:
    **Loading state stuck after query failure**
    
    `getData()` sets `isLoading = true` on entry but only calls `setIsLoading(false)` on the happy-path completion. When `dataSource.query.studies.search()` throws, the `.catch()` handler logs the error or shows a modal but never resets `isLoading`. Because `isDataInvalid` is guarded by `!isLoading`, every subsequent location/filter change will be ignored — no further queries fire until the page is reloaded.
    
    The new `!hasFetchedOnce` condition in `showStudyListLoading` makes this worse: after a failed first fetch both the loading spinner and the empty-state are suppressed indefinitely, leaving users with a blank, unresponsive worklist.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. platform/app/src/routes/DataSourceWrapper.tsx, line 141-156 (link)

    P1 getData() calls setIsLoading(true) on entry but only calls setIsLoading(false) on the happy path. If dataSource.query.studies.search() throws, the .catch() error handler shows a modal but never resets the flag. Because isDataInvalid = !isLoading && filtersChanged, the wrapper refuses to fire any further query once isLoading is stuck at true. Combined with !hasFetchedOnce remaining false, both the loading spinner and the empty state are suppressed indefinitely after a failed first fetch, leaving users with a blank, unresponsive worklist until they reload the page.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: platform/app/src/routes/DataSourceWrapper.tsx
    Line: 141-156
    
    Comment:
    `getData()` calls `setIsLoading(true)` on entry but only calls `setIsLoading(false)` on the happy path. If `dataSource.query.studies.search()` throws, the `.catch()` error handler shows a modal but never resets the flag. Because `isDataInvalid = !isLoading && filtersChanged`, the wrapper refuses to fire any further query once `isLoading` is stuck at `true`. Combined with `!hasFetchedOnce` remaining `false`, both the loading spinner and the empty state are suppressed indefinitely after a failed first fetch, leaving users with a blank, unresponsive worklist until they reload the page.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
platform/app/src/routes/WorkList/SidePanelPreview.tsx:38-41
**Thumbnail view silently disabled for the default data source config**

`thumbnailRequestStrategy` defaults to `'bulkDataRetrieve'` when not set, making `forceListView = true` for any data source that does not explicitly declare `thumbnailRequestStrategy: 'fetch'`. The result is that the thumbnail/list toggle and thumbnail grid are hidden by default even when `thumbnailRendering` is `'thumbnail'` or `'rendered'` and the server supports those endpoints. Existing integrators who already have a working thumbnail configuration will find the new preview panel locked to list-only mode until they add the extra config key. The note in `workListCustomization.ts` documents this, but it is easy to miss when migrating.

### Issue 2 of 2
platform/app/src/utils/shallowEqualIgnoringArrayOrder.ts:21-42
The Set-based comparison discards duplicate elements, so two arrays that differ only in element multiplicity compare as equal. For example `['CT', 'MR', 'CT']` and `['CT', 'MR', 'MR']` produce identical Sets `{CT, MR}` — same size, same members — so the function returns `true`. The existing length pre-check only catches arrays whose total lengths differ. For DICOM modality selections duplicates are unlikely, but the logic is subtly wrong for the general case the function claims to handle.

```suggestion
  for (const key of allKeys) {
    const val1 = a[key];
    const val2 = b[key];

    if (Array.isArray(val1) && Array.isArray(val2)) {
      if (val1.length !== val2.length) {
        return false;
      }
      // Count occurrences to handle duplicates correctly.
      const counts = new Map<unknown, number>();
      for (const v of val1) {
        counts.set(v, (counts.get(v) ?? 0) + 1);
      }
      for (const v of val2) {
        const n = counts.get(v);
        if (!n) {
          return false;
        }
        counts.set(v, n - 1);
      }
    } else if (val1 !== val2) {
      return false;
    }
  }
```

Reviews (17): Last reviewed commit: "Deselect rows that are no longer on the ..." | Re-trigger Greptile

Comment thread platform/app/src/routes/WorkListUINext/WorkListUINext.tsx Outdated
Comment thread platform/app/src/routes/WorkListUINext/WorkListUINext.tsx Outdated
Comment thread platform/app/src/routes/WorkListUINext/WorkListUINext.tsx Outdated
Comment thread platform/app/src/routes/WorkListUINext/WorkListUINext.tsx Outdated
Comment thread platform/app/src/routes/WorkListUINext/WorkListUINext.tsx Outdated
@wayfarer3130
Copy link
Copy Markdown
Contributor

I'd really like to see a few customization examples for simple things like adding columns/re-ordering them, replacing the about box, but also thinking about how to use the worklistuinext with a different layout, but the same fetch/retrieve/thumbnail type logic.

Copy link
Copy Markdown
Collaborator

@hongxiao-gif hongxiao-gif left a comment

Choose a reason for hiding this comment

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

I have two defects created:
#6029
#6028

I’m not sure if this is expected behavior or by design.

  1. When all columns are unselected from the column multiselect list, the study list page displays as empty. A default column set or validation should prevent the page from rendering without any visible columns.
  2. The column widths are not resizable.
  3. When a row is selected, closing the default workflow mode in the preview panel also disables the default mode in the settings.

Testing environment:
npx envinfo --system --browsers

System:
OS: Windows 11 10.0.26200
CPU: (8) x64 Intel(R) Core(TM) Ultra 5 226V
Memory: 3.90 GB / 15.67 GB
Browsers:
Chrome: 148.0.7778.168
Edge: Spartan (24.13.1), Chromium (24.13.1), ChromiumDev (24.13.1)

npm notice
npm notice New minor version of npm available! 11.8.0 -> 11.14.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.14.1
npm notice To update run: npm install -g npm@11.14.1
npm notice

https://deploy-preview-6005--ohif-dev.netlify.app/

@jbocce
Copy link
Copy Markdown
Collaborator Author

jbocce commented May 20, 2026

I'd really like to see a few customization examples for simple things like adding columns/re-ordering them, replacing the about box, but also thinking about how to use the worklistuinext with a different layout, but the same fetch/retrieve/thumbnail type logic.

@wayfarer3130 I believe I have addressed most, if not all, of the customization improvement you have suggested. Note that when we first set out to replace the previous WorkList we agreed to minimize the customization changes. So I think what we have here thus far is more than sufficient. Let me know what you think.

@jbocce jbocce requested a review from wayfarer3130 May 20, 2026 16:48
@jbocce
Copy link
Copy Markdown
Collaborator Author

jbocce commented May 20, 2026

@greptileai I believe even your comments outside diff have been addressed.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

You're right — the fix is in place. The getData() function now uses a finally block to call setIsLoading(false) and setHasFetchedOnce(true) unconditionally, and the catch block records the failed queryFilterValues to prevent tight retry loops. Both issues are resolved.

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.

4 participants