When the modal is open, the targeted component re-renders with the
candidate theme as soon as the user picks an option — no dashboard-
dirty round-trip required. Cancel reverts; Apply commits to Redux.
Implementation:
- New \`previewThemeStore\` (module-level subscribable map keyed by
layoutId, distinguishing "no preview" / "preview value=null" /
"preview value=number"). Tiny surface: \`set\`/\`clear\`/\`get\`/
\`subscribe\`. No-op \`set\` / \`clear\` calls don't fire listeners.
- \`useEffectiveThemeId\` now subscribes via \`useSyncExternalStore\`
and prefers the preview value over the Redux-resolved id when
present.
- \`ThemeSelectorModal\` writes the in-flight selection through the
store as the user picks options; cleanup on close (Cancel, X
button, escape) clears it. Apply dispatches the Redux action
*before* hiding, so the post-cleanup re-resolution lands on the
saved value (no flicker).
Snapshot of the resolved id at open-time goes through a \`useRef\`
because \`currentThemeId\` itself becomes reactive (it would already
reflect the in-flight preview), so we can't read it for "what should
we revert to?".
6 new tests for the preview store: get-undefined-for-unknown,
set-stores-numeric, set-stores-explicit-null, clear-removes,
subscriber-fires-on-real-change-and-not-on-no-op, multi-layoutId-
independence. Total dashboard ComponentThemeProvider suite is now
14 passing tests.
Drops "live preview" from the deferred-items list in SIP.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same three-step recipe applied to each grid-component type:
(a) wrap body in <ComponentThemeProvider layoutId={id}>
(b) add "Apply theme" item to the component's menu via
ComponentHeaderControls
(c) mount <ThemeSelectorModal> gated on editMode
- TabsRenderer (4b): wraps StyledTabsContainer; dots menu lands in the
existing left HoverMenu next to drag/delete.
- Row (4c): wraps WithPopoverMenu body; dots menu in the left HoverMenu
next to drag/delete/setting-icon. The existing gear icon (opens the
BackgroundStyleDropdown focus popover) is preserved as-is.
- Column (4d): same recipe as Row, top-positioned HoverMenu.
- Markdown (4e): class component, so themeModalOpen lives on
this.state. Dots menu lands inside the existing WithPopoverMenu
menuItems array next to MarkdownModeDropdown; the Edit/Preview
toggle is intentionally preserved unchanged.
Note on scope: the SIP originally imagined Phase 4 would also converge
MarkdownModeDropdown and the Row/Column gear icon onto the shared dots
menu. Those user-visible UX displacements are intentionally deferred
so this phase adds the theming affordance *additively* — every existing
menu control is untouched. The menu-pattern unification can be picked
up later without coupling it to theming.
Functional outcome: every grid-component type (Chart, Markdown, Row,
Column, Tabs) now supports the full inheritance chain end-to-end:
Instance -> Dashboard -> Tab -> Row/Col -> Chart/Markdown. Setting a
themeId at any level applies to that subtree; clearing it falls
through to the parent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the loop on per-component theming for Chart: adds an "Apply
theme" item to \`SliceHeaderControls\` (gated on dashboard edit mode)
that opens the Phase-3 \`ThemeSelectorModal\`. On save, the Phase-3
\`setComponentThemeId\` action updates \`meta.themeId\`; the Phase-1
\`ComponentThemeProvider\` (already wrapping ChartHolder since Phase 1)
re-resolves and re-renders the chart with the new theme tokens.
The full inheritance chain (Instance → Dashboard → Tab → Row/Col →
Chart) is functionally complete for Chart with this commit. Subsequent
per-component PRs (Markdown, Row, Column, Tabs) will repeat the same
three-step recipe — menu item, modal mount, body wrapper — in
isolation so each user-visible menu/UX change can be reviewed without
dragging in the theming framework changes (already merged via Phases
1-3).
- Adds \`MenuKeys.ApplyTheme\` to the dashboard menu-key enum.
- \`SliceHeaderControls\` gets local \`themeModalOpen\` state, a
Redux selector for \`dashboardState.editMode\`, a handler that opens
the modal on menu click, the menu item itself (push gated on
editMode), and a \`<ThemeSelectorModal>\` mounted with the
component's \`layoutId\`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
End-to-end mechanism for applying a CRUD theme to a single dashboard
grid component. Two pieces:
1. `setComponentThemeId(componentId, themeId | null)` — thin Redux
action that merges \`themeId\` into the target component's \`meta\`
via the existing \`updateComponents\` thunk, preserving every other
meta field. Explicit \`null\` clears the override and falls back to
the inherited theme; the resolver in Phase 1 treats null and
undefined identically. No-ops when the component id isn't in the
layout.
2. \`ThemeSelectorModal\` — parent-owned modal that fetches non-system
themes (same query as the dashboard Properties modal:
\`is_system:false\` filter on \`/api/v1/theme/\`), preselects the
currently-resolved override via the Phase-1 \`useEffectiveThemeId\`
hook, and exposes Apply / Cancel / Clear-override-(inherit) actions.
Each call site provides \`layoutId\` + the \`show\`/\`onHide\` toggle.
No call site for the modal yet — Phase 4 wires the "Apply theme" menu
item into each component's \`ComponentHeaderControls\` to open it.
3 passing tests on the action: merge preserves other meta keys, clear
stores explicit null (not undefined), no-op for missing component.
SIP.md updated with the Phase 3 implementation notes and the deferred-
to-Phase-4 wiring detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared vertical-dots menu component for dashboard grid components.
Generic `items: ComponentMenuItem[]` API — each component (Chart,
Markdown, Row, Column, Tabs) plugs in its own list; the visual chrome
(dots icon trigger, dropdown surface, accessible label, divider
handling, danger/disabled styling) lives in this one component.
Built on `MenuDotsDropdown` from `@superset-ui/core/components` so the
trigger styling matches Chart's existing `SliceHeaderControls` — Phase
4's per-component PRs will converge `SliceHeaderControls` and the
other menu patterns (Markdown's `MarkdownModeDropdown`, Row/Col's
gear-icon + `WithPopoverMenu`) onto this same component.
Phase 2 lands the component + tests only. The actual per-component
menu conversions are user-visible UX changes (e.g. Markdown loses its
toggle-style Edit/Preview switcher and gains a dots menu) and ship in
Phase 4 alongside theme wiring per component, so each can be reviewed
in isolation rather than as a sweeping refactor.
4 passing tests: empty items renders nothing, trigger renders, onClick
fires from menu selection, disabled items don't fire onClick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the skeleton for granular (per-component) theming on dashboard grid
components, with the inheritance chain:
Instance theme -> Dashboard theme -> Tab theme
-> Row/Col theme -> Chart/Markdown theme
This commit lands Phase 1 from the SIP (`SIP.md` at repo root): the
storage shape and the resolver, wired into `ChartHolder` as the
proof-of-concept call site. No UI yet — `themeId` must be set via Redux
devtools / position_json hand-edit to verify visually. Phase 2 will
introduce the `ComponentHeaderControls` menu and Phase 3 the
`ThemeSelectorModal` that drives this from a real UI.
Surface:
- `LayoutItemMeta.themeId?: number | null` — optional CRUD theme id
stored per-component in `position_json` meta (no schema migration; the
meta map is already open-ended). `null` and `undefined` both mean
"inherit from parent".
- `pickEffectiveThemeId(layoutId, layout)` — pure resolver. Walks
`parents` up the layout map from the given node until it finds a
numeric `themeId` or hits `DASHBOARD_ROOT_ID`. Hop-capped at 32 to
defend against malformed parents chains.
- `useEffectiveThemeId(layoutId)` — Redux hook variant.
- `<ComponentThemeProvider layoutId={...}>` — wraps children in the
resolved theme's `SupersetThemeProvider`. Lazy-fetches via the
existing `ThemeController.createDashboardThemeProvider`, which already
caches by id so N components sharing one theme = 1 fetch. Pass-through
when no ancestor sets a `themeId`.
- `ChartHolder.tsx` — wraps the existing `<AntdThemeProvider>` (which is
a popup-container shim for fullscreen mode, not a token provider) so
per-component tokens are set before antd's ConfigProvider for popup
targeting.
Tests: 8 unit cases for `pickEffectiveThemeId` covering own / inherited /
null-skip / no-ancestor / root-stop / malformed-parents / other-meta /
missing-id.
Closes the spirit of the closed PR #36749 (which became unrebasable
after .jsx -> .tsx + React 18 + theme controller churn).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draft Superset Improvement Proposal for component-level theming on
dashboard grid components (Charts, Markdown, Row, Column, Tabs) with
inheritance Instance -> Dashboard -> Tab -> Row/Col -> Chart.
Supersedes the closed PR #36749 (became unrebasable after .jsx -> .tsx
conversion, React 18 upgrade, and theme-controller churn since 2025-12).
This is a living document — kept in lockstep with the work on
`feat/granular-theming-v2`. Each phase updates the status / shortcomings
/ test-plan sections.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>