feat(dashboard): end-to-end component theming on Chart — Phase 4a

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>
This commit is contained in:
Claude
2026-05-13 11:07:54 -07:00
parent 96880a5e8a
commit 1f3d2cc305
3 changed files with 46 additions and 1 deletions

21
SIP.md
View File

@@ -196,7 +196,26 @@ Each phase brings its own tests; the cumulative bar:
3 passing tests on `setComponentThemeId`: preserves other meta keys
+ sets numeric `themeId`; stores explicit `null` for the clear path;
no-op when the component id isn't in the layout.
- _(Phase 4)_ — pending.
- _(Phase 4)_ — in progress.
- **Chart (4a)**: ✅ landed locally. End-to-end demo on Chart works
now: `SliceHeaderControls` has a new "Apply theme" item (gated on
dashboard edit mode); clicking it opens the Phase-3
`ThemeSelectorModal` keyed to the component's layoutId; on save the
Phase-3 action updates `meta.themeId`; the Phase-1
`ComponentThemeProvider` (already wrapping ChartHolder) re-resolves
and re-renders the chart with the new theme tokens. The full
Instance → Dashboard → Tab → Row/Col → Chart inheritance chain is
functionally complete for Chart.
Open follow-ups for the **Markdown / Row / Column / Tabs** PRs:
- Each gets the menu-pattern conversion (`MarkdownModeDropdown`,
gear icon, none → shared `ComponentHeaderControls`).
- Each wraps its body in `<ComponentThemeProvider layoutId=...>`.
- Each mounts a `<ThemeSelectorModal>` with an "Apply theme" menu
item that opens it.
- Each per-component PR can be reviewed in isolation for the menu/UX
change without dragging in the theming framework changes (those
are already merged in Phases 1-3).
### Phase 1 status

View File

@@ -57,6 +57,7 @@ import { useDrillDetailMenuItems } from 'src/components/Chart/useDrillDetailMenu
import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal';
import ThemeSelectorModal from 'src/dashboard/components/ThemeSelectorModal';
import { usePermissions } from 'src/hooks/usePermissions';
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
@@ -166,6 +167,13 @@ const SliceHeaderControls = (
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
// setting openKeys undefined falls back to uncontrolled behaviour
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [themeModalOpen, setThemeModalOpen] = useState(false);
// Per-component theming is an edit-mode affordance only — viewers see the
// applied theme but can't change it.
const editMode = useSelector<RootState, boolean>(
state => !!state.dashboardState.editMode,
);
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
props.slice.slice_id,
);
@@ -258,6 +266,9 @@ const SliceHeaderControls = (
// eslint-disable-next-line no-unused-expressions
props.toggleExpandSlice?.(props.slice.slice_id);
break;
case MenuKeys.ApplyTheme:
setThemeModalOpen(true);
break;
case MenuKeys.ExploreChart:
// eslint-disable-next-line no-unused-expressions
props.logExploreChart?.(props.slice.slice_id);
@@ -450,6 +461,13 @@ const SliceHeaderControls = (
});
}
if (editMode) {
newMenuItems.push({
key: MenuKeys.ApplyTheme,
label: t('Apply theme'),
});
}
if (canExplore) {
newMenuItems.push({
key: MenuKeys.ExploreChart,
@@ -681,6 +699,13 @@ const SliceHeaderControls = (
dataset={datasetWithVerboseMap}
/>
{canEditCrossFilters && scopingModal}
{editMode && (
<ThemeSelectorModal
layoutId={componentId}
show={themeModalOpen}
onHide={() => setThemeModalOpen(false)}
/>
)}
{isFullSize && <Global styles={fullscreenStyles(theme)} />}
</>
);

View File

@@ -379,6 +379,7 @@ export enum MenuKeys {
ExportFullXlsx = 'export_full_xlsx',
ForceRefresh = 'force_refresh',
Fullscreen = 'fullscreen',
ApplyTheme = 'apply_theme',
ToggleChartDescription = 'toggle_chart_description',
ViewQuery = 'view_query',
ViewResults = 'view_results',