Skip to content

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

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

feat(WorkListUINext): New Study List (WorkListUINext)#6005
jbocce wants to merge 173 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 study list with a new WorkListUINext system built on ui-next components, covering the full worklist surface: table state synced to URL and sessionStorage, client-side pagination (single server query per filter change), a preview side panel with bounded-concurrency thumbnail fetching, and a new thumbnailRequestStrategy for the DICOMweb data source.

  • DataSourceWrapper is simplified to one query per filter change with a hasFetchedOnce gate, proper finally-block loading-state reset, and a retry button on failure; DEFAULT_DATA is now module-scoped.
  • useStudyListStateSync and studyListFilterContract provide a typed, drift-proof URL contract for sorting, pagination, and all filter keys \u2014 including the previously flagged patientName mismatch, which is now resolved.
  • SidePanelPreview manages thumbnail fetching with an AbortController, a capped worker pool, and explicit URL.revokeObjectURL tracking via ownedBlobUrlsRef, addressing prior blob-leak concerns.

Confidence Score: 4/5

Safe to merge after the rows-per-page dropdown close bug is fixed; the rest of the change is well-structured with no data-loss paths.

The rows-per-page dropdown in Pagination.tsx calls e.preventDefault() inside onSelect, which per Radix UI docs prevents the menu from auto-closing after every page-size selection. All other findings are non-blocking style or edge-case correctness issues.

platform/ui-next/src/components/DataTable/Pagination.tsx — the rows-per-page dropdown; platform/app/src/routes/DataSourceWrapper.tsx — the pre-existing stale in-flight query on data-source switch (open from prior review round).

Important Files Changed

Filename Overview
platform/ui-next/src/components/DataTable/Pagination.tsx New pagination component; e.preventDefault() in DropdownMenuItem.onSelect prevents the rows-per-page dropdown from auto-closing after selection
platform/app/src/routes/DataSourceWrapper.tsx Refactored to client-side pagination with filter-change-only refetch, proper error recovery (finally block, retry button), and hasFetchedOnce gate; stale in-flight query on data-source switch remains a known open issue
platform/app/src/routes/WorkList/SidePanelPreview.tsx New component with bounded-concurrency thumbnail pool, AbortController cleanup, and blob URL revocation via ownedBlobUrlsRef; correctly addresses previously flagged memory-leak concerns
platform/app/src/hooks/useStudyListStateSync.ts New hook syncing sort/page/filter state across URL, sessionStorage, and component state with 200ms debounce; uses centralized studyListFilterContract to avoid key drift
platform/app/src/utils/studyListFilterContract.ts New single source of truth for URL query keys; resolves the previously flagged patientName key mismatch between the URL writer and DataSourceWrapper reader
extensions/default/src/DicomWebDataSource/retrieveThumbnail.ts New thumbnail strategy module with fetch+QIDO-fallback path; thumbnail and rendered bulkDataRetrieve branches are duplicated code
platform/app/src/utils/shallowEqualIgnoringArrayOrder.ts New utility; set-based array comparison incorrectly equates arrays with duplicate elements; not a practical concern for modalities but violates the documented contract
platform/app/src/routes/WorkList/WorkList.tsx Replaced with thin ui-next StudyList wrapper; delegates sorting/pagination/filters to useStudyListStateSync and hands preview management to SidePanelPreview
platform/app/src/routes/index.tsx Default / route now selects WorkList (ui-next) or LegacyWorkList based on workList.variant customization; clean migration path preserved

Comments Outside Diff (3)

  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.
  3. platform/app/src/routes/DataSourceWrapper.tsx, line 128-144 (link)

    P1 Stale in-flight query overwrites new data source results after a switch

    dataSourceChangedCallback calls refresh() (which sets data.queryFilterValues = null) to ensure a new query fires, but any in-flight getData() from the old data source is not cancelled. If the old query completes after the new data source has already fetched and populated data.queryFilterValues, the old query's setData({ ..., queryFilterValues: oldQFV }) overwrites the new results. The effect then re-evaluates filtersChanged = !shallowEqualIgnoringArrayOrder(oldQFV, newQFV) — since both derive from the same URL, they are equal → filtersChanged = false → no corrective refetch fires. The user sees the old data source's studies with no indication that anything is wrong, and no further query is issued.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: platform/app/src/routes/DataSourceWrapper.tsx
    Line: 128-144
    
    Comment:
    **Stale in-flight query overwrites new data source results after a switch**
    
    `dataSourceChangedCallback` calls `refresh()` (which sets `data.queryFilterValues = null`) to ensure a new query fires, but any in-flight `getData()` from the old data source is not cancelled. If the old query completes *after* the new data source has already fetched and populated `data.queryFilterValues`, the old query's `setData({ ..., queryFilterValues: oldQFV })` overwrites the new results. The effect then re-evaluates `filtersChanged = !shallowEqualIgnoringArrayOrder(oldQFV, newQFV)` — since both derive from the same URL, they are equal → `filtersChanged = false` → no corrective refetch fires. The user sees the old data source's studies with no indication that anything is wrong, and no further query is issued.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
platform/ui-next/src/components/DataTable/Pagination.tsx:45-48
Rows-per-page dropdown never auto-closes after selection. Radix UI's documented behavior for `DropdownMenuItem.onSelect` is: calling `event.preventDefault()` prevents the dropdown from auto-closing when the item is selected. Here `e.preventDefault()` is called before `table.setPageSize(size)`, so after clicking a page size the menu stays open indefinitely — users must press Escape or click outside to dismiss it. Remove `e.preventDefault()` to restore the expected close-on-select behavior.

```suggestion
              onSelect={() => {
                table.setPageSize(size);
              }}
```

### Issue 2 of 3
platform/app/src/utils/shallowEqualIgnoringArrayOrder.ts:21-42
Set-based comparison silently mishandles arrays with duplicate values. For `['CT','CT','MR']` vs `['CT','MR','MR']`: both have length 3, and `new Set(...)` on each yields `{CT, MR}` with size 2 — so the function returns `true` even though the arrays differ. In the current modality use-case duplicates are unlikely, but this violates the documented "unordered set" contract for any repeated-element array. Sorting-then-comparing fixes it correctly.

```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;
      }
      const sorted1 = [...val1].sort();
      const sorted2 = [...val2].sort();
      if (sorted1.some((v, i) => v !== sorted2[i])) {
        return false;
      }
    } else if (val1 !== val2) {
      return false;
    }
  }
```

### Issue 3 of 3
extensions/default/src/DicomWebDataSource/retrieveThumbnail.ts:110-151
The `thumbnail` and `rendered` bulkDataRetrieve branches are character-for-character identical — both build a `bulkDataURI` from the endpoint path and wrap the result in a `Blob`. The only difference is the `thumbnailRendering` string itself, which is already captured by `buildThumbnailEndpointPath`. Consider extracting a shared `buildBulkDataThumbnailSrc` helper to reduce the maintenance surface and eliminate the risk that a future fix in one branch is missed in the other.

Reviews (21): Last reviewed commit: "Refactor the thumbnail retrieval to a se..." | Re-trigger Greptile

@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.

getGetThumbnailSrc: function (instance, imageId) {
if (dicomWebConfig.thumbnailRendering === 'wadors') {
getGetThumbnailSrc: function (thumbnailContext: ThumbnailContext, imageId) {
const thumbnailRendering = dicomWebConfig.thumbnailRendering;
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.

can we refactor this out, it is adding noise to data source

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.

What is your suggestion for what part remains in datasource? The problem is that different datasources end up with different methods/capabilities here. I'm ok with it moving out, but would like to know what pieces go where and how the choices are made.

Copy link
Copy Markdown
Collaborator Author

@jbocce jbocce May 26, 2026

Choose a reason for hiding this comment

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

I refactored all of the thumbnail retrieval logic/code to a file retrieveThumbnail.ts that lives next to the index.ts file. I hope you both find this adequate.

});
}

function buildThumbnailEndpointPath(
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.

same

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

maxNumRequests: {
interaction: 100,
thumbnail: 75,
thumbnail: 5,
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 should get communicated as breaking change in the docs, same as below

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would say these are NOT breaking changes. Those values should never have been that high for thumbnail requests.

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.

maybe don't label as breaking change but communicate we have changed these

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

: 'all';
const seriesView: PreviewSeriesView = forceListView ? 'list' : configuredSeriesView;

useEffect(() => {
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 massive useEffect, it should be its own useSeriesFetch hook

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.

That will also make it effectively useable by other components.

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.

same here this useEffect better be refactored as a hook

* @param {object} b - Second object
* @returns {boolean} True if the two are equal under the rules above.
*/
export function shallowEqualIgnoringArrayOrder(a, b): boolean {
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.

can we add tests for this

startDate,
endDate,
onChange,
inputClassName,
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.

lots of props mean we are not following shadcn composition correctly, can we rethink this with composition instead of props

* @param time - Raw time string (HH, HHmm, HHmmss, or HHmmss.SSS format)
* @returns Formatted date string (DD-MMM-YYYY HH:mm) or empty string if invalid
*/
export function formatStudyDate(date?: string, time?: string): string {
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 feels like it should have been implemented elsewhere already, please make sure we are not adding a new one

"cmdk": "1.1.1",
"date-fns": "3.6.0",
"framer-motion": "6.2.4",
"moment": "2.30.1",
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.

i really don't want to add a new time-related dependency, can we use the new Temporal api that is widely supported

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal

see also other blog posts on how to migrate

https://www.smashingmagazine.com/2026/03/moving-from-moment-to-temporal-api/

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

moment is already a dependency in core, ui and devDependencies. I am simply adding it also to ui-next. So it is not new at all.

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.

please see my comments

customizationService: [
{
'workList.columns': {
// Hide the MRN column and move Instances to the front.
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.

Can you include an example of adding a column?


## `workList.columns`

Builds the column set for the WorkList table. The customization is a function with the same shape as `StudyList.defaultColumns` — it receives the default `ColumnDef[]` and must return a `ColumnDef[]`. Use it to reorder, hide, or insert columns without rewriting the defaults.
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.

Can this be done as a fixed list rather than a function returning a new column def? That could then be modified directly by the invariant $ functions using straight properties, at least as long as there was a basic column type that could display a column.

COLUMN_IDS.DESCRIPTION,
] as const;

export function defaultColumns(): ColumnDef<StudyRow, unknown>[] {
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.

Is there a reason this has to be a function? It looks more or less like a constant which could be re-used/re-created.
Ideally the shadcn component composition could be used to create a completely different layout by including alternate header functions/render body functions, or the default columns could be applied.

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.

5 participants