From cf587caca7c19b7eb49b15143149859cdf1346d3 Mon Sep 17 00:00:00 2001 From: Mehmet Salih Yavuz Date: Tue, 28 Apr 2026 11:13:37 +0300 Subject: [PATCH] fix(plugin-chart-handlebars): preserve template on explore open (#39442) Co-authored-by: Claude Opus 4.7 (1M context) --- .../src/plugin/controls/handlebarTemplate.tsx | 4 +- .../src/plugin/controls/style.tsx | 4 +- .../test/plugin/controls.test.ts | 116 ++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts 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..a0e1f96cffc 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?.handlebarsTemplate ?? form_data?.handlebars_template, }), }, }; 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..8997e80e633 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?.styleTemplate ?? form_data?.style_template, 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..ae92c1a3f3f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts @@ -0,0 +1,116 @@ +/** + * 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, + ControlState, + 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) => + ({ + form_data: form_data as QueryFormData, + controls: {}, + datasource: null, + common: { conf: { HTML_SANITIZATION: true } }, + slice: { slice_id: 1 }, + }) as unknown as ControlPanelState; + +const CUSTOM = '
custom template
'; +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), + {} as ControlState, + ); + expect(result.value).toBe(CUSTOM); +}); + +test('handlebarsTemplate mapStateToProps reads camelCase handlebarsTemplate (UI-created charts)', () => { + const result = handlebarsConfig.mapStateToProps!( + buildState({ handlebarsTemplate: CUSTOM } as Partial), + {} as ControlState, + ); + expect(result.value).toBe(CUSTOM); +}); + +test('handlebarsTemplate mapStateToProps prefers camelCase when both keys present (latest edit wins over legacy snake_case)', () => { + const result = handlebarsConfig.mapStateToProps!( + buildState({ + handlebars_template: 'stale legacy value', + handlebarsTemplate: 'latest edit', + } as Partial), + {} as ControlState, + ); + expect(result.value).toBe('latest edit'); +}); + +test('handlebarsTemplate mapStateToProps returns undefined when no template stored (allows default)', () => { + const result = handlebarsConfig.mapStateToProps!( + buildState({}), + {} as ControlState, + ); + expect(result.value).toBeUndefined(); +}); + +test('styleTemplate mapStateToProps reads camelCase styleTemplate (MCP and UI charts)', () => { + const result = styleConfig.mapStateToProps!( + buildState({ styleTemplate: CUSTOM_CSS } as Partial), + {} as ControlState, + ); + expect(result.value).toBe(CUSTOM_CSS); + expect(result.htmlSanitization).toBe(true); +}); + +test('styleTemplate mapStateToProps prefers camelCase when both keys present', () => { + const result = styleConfig.mapStateToProps!( + buildState({ + style_template: 'stale', + styleTemplate: 'latest', + } as Partial), + {} as ControlState, + ); + expect(result.value).toBe('latest'); +}); + +test('styleTemplate mapStateToProps reads snake_case style_template as fallback', () => { + const result = styleConfig.mapStateToProps!( + buildState({ style_template: CUSTOM_CSS } as Partial), + {} as ControlState, + ); + 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, + {} as ControlState, + ); + expect(result.htmlSanitization).toBe(false); +});