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

amaan pushed a commit to branch fix/ag-grid-column-filters-permalink-persistence
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1a61c14843531002dcf27c75ed2879d050fc8761
Author: amaannawab923 <[email protected]>
AuthorDate: Wed Mar 4 14:46:52 2026 +0530

    fix ag grid table column filters not persisting in explore permalinks
---
 superset-frontend/src/dataMask/reducer.ts          | 20 ++++++-
 .../src/explore/actions/exploreActions.ts          | 13 +++++
 .../src/explore/actions/hydrateExplore.ts          | 14 +++--
 .../explore/components/ExploreChartPanel/index.tsx | 61 +++++++++++++++++++++-
 .../useExploreAdditionalActionsMenu/index.tsx      | 24 +++++++--
 .../src/explore/reducers/exploreReducer.ts         | 33 +++++++++++-
 superset-frontend/src/explore/types.ts             |  5 +-
 superset-frontend/src/pages/Chart/index.tsx        | 16 ++++++
 superset-frontend/src/utils/urlUtils.ts            |  9 +++-
 superset/commands/explore/get.py                   |  7 ++-
 superset/explore/permalink/schemas.py              | 10 ++++
 superset/explore/permalink/types.py                |  1 +
 12 files changed, 198 insertions(+), 15 deletions(-)

diff --git a/superset-frontend/src/dataMask/reducer.ts 
b/superset-frontend/src/dataMask/reducer.ts
index 011cc49440c..90051cbe54a 100644
--- a/superset-frontend/src/dataMask/reducer.ts
+++ b/superset-frontend/src/dataMask/reducer.ts
@@ -36,6 +36,10 @@ import {
   isChartCustomization,
 } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
 import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
+import {
+  HYDRATE_EXPLORE,
+  HydrateExplore,
+} from 'src/explore/actions/hydrateExplore';
 import { SaveFilterChangesType } from 
'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
 import {
   migrateChartCustomizationArray,
@@ -195,7 +199,7 @@ function updateDataMaskForFilterChanges(
 const dataMaskReducer = produce(
   (
     draft: DataMaskStateWithId,
-    action: AnyDataMaskAction | HydrateDashboardAction,
+    action: AnyDataMaskAction | HydrateDashboardAction | HydrateExplore,
   ) => {
     const cleanState: DataMaskStateWithId = {};
     switch (action.type) {
@@ -286,6 +290,20 @@ const dataMaskReducer = produce(
 
         return cleanState;
       }
+      case HYDRATE_EXPLORE: {
+        const hydrateExploreAction = action as HydrateExplore;
+        const loadedDataMask = hydrateExploreAction.data.dataMask;
+        if (loadedDataMask) {
+          Object.entries(loadedDataMask).forEach(([id, mask]) => {
+            draft[id] = {
+              ...getInitialDataMask(id),
+              ...draft[id],
+              ...mask,
+            };
+          });
+        }
+        return draft;
+      }
       case SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE:
         updateDataMaskForFilterChanges(
           action.filterChanges,
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts 
b/superset-frontend/src/explore/actions/exploreActions.ts
index 3855e3b93bf..4c59a6783c3 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -153,6 +153,19 @@ export function setForceQuery(force: boolean) {
   };
 }
 
+export const UPDATE_EXPLORE_CHART_STATE = 'UPDATE_EXPLORE_CHART_STATE';
+export function updateExploreChartState(
+  chartId: number,
+  chartState: Record<string, unknown>,
+) {
+  return {
+    type: UPDATE_EXPLORE_CHART_STATE,
+    chartId,
+    chartState,
+    lastModified: Date.now(),
+  };
+}
+
 export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA';
 export function setStashFormData(
   isHidden: boolean,
diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts 
b/superset-frontend/src/explore/actions/hydrateExplore.ts
index b8e0375a648..3201fd3f8ff 100644
--- a/superset-frontend/src/explore/actions/hydrateExplore.ts
+++ b/superset-frontend/src/explore/actions/hydrateExplore.ts
@@ -28,6 +28,8 @@ import { getControlsState } from 'src/explore/store';
 import { Dispatch } from 'redux';
 import {
   Currency,
+  DataMaskStateWithId,
+  JsonObject,
   ensureIsArray,
   getCategoricalSchemeRegistry,
   getColumnLabel,
@@ -58,7 +60,12 @@ export const hydrateExplore =
     dataset,
     metadata,
     saveAction = null,
-  }: ExplorePageInitialData) =>
+    dataMask,
+    chartStates,
+  }: ExplorePageInitialData & {
+    dataMask?: DataMaskStateWithId;
+    chartStates?: Record<number, JsonObject>;
+  }) =>
   (dispatch: Dispatch, getState: () => ExplorePageState) => {
     const { user, datasources, charts, sliceEntities, common, explore } =
       getState();
@@ -213,12 +220,13 @@ export const hydrateExplore =
           saveModalAlert: null,
           isVisible: false,
         },
-        explore: exploreState,
+        explore: { ...exploreState, chartStates },
+        dataMask,
       },
     });
   };
 
 export type HydrateExplore = {
   type: typeof HYDRATE_EXPLORE;
-  data: ExplorePageState;
+  data: ExplorePageState & { dataMask?: DataMaskStateWithId };
 };
diff --git 
a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx 
b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
index fd29a604383..55bfb3e5e03 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { useState, useEffect, useCallback, useMemo, ReactNode } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
 import Split from 'react-split';
 import { t } from '@apache-superset/core';
 import {
@@ -32,6 +33,11 @@ import {
 } from '@superset-ui/core';
 import { css, styled, useTheme, Alert } from '@apache-superset/core/ui';
 import ChartContainer from 'src/components/Chart/ChartContainer';
+import { updateExploreChartState } from 'src/explore/actions/exploreActions';
+import {
+  convertChartStateToOwnState,
+  hasChartStateConverter,
+} from 'src/dashboard/util/chartStateConverter';
 import {
   getItem,
   setItem,
@@ -42,6 +48,7 @@ import { getDatasourceAsSaveableDataset } from 
'src/utils/datasourceUtils';
 import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
 import { getChartRequiredFieldsMissingMessage } from 
'src/utils/getChartRequiredFieldsMissingMessage';
 import type { ChartState, Datasource } from 'src/explore/types';
+import type { ExploreState } from 'src/explore/reducers/exploreReducer';
 import type { Slice } from 'src/types/Chart';
 import LastQueriedLabel from 'src/components/LastQueriedLabel';
 import { DataTablesPane } from '../DataTablesPane';
@@ -125,6 +132,28 @@ const Styles = styled.div<{ showSplite: boolean }>`
   }
 `;
 
+const EMPTY_OBJECT: Record<string, never> = {};
+
+const createOwnStateWithChartState = (
+  baseOwnState: JsonObject,
+  chartState: { state?: JsonObject } | undefined,
+  vizTypeArg: string,
+): JsonObject => {
+  if (!hasChartStateConverter(vizTypeArg)) {
+    return baseOwnState;
+  }
+  const state = chartState?.state;
+  if (!state) {
+    return baseOwnState;
+  }
+  const convertedState = convertChartStateToOwnState(vizTypeArg, state);
+  return {
+    ...baseOwnState,
+    ...convertedState,
+    chartState: state,
+  };
+};
+
 const ExploreChartPanel = ({
   chart,
   slice,
@@ -144,8 +173,34 @@ const ExploreChartPanel = ({
   can_download: canDownload,
 }: ExploreChartPanelProps) => {
   const theme = useTheme();
+  const dispatch = useDispatch();
   const gutterMargin = theme.sizeUnit * GUTTER_SIZE_FACTOR;
   const gutterHeight = theme.sizeUnit * GUTTER_SIZE_FACTOR;
+
+  const chartState = useSelector(
+    (state: { explore?: ExploreState }) =>
+      state.explore?.chartStates?.[chart.id],
+  );
+
+  const handleChartStateChange = useCallback(
+    (chartStateArg: JsonObject) => {
+      if (hasChartStateConverter(vizType)) {
+        dispatch(updateExploreChartState(chart.id, chartStateArg));
+      }
+    },
+    [dispatch, chart.id, vizType],
+  );
+
+  const mergedOwnState = useMemo(
+    () =>
+      createOwnStateWithChartState(
+        ownState || EMPTY_OBJECT,
+        chartState as { state?: JsonObject } | undefined,
+        vizType,
+      ),
+    [ownState, chartState, vizType],
+  );
+
   const {
     ref: chartPanelRef,
     observerRef: resizeObserverRef,
@@ -258,7 +313,7 @@ const ExploreChartPanel = ({
           <ChartContainer
             width={Math.floor(chartPanelWidth)}
             height={chartPanelHeight}
-            ownState={ownState}
+            ownState={mergedOwnState}
             annotationData={chart.annotationData}
             chartId={chart.id}
             triggerRender={triggerRender}
@@ -276,6 +331,7 @@ const ExploreChartPanel = ({
             timeout={timeout}
             triggerQuery={chart.triggerQuery}
             vizType={vizType}
+            onChartStateChange={handleChartStateChange}
             {...(chart.chartAlert && { chartAlert: chart.chartAlert })}
             {...(chart.chartStackTrace && {
               chartStackTrace: chart.chartStackTrace,
@@ -303,8 +359,9 @@ const ExploreChartPanel = ({
       errorMessage,
       force,
       formData,
+      handleChartStateChange,
       onQuery,
-      ownState,
+      mergedOwnState,
       timeout,
       triggerRender,
       vizType,
diff --git 
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
 
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
index db491c134ba..de6f59cd854 100644
--- 
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
+++ 
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
@@ -171,7 +171,9 @@ interface ExploreSlice {
 
 interface ExploreState {
   charts?: Record<number, ChartState>;
-  explore?: ExploreSlice;
+  explore?: ExploreSlice & {
+    chartStates?: Record<number, JsonObject>;
+  };
   common?: {
     conf?: {
       CSV_STREAMING_ROW_THRESHOLD?: number;
@@ -220,6 +222,15 @@ export const useExploreAdditionalActionsMenu = (
       state.common?.conf?.CSV_STREAMING_ROW_THRESHOLD ||
       DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
   );
+  const exploreChartState = useSelector<
+    ExploreState,
+    JsonObject | undefined
+  >(state => {
+    const chartKey = state.explore ? getChartKey(state.explore) : undefined;
+    return chartKey != null
+      ? state.explore?.chartStates?.[chartKey]
+      : undefined;
+  });
 
   // Streaming export state and handlers
   const [isStreamingModalVisible, setIsStreamingModalVisible] = 
useState(false);
@@ -273,6 +284,9 @@ export const useExploreAdditionalActionsMenu = (
     'EXPORT_CURRENT_VIEW' as Behavior,
   );
 
+  const permalinkChartState = (exploreChartState as { state?: JsonObject })
+    ?.state;
+
   const shareByEmail = useCallback(async () => {
     try {
       const subject = t('Superset Chart');
@@ -281,6 +295,8 @@ export const useExploreAdditionalActionsMenu = (
       }
       const result = await getChartPermalink(
         latestQueryFormData as Pick<QueryFormData, 'datasource'>,
+        undefined,
+        permalinkChartState,
       );
       if (!result?.url) {
         throw new Error('Failed to generate permalink');
@@ -292,7 +308,7 @@ export const useExploreAdditionalActionsMenu = (
     } catch (error) {
       addDangerToast(t('Sorry, something went wrong. Try again later.'));
     }
-  }, [addDangerToast, latestQueryFormData]);
+  }, [addDangerToast, latestQueryFormData, permalinkChartState]);
 
   const exportCSV = useCallback(() => {
     if (!canDownloadCSV) return null;
@@ -410,6 +426,8 @@ export const useExploreAdditionalActionsMenu = (
       await copyTextToClipboard(async () => {
         const result = await getChartPermalink(
           latestQueryFormData as Pick<QueryFormData, 'datasource'>,
+          undefined,
+          permalinkChartState,
         );
         if (!result?.url) {
           throw new Error('Failed to generate permalink');
@@ -420,7 +438,7 @@ export const useExploreAdditionalActionsMenu = (
     } catch (error) {
       addDangerToast(t('Sorry, something went wrong. Try again later.'));
     }
-  }, [addDangerToast, addSuccessToast, latestQueryFormData]);
+  }, [addDangerToast, addSuccessToast, latestQueryFormData, 
permalinkChartState]);
 
   // Minimal client-side CSV builder used for "Current View" when pagination 
is disabled
   const downloadClientCSV = (
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.ts 
b/superset-frontend/src/explore/reducers/exploreReducer.ts
index d038dd40003..47b0bf9f01b 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.ts
+++ b/superset-frontend/src/explore/reducers/exploreReducer.ts
@@ -17,7 +17,12 @@
  * under the License.
  */
 /* eslint camelcase: 0 */
-import { ensureIsArray, QueryFormData, JsonValue } from '@superset-ui/core';
+import {
+  ensureIsArray,
+  QueryFormData,
+  JsonValue,
+  JsonObject,
+} from '@superset-ui/core';
 import {
   ControlState,
   ControlStateMapping,
@@ -64,6 +69,7 @@ export interface ExploreState {
     owners?: string[] | null;
   };
   saveAction?: SaveActionType | null;
+  chartStates?: Record<number, JsonObject>;
 }
 
 // Action type definitions
@@ -163,6 +169,13 @@ interface SetForceQueryAction {
   force: boolean;
 }
 
+interface UpdateExploreChartStateAction {
+  type: typeof actions.UPDATE_EXPLORE_CHART_STATE;
+  chartId: number;
+  chartState: Record<string, unknown>;
+  lastModified: number;
+}
+
 type ExploreAction =
   | DynamicPluginControlsReadyAction
   | ToggleFaveStarAction
@@ -181,6 +194,7 @@ type ExploreAction =
   | SetStashFormDataAction
   | SliceUpdatedAction
   | SetForceQueryAction
+  | UpdateExploreChartStateAction
   | HydrateExplore;
 
 // Extended control state for dynamic form controls - uses Record for 
flexibility
@@ -619,10 +633,25 @@ export default function exploreReducer(
         force: typedAction.force,
       };
     },
+    [actions.UPDATE_EXPLORE_CHART_STATE]() {
+      const typedAction = action as UpdateExploreChartStateAction;
+      return {
+        ...state,
+        chartStates: {
+          ...state.chartStates,
+          [typedAction.chartId]: {
+            chartId: typedAction.chartId,
+            state: typedAction.chartState,
+            lastModified: typedAction.lastModified,
+          },
+        },
+      };
+    },
     [HYDRATE_EXPLORE]() {
       const typedAction = action as HydrateExplore;
+      const exploreData = typedAction.data.explore;
       return {
-        ...typedAction.data.explore,
+        ...exploreData,
       } as ExploreState;
     },
   };
diff --git a/superset-frontend/src/explore/types.ts 
b/superset-frontend/src/explore/types.ts
index 6bc6b1bb303..c3f8614ec3e 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -98,7 +98,10 @@ export interface ExplorePageInitialData {
 }
 
 export interface ExploreResponsePayload {
-  result: ExplorePageInitialData & { message: string };
+  result: ExplorePageInitialData & {
+    message: string;
+    chartState?: JsonObject;
+  };
 }
 
 export interface ExplorePageState {
diff --git a/superset-frontend/src/pages/Chart/index.tsx 
b/superset-frontend/src/pages/Chart/index.tsx
index 26926a1c8d6..cd8e3aa4b4c 100644
--- a/superset-frontend/src/pages/Chart/index.tsx
+++ b/superset-frontend/src/pages/Chart/index.tsx
@@ -150,11 +150,27 @@ export default function ExplorePage() {
               )
             : result.form_data;
 
+          let chartStates: Record<number, JsonObject> | undefined;
+          if (result.chartState) {
+            const sliceId =
+              getUrlParam(URL_PARAMS.sliceId) ||
+              (formData as JsonObject).slice_id ||
+              0;
+            chartStates = {
+              [sliceId]: {
+                chartId: sliceId,
+                state: result.chartState,
+                lastModified: Date.now(),
+              },
+            };
+          }
+
           dispatch(
             hydrateExplore({
               ...result,
               form_data: formData,
               saveAction,
+              chartStates,
             }),
           );
         })
diff --git a/superset-frontend/src/utils/urlUtils.ts 
b/superset-frontend/src/utils/urlUtils.ts
index 6c3a51a987a..4b218201144 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -195,11 +195,16 @@ async function resolvePermalinkUrl(
 export async function getChartPermalink(
   formData: Pick<QueryFormData, 'datasource'>,
   excludedUrlParams?: string[],
+  chartState?: JsonObject,
 ): Promise<PermalinkResult> {
-  const result = await getPermalink('/api/v1/explore/permalink', {
+  const payload: JsonObject = {
     formData,
     urlParams: getChartUrlParams(excludedUrlParams),
-  });
+  };
+  if (chartState && Object.keys(chartState).length > 0) {
+    payload.chartState = chartState;
+  }
+  const result = await getPermalink('/api/v1/explore/permalink', payload);
   return resolvePermalinkUrl(result);
 }
 
diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py
index 78142eb5ec1..31b10c42049 100644
--- a/superset/commands/explore/get.py
+++ b/superset/commands/explore/get.py
@@ -62,6 +62,7 @@ class GetExploreCommand(BaseCommand, ABC):
     # pylint: disable=too-many-locals,too-many-branches,too-many-statements
     def run(self) -> Optional[dict[str, Any]]:  # noqa: C901
         initial_form_data = {}
+        permalink_chart_state = None
         if self._permalink_key is not None:
             command = GetExplorePermalinkCommand(self._permalink_key)
             permalink_value = command.run()
@@ -72,6 +73,7 @@ class GetExploreCommand(BaseCommand, ABC):
             url_params = state.get("urlParams")
             if url_params:
                 initial_form_data["url_params"] = dict(url_params)
+            permalink_chart_state = state.get("chartState")
         elif self._form_data_key:
             parameters = FormDataCommandParameters(key=self._form_data_key)
             value = GetFormDataCommand(parameters).run()
@@ -168,13 +170,16 @@ class GetExploreCommand(BaseCommand, ABC):
             if slc.changed_by:
                 metadata["changed_by"] = slc.changed_by.get_full_name()
 
-        return {
+        result: dict[str, Any] = {
             "dataset": sanitize_datasource_data(datasource_data),
             "form_data": form_data,
             "slice": slc.data if slc else None,
             "message": message,
             "metadata": metadata,
         }
+        if permalink_chart_state:
+            result["chartState"] = permalink_chart_state
+        return result
 
     def validate(self) -> None:
         pass
diff --git a/superset/explore/permalink/schemas.py 
b/superset/explore/permalink/schemas.py
index 63466b0c1ba..aa5fef0c46b 100644
--- a/superset/explore/permalink/schemas.py
+++ b/superset/explore/permalink/schemas.py
@@ -41,6 +41,16 @@ class ExplorePermalinkStateSchema(Schema):
         allow_none=True,
         metadata={"description": "URL Parameters"},
     )
+    chartState = fields.Dict(  # noqa: N815
+        required=False,
+        allow_none=True,
+        metadata={
+            "description": (
+                "Chart-level state for stateful tables "
+                "(column filters, sorting, column order)"
+            )
+        },
+    )
 
 
 class ExplorePermalinkSchema(Schema):
diff --git a/superset/explore/permalink/types.py 
b/superset/explore/permalink/types.py
index 7eb4a7cb6b1..8cf8fc930ab 100644
--- a/superset/explore/permalink/types.py
+++ b/superset/explore/permalink/types.py
@@ -20,6 +20,7 @@ from typing import Any, Optional, TypedDict
 class ExplorePermalinkState(TypedDict, total=False):
     formData: dict[str, Any]
     urlParams: Optional[list[tuple[str, str]]]
+    chartState: Optional[dict[str, Any]]
 
 
 class ExplorePermalinkValue(TypedDict):

Reply via email to