Skip to content

feat: add bidirectional map/table selection sync to collapsible data table [PR2] - #3720

Open
BRaimbault wants to merge 7 commits into
feat/datatable-pr1-toolbarfrom
feat/datatable-pr2-bidirectional-sync
Open

feat: add bidirectional map/table selection sync to collapsible data table [PR2]#3720
BRaimbault wants to merge 7 commits into
feat/datatable-pr1-toolbarfrom
feat/datatable-pr2-bidirectional-sync

Conversation

@BRaimbault

@BRaimbault BRaimbault commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Part of DHIS2-21456

Diff size note: this PR adds ~2,200 lines: ~1,400 are runtime code / ~800 (36%) are tests.

Description

Adds bidirectional highlighting/selection sync between the map and data table, persistent multi-feature selection, and a collapsible/resizable table panel. Second in the data table improvement series, building on PR1's toolbar and context menu.

Selection (DataTable)

  • New checkbox column; header checkbox selects/deselects all currently visible rows in one action
  • Ctrl/Cmd-click a row to toggle its selection; Shift-click selects a range from the last-clicked row (falls back to a single-row toggle if there's no prior anchor yet)
  • Selection is tracked in a new selection reducer, scoped per layer, and resets on layer switch

Map ↔ table sync

  • Hovering a map feature highlights and scrolls to its table row; hovering a table row highlights the feature on the map — consistent styling in both directions (previously cursor-hover fell through to @dhis2/ui's native grey tr:hover style while map-hover showed our own blue highlight)
  • Left/right-clicking a map feature scrolls the table to its row without changing selection; Ctrl/Cmd-click also toggles selection
  • Double-clicking a table row zooms the map to that feature
  • A row that's both selected and hovered now shows the hover color, not the selected color

Filtering

  • "Show only selected features" and "show only in view" toolbar toggles filter both the table and the map
  • "Show only in view" now uses true bounding-box overlap instead of centroid-only, and correctly handles map viewports that cross the antimeridian

Zoom actions

  • "Zoom to layer" added to the table's row context menu (previously map-only)
  • "Zoom to selected features" added to both the map and table context menus, disabled when nothing's selected; zooms to the combined bounds of every selected feature

Table panel (BottomPanel)

  • Collapse/restore button and double-click-the-header-to-toggle, shrinking the panel down to just the toolbar strip
  • The resize handle is always available, including collapsed: dragging up exits collapsed mode and resizes live; dragging down to the floor enters collapsed mode
  • Restoring (button, double-click, or dragging back up) returns to the exact height the panel had before collapsing

Bug fixes

  • Long unbroken text (e.g. URLs) in table cells now wraps instead of overflowing
  • O(n·m) .includes() scans in selection filtering replaced with O(1) Set lookups
  • Fixed a stale-effect bug where Ctrl-clicking a table row (or any selection change) re-triggered the map-click scroll effect and yanked the table back to a previously map-clicked row
  • Fixed a crash in the map context menu's mapStateToProps when no context menu is open
  • Fixed the toolbar layout bug where the resize handle's runaway flex-basis squeezed the layer name to zero width

maps-gl

  • New Cluster.setVisibleIds() override: "show only selected" now re-clusters from the filtered feature set (via source.setData()) instead of applying a GL filter expression, which can't target cluster circles (they carry no per-feature id). Applies to client- and donut-clustered event layers; server-clustered layers are unaffected since their data table is already unsupported today (unrelated pre-existing limitation)
  • Layer#panToFeature() now accepts an array of ids, fitting the map to their combined bounds

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 (maps-gl)
  • Tester approved (name)

Screenshots

supporting images

@dhis2-bot

Copy link
Copy Markdown
Contributor

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

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 15:58 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 16:04 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 18:51 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 18:54 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 13, 2026 22:37 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 14, 2026 17:21 Inactive
@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 10:03 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 17, 2026 18:21 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 10:17 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 10:21 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:52 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 21:23 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 19, 2026 23:26 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 20, 2026 07:19 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 20, 2026 12:06 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 20, 2026 14:13 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 20, 2026 15:00 Inactive
@BRaimbault
BRaimbault marked this pull request as ready for review July 27, 2026 09:45
@BRaimbault
BRaimbault force-pushed the feat/datatable-pr2-bidirectional-sync branch from 1489b0f to 782301d Compare July 27, 2026 09:58
@BRaimbault
BRaimbault marked this pull request as draft July 27, 2026 09:59
@dhis2-bot
dhis2-bot temporarily deployed to netlify July 27, 2026 10:00 Inactive
@BRaimbault
BRaimbault force-pushed the feat/datatable-pr2-bidirectional-sync branch from 782301d to 2e5fe4f Compare July 27, 2026 10:02
@sonarqubecloud

Copy link
Copy Markdown

@dhis2-bot
dhis2-bot temporarily deployed to netlify July 27, 2026 10:04 Inactive
@BRaimbault BRaimbault changed the title feat: add bidirectional map/table selection sync and collapsible datatable [PR2] feat: add bidirectional map/table selection sync to data table [PR2] Jul 28, 2026
@BRaimbault BRaimbault changed the title feat: add bidirectional map/table selection sync to data table [PR2] feat: add bidirectional map/table selection sync to collapsible data table [PR2] Jul 28, 2026
@BRaimbault
BRaimbault marked this pull request as ready for review August 4, 2026 15:21

@HendrikThePendric HendrikThePendric left a comment

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.

In general this looks good. I did leave some comments which you can take or leave.

Another thing I noticed while reviewing was a general pattern where I saw:

  • A diff in the prop-types
  • A diff in the props in the function signature
  • No changes in the function body
  • A diff in the JSX where the added props being passed down to descendants

So this is prop-drilling and you could consider addressing it by introducing a/multiple context providers and maybe some hooks. But this is just a casual comment. I didn't look into it deeply and also realise this is a codebase with a mix of function and class components, so it could get messy.

index,
}))
}, [data, dataWithoutCoords, aggregations, serverCluster, layerType])
// boundsDependency intentionally proxies mapBounds only while the toggle is on

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.

Comment is not very clear and possible misplaced too, it looks to be referring to L227

this.handleFeatureChange(prevProps)
this.handleSelectionChange(prevProps)
this.handleHighlightColorChange(prevProps)
this.handleVisibleIdsChange(prevProps)

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.

While it's good to extract things into separate methods, the naming you went with is a bit misleading on various levels:

  • "handling change" creates an association with event handlers
  • "handle" + some noun + "change" suggests that thing changed, while the bulk of the code is actually there to detect if a relevant change actually happened

Maybe something with "sync" and then what will conditionally be synced, i.e.

syncLayerData(prevProps, prevState)   // MAYBE NOT SO GREAT
syncLayerOrder(prevProps)
syncOpacity(prevProps)
syncVisibility(prevProps)
syncFeature(prevProps)
syncSelectedFeatures(prevProps)
syncHighlightColor(prevProps)
syncVisibleIds(prevProps)

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.

3 participants