Skip to content

feat(dashboard): add selector widget type (issue #1381) - #1504

Merged
giswqs merged 13 commits into
opengeos:mainfrom
bgoniasa:feature/dashboard-selector-widget
Aug 1, 2026
Merged

feat(dashboard): add selector widget type (issue #1381)#1504
giswqs merged 13 commits into
opengeos:mainfrom
bgoniasa:feature/dashboard-selector-widget

Conversation

@bgoniasa

@bgoniasa bgoniasa commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the selector widget — the second of three proposed in #1381 (after the indicator widget in #1392).

A category dropdown / chip-list bound to a layer field. Selecting a value highlights it; in multi-select mode, multiple chips can be active at once. This is the UI foundation for cross-filtering — the selection state is ready to feed a filter signal bus.

What's included

  • DashboardWidgetType: added "selector"
  • DashboardWidget: added optional multiple flag for multi-select
  • WidgetEditorDialog: category field picker + multi-select checkbox (reuses existing categoryCols)
  • DashboardPanel: new SelectorValues component renders a scrollable chip list
    • Extracts distinct values from the category field, sorted alphabetically
    • Single-select: clicking a chip selects only it
    • Multi-select: chips toggle independently
  • i18n: chartType.selector + editor.multiSelect keys (en)

Screenshot

The widget renders as a compact card with clickable value chips inside the dashboard grid, matching the existing widget card layout (title bar, layer name, edit/move/delete controls).

What's NOT included (intentionally)

Test plan

  • tsc --noEmit passes on core and desktop (no new errors)
  • Add a vector layer with a categorical field
  • Open Dashboard → Add widget → choose Selector
  • Pick a category field → verify chips appear with distinct values
  • Click chips in single mode → only one active at a time
  • Enable multi-select → multiple chips active
  • Edit existing selector widget → fields pre-fill correctly
  • Save project → reopen → selector widgets persist

Closes part of #1381.

Summary by CodeRabbit

  • New Features
    • Added a new selector dashboard widget type with category-based filtering, including single- and multi-select.
    • Updated the dashboard widget editor to configure selector category and “allow multiple selection”.
  • Improvements
    • Selector widgets display titles derived from their configuration and show a clear “no data” state when no distinct categories exist.
    • Enhanced widget persistence using a replace behavior so omitted fields don’t carry over unintentionally.
  • Localization
    • Added localized labels for the selector widget type and multi-select toggle.
  • Tests
    • Added coverage for distinct category extraction and selector persistence/round-trips.

bgoniasa and others added 2 commits July 28, 2026 16:33
Adds a category dropdown / chip-list widget that shows distinct values
from a layer field. Supports single-select (default) and multi-select
mode. This is the UI foundation for cross-filtering (issue opengeos#1381 part 2):
the selection state lives in the widget card, ready to feed a filter bus.

Changes:
- DashboardWidgetType: add "selector"
- DashboardWidget: add optional multiple flag
- WidgetEditorDialog: category picker + multi-select checkbox
- DashboardPanel: SelectorValues chip list component
- i18n: chartType.selector + editor.multiSelect keys
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new dashboard selector widget type with category-based configuration, optional multi-select behavior, sorted selectable category values, full-record replacement on edit, project normalization, and persistence tests.

Changes

Dashboard selector widgets

Layer / File(s) Summary
Selector configuration and editor
packages/core/src/types.ts, apps/geolibre-desktop/src/components/panels/WidgetEditorDialog.tsx, apps/geolibre-desktop/src/i18n/locales/en.json
Adds selector configuration, editor validation and controls, save handling, and localized labels.
Selector normalization and replacement persistence
packages/core/src/project.ts, packages/core/src/store.ts, apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
Preserves selector widgets during project normalization and replaces edited widget records without retaining omitted fields.
Category extraction and dashboard rendering
apps/geolibre-desktop/src/lib/attribute-charts.ts, apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
Extracts sorted distinct category values, skips chart computation for selectors, and renders single- or multi-select chips.
Selector validation and persistence
tests/dashboard-widgets.test.ts
Tests normalization, project round trips, category extraction, and replacement versus merge persistence semantics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WidgetEditorDialog
  participant DashboardPanel
  participant AppState
  participant distinctCategoryValues
  participant SelectorValues
  WidgetEditorDialog->>DashboardPanel: save selector configuration
  DashboardPanel->>AppState: replaceWidget with complete record
  AppState-->>DashboardPanel: return stored widget
  DashboardPanel->>distinctCategoryValues: extract values from chart rows
  distinctCategoryValues-->>DashboardPanel: return sorted distinct values
  DashboardPanel->>SelectorValues: render values and selection mode
  SelectorValues->>SelectorValues: toggle selected values
Loading

Suggested reviewers: giswqs

Poem

I’m a bunny selecting, one hop or a few,
With categories sorted in tidy rows too.
A chip turns bright when I tap with delight,
Single or many—both choices feel right.
Dashboard carrots now grow in the view!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely reflects the main change: adding a new dashboard selector widget type.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@giswqs

giswqs commented Jul 28, 2026

Copy link
Copy Markdown
Member

/claude-review

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://a1787a27.geolibre-preview.pages.dev
Demo app https://a1787a27.geolibre-preview.pages.dev/demo/
Commit a261eae

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1504/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1504/demo/
Commit a261eae

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx`:
- Around line 568-579: Add aria-pressed={isSelected} to the selector chip button
in the toggle rendering, preserving the existing toggle behavior and styling so
assistive technology can identify each chip’s selected state.
- Around line 547-560: Update SelectorValues so its selected state is reset or
normalized whenever the multiple prop changes, ensuring single mode cannot
retain multiple selections from the prior mode. Prefer remounting via a key tied
to the selector configuration or add an effect that preserves at most one
selected value when multiple becomes false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2d56fe0-ea1f-488a-82df-961a2cd35051

📥 Commits

Reviewing files that changed from the base of the PR and between 0d2c916 and 9f89930.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
  • apps/geolibre-desktop/src/components/panels/WidgetEditorDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • packages/core/src/types.ts

Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
giswqs added 2 commits July 28, 2026 13:03
- Key SelectorValues on the selector config (category + multiple) so the
  widget remounts with an empty selection when the field or single/multi
  mode changes, instead of leaving a stale multi-selection visible in
  single mode.
- Add aria-pressed to the selector chips so assistive technology can
  report which values are selected, rather than conveying it through
  styling alone.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx`:
- Around line 525-534: Update the SelectorValues key in the widget rendering
logic to include widget.layerId alongside the existing category and
multiple-mode values, ensuring the component remounts and clears its selection
whenever the source layer changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 10b46ab0-c11d-477d-9a07-6b62a816c5c5

📥 Commits

Reviewing files that changed from the base of the PR and between 9f89930 and 95d6c8a.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx

Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx Outdated
Verified against Natural Earth 110m countries (177 features) in the running
app: the CONTINENT field yields its 8 distinct values, REGION_UN its 6.

- Read category values from `row.properties[field]` instead of indexing the
  row itself. `ChartRow` is `{ properties }`, so the old access returned
  undefined for every feature, leaving the widget permanently showing its
  "This layer has no chartable attributes." fallback — no chips ever
  rendered. A double cast to `Record<string, unknown>` had hidden the
  mismatch from the compiler.
- Extract the extraction into `distinctCategoryValues` in attribute-charts so
  the row shape is covered by unit tests rather than an inline cast.
- Always write `multiple` when saving a selector. `updateWidget` merges its
  patch onto the stored widget, so omitting the key when the box was
  unchecked left an earlier `true` in place: switching multi-select off
  silently did nothing and the widget stayed in multi mode.
- Add regression tests for the row shape, blank/nullish filtering, and the
  merge semantics that made the omitted flag stick.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/lib/attribute-charts.ts`:
- Around line 54-55: Update the category-value filtering in the attribute chart
value collection to test value.trim() rather than only value !== "", while
continuing to add the original value so nonblank display text is preserved. Add
a regression test covering whitespace-only property values and verify they are
excluded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1d8bc074-6b26-490b-8138-be0bde78f227

📥 Commits

Reviewing files that changed from the base of the PR and between 95d6c8a and fed1a3e.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
  • apps/geolibre-desktop/src/components/panels/WidgetEditorDialog.tsx
  • apps/geolibre-desktop/src/lib/attribute-charts.ts
  • tests/dashboard-widgets.test.ts

Comment thread apps/geolibre-desktop/src/lib/attribute-charts.ts Outdated
Editing a widget could not clear an optional field, and selector widgets did
not survive a save at all.

- Add a `replaceWidget` store action and use it when saving an edited widget.
  The editor hands back a complete record and omits the optional fields left
  empty, but `updateWidget` merges its patch, so an emptied title, color,
  prefix, or suffix kept its previous value and the change silently reverted
  when the dialog closed. Replacing also drops fields left over from the
  widget's previous type. `updateWidget` keeps its partial-patch semantics for
  every other caller.
- Add "selector" to the widget-type allow-list in normalizeWidgets. It was
  missing, so every selector widget was discarded on save and never came back
  on reload.
- Spell that allow-list as a Record keyed by DashboardWidgetType so a new
  member fails to compile until it is listed. The previous array was annotated
  rather than checked, which is how the omission went unnoticed.
- Persist the selector's `multiple` flag in normalizeWidgets, which dropped it
  even once the type was accepted, and revert the editor to the file's
  omit-when-falsy style now that saving replaces rather than merges.
- Cover the round-trip, the allow-list, and replace-vs-merge semantics.

Verified in the app against Natural Earth 110m countries: setting a title,
prefix, suffix, and color and then clearing each one now sticks, where the
merging path left the old title in place.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/dashboard-widgets.test.ts`:
- Around line 190-192: Update the assertions around each widget lookup in the
test to assign the result of find to a saved variable, call assert.ok(saved)
before property checks, and then inspect saved directly without ?? {}. Apply
this pattern consistently to the cases at the current assertion and the
corresponding widget checks near the other referenced locations, preserving the
existing field expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ebe4844-277d-4b98-ac66-1bd6677a7e21

📥 Commits

Reviewing files that changed from the base of the PR and between fed1a3e and 3f3f931.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
  • apps/geolibre-desktop/src/components/panels/WidgetEditorDialog.tsx
  • packages/core/src/project.ts
  • packages/core/src/store.ts
  • tests/dashboard-widgets.test.ts

Comment thread tests/dashboard-widgets.test.ts
- Include the layer id in the SelectorValues key. Switching a widget to
  another layer while the category field and single/multi mode stayed the
  same reused the component and carried the old layer's selection over.
- Drop whitespace-only category values in distinctCategoryValues. Only ""
  was excluded, so a value of "   " rendered as an empty, unlabelled chip.
  The original spelling is still what the chip displays, so a value with
  meaningful padding keeps it.
- Replace the `find(...) ?? {}` fallbacks in the tests added by this branch
  with assert.ok narrowing. The fallback made the `"field" in saved` checks
  pass against an empty object, so a widget being dropped entirely — the
  regression these tests exist to catch — would not have failed them. The
  pre-existing assertions elsewhere in the file are left as they are.
A selector's chips highlighted but nothing consumed the selection, so the
widget looked broken: picking a value changed nothing on the dashboard.

- Lift each selector's chosen values into DashboardPanel, keyed by widget id,
  and make SelectorValues a controlled component. The selection has to live
  where sibling widgets can read it.
- Add `filterRowsBySelections`, which narrows rows by the active selections:
  values within one selector are OR-ed, separate selectors are AND-ed, and a
  selector with nothing picked filters nothing.
- Every chart and indicator widget now renders from the rows left by the other
  selectors bound to its layer. A selector reads unfiltered rows so its own
  chip list stays complete and a choice can always be undone.
- Drop a widget's selection when an edit repoints it at another layer, field,
  or selection mode, and when the widget is removed. This replaces the key-based
  remount, which no longer resets anything now that the state lives in the panel.
- Selections are deliberately not persisted to the project: they are a way of
  looking at the data, not part of it.

Filtering the map layer itself stays out of scope, per opengeos#1381 part 4.

Verified against Natural Earth 110m countries (177 features): a count
indicator reads 177, then 37 for "5. Low income", 28 with "Africa" added,
51 with the income filter removed, and 177 once cleared — each matching the
dataset. Multi-select Africa+Asia gives 98 (51+47), and a pie over REGION_UN
drops from 6 slices to 2.
giswqs and others added 3 commits July 28, 2026 15:58
Cross-filtering only shows up in the *other* widgets, so on a dashboard
holding just a selector, clicking a value appeared to do nothing at all —
the chip highlight was the only feedback the widget could give.

- Report the matching feature count under the chips ("51 of 176 features"),
  counting against every active selection on the layer, not just this one.
- Add a Clear action, so a selection can be dropped without hunting for the
  chip that set it (in single mode that was the only way back).
- Mark the count aria-live: the chips convey selection, but nothing was
  announcing its effect.

Verified with the Countries sample (remote GeoParquet, 176 features):
selecting Africa reads "51 of 176 features" and Asia "47 of 176", matching
a DuckDB count over the same parquet, and Clear returns the widget to
showing no count.
Adds a scrollable table widget showing top-N features from a layer with
configurable columns, sort field/direction, and row limit. Completes the
three widget types proposed in opengeos#1381 (after indicator opengeos#1392 and selector
but does not yet filter other widgets.

Changes:
- DashboardWidgetType: add "list"
- DashboardWidget: add listFields, sortBy, sortDir, limit fields
- project.ts: add "selector" and "list" to DASHBOARD_WIDGET_TYPES
  (selector was missing from the validator in the previous PR)
- WidgetEditorDialog: column checkboxes + sort by/dir + limit input
- DashboardPanel: ListTable component with sortable rows
- i18n: chartType.list + 6 editor keys (en)
@bgoniasa

Copy link
Copy Markdown
Contributor Author

Hi @giswqs, thank you for the feedback and for all the work you've put into this PR — the cross-filtering and selection display are fantastic additions!

I've consolidated the list widget into this PR as you requested (closed #1508). The branch now includes:

  1. Selector widget — category-based chip picker (single/multi mode)
  2. List widget — scrollable table with configurable columns, sort field/direction, and row limit
  3. Cross-filtering — your commits that wire selector selections to other widgets

I'll add screenshots demonstrating both widgets working together as soon as possible.

@giswqs

giswqs commented Jul 31, 2026

Copy link
Copy Markdown
Member

/claude-review

Comment thread packages/core/src/project.ts
Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

All findings have been posted as buffered inline comments. Here's the final summary.

Code review

Bugs

  • packages/core/src/project.ts (normalizeWidgets, ~L837-841): the new list widget type is added to DASHBOARD_WIDGET_TYPES but normalizeWidgets never copies over its listFields/sortBy/sortDir/limit fields. Since this function also runs on the save path (projectFromStore), a newly created list widget loses all its configuration the instant the project is saved, and the render code then falls back to "no data" — the exact bug class this PR's own comment describes fixing for selector, reintroduced for list. High confidence.
  • apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx:636: inside the list render branch, const rows = data.rows; shadows the outer memoized rows (which applies filterRowsBySelections), so list widgets always render unfiltered data and never honor cross-filtering from selector widgets on the same layer. Medium-high confidence.
  • DashboardPanel.tsx:495: the default title for a list widget uses the raw widget.layerId (an internal id/UUID) instead of a human-readable name, unlike every other widget type's default title. Medium confidence.

Quality

  • The PR description states it adds only the selector widget, but the diff also fully implements a separate, undocumented list widget type (its own editor UI, type, fields, and rendering) that isn't mentioned in the PR body — and, per the bugs above, is incompletely wired up (no persistence, no cross-filtering, ID-based title). Worth confirming this is intentional/in-scope. Low-medium confidence.
  • DashboardPanel.tsx:595-620: the selector's displayed "N of total" mixes an unfiltered total with a matched count that already incorporates other active selectors' filters, which can read inconsistently when another selector is active. Low confidence, UX nit only.
  • Same region: distinctCategoryValues and the matched computation run unmemoized on every render (unlike the sibling rows/result values), repeating O(n) work on unrelated re-renders. Low confidence, minor performance nit.

Security / CLAUDE.md: nothing found. New UI strings use t() and only logical Tailwind spacing utilities (no RTL-unsafe ml-/left- classes); no unsafe HTML injection in the new ListTable/SelectorValues components.

giswqs added 2 commits July 31, 2026 18:52
- packages/core/src/project.ts: normalizeWidgets now carries a list widget's
  listFields/sortBy/sortDir/limit. It also runs on the save path
  (projectFromStore), so those fields were dropped the moment a project was
  saved and the widget rendered its "no data" fallback on reopen. The row
  limit is clamped to the editor's [1, 500] range.
- DashboardPanel.tsx: drop the `const rows = data.rows` shadowing in the list
  render branch so a list honors the outer memo's filterRowsBySelections and
  cross-filters like every other widget.
- DashboardPanel.tsx: default title for a list widget now reads from the
  chosen columns instead of the internal layer id (the layer name is already
  the card subtitle).
- DashboardPanel.tsx: a selector's "N of total" now measures both counts
  against the rows left by the *other* selectors, so the fraction cannot read
  as a share of the whole layer while another selector has narrowed it.
- DashboardPanel.tsx: memoize the selector's distinct values and match counts
  so an unrelated re-render no longer repeats a full scan of the layer's rows.
- tests/dashboard-widgets.test.ts: cover list-widget normalization (kept
  fields, dropped/clamped junk, dropped from a non-list widget) and a
  serialize/parse round-trip.
@giswqs
giswqs merged commit 195e709 into opengeos:main Aug 1, 2026
31 of 32 checks passed
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.

2 participants