You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ms...@apache.org on 2026/04/17 15:57:24 UTC
(superset) branch fix/handlebars-template-explore-hydration created (now b282feba894)
This is an automated email from the ASF dual-hosted git repository.
msyavuz pushed a change to branch fix/handlebars-template-explore-hydration
in repository https://gitbox.apache.org/repos/asf/superset.git
at b282feba894 fix(plugin-chart-handlebars): preserve template on explore open
This branch includes the following new commits:
new b282feba894 fix(plugin-chart-handlebars): preserve template on explore open
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/01: fix(plugin-chart-handlebars): preserve template on explore open
Posted by ms...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
msyavuz pushed a commit to branch fix/handlebars-template-explore-hydration
in repository https://gitbox.apache.org/repos/asf/superset.git
commit b282feba8943fed1f14853332d06063250644071
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Fri Apr 17 18:22:27 2026 +0300
fix(plugin-chart-handlebars): preserve template on explore open
Handlebars controls use camelCase names (`handlebarsTemplate`,
`styleTemplate`) while Superset's form_data convention is snake_case.
Charts created via the MCP service store params with the snake_case key
`handlebars_template`, so on explore hydration the control framework's
`formData[name]` lookup misses and the control falls back to its default
template — silently overwriting the saved custom template on the next save.
The existing `mapStateToProps` also read from the wrong key (controls[snake]
instead of the camelCase control state), so it couldn't rescue the value.
Fix by reading the value from `form_data` with both casings as fallbacks,
keeping old and new charts compatible without requiring a migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
---
.../src/plugin/controls/handlebarTemplate.tsx | 4 +-
.../src/plugin/controls/style.tsx | 4 +-
.../test/plugin/controls.test.ts | 90 ++++++++++++++++++++++
3 files changed, 94 insertions(+), 4 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
index 2f407c6dd92..d6c55b18f3a 100644
--- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx
@@ -106,8 +106,8 @@ export const handlebarsTemplateControlSetItem: ControlSetItem = {
valueKey: null,
validators: [validateNonEmpty],
- mapStateToProps: ({ controls }) => ({
- value: controls?.handlebars_template?.value,
+ mapStateToProps: ({ form_data }) => ({
+ value: form_data?.handlebars_template ?? form_data?.handlebarsTemplate,
}),
},
};
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
index b38635faa6d..af505d7f940 100644
--- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
+++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx
@@ -87,8 +87,8 @@ export const styleControlSetItem: ControlSetItem = {
valueKey: null,
validators: [],
- mapStateToProps: ({ controls, common }) => ({
- value: controls?.handlebars_template?.value,
+ mapStateToProps: ({ form_data, common }) => ({
+ value: form_data?.style_template ?? form_data?.styleTemplate,
htmlSanitization: common?.conf?.HTML_SANITIZATION ?? true,
}),
},
diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts
new file mode 100644
index 00000000000..d1630c45126
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+ ControlPanelState,
+ CustomControlItem,
+} from '@superset-ui/chart-controls';
+import { QueryFormData } from '@superset-ui/core';
+import { handlebarsTemplateControlSetItem } from '../../src/plugin/controls/handlebarTemplate';
+import { styleControlSetItem } from '../../src/plugin/controls/style';
+
+const handlebarsConfig = (handlebarsTemplateControlSetItem as CustomControlItem)
+ .config;
+const styleConfig = (styleControlSetItem as CustomControlItem).config;
+
+const buildState = (form_data: Partial<QueryFormData>) =>
+ ({
+ form_data: form_data as QueryFormData,
+ controls: {},
+ datasource: null,
+ common: { conf: { HTML_SANITIZATION: true } },
+ slice: { slice_id: 1 },
+ }) as unknown as ControlPanelState;
+
+const CUSTOM = '<div>custom template</div>';
+const CUSTOM_CSS = '.foo { color: red; }';
+
+test('handlebarsTemplate mapStateToProps reads snake_case handlebars_template (MCP-created charts)', () => {
+ const result = handlebarsConfig.mapStateToProps!(
+ buildState({ handlebars_template: CUSTOM } as Partial<QueryFormData>),
+ {},
+ );
+ expect(result.value).toBe(CUSTOM);
+});
+
+test('handlebarsTemplate mapStateToProps reads camelCase handlebarsTemplate (UI-created charts)', () => {
+ const result = handlebarsConfig.mapStateToProps!(
+ buildState({ handlebarsTemplate: CUSTOM } as Partial<QueryFormData>),
+ {},
+ );
+ expect(result.value).toBe(CUSTOM);
+});
+
+test('handlebarsTemplate mapStateToProps returns undefined when no template stored (allows default)', () => {
+ const result = handlebarsConfig.mapStateToProps!(buildState({}), {});
+ expect(result.value).toBeUndefined();
+});
+
+test('styleTemplate mapStateToProps reads camelCase styleTemplate (MCP and UI charts)', () => {
+ const result = styleConfig.mapStateToProps!(
+ buildState({ styleTemplate: CUSTOM_CSS } as Partial<QueryFormData>),
+ {},
+ );
+ expect(result.value).toBe(CUSTOM_CSS);
+ expect(result.htmlSanitization).toBe(true);
+});
+
+test('styleTemplate mapStateToProps reads snake_case style_template as fallback', () => {
+ const result = styleConfig.mapStateToProps!(
+ buildState({ style_template: CUSTOM_CSS } as Partial<QueryFormData>),
+ {},
+ );
+ expect(result.value).toBe(CUSTOM_CSS);
+});
+
+test('styleTemplate mapStateToProps uses HTML_SANITIZATION=false from config', () => {
+ const result = styleConfig.mapStateToProps!(
+ {
+ ...buildState({}),
+ common: { conf: { HTML_SANITIZATION: false } },
+ } as unknown as ControlPanelState,
+ {},
+ );
+ expect(result.htmlSanitization).toBe(false);
+});