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

michaelsmolina 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 07cd1d89d0 fix(explore): hide advanced analytics for non temporal 
xaxis (#28312)
07cd1d89d0 is described below

commit 07cd1d89d0c57b2987e1d9aeb23c2aad518a3dc2
Author: JUST.in DO IT <[email protected]>
AuthorDate: Wed May 8 13:40:28 2024 -0700

    fix(explore): hide advanced analytics for non temporal xaxis (#28312)
---
 .../src/sections/advancedAnalytics.tsx             |   3 +-
 .../src/sections/forecastInterval.tsx              |   2 +
 .../src/shared-controls/sharedControls.tsx         |  22 +--
 .../superset-ui-chart-controls/src/types.ts        |   4 +
 .../{index.ts => displayTimeRelatedControls.ts}    |  32 +++--
 .../superset-ui-chart-controls/src/utils/index.ts  |   1 +
 .../test/utils/displayTimeRelatedControls.test.ts  | 118 ++++++++++++++++
 .../src/explore/actions/exploreActions.test.js     |  21 +++
 .../src/explore/actions/exploreActions.ts          |  13 ++
 .../components/ControlPanelsContainer.test.tsx     |  62 ++++++++-
 .../explore/components/ControlPanelsContainer.tsx  | 155 ++++++++++++---------
 .../components/ExploreViewContainer/index.jsx      |   9 +-
 .../StashFormDataContainer.test.tsx                |  57 ++++++++
 .../components/StashFormDataContainer/index.tsx    |  50 +++++++
 .../src/explore/reducers/exploreReducer.js         |  25 ++++
 superset-frontend/src/explore/types.ts             |   1 +
 16 files changed, 475 insertions(+), 100 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
index 326e26fd5d..926488f51e 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
@@ -21,7 +21,7 @@ import { t, RollingType, ComparisonType } from 
'@superset-ui/core';
 
 import { ControlSubSectionHeader } from 
'../components/ControlSubSectionHeader';
 import { ControlPanelSectionConfig } from '../types';
-import { formatSelectOptions } from '../utils';
+import { formatSelectOptions, displayTimeRelatedControls } from '../utils';
 
 export const advancedAnalyticsControls: ControlPanelSectionConfig = {
   label: t('Advanced analytics'),
@@ -31,6 +31,7 @@ export const advancedAnalyticsControls: 
ControlPanelSectionConfig = {
       'that allow for advanced analytical post processing ' +
       'of query results',
   ),
+  visibility: displayTimeRelatedControls,
   controlSetRows: [
     [<ControlSubSectionHeader>{t('Rolling window')}</ControlSubSectionHeader>],
     [
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
index 1dff19b83c..67c64725c0 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
@@ -22,6 +22,7 @@ import {
   t,
 } from '@superset-ui/core';
 import { ControlPanelSectionConfig } from '../types';
+import { displayTimeRelatedControls } from '../utils';
 
 export const FORECAST_DEFAULT_DATA = {
   forecastEnabled: false,
@@ -35,6 +36,7 @@ export const FORECAST_DEFAULT_DATA = {
 export const forecastIntervalControls: ControlPanelSectionConfig = {
   label: t('Predictive Analytics'),
   expanded: false,
+  visibility: displayTimeRelatedControls,
   controlSetRows: [
     [
       {
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index 2be91cf5d4..c01b4052d1 100644
--- 
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -41,8 +41,6 @@ import {
   SequentialScheme,
   legacyValidateInteger,
   ComparisonType,
-  isAdhocColumn,
-  isPhysicalColumn,
   ensureIsArray,
   isDefined,
   NO_TIME_RANGE,
@@ -51,6 +49,7 @@ import {
 
 import {
   formatSelectOptions,
+  displayTimeRelatedControls,
   D3_FORMAT_OPTIONS,
   D3_FORMAT_DOCS,
   D3_TIME_FORMAT_OPTIONS,
@@ -62,7 +61,6 @@ import { DEFAULT_MAX_ROW, TIME_FILTER_LABELS } from 
'../constants';
 import {
   SharedControlConfig,
   Dataset,
-  ColumnMeta,
   ControlState,
   ControlPanelState,
 } from '../types';
@@ -203,23 +201,7 @@ const time_grain_sqla: 
SharedControlConfig<'SelectControl'> = {
   mapStateToProps: ({ datasource }) => ({
     choices: (datasource as Dataset)?.time_grain_sqla || [],
   }),
-  visibility: ({ controls }) => {
-    if (!controls?.x_axis) {
-      return true;
-    }
-
-    const xAxis = controls?.x_axis;
-    const xAxisValue = xAxis?.value;
-    if (isAdhocColumn(xAxisValue)) {
-      return true;
-    }
-    if (isPhysicalColumn(xAxisValue)) {
-      return !!(xAxis?.options ?? []).find(
-        (col: ColumnMeta) => col?.column_name === xAxisValue,
-      )?.is_dttm;
-    }
-    return false;
-  },
+  visibility: displayTimeRelatedControls,
 };
 
 const time_range: SharedControlConfig<'DateFilterControl'> = {
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 3d149b1299..fa8154677d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -376,6 +376,10 @@ export interface ControlPanelSectionConfig {
   expanded?: boolean;
   tabOverride?: TabOverride;
   controlSetRows: ControlSetRow[];
+  visibility?: (
+    props: ControlPanelsContainerProps,
+    controlData: AnyDict,
+  ) => boolean;
 }
 
 export interface StandardizedControls {
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
similarity index 57%
copy from 
superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
copy to 
superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
index 208d708a96..e5e430d158 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
@@ -16,13 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export * from './checkColumnType';
-export * from './selectOptions';
-export * from './D3Formatting';
-export * from './expandControlConfig';
-export * from './getColorFormatters';
-export { default as mainMetric } from './mainMetric';
-export { default as columnChoices } from './columnChoices';
-export * from './defineSavedMetrics';
-export * from './getStandardizedControls';
-export * from './getTemporalColumns';
+import { isAdhocColumn, isPhysicalColumn } from '@superset-ui/core';
+import type { ColumnMeta, ControlPanelsContainerProps } from '../types';
+
+export default function displayTimeRelatedControls({
+  controls,
+}: ControlPanelsContainerProps) {
+  if (!controls?.x_axis) {
+    return true;
+  }
+
+  const xAxis = controls?.x_axis;
+  const xAxisValue = xAxis?.value;
+  if (isAdhocColumn(xAxisValue)) {
+    return true;
+  }
+  if (isPhysicalColumn(xAxisValue)) {
+    return !!(xAxis?.options ?? []).find(
+      (col: ColumnMeta) => col?.column_name === xAxisValue,
+    )?.is_dttm;
+  }
+  return false;
+}
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts 
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
index 208d708a96..fb829ea057 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
@@ -26,3 +26,4 @@ export { default as columnChoices } from './columnChoices';
 export * from './defineSavedMetrics';
 export * from './getStandardizedControls';
 export * from './getTemporalColumns';
+export { default as displayTimeRelatedControls } from 
'./displayTimeRelatedControls';
diff --git 
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
new file mode 100644
index 0000000000..f96049293f
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
@@ -0,0 +1,118 @@
+/**
+ * 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 { displayTimeRelatedControls } from '../../src';
+
+const mockData = {
+  actions: {
+    setDatasource: jest.fn(),
+  },
+  controls: {
+    x_axis: {
+      type: 'SelectControl' as const,
+      value: 'not_temporal',
+      options: [
+        { column_name: 'not_temporal', is_dttm: false },
+        { column_name: 'ds', is_dttm: true },
+      ],
+    },
+  },
+  exportState: {},
+  form_data: {
+    datasource: '22__table',
+    viz_type: 'table',
+  },
+};
+
+test('returns true when no x-axis exists', () => {
+  expect(
+    displayTimeRelatedControls({
+      ...mockData,
+      controls: {
+        control_options: {
+          type: 'SelectControl',
+          value: 'not_temporal',
+          options: [],
+        },
+      },
+    }),
+  ).toBeTruthy();
+});
+
+test('returns false when x-axis value is not temporal', () => {
+  expect(displayTimeRelatedControls(mockData)).toBeFalsy();
+});
+test('returns true when x-axis value is temporal', () => {
+  expect(
+    displayTimeRelatedControls({
+      ...mockData,
+      controls: {
+        x_axis: {
+          ...mockData.controls.x_axis,
+          value: 'ds',
+        },
+      },
+    }),
+  ).toBeTruthy();
+});
+
+test('returns false when x-axis value without options', () => {
+  expect(
+    displayTimeRelatedControls({
+      ...mockData,
+      controls: {
+        x_axis: {
+          type: 'SelectControl' as const,
+          value: 'not_temporal',
+        },
+      },
+    }),
+  ).toBeFalsy();
+});
+
+test('returns true when x-axis is ad-hoc column', () => {
+  expect(
+    displayTimeRelatedControls({
+      ...mockData,
+      controls: {
+        x_axis: {
+          ...mockData.controls.x_axis,
+          value: {
+            sqlExpression: 'ds',
+            label: 'ds',
+            expressionType: 'SQL',
+          },
+        },
+      },
+    }),
+  ).toBeTruthy();
+});
+
+test('returns false when the x-axis is neither an ad-hoc column nor a physical 
column', () => {
+  expect(
+    displayTimeRelatedControls({
+      ...mockData,
+      controls: {
+        x_axis: {
+          ...mockData.controls.x_axis,
+          value: {},
+        },
+      },
+    }),
+  ).toBeFalsy();
+});
diff --git a/superset-frontend/src/explore/actions/exploreActions.test.js 
b/superset-frontend/src/explore/actions/exploreActions.test.js
index 54cf8f16c5..700987a0e5 100644
--- a/superset-frontend/src/explore/actions/exploreActions.test.js
+++ b/superset-frontend/src/explore/actions/exploreActions.test.js
@@ -217,4 +217,25 @@ describe('reducers', () => {
       expectedColumnConfig,
     );
   });
+
+  test('setStashFormData works as expected with fieldNames', () => {
+    const newState = exploreReducer(
+      defaultState,
+      actions.setStashFormData(true, ['y_axis_format']),
+    );
+    expect(newState.hiddenFormData).toEqual({
+      y_axis_format: defaultState.form_data.y_axis_format,
+    });
+    expect(newState.form_data.y_axis_format).toBeFalsy();
+    const updatedState = exploreReducer(
+      newState,
+      actions.setStashFormData(false, ['y_axis_format']),
+    );
+    expect(updatedState.hiddenFormData).toEqual({
+      y_axis_format: defaultState.form_data.y_axis_format,
+    });
+    expect(updatedState.form_data.y_axis_format).toEqual(
+      defaultState.form_data.y_axis_format,
+    );
+  });
 });
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts 
b/superset-frontend/src/explore/actions/exploreActions.ts
index 36300b4a12..da702ac16f 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -152,6 +152,18 @@ export function setForceQuery(force: boolean) {
   };
 }
 
+export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA';
+export function setStashFormData(
+  isHidden: boolean,
+  fieldNames: ReadonlyArray<string>,
+) {
+  return {
+    type: SET_STASH_FORM_DATA,
+    isHidden,
+    fieldNames,
+  };
+}
+
 export const exploreActions = {
   ...toastActions,
   fetchDatasourcesStarted,
@@ -161,6 +173,7 @@ export const exploreActions = {
   saveFaveStar,
   setControlValue,
   setExploreControls,
+  setStashFormData,
   updateChartTitle,
   createNewSlice,
   sliceUpdated,
diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
index 333d3ec799..37bdfb4fc0 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import React from 'react';
+import { useSelector } from 'react-redux';
 import userEvent from '@testing-library/user-event';
 import { render, screen } from 'spec/helpers/testing-library';
 import {
@@ -24,13 +25,22 @@ import {
   getChartControlPanelRegistry,
   t,
 } from '@superset-ui/core';
-import { defaultControls } from 'src/explore/store';
+import { defaultControls, defaultState } from 'src/explore/store';
+import { ExplorePageState } from 'src/explore/types';
 import { getFormDataFromControls } from 'src/explore/controlUtils';
 import {
   ControlPanelsContainer,
   ControlPanelsContainerProps,
 } from 'src/explore/components/ControlPanelsContainer';
 
+const FormDataMock = () => {
+  const formData = useSelector(
+    (state: ExplorePageState) => state.explore.form_data,
+  );
+
+  return <div 
data-test="mock-formdata">{Object.keys(formData).join(':')}</div>;
+};
+
 describe('ControlPanelsContainer', () => {
   beforeAll(() => {
     getChartControlPanelRegistry().registerValue('table', {
@@ -144,4 +154,54 @@ describe('ControlPanelsContainer', () => {
       await screen.findAllByTestId('collapsible-control-panel-header'),
     ).toHaveLength(2);
   });
+
+  test('visibility of panels is correctly applied', async () => {
+    getChartControlPanelRegistry().registerValue('table', {
+      controlPanelSections: [
+        {
+          label: t('Advanced analytics'),
+          description: t('Advanced analytics post processing'),
+          expanded: true,
+          controlSetRows: [['groupby'], ['metrics'], ['percent_metrics']],
+          visibility: () => false,
+        },
+        {
+          label: t('Chart Title'),
+          visibility: () => true,
+          controlSetRows: [['timeseries_limit_metric', 'row_limit']],
+        },
+        {
+          label: t('Chart Options'),
+          controlSetRows: [['include_time', 'order_desc']],
+        },
+      ],
+    });
+    const { getByTestId } = render(
+      <>
+        <ControlPanelsContainer {...getDefaultProps()} />
+        <FormDataMock />
+      </>,
+      {
+        useRedux: true,
+        initialState: { explore: { form_data: defaultState.form_data } },
+      },
+    );
+
+    const disabledSection = screen.queryByRole('button', {
+      name: /advanced analytics/i,
+    });
+    expect(disabledSection).not.toBeInTheDocument();
+    expect(
+      screen.getByRole('button', { name: /chart title/i }),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', { name: /chart options/i }),
+    ).toBeInTheDocument();
+
+    expect(getByTestId('mock-formdata')).not.toHaveTextContent('groupby');
+    expect(getByTestId('mock-formdata')).not.toHaveTextContent('metrics');
+    expect(getByTestId('mock-formdata')).not.toHaveTextContent(
+      'percent_metrics',
+    );
+  });
 });
diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index d47c1abaf8..06a9072aae 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -71,6 +71,7 @@ import { ExploreAlert } from './ExploreAlert';
 import { RunQueryButton } from './RunQueryButton';
 import { Operators } from '../constants';
 import { Clauses } from './controls/FilterControl/types';
+import StashFormDataContainer from './StashFormDataContainer';
 
 const { confirm } = Modal;
 
@@ -521,16 +522,22 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
     }
 
     return (
-      <Control
-        key={`control-${name}`}
-        name={name}
-        label={label}
-        description={description}
-        validationErrors={validationErrors}
-        actions={props.actions}
-        isVisible={isVisible}
-        {...restProps}
-      />
+      <StashFormDataContainer
+        shouldStash={isVisible === false}
+        fieldNames={[name]}
+        key={`control-container-${name}`}
+      >
+        <Control
+          key={`control-${name}`}
+          name={name}
+          label={label}
+          description={description}
+          validationErrors={validationErrors}
+          actions={props.actions}
+          isVisible={isVisible}
+          {...restProps}
+        />
+      </StashFormDataContainer>
     );
   };
 
@@ -543,13 +550,13 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
     section: ExpandedControlPanelSectionConfig,
   ) => {
     const { controls } = props;
-    const { label, description } = section;
+    const { label, description, visibility } = section;
 
     // Section label can be a ReactNode but in some places we want to
     // have a string ID. Using forced type conversion for now,
     // should probably add a `id` field to sections in the future.
     const sectionId = String(label);
-
+    const isVisible = visibility?.call(this, props, controls) !== false;
     const hasErrors = section.controlSetRows.some(rows =>
       rows.some(item => {
         const controlName =
@@ -607,67 +614,85 @@ export const ControlPanelsContainer = (props: 
ControlPanelsContainerProps) => {
     );
 
     return (
-      <Collapse.Panel
-        css={theme => css`
-          margin-bottom: 0;
-          box-shadow: none;
-
-          &:last-child {
-            padding-bottom: ${theme.gridUnit * 16}px;
-            border-bottom: 0;
-          }
+      <>
+        <StashFormDataContainer
+          key={`sectionId-${sectionId}`}
+          shouldStash={!isVisible}
+          fieldNames={section.controlSetRows
+            .flat()
+            .map(item =>
+              item && typeof item === 'object'
+                ? 'name' in item
+                  ? item.name
+                  : ''
+                : String(item || ''),
+            )
+            .filter(Boolean)}
+        />
+        {isVisible && (
+          <Collapse.Panel
+            css={theme => css`
+              margin-bottom: 0;
+              box-shadow: none;
+
+              &:last-child {
+                padding-bottom: ${theme.gridUnit * 16}px;
+                border-bottom: 0;
+              }
 
-          .panel-body {
-            margin-left: ${theme.gridUnit * 4}px;
-            padding-bottom: 0;
-          }
+              .panel-body {
+                margin-left: ${theme.gridUnit * 4}px;
+                padding-bottom: 0;
+              }
 
-          span.label {
-            display: inline-block;
-          }
-          ${!section.label &&
-          `
+              span.label {
+                display: inline-block;
+              }
+              ${!section.label &&
+              `
             .ant-collapse-header {
               display: none;
             }
           `}
-        `}
-        header={<PanelHeader />}
-        key={sectionId}
-      >
-        {section.controlSetRows.map((controlSets, i) => {
-          const renderedControls = controlSets
-            .map(controlItem => {
-              if (!controlItem) {
-                // When the item is invalid
+            `}
+            header={<PanelHeader />}
+            key={sectionId}
+          >
+            {section.controlSetRows.map((controlSets, i) => {
+              const renderedControls = controlSets
+                .map(controlItem => {
+                  if (!controlItem) {
+                    // When the item is invalid
+                    return null;
+                  }
+                  if (React.isValidElement(controlItem)) {
+                    // When the item is a React element
+                    return controlItem;
+                  }
+                  if (
+                    controlItem.name &&
+                    controlItem.config &&
+                    controlItem.name !== 'datasource'
+                  ) {
+                    return renderControl(controlItem);
+                  }
+                  return null;
+                })
+                .filter(x => x !== null);
+              // don't show the row if it is empty
+              if (renderedControls.length === 0) {
                 return null;
               }
-              if (React.isValidElement(controlItem)) {
-                // When the item is a React element
-                return controlItem;
-              }
-              if (
-                controlItem.name &&
-                controlItem.config &&
-                controlItem.name !== 'datasource'
-              ) {
-                return renderControl(controlItem);
-              }
-              return null;
-            })
-            .filter(x => x !== null);
-          // don't show the row if it is empty
-          if (renderedControls.length === 0) {
-            return null;
-          }
-          return (
-            <ControlRow
-              key={`controlsetrow-${i}`}
-              controls={renderedControls}
-            />
-          );
-        })}
-      </Collapse.Panel>
+              return (
+                <ControlRow
+                  key={`controlsetrow-${i}`}
+                  controls={renderedControls}
+                />
+              );
+            })}
+          </Collapse.Panel>
+        )}
+      </>
     );
   };
 
diff --git 
a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx 
b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index 1aeb45cb15..fe5d277242 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -31,7 +31,7 @@ import {
   useComponentDidMount,
   usePrevious,
 } from '@superset-ui/core';
-import { debounce, pick } from 'lodash';
+import { debounce, omit, pick } from 'lodash';
 import { Resizable } from 're-resizable';
 import { usePluginContext } from 'src/components/DynamicPlugins';
 import { Global } from '@emotion/react';
@@ -715,8 +715,11 @@ function mapStateToProps(state) {
     user,
     saveModal,
   } = state;
-  const { controls, slice, datasource, metadata } = explore;
-  const form_data = getFormDataFromControls(controls);
+  const { controls, slice, datasource, metadata, hiddenFormData } = explore;
+  const form_data = omit(
+    getFormDataFromControls(controls),
+    Object.keys(hiddenFormData ?? {}),
+  );
   const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved 
chart
   form_data.extra_form_data = mergeExtraFormData(
     { ...form_data.extra_form_data },
diff --git 
a/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
 
b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
new file mode 100644
index 0000000000..1316411945
--- /dev/null
+++ 
b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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 React from 'react';
+import { defaultState } from 'src/explore/store';
+import { render } from 'spec/helpers/testing-library';
+import { useSelector } from 'react-redux';
+import { ExplorePageState } from 'src/explore/types';
+import StashFormDataContainer from '.';
+
+const FormDataMock = () => {
+  const formData = useSelector(
+    (state: ExplorePageState) => state.explore.form_data,
+  );
+
+  return <div>{Object.keys(formData).join(':')}</div>;
+};
+
+test('should stash form data from fieldNames', () => {
+  const { rerender, container } = render(
+    <StashFormDataContainer
+      shouldStash={false}
+      fieldNames={['granularity_sqla']}
+    >
+      <FormDataMock />
+    </StashFormDataContainer>,
+    {
+      useRedux: true,
+      initialState: { explore: { form_data: defaultState.form_data } },
+    },
+  );
+  expect(container.querySelector('div')).toHaveTextContent('granularity_sqla');
+
+  rerender(
+    <StashFormDataContainer shouldStash fieldNames={['granularity_sqla']}>
+      <FormDataMock />
+    </StashFormDataContainer>,
+  );
+  expect(container.querySelector('div')).not.toHaveTextContent(
+    'granularity_sqla',
+  );
+});
diff --git 
a/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx 
b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx
new file mode 100644
index 0000000000..9684ddf774
--- /dev/null
+++ b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx
@@ -0,0 +1,50 @@
+/**
+ * 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 React, { useEffect, useRef } from 'react';
+import { useDispatch } from 'react-redux';
+import { setStashFormData } from 'src/explore/actions/exploreActions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+
+type Props = {
+  shouldStash: boolean;
+  fieldNames: ReadonlyArray<string>;
+};
+
+const StashFormDataContainer: React.FC<Props> = ({
+  shouldStash,
+  fieldNames,
+  children,
+}) => {
+  const dispatch = useDispatch();
+  const isMounted = useRef(false);
+  const onVisibleUpdate = useEffectEvent((shouldStash: boolean) =>
+    dispatch(setStashFormData(shouldStash, fieldNames)),
+  );
+  useEffect(() => {
+    if (!isMounted.current && !shouldStash) {
+      isMounted.current = true;
+    } else {
+      onVisibleUpdate(shouldStash);
+    }
+  }, [shouldStash, onVisibleUpdate]);
+
+  return <>{children}</>;
+};
+
+export default StashFormDataContainer;
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js 
b/superset-frontend/src/explore/reducers/exploreReducer.js
index 1797c57637..9eddd2f678 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -18,6 +18,7 @@
  */
 /* eslint camelcase: 0 */
 import { ensureIsArray } from '@superset-ui/core';
+import { omit, pick } from 'lodash';
 import { DYNAMIC_PLUGIN_CONTROLS_READY } from 
'src/components/Chart/chartAction';
 import { getControlsState } from 'src/explore/store';
 import {
@@ -245,6 +246,30 @@ export default function exploreReducer(state = {}, action) 
{
         can_overwrite: action.can_overwrite,
       };
     },
+    [actions.SET_STASH_FORM_DATA]() {
+      const { form_data, hiddenFormData } = state;
+      const { fieldNames, isHidden } = action;
+      if (isHidden) {
+        return {
+          ...state,
+          hiddenFormData: {
+            ...hiddenFormData,
+            ...pick(form_data, fieldNames),
+          },
+          form_data: omit(form_data, fieldNames),
+        };
+      }
+
+      const restoredField = pick(hiddenFormData, fieldNames);
+      return {
+        ...state,
+        form_data: {
+          ...form_data,
+          ...restoredField,
+        },
+        hiddenFormData,
+      };
+    },
     [actions.SLICE_UPDATED]() {
       return {
         ...state,
diff --git a/superset-frontend/src/explore/types.ts 
b/superset-frontend/src/explore/types.ts
index ee249e0fc3..51b3013233 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -109,6 +109,7 @@ export interface ExplorePageState {
     datasource: Dataset;
     controls: ControlStateMapping;
     form_data: QueryFormData;
+    hiddenFormData?: Partial<QueryFormData>;
     slice: Slice;
     controlsTransferred: string[];
     standalone: boolean;

Reply via email to