Skip to content

refactor(ui): widget menu cleanup — Export submenu + Save in modal (#912, #913) - #959

Merged
alfredo1996 merged 3 commits into
devfrom
fix/issue-912-913-widget-menu-cluster
Jun 6, 2026
Merged

refactor(ui): widget menu cleanup — Export submenu + Save in modal (#912, #913)#959
alfredo1996 merged 3 commits into
devfrom
fix/issue-912-913-widget-menu-cluster

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Closes #912. Closes #913.

Summary

Two related widget action-menu changes from the dogfood session, shipped together as a single PR since both touch the same dropdown surface.

#912 — Export submenu

Export CSV/PNG/SVG were three flat top-level items, crowding the menu next to primary actions like Edit / Remove. They now collapse into a single `Export ▸` submenu via Radix `DropdownMenuSub`. Single-format widgets (e.g. a data-only widget with no ECharts rendering) keep a flat "Export CSV" entry to avoid an extra click for the common case.

#913 — Save as new template moves to modal

"Save to Widget Library" moved from the per-widget dropdown into the widget editor modal footer, left-aligned next to Cancel/Save:

```
[ Save as new template ] ... [ Cancel ] [ Save ]
```

Visible only when `mode === "edit"`. The discoverability point: users want to reuse a widget at the moment they've finished configuring it, not via a dropdown next to Edit/Remove.

Implementation

  • `component/src/components/composed/widget-card.tsx` — `WidgetCardAction` gains `children?: WidgetCardAction[]`. Render block switches to `DropdownMenuSub` when children are present. Backwards-compatible (`children` optional).
  • `app/src/components/dashboard-container.tsx` — collapses the three Export pushes into one submenu; drops the Save action and the `onSaveAsTemplate` prop.
  • `app/src/components/widget-editor-modal.tsx` + `modal-footer.tsx` — new optional `onSaveAsTemplate` prop wired through. Footer renders "Save as new template" only in edit mode.
  • Parent pages — `[id]/edit/page.tsx` passes the callback to the modal (re-using the existing SaveTemplateDialog state). `[id]/page.tsx` (view mode) no longer renders SaveTemplateDialog at all.

Trade-off explicitly accepted

View-mode users (creators viewing a dashboard they didn't author) lose the one-click "Save as template" from the menu. They have to enter Edit mode → open the widget editor → click the footer button. The issue makes this trade-off deliberately: the new placement is much more discoverable at the right moment.

Tests

  • `widget-card` vitest — new "submenu actions" describe (renders trigger, child clicks fire via keyboard nav)
  • `dashboard-container-branches.test.tsx` — flat stub flattens submenu children so existing `action-csv` testids keep working; obsolete "includes Save to Widget Library" test replaced with a negative assertion
  • `chart-export.spec.ts` — traverses the new `Export ▸` submenu
  • `widget-library.spec.ts` — main save-as-template test now goes through the editor modal; three "from view mode" tests deleted (feature gone from view mode by design)

Test plan

  • `npm -w app run test` — 2889/2889 pass
  • `npm -w component run test` — 1670/1670 pass
  • `npm run lint` — no new errors (5 pre-existing issues unchanged)
  • `npm run build` — passes
  • `cd app && npx playwright test chart-export widget-library` — 17/17 passed
  • CI: E2E shards
  • Manual: visit a dashboard with bar/line widgets, confirm Export submenu; open editor on an existing widget, confirm "Save as new template" button

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Export formats are now grouped under an "Export ▸" submenu in widget menus
    • "Save as new template" button added to the widget editor modal footer
  • Refactor

    • Moved template-saving flow from dashboard view into the editor modal
    • Widget action menus now support nested submenu actions for cleaner organization
  • Tests

    • Updated E2E and unit tests to reflect the new menu structure and modal flow

, #913)

Closes #912 and #913 together — same dropdown surface.

#912: Export CSV/PNG/SVG were three separate top-level items in every
chart widget's action dropdown, crowding the menu. They now collapse
into a single "Export ▸" submenu (Radix DropdownMenuSub). Single-format
widgets (e.g. data-only, no ECharts) keep a flat "Export CSV" entry to
avoid an extra click for the common case.

#913: "Save to Widget Library" moved from the per-widget dropdown into
the widget editor modal footer. Lives left-aligned next to Cancel/Save
as "Save as new template", visible only in edit mode. Discoverable at
the moment users finish configuring, which is when they're most likely
to want to reuse the result.

Implementation:
- component/src/components/composed/widget-card.tsx — WidgetCardAction
  gains an optional `children?: WidgetCardAction[]` field; render block
  switches to DropdownMenuSub when children are present.
- app/src/components/dashboard-container.tsx — collapses the three
  Export pushes into one submenu (flat fallback for 1 format), drops
  the Save action and the onSaveAsTemplate prop.
- app/src/components/widget-editor-modal.tsx + modal-footer.tsx — new
  onSaveAsTemplate prop wired through; footer renders "Save as new
  template" only when mode === "edit" and the callback is provided.
- app/src/app/(dashboard)/[id]/edit/page.tsx — passes the callback to
  WidgetEditorModal (was already wiring SaveTemplateDialog state).
- app/src/app/(dashboard)/[id]/page.tsx — view mode no longer renders
  SaveTemplateDialog at all (users access via Edit → modal).

Tests:
- widget-card vitest: new "submenu actions" describe — submenu trigger
  renders, child click fires correctly via keyboard nav.
- dashboard-container test stub flattens submenu children so existing
  testid assertions ("action-csv") keep working; replaced the obsolete
  "Save to Widget Library in dropdown" test with a negative assertion.
- chart-export.spec.ts updated to traverse the new "Export ▸" submenu.
- widget-library.spec.ts: the main happy-path test now opens the editor
  modal and clicks "Save as new template"; the three "from view mode"
  tests deleted (feature no longer available from view mode by design).

E2E: 17/17 passed locally (chart-export + widget-library).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f21582c2-95af-4485-a4a3-5add6c4585e1

📥 Commits

Reviewing files that changed from the base of the PR and between a34020c and 39cc112.

📒 Files selected for processing (4)
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/__tests__/modal-footer.test.tsx
  • component/src/components/composed/__tests__/widget-card.test.tsx
  • component/src/components/composed/widget-card.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/components/widget-editor-modal.tsx
  • component/src/components/composed/widget-card.tsx
  • component/src/components/composed/tests/widget-card.test.tsx

Walkthrough

Adds nested submenu support to WidgetCard actions, consolidates multiple export formats under an "Export ▸" submenu, moves "Save as template" into the widget editor modal footer (with callback wiring), and updates tests and dashboard pages to match.

Changes

Widget Action Menu Refactor & Save-as-Template Migration

Layer / File(s) Summary
Component: WidgetCard Submenu Support
component/src/components/composed/widget-card.tsx, component/src/components/composed/__tests__/widget-card.test.tsx
WidgetCardAction accepts optional children; WidgetCard conditionally renders Radix submenu primitives for actions with children. Tests added/updated to verify submenu rendering, keyboard activation, and backward compatibility.
Dashboard: Export Action Grouping into Submenu
app/src/components/dashboard-container.tsx, app/src/components/__tests__/dashboard-container-branches.test.tsx, app/e2e/chart-export.spec.ts
Removes onSaveAsTemplate from WidgetActions. Action builder consolidates CSV/PNG/SVG into a single Export submenu when multiple formats exist and preserves flat Export X for single-format cases. Tests and E2E updated for new action ids and submenu interaction.
Widget Editor Modal: Save as Template Button
app/src/components/widget-editor-modal.tsx, app/src/components/widget-editor/modal-footer.tsx
Adds optional onSaveAsTemplate prop to modal and footer; renders gated "Save as new template" outline button in edit mode when callback present and no lab error; clicking builds widget, closes modal, and calls the callback.
Dashboard Edit Page: Wire Save Template Callback
app/src/app/(dashboard)/[id]/edit/page.tsx
Wires onSaveAsTemplate on WidgetEditorModal to call setTemplateWidget so the modal footer can open SaveTemplateDialog.
Dashboard Viewer Page: Remove View-Mode Template Saving
app/src/app/(dashboard)/[id]/page.tsx
Removes useSession-derived write gating, template-widget state, connections fetch, and SaveTemplateDialog render; stops passing onSaveAsTemplate to DashboardContainer.
E2E & Branch Tests: Updated Flows & IDs
app/e2e/widget-library.spec.ts, app/e2e/chart-export.spec.ts, app/src/components/__tests__/dashboard-container-branches.test.tsx
Chart export E2E now opens the Export submenu and clicks PNG; widget-library E2E saves via editor modal footer; dashboard-container branch tests updated for flattened/leaf action ids (e.g., action-csv) and absence of action-save-to-widget-library.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • alfredo1996/neoboard#873: Introduced PNG/SVG export support and prior export actions that are reorganized into the new submenu here.
  • alfredo1996/neoboard#475: Previously added view-mode save-as-template wiring which this PR removes in favor of modal footer wiring.
  • alfredo1996/neoboard#627: Refactored the widget-editor modal surface that this PR extends with onSaveAsTemplate/footer behavior.

Suggested labels

enhancement, pkg:component, pkg:app, area:widgets, area:dashboard

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.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 accurately describes the main changes: refactoring the widget menu by adding an Export submenu and moving Save-as-template into the editor modal, with linked issue references.
Linked Issues check ✅ Passed All coding requirements from #912 and #913 are met: WidgetCardAction supports optional children; submenu rendering via DropdownMenuSub is implemented; dashboard-container builds unified Export actions; widget-editor-modal and modal-footer add onSaveAsTemplate callbacks; Save action removed from dropdown; E2E tests updated.
Out of Scope Changes check ✅ Passed All changes are scoped to #912 and #913 objectives: refactoring export menu into submenu, relocating save-as-template to modal, updating affected tests, and supporting infrastructure. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-912-913-widget-menu-cluster

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 and usage tips.

@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

🧹 Nitpick comments (1)
app/src/components/__tests__/dashboard-container-branches.test.tsx (1)

41-47: ⚡ Quick win

Assert the grouped Export action shape at least once in unit tests

Line 45 flattens submenu actions, so these tests can still pass even if buildActions stops emitting the Export parent with children. Add one assertion against captured widgetCardProps to verify the grouped structure for multi-format widgets.

As per coding guidelines, **/*.ts: “Every new behavior, bug fix, and edge case gets a test.”

Also applies to: 309-324

🤖 Prompt for 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.

In `@app/src/components/__tests__/dashboard-container-branches.test.tsx` around
lines 41 - 47, Add a unit assertion that verifies buildActions emits a grouped
"Export" parent with a non-empty children array for multi-format widgets: after
computing flatActions in dashboard-container-branches.test.tsx (the test that
captures widgetCardProps), assert that widgetCardProps.actions (or the captured
actions passed into the WidgetCard mock) contains an entry with label "Export"
whose children is an array including the expected leaf entries (e.g., "CSV",
"JSON" or whatever formats your fixtures use); this ensures the grouped
parent+children shape created by buildActions is tested even though flatActions
flattens them for other lookups.

Source: Coding guidelines

🤖 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 `@app/src/components/widget-editor-modal.tsx`:
- Around line 910-917: The save-as-template handler currently forwards the stale
prop `widget` to `onSaveAsTemplate`, so unsaved edits are lost; change the
handler to call the editor state builder (e.g., `buildWidgetForSave()`) and pass
its result to `onSaveAsTemplate` instead of `widget`, while still calling
`onOpenChange(false)` to close the modal (ensure `buildWidgetForSave()` is
invoked at the time of the click and its return value is passed to
`onSaveAsTemplate`).

In `@component/src/components/composed/__tests__/widget-card.test.tsx`:
- Around line 296-307: The test "renders a flat menu item when action has no
children (backwards compat)" is a false positive because the menu is never
opened and portal content isn’t inside container; update the test to open the
menu before asserting. Locate the menu trigger in the rendered WidgetCard (e.g.,
find the button with role="button" or aria-haspopup using
container.querySelector or screen.getByRole), simulate a click (use
userEvent.click or fireEvent.click) to open the dropdown, then query document
(not container) or use within(document.body) for '[data-state][role="menuitem"]'
and assert it is present for the flat action; reference the test name,
WidgetCard render call, the actions array, and the menu trigger/button when
making changes.

---

Nitpick comments:
In `@app/src/components/__tests__/dashboard-container-branches.test.tsx`:
- Around line 41-47: Add a unit assertion that verifies buildActions emits a
grouped "Export" parent with a non-empty children array for multi-format
widgets: after computing flatActions in dashboard-container-branches.test.tsx
(the test that captures widgetCardProps), assert that widgetCardProps.actions
(or the captured actions passed into the WidgetCard mock) contains an entry with
label "Export" whose children is an array including the expected leaf entries
(e.g., "CSV", "JSON" or whatever formats your fixtures use); this ensures the
grouped parent+children shape created by buildActions is tested even though
flatActions flattens them for other lookups.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb707f19-cc46-422a-afe0-42c0f2d59a0e

📥 Commits

Reviewing files that changed from the base of the PR and between aa4583c and a34020c.

⛔ Files ignored due to path filters (1)
  • app/tsconfig.tsbuildinfo is excluded by !app/tsconfig.tsbuildinfo
📒 Files selected for processing (10)
  • app/e2e/chart-export.spec.ts
  • app/e2e/widget-library.spec.ts
  • app/src/app/(dashboard)/[id]/edit/page.tsx
  • app/src/app/(dashboard)/[id]/page.tsx
  • app/src/components/__tests__/dashboard-container-branches.test.tsx
  • app/src/components/dashboard-container.tsx
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/modal-footer.tsx
  • component/src/components/composed/__tests__/widget-card.test.tsx
  • component/src/components/composed/widget-card.tsx

Comment thread app/src/components/widget-editor-modal.tsx Outdated
Comment thread component/src/components/composed/__tests__/widget-card.test.tsx Outdated
alfredorubin96 and others added 2 commits June 6, 2026 18:40
- widget-card.tsx: drop array indices from React keys (label-based now).
- widget-editor-modal.tsx: extract the SaveAsTemplate inline conditional
  into a top-level handler so the component's cognitive complexity stays
  under Sonar's threshold of 15.
- modal-footer.test.tsx: 6 new tests covering the "Save as new template"
  button (#913). Lifts the new-code coverage from 79.4% back above 80%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- widget-editor-modal.tsx: SaveAsTemplate now snapshots current editor
  state via buildWidgetForSave() instead of the (potentially stale)
  widget prop, so unsaved edits land in the template payload. Wrapped
  the handler in useCallback to also reduce the function's overall
  cognitive complexity per Sonar.
- widget-card.test.tsx: backwards-compat submenu test was a false
  positive — it queried the unopened dropdown via container, not the
  portal. Rewrote to actually open the menu, assert aria-haspopup is
  absent (flat menuitem, not a sub-trigger), and confirm click fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alfredo1996
alfredo1996 merged commit e146e19 into dev Jun 6, 2026
10 checks passed
@alfredo1996
alfredo1996 deleted the fix/issue-912-913-widget-menu-cluster branch June 6, 2026 18:21
@sonarqubecloud

sonarqubecloud Bot commented Jun 6, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
79.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants