This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new e053418c97 fix(Matrixify): Do not clear values when saving (#37090)
e053418c97 is described below

commit e053418c9700dd75629f3d15f8cd5bd972e7ed09
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Tue Jan 13 23:22:51 2026 +0300

    fix(Matrixify): Do not clear values when saving (#37090)
---
 .../components/ControlPanelsContainer.test.tsx     | 101 +++++++++++++++++
 .../explore/components/ControlPanelsContainer.tsx  |  15 +++
 .../controls/MatrixifyDimensionControl.test.tsx    | 119 +++++++++++++++++++++
 .../controls/MatrixifyDimensionControl.tsx         |  36 +++----
 4 files changed, 249 insertions(+), 22 deletions(-)

diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
index 5739496f77..a5b1665056 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
@@ -27,6 +27,8 @@ import { t } from '@apache-superset/core';
 import {
   DatasourceType,
   getChartControlPanelRegistry,
+  isFeatureEnabled,
+  FeatureFlag,
 } from '@superset-ui/core';
 import { defaultControls, defaultState } from 'src/explore/store';
 import { ExplorePageState } from 'src/explore/types';
@@ -36,6 +38,13 @@ import {
   ControlPanelsContainerProps,
 } from 'src/explore/components/ControlPanelsContainer';
 
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+const mockIsFeatureEnabled = isFeatureEnabled as jest.Mock;
+
 const FormDataMock = () => {
   const formData = useSelector(
     (state: ExplorePageState) => state.explore.form_data,
@@ -90,10 +99,14 @@ describe('ControlPanelsContainer', () => {
 
   beforeEach(() => {
     getChartControlPanelRegistry().registerValue('table', defaultTableConfig);
+    jest.clearAllMocks();
+    // Default: feature disabled
+    mockIsFeatureEnabled.mockReturnValue(false);
   });
 
   afterEach(() => {
     getChartControlPanelRegistry().remove('table');
+    jest.clearAllMocks();
   });
 
   afterAll(() => {
@@ -277,4 +290,92 @@ describe('ControlPanelsContainer', () => {
     expect(screen.getByText('Shift start date')).not.toBeVisible();
     expect(screen.getByText('Calculation type')).not.toBeVisible();
   });
+
+  test('should stay on Matrixify tab when matrixify is enabled', async () => {
+    // Enable Matrixify feature flag
+    mockIsFeatureEnabled.mockImplementation(
+      (featureFlag: FeatureFlag) => featureFlag === FeatureFlag.Matrixify,
+    );
+
+    const props = getDefaultProps();
+    props.form_data = {
+      ...props.form_data,
+      matrixify_enable_vertical_layout: true,
+    };
+
+    const { rerender } = render(<ControlPanelsContainer {...props} />, {
+      useRedux: true,
+    });
+
+    // Check that Matrixify tab exists and is active
+    await waitFor(() => {
+      const matrixifyTab = screen.getByRole('tab', { name: /matrixify/i });
+      expect(matrixifyTab).toBeInTheDocument();
+      expect(matrixifyTab).toHaveAttribute('aria-selected', 'true');
+    });
+
+    // Simulate saving with updated dimension values
+    const updatedProps = {
+      ...props,
+      form_data: {
+        ...props.form_data,
+        matrixify_enable_vertical_layout: true,
+        matrixify_dimension_columns: {
+          dimension: 'country',
+          values: ['USA', 'Canada'],
+        },
+      },
+    };
+
+    rerender(<ControlPanelsContainer {...updatedProps} />);
+
+    // Matrixify tab should still be active after rerender
+    await waitFor(() => {
+      const matrixifyTabAfterSave = screen.getByRole('tab', {
+        name: /matrixify/i,
+      });
+      expect(matrixifyTabAfterSave).toHaveAttribute('aria-selected', 'true');
+    });
+  });
+
+  test('should automatically switch to Matrixify tab when matrixify becomes 
enabled', async () => {
+    // Enable Matrixify feature flag
+    mockIsFeatureEnabled.mockImplementation(
+      (featureFlag: FeatureFlag) => featureFlag === FeatureFlag.Matrixify,
+    );
+
+    const props = getDefaultProps();
+
+    const { rerender } = render(<ControlPanelsContainer {...props} />, {
+      useRedux: true,
+    });
+
+    // Initially, Data tab should be active
+    const dataTab = screen.getByRole('tab', { name: /data/i });
+    expect(dataTab).toHaveAttribute('aria-selected', 'true');
+
+    // Enable matrixify
+    const updatedProps = {
+      ...props,
+      form_data: {
+        ...props.form_data,
+        matrixify_enable_horizontal_layout: true,
+      },
+    };
+
+    rerender(<ControlPanelsContainer {...updatedProps} />);
+
+    // Matrixify tab should now be active
+    await waitFor(() => {
+      const matrixifyTab = screen.getByRole('tab', { name: /matrixify/i });
+      expect(matrixifyTab).toBeInTheDocument();
+      expect(matrixifyTab).toHaveAttribute('aria-selected', 'true');
+    });
+
+    // Data tab should no longer be active
+    expect(screen.getByRole('tab', { name: /data/i })).toHaveAttribute(
+      'aria-selected',
+      'false',
+    );
+  });
 });
diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index 057a381cad..6f9f3e4f3f 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -290,6 +290,7 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
   const prevChartStatus = usePrevious(props.chart.chartStatus);
 
   const [showDatasourceAlert, setShowDatasourceAlert] = useState(false);
+  const [activeTabKey, setActiveTabKey] = useState<string>(TABS_KEYS.DATA);
 
   const containerRef = useRef<HTMLDivElement>(null);
 
@@ -795,6 +796,18 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
   const showCustomizeTab = customizeSections.length > 0;
   const showMatrixifyTab = isFeatureEnabled(FeatureFlag.Matrixify);
 
+  // Check if matrixify is enabled in form_data
+  const matrixifyIsEnabled =
+    form_data.matrixify_enable_vertical_layout ||
+    form_data.matrixify_enable_horizontal_layout;
+
+  // Auto-switch to Matrixify tab when it's enabled
+  useEffect(() => {
+    if (showMatrixifyTab && matrixifyIsEnabled) {
+      setActiveTabKey(TABS_KEYS.MATRIXIFY);
+    }
+  }, [showMatrixifyTab, matrixifyIsEnabled]);
+
   // Check if matrixify sections have validation errors
   const matrixifyHasErrors = useMemo(() => {
     if (!showMatrixifyTab) return false;
@@ -882,6 +895,8 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
           id="controlSections"
           data-test="control-tabs"
           allowOverflow={false}
+          activeKey={activeTabKey}
+          onChange={(key: string) => setActiveTabKey(key)}
           items={[
             {
               key: TABS_KEYS.DATA,
diff --git 
a/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.test.tsx
 
b/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.test.tsx
index 0acf0fd89f..961ea5e729 100644
--- 
a/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.test.tsx
+++ 
b/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.test.tsx
@@ -430,3 +430,122 @@ test('should clear values when switching from topn to 
members mode', async () =>
     });
   });
 });
+
+test('should preserve dimension values when rerendering with same mode', async 
() => {
+  // Regression test for bug where values disappear on save
+  const onChange = jest.fn();
+  const value: MatrixifyDimensionControlValue = {
+    dimension: 'country',
+    values: ['USA', 'Australia'],
+  };
+
+  (SupersetClient.get as jest.Mock).mockResolvedValue({
+    json: { result: ['USA', 'Canada', 'Australia', 'Mexico'] },
+  });
+
+  const { rerender } = render(
+    <MatrixifyDimensionControl
+      {...defaultProps}
+      value={value}
+      onChange={onChange}
+      selectionMode="members"
+    />,
+  );
+
+  // Simulate a rerender that would happen during save
+  rerender(
+    <MatrixifyDimensionControl
+      {...defaultProps}
+      value={value}
+      onChange={onChange}
+      selectionMode="members"
+    />,
+  );
+
+  // Values should not be cleared when mode hasn't changed
+  expect(onChange).not.toHaveBeenCalledWith({
+    dimension: 'country',
+    values: [],
+    topNValues: [],
+  });
+
+  // The values should still be present in the UI
+  expect(
+    screen.getByRole('combobox', { name: 'Select dimension values' }),
+  ).toBeInTheDocument();
+});
+
+test('should not clear values on initial render with members mode', async () 
=> {
+  // Regression test: ensure values are not cleared on initial mount
+  const onChange = jest.fn();
+  const value: MatrixifyDimensionControlValue = {
+    dimension: 'country',
+    values: ['USA', 'Australia'],
+  };
+
+  (SupersetClient.get as jest.Mock).mockResolvedValue({
+    json: { result: ['USA', 'Canada', 'Australia', 'Mexico'] },
+  });
+
+  render(
+    <MatrixifyDimensionControl
+      {...defaultProps}
+      value={value}
+      onChange={onChange}
+      selectionMode="members"
+    />,
+  );
+
+  // onChange should not be called to clear values on initial render
+  expect(onChange).not.toHaveBeenCalledWith({
+    dimension: 'country',
+    values: [],
+    topNValues: [],
+  });
+
+  // The values selector should be visible
+  expect(
+    screen.getByRole('combobox', { name: 'Select dimension values' }),
+  ).toBeInTheDocument();
+});
+
+test('should preserve values when other props change but mode stays the same', 
async () => {
+  // Regression test for save scenario where form_data updates trigger 
rerenders
+  const onChange = jest.fn();
+  const value: MatrixifyDimensionControlValue = {
+    dimension: 'country',
+    values: ['USA', 'Australia'],
+  };
+
+  (SupersetClient.get as jest.Mock).mockResolvedValue({
+    json: { result: ['USA', 'Canada', 'Australia', 'Mexico'] },
+  });
+
+  const { rerender } = render(
+    <MatrixifyDimensionControl
+      {...defaultProps}
+      value={value}
+      onChange={onChange}
+      selectionMode="members"
+      formData={{ some_field: 'initial' }}
+    />,
+  );
+
+  // Simulate form_data change (like what happens during save)
+  rerender(
+    <MatrixifyDimensionControl
+      {...defaultProps}
+      value={value}
+      onChange={onChange}
+      selectionMode="members"
+      formData={{ some_field: 'updated', another_field: 'new' }}
+    />,
+  );
+
+  // Values should not be cleared when only form_data changes
+  expect(onChange).not.toHaveBeenCalledWith({
+    dimension: 'country',
+    values: [],
+    topNValues: [],
+  });
+});
diff --git 
a/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.tsx
 
b/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.tsx
index 87a1efe291..35face1e70 100644
--- 
a/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.tsx
+++ 
b/superset-frontend/src/explore/components/controls/MatrixifyDimensionControl.tsx
@@ -82,17 +82,21 @@ export default function MatrixifyDimensionControl(
 
   // Reset values when selection mode changes
   useEffect(() => {
-    if (prevSelectionMode.current !== selectionMode) {
-      prevSelectionMode.current = selectionMode;
-      if (value?.values && value.values.length > 0) {
-        onChange({
-          dimension: value.dimension,
-          values: [],
-          topNValues: [],
-        });
-      }
+    // Only clear values when actually switching between modes, not on initial 
load or re-render
+    if (
+      prevSelectionMode.current !== selectionMode &&
+      prevSelectionMode.current !== undefined &&
+      value?.dimension
+    ) {
+      // Clear values when switching between members and topn modes
+      onChange({
+        dimension: value.dimension,
+        values: [],
+        topNValues: [],
+      });
     }
-  }, [selectionMode, value]);
+    prevSelectionMode.current = selectionMode;
+  }, [selectionMode]);
 
   // Initialize dimension options from datasource
   useEffect(() => {
@@ -175,18 +179,6 @@ export default function MatrixifyDimensionControl(
   // Load TopN values when in TopN mode
   useEffect(() => {
     if (!value?.dimension || !datasource || selectionMode !== 'topn') {
-      // Clear values when switching away from topn mode
-      if (
-        selectionMode !== 'topn' &&
-        value?.values &&
-        value.values.length > 0
-      ) {
-        onChange({
-          dimension: value.dimension,
-          values: [],
-          topNValues: [],
-        });
-      }
       return undefined;
     }
 

Reply via email to