This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch fix-cannot-view-dashboard-if-no-theme-role
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to
refs/heads/fix-cannot-view-dashboard-if-no-theme-role by this push:
new 23173c91804 fix(dashboard): preserve native filter scopes during
metadata refresh
23173c91804 is described below
commit 23173c9180457533537413fa05f9200481cfffaf
Author: Joe Li <[email protected]>
AuthorDate: Thu Mar 5 20:22:54 2026 -0800
fix(dashboard): preserve native filter scopes during metadata refresh
The DASHBOARD_INFO_UPDATED reducer case did a shallow metadata overwrite,
losing client-only chartsInScope/tabsInScope when the server response
(via setDashboardMetadata after save) replaced native_filter_configuration
and chart_customization_config.
Apply preserveScopes() for both configs during DASHBOARD_INFO_UPDATED,
matching the existing HYDRATE_DASHBOARD pattern. Add regression tests
for scope preservation, chart customization scopes, and metadata
passthrough when no metadata key is present.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../src/dashboard/reducers/dashboardInfo.test.ts | 80 ++++++++++++++++++++++
.../src/dashboard/reducers/dashboardInfo.ts | 20 ++++++
2 files changed, 100 insertions(+)
diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
b/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
index 752e1f9b3a6..ff50597a945 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
@@ -36,6 +36,35 @@ const stateWithTheme = {
metadata: {},
} as Partial<DashboardInfo>;
+const stateWithScopes = {
+ id: 1,
+ metadata: {
+ native_filter_configuration: [
+ {
+ id: 'FILTER-1',
+ name: 'Region',
+ chartsInScope: [10, 20, 30],
+ tabsInScope: ['TAB-A'],
+ targets: [{ datasetId: 1 }],
+ },
+ {
+ id: 'FILTER-2',
+ name: 'Date Range',
+ chartsInScope: [40],
+ tabsInScope: [],
+ targets: [{ datasetId: 2 }],
+ },
+ ],
+ chart_customization_config: [
+ {
+ id: 'CUSTOM-1',
+ chartsInScope: [100],
+ tabsInScope: ['TAB-X'],
+ },
+ ],
+ },
+} as unknown as Partial<DashboardInfo>;
+
test('preserves existing theme when DASHBOARD_INFO_UPDATED payload omits theme
key', () => {
// Simulates the fixed Header behavior: conditional spread omits `theme`
// key when PropertiesModal returns theme: undefined (theme not in fetched
list).
@@ -76,3 +105,54 @@ test('overwrites theme when DASHBOARD_INFO_UPDATED payload
has theme: undefined'
const themeId = result.theme ? result.theme.id : null;
expect(themeId).toBeNull();
});
+
+test('preserves native_filter_configuration scopes during
DASHBOARD_INFO_UPDATED metadata refresh', () => {
+ // Simulates the save flow: onUpdateSuccess →
setDashboardMetadata(serverMetadata)
+ // → dashboardInfoChanged({ metadata: ... }) → DASHBOARD_INFO_UPDATED.
+ // The server response contains filters WITHOUT chartsInScope/tabsInScope
(client-only).
+ const serverMetadata = {
+ native_filter_configuration: [
+ { id: 'FILTER-1', name: 'Region (updated)', targets: [{ datasetId: 1 }]
},
+ { id: 'FILTER-2', name: 'Date Range', targets: [{ datasetId: 2 }] },
+ ],
+ } as unknown as DashboardInfo['metadata'];
+
+ const action = dashboardInfoChanged({ metadata: serverMetadata });
+ const result = dashboardInfoReducer(stateWithScopes, action);
+
+ const filters = result.metadata?.native_filter_configuration;
+ expect(filters).toHaveLength(2);
+
+ // FILTER-1: server updated the name, scopes preserved from existing state
+ expect(filters![0].name).toBe('Region (updated)');
+ expect(filters![0].chartsInScope).toEqual([10, 20, 30]);
+ expect(filters![0].tabsInScope).toEqual(['TAB-A']);
+
+ // FILTER-2: unchanged, scopes preserved
+ expect(filters![1].chartsInScope).toEqual([40]);
+ expect(filters![1].tabsInScope).toEqual([]);
+});
+
+test('preserves chart_customization_config scopes during
DASHBOARD_INFO_UPDATED metadata refresh', () => {
+ const serverMetadata = {
+ chart_customization_config: [
+ { id: 'CUSTOM-1' },
+ ],
+ } as unknown as DashboardInfo['metadata'];
+
+ const action = dashboardInfoChanged({ metadata: serverMetadata });
+ const result = dashboardInfoReducer(stateWithScopes, action);
+
+ const customizations = result.metadata?.chart_customization_config;
+ expect(customizations).toHaveLength(1);
+ expect(customizations![0].chartsInScope).toEqual([100]);
+ expect(customizations![0].tabsInScope).toEqual(['TAB-X']);
+});
+
+test('does not affect metadata when DASHBOARD_INFO_UPDATED has no metadata
key', () => {
+ const action = dashboardInfoChanged({ slug: 'new-slug' });
+ const result = dashboardInfoReducer(stateWithScopes, action);
+
+ // Metadata untouched — same reference
+ expect(result.metadata).toBe(stateWithScopes.metadata);
+});
diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
b/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
index 4e5a7fb45e6..f6dbe8b7bd8 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
@@ -122,9 +122,29 @@ export default function dashboardInfoReducer(
case DASHBOARD_INFO_UPDATED: {
const dashAction = action as DashboardInfoAction;
const newInfo = dashAction.newInfo || {};
+ const incomingMeta = newInfo.metadata;
return {
...state,
...newInfo,
+ // Preserve client-only scope data (chartsInScope, tabsInScope) when
+ // metadata is refreshed from the server, matching HYDRATE_DASHBOARD.
+ ...(incomingMeta && {
+ metadata: {
+ ...incomingMeta,
+ ...(incomingMeta.native_filter_configuration && {
+ native_filter_configuration: preserveScopes(
+ state.metadata?.native_filter_configuration,
+ incomingMeta.native_filter_configuration,
+ ),
+ }),
+ ...(incomingMeta.chart_customization_config && {
+ chart_customization_config: preserveScopes(
+ state.metadata?.chart_customization_config,
+ incomingMeta.chart_customization_config,
+ ),
+ }),
+ },
+ }),
last_modified_time: Math.round(new Date().getTime() / 1000),
};
}