Skip to content

feat: multi-select filters, global search, and richer selection filtering for data table [PR3]#3721

Draft
BRaimbault wants to merge 30 commits into
feat/datatable-pr2-bidirectional-syncfrom
feat/datatable-pr3-filtering
Draft

feat: multi-select filters, global search, and richer selection filtering for data table [PR3]#3721
BRaimbault wants to merge 30 commits into
feat/datatable-pr2-bidirectional-syncfrom
feat/datatable-pr3-filtering

Conversation

@BRaimbault

@BRaimbault BRaimbault commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Part of DHIS2-21456

Description

Redesigns the data table's per-column filters into one unified multi-select widget, adds a global search box, and replaces the boolean "show only selected" toggle with a richer selected/not-selected filter. Third in the data table improvement series, building on PR1's toolbar and PR2's selection sync.

Column filters (FilterInput)

  • Every column now gets the same searchable, multi-select popover: the trigger doubles as a live search box, narrowing a virtualized checkbox list of the column's actual distinct values
  • Checkboxes support multi-select (OR) filtering, e.g. "High" + "Low" on the Legend column
  • Typed text that doesn't match an existing value applies live as a custom filter ("Contains" for text columns, "Use filter" for numeric columns, reusing the existing >/>=/</<=/,/& syntax); a help tooltip explains the numeric syntax
  • Pinned "Any value" (matches every non-blank value) and "No value" (blank-cell sentinel) options, kept above a divider so they stay visible while searching; "Any value" acts as a select-all/deselect-all for the column's real values, kept in sync both ways
  • New "Reverse selection" button per column, inverting that column's full value domain (ignores the current search-narrowed list)
  • Dropdown sort order now matches the column's active sort direction; the Range column's dropdown sorts numerically instead of lexically
  • Sentinel values (blank/"any value"/"selected row") consolidated into constants/dataTable.js (SENTINEL_NO_VALUE, SENTINEL_ANY_VALUE, SENTINEL_SELECTED_ROW)

Global search

  • New "Search all columns" input in the toolbar, case-insensitive substring match across every string column (filterByGlobalSearch), combined with column filters and the selection filter

Selection filter (replaces the boolean "show only selected" toggle)

  • The checkbox column header now has its own filter popover ("Selected" / "Not selected", either/both/neither), synced to the map the same way column filters are (Layer.js's getVisibleIds)
  • Header "select all" checkbox and the new "Reverse selection" button next to it both only ever touch currently visible/filtered rows — anything selected outside the current filter is left untouched; both now have clearer tooltips ("Select all visible rows" / "Reverse selection of visible rows")

"Clear filters"

  • Now resets column filters, the selection filter, global search, and "show only in view" together, and is always visible (disabled instead of hidden) so it's discoverable

Toolbar polish

  • Vertical dividers group related icons; the highlight color picker moved next to the layer name
  • Collapse/restore icon simplified from double chevrons to a single chevron
  • Consistent top-side tooltip placement across toolbar controls

Resize handle

  • Rewritten from HTML5 native drag-and-drop to Pointer Events — fixes a "not-allowed" cursor during drag and a cursor that never switched to "grabbing"
  • Restyled to match the drag-handle look used elsewhere in the app (e.g. layer cards)

Context menu

  • "Zoom to filtered features" added to both the map and table context menus, disabled when nothing's currently filtered

Removed

  • The synthetic "Index" column — it was never a real feature property, only an internal row-order tiebreaker, and filtering by it made map layers disappear entirely (an index filter never matched any real feature)

Refactors (no behavior change)

  • FilterInput.jsx split: dropdown-positioning logic → FilterDropdownPopover.jsx (also removes a cross-component dependency, since DataTable.jsx was reaching into FilterInput.jsx's internals for it); the sentinel-value toggle/reverse logic → util/filterSelection.js, now directly unit-tested
  • DataTable.jsx split: column-width measurement/resize logic → useColumnWidths.js; row-selection bookkeeping → useRowSelection.js
  • useTableData.js's sort/compare logic → util/tableSort.js, now directly unit-tested instead of only covered indirectly through the hook
  • Sort-direction and DHIS2 CSS token usage consolidated (SORT_ASCENDING/SORT_DESCENDING in constants/dataTable.js; --colors-white/--elevations-popover custom properties instead of hardcoded values in the datatable stylesheets)

Bug fixes

  • 5 Cypress tests updated for the new UI (checkbox column shifted other column indices; filter input data-test renamed from -input- to -search-)
  • Text no longer overflows unbroken in table cells
  • Fixed a crash in the map context menu's mapStateToProps when no context menu is open

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

Needed so the multi-select column filter can distinguish option-set-backed
columns (which need code->name resolution) from plain categorical columns.
Adds a columnOptions memo to useTableData, allowlisted to the legend/type
built-in columns plus any optionSet-backed event column, capped at 30
distinct values before falling back to free text. A blanket "any string
column with few distinct values" check would wrongly turn name/id/parentName
into dropdowns on small datasets.
Adds an Array.isArray short-circuit before the existing numeric/string
dispatch so a multi-select column filter (built from user checkbox
selections) OR-matches exactly against the raw stored value, without
touching stringFilter/numericFilter's existing behavior.
…h useTableData

Applies the global search after column filters and before selection
filtering/sorting in the rows memo, matching case-insensitively across
all string-typed columns.
Adds a Popover + checkbox-list path (reusing the pattern already
established by TableContextMenu.jsx) rendered when an `options` prop is
passed in. Option-set-backed columns get a separate wrapper component
(OptionSetMultiSelectFilter) so useOptionSet/useDataQuery is only ever
mounted when an optionSetId actually exists - legend/type columns never
have one, and no test in the repo mocks useDataQuery.
Replaces the cryptic '2,>3&<8' placeholder with a simpler '> 5, < 8' and
adds an info icon + Tooltip explaining the AND/OR/comparison syntax,
consistent with Tooltip's existing use throughout DataTable.jsx/BottomPanel.jsx.
Table now forwards globalSearch into useTableData and threads
columnOptions[dataKey]/optionSet.id from the header objects into
FilterInput so it can pick between free-text and multi-select rendering.
Adds a dense Input between the "Clear filters" button and the show-only
toggles, sized to shrink before the layer name has to truncate further.
"Clear filters" now also resets the search box, and hasActiveFilters
accounts for both column filters and the search string.
Threads an optional onItemClick/activeLegendNames pair through
Legend -> LegendItem, scoped to THEMATIC_LAYER in OverlayCard (the only
layer type with a legend data-table column). Clicking a legend class
adds/removes it from the legend filter array and opens the data table
for that layer if it isn't already showing. Other Legend call sites
(plugin, download, edit preview, classification) never pass the new
props, so they're unaffected.
Import order in FilterInput.spec.jsx and Prettier formatting across the
files touched by the filtering changes.
@dhis2-bot

Copy link
Copy Markdown
Contributor

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

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 22:17 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 22:47 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 08:53 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 20:21 Inactive
Reverts c2b5aac. The thematic-only implementation is being pulled out
of this PR - the feature should work across bubble, event, facility,
and org-unit layers too, which needs its own investigation and belongs
in a separate PR rather than this one.
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 20:37 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 21:03 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 21:09 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 21:12 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 22:05 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 08:49 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 11:37 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 11:42 Inactive
@BRaimbault BRaimbault changed the title feat: multi-select filters, global search, and legend-driven filtering for data table [PR3] feat: multi-select filters, global search, and richer selection filtering for data table [PR3] Jul 16, 2026
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 12:18 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 22:15 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 22:26 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 16, 2026 22:47 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 08:50 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 10:03 Inactive
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4 New issues
4 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 18:21 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