Skip to content

feat: column show/hide, pinning, reordering, and persistence for data table [PR4]#3724

Draft
BRaimbault wants to merge 29 commits into
feat/datatable-pr3-filteringfrom
feat/datatable-pr4-columns
Draft

feat: column show/hide, pinning, reordering, and persistence for data table [PR4]#3724
BRaimbault wants to merge 29 commits into
feat/datatable-pr3-filteringfrom
feat/datatable-pr4-columns

Conversation

@BRaimbault

@BRaimbault BRaimbault commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Part of DHIS2-21456

Description

Adds a column picker to the data table toolbar for showing/hiding, pinning (freeze left), and reordering columns, and persists the configuration per layer in the map's saved favourite. Fourth in the data table improvement series, building on PR1's toolbar, PR2's selection sync, and PR3's filtering.

Column picker (ColumnPicker)

  • New "Configure columns" toolbar icon opens a popover listing every column for the active layer, each with a checkbox (show/hide), a drag handle (reorder, via @dnd-kit, consistent with the drag-reorder already used in LayersPanel.jsx), and a pin button (freeze to the left of the table)
  • Pinned columns are grouped at the top of the list with a divider separating them from the rest; the pin icon uses this app's teal "active" accent when a column is pinned
  • Per-control tooltips (drag handle, pin button) replace a persistent hint line; the drag preview is portaled to document.body so it renders above the rest of the UI instead of being clipped by the popover's own positioned ancestor
  • Long column names truncate with an ellipsis instead of wrapping and breaking the row layout
  • A bulk-actions row above the list adds a checkbox to show/hide every column at once, a reverse-selection button (the same IconSync16 already used elsewhere in the table), and a reset-to-defaults button that clears the saved config back to the default order/visibility
  • A search box appears once the column list is long enough to actually scroll, filtering by name without ever resizing the popover
  • The popover's width is measured once from its content when it opens (the widest column label) and then held fixed, so searching/filtering never resizes it

Pinned columns in the table

  • Pinned columns render as sticky (fixed/left) cells using @dhis2/ui's built-in support, staying in place while the rest of the table scrolls horizontally
  • The row-selection checkbox column only becomes sticky once something else is pinned, avoiding an unconditional <th>-vs-<td> DOM shape change for the common nothing-pinned case

Persistence

  • New dataTableColumnConfig field on each mapView, following the same config JSON-blob pattern already used for legendDecimalPlaces/legendIsolated/etc.: DATA_TABLE_COLUMN_CONFIG_SET reducer action, round-tripped through favorites.js and every layer loader (thematic, event, org unit, facility, tracked entity, GeoJSON URL, Earth Engine) so column visibility/order/pinning survives reopening or sharing a saved map

Filter popover polish

  • Multi-select filter dropdown width is now driven by its own content (the widest option label, measured directly since the list renders through a virtualized react-virtuoso component that CSS's own intrinsic sizing can't see through) instead of the anchor column's width, so a wide column no longer forces an oversized dropdown
  • Fixed a layout bug where option rows used a collapsing CSS margin for spacing, understating the list's real height and leaving a visible gap before the "Any value" row
  • "Reverse selection" button gained a missing hover state and a fix for its icon being compressed under the checkbox column's tight layout

Toolbar polish

  • Double-clicking the toolbar to collapse/restore the panel no longer fires when the click lands on one of the toolbar's own controls (icon buttons, the color picker, the search field)
  • Sort icons are top-aligned against wrapped column headers instead of centering against the full wrapped label; the active sort arrow and the row-selection checkboxes now use this app's teal accent for consistency with the pin icon
  • Each toolbar control (search, row count, show-in-view toggle, clear filters, highlight color, active layer name, resize handle, close, collapse, column picker) is now its own file under datatable/controls/, replacing one large block of inline JSX in BottomPanel.jsx

Bug fixes

  • Fixed the column picker's trigger button being permanently disabled for layer types with synchronous header computation (Thematic/OrgUnit/Facility) - a child-before-parent React effect-ordering race meant the picker's header state was cleared right after being set; Event layers happened to avoid it only because their extended-events loading triggers a second, later header recompute

Refactors (no behavior change)

  • Column visibility/order/pin computation (getVisibleHeaders/getPinnedLeftOffsets) extracted into util/tableColumns.js, directly unit-tested, matching the filterSelection.js/tableSort.js pattern from PR3
  • FilterInput.jsx's pure filter/search/display-value logic and canvas text measurement extracted into util/filterInput.js (unit-tested); its self-contained help tooltip extracted into FilterHelpTooltip.jsx
  • DataTable.jsx's self-contained pieces (TopTooltip, SelectionFilterButton, the TableVirtuoso "components" glue) extracted into their own files; its pure row/sort/filterable-column helpers moved into util/dataTable.js
  • ColumnPickerControl.jsx's row rendering extracted into ColumnRow.jsx; its column-key toggle/pin/reverse-selection logic and pinned-cell layout math moved into util/tableColumns.js alongside the existing column helpers - this also removed a duplicated pinned-column-count calculation that had crept into DataTable.jsx
  • A couple of values shared between CSS and JS (a tooltip's vertical padding, the toolbar's collapsed height) consolidated into CSS custom properties read via the existing getCssVar helper, instead of hand-maintained duplicate constants

Quality checklist

Add N/A to items that are not applicable.

  • Dashboard tested
  • Cypress and/or Jest tests added/updated
  • Docs added N/A
  • d2-ci dependencies replaced N/A
  • Tester approved (name)

Screenshots

supporting images

… plumbing

Adds the Redux action/reducer for a per-layer dataTableColumnConfig field
and wires it through favorites.js save/load and all layer loaders, mirroring
the existing legendDecimalPlaces config-blob pattern. No UI yet.
getVisibleHeaders/getPinnedLeftOffsets in a new src/util/tableColumns.js,
directly unit-tested, matching the filterSelection.js/tableSort.js pattern.
Not yet wired into DataTable.jsx.
Computes visibleHeaders from dataTableColumnConfig, feeds it into
useColumnWidths, and applies @dhis2/ui's built-in fixed/left sticky-column
support for pinned columns. The checkbox column only becomes fixed when
something else is pinned, since @dhis2/ui renders fixed cells as <th>
instead of <td> - doing this unconditionally would break every existing
Cypress td-index assertion. Also fires onHeadersChange with the full
unfiltered header list for a future ColumnPicker to consume.

CSS selectors that were td-only (dataCell/lightText/monoCell/selected/
hovered) now also match th, since a pinned column's cells render as <th>.
New popover, built on the existing FilterDropdownPopover shell and the
same @dnd-kit drag-reorder pattern already used in LayersPanel.jsx (not
the raw HTML5 drag-and-drop the original design sketch used). Dispatches
setDataTableColumnConfig directly; not yet wired into BottomPanel.
Adds allHeaders state fed by DataTable's onHeadersChange, cleared
immediately on layer switch to avoid briefly showing the previous
layer's columns alongside the new layer's saved column config.
…cker

favorites.spec.js: end-to-end cleanMapConfig round-trip for
dataTableColumnConfig across thematic/earthEngine/TEI/geojson layer types,
verified to actually depend on the validLayerProperties whitelist entry.

ColumnPicker.spec.jsx: render-and-interact tests (disabled state, default
visibility, toggle/pin dispatch with full config assertions, pinned-first
ordering). Drag-reorder (onDragEnd) isn't covered - confirmed by direct
experiment that dnd-kit's KeyboardSensor doesn't produce a usable drag in
jsdom without much heavier mocking than is worthwhile here.
…us headers

React fires child effects before parent effects within the same commit.
DataTable's header-reporting effect (child) and BottomPanel's own
"reset allHeaders on layer switch" effect (parent) both fired on
mount/layer-switch; the parent's reset always ran second in that commit,
clobbering the real headers DataTable had just reported. Layer types with
a synchronous header computation (Thematic/OrgUnit/Facility) never got a
second chance to set it, leaving the picker's trigger button permanently
disabled - Event layers only worked because their extended-events loading
triggers a second, later header recompute that escapes the race.

Fix: track headers keyed by which layer produced them
(headersByLayer = {layerId, headers}, tagged by DataTable's own effect
closure, not a "latest activeLayerId" guess) and derive staleness at
render time via a plain comparison, instead of a second effect racing to
clear the same state.
Auto-generated by the i18n extraction tooling to include the new
translatable strings introduced in ColumnPicker.jsx/BottomPanel.jsx.
@dhis2-bot

Copy link
Copy Markdown
Contributor

🚀 Deployed on https://pr-3724.maps.netlify.dhis2.org

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 22:08 Inactive
@BRaimbault BRaimbault changed the title Feat/datatable pr4 columns feat: column show/hide, pinning, reordering, and persistence for data table [PR4] Jul 16, 2026
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 22:15 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 09:15 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 10:03 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 12:17 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 14:55 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 18:28 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 18:52 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 18:56 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 19:08 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 19:24 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 10:28 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 13:42 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 15:49 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 20:53 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 21:24 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 23:28 Inactive
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 20, 2026 07:19 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants