You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/05 17:33:00 UTC

(superset) branch master updated: fix(dashboard): sort Dynamic Group By display values alphabetically (#40220)

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

rusackas 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 81192048572 fix(dashboard): sort Dynamic Group By display values alphabetically (#40220)
81192048572 is described below

commit 8119204857201298f5328f157cbd79a815b3aa7e
Author: jesperct <jm...@gmail.com>
AuthorDate: Fri Jun 5 14:32:54 2026 -0300

    fix(dashboard): sort Dynamic Group By display values alphabetically (#40220)
---
 UPDATING.md                                        |  4 ++
 .../superset-ui-core/src/query/types/Dashboard.ts  |  1 -
 .../DynamicGroupBy/DynamicGroupByPlugin.test.tsx   | 81 ++++++++++++++++++++++
 .../DynamicGroupBy/DynamicGroupByPlugin.tsx        | 19 ++++-
 .../components/DynamicGroupBy/types.ts             |  1 -
 .../FilterControls/GroupByFilterCard.test.tsx      | 41 +++++++++++
 .../FilterBar/FilterControls/GroupByFilterCard.tsx | 36 ++++++----
 .../FiltersConfigForm/FiltersConfigForm.tsx        |  2 +-
 .../util/charts/getFormDataWithExtraFilters.ts     | 12 ----
 .../util/migrateChartCustomization.test.ts         | 24 ++++++-
 .../dashboard/util/migrateChartCustomization.ts    |  7 +-
 11 files changed, 195 insertions(+), 33 deletions(-)

diff --git a/UPDATING.md b/UPDATING.md
index 612a6938d6b..ce90df733c2 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -49,6 +49,10 @@ Two opt-in static gates control which extensions are allowed to load:
 
 Both default to empty (no behavior change). They apply to both the `LOCAL_EXTENSIONS` and `EXTENSIONS_PATH` load paths.
 
+### Dynamic Group By respects the sort toggle for display values
+
+The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (A–Z), descending (Z–A), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of A–Z; open the customization and enable the toggle to restore alphabetical ordering.
+
 ### Granular Export Controls
 
 A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
index 3fdf674f4f5..c8c9c4a3775 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
@@ -102,7 +102,6 @@ export type ChartCustomization = {
   defaultDataMask: DataMask;
   controlValues: {
     sortAscending?: boolean;
-    sortMetric?: string;
     [key: string]: any;
   };
   description?: string;
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx
new file mode 100644
index 00000000000..175ec9ac3e8
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx
@@ -0,0 +1,81 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { supersetTheme } from '@apache-superset/core/theme';
+import { render, screen, userEvent } from 'spec/helpers/testing-library';
+import PluginFilterDynamicGroupBy from './DynamicGroupByPlugin';
+import transformProps from './transformProps';
+import { PluginFilterGroupByProps } from './types';
+
+const baseProps = {
+  width: 220,
+  height: 20,
+  hooks: {},
+  filterState: { value: [] },
+  queriesData: [
+    {
+      data: [
+        { column_name: 'banana' },
+        { column_name: 'apple' },
+        { column_name: 'cherry' },
+      ],
+    },
+  ],
+  formData: {
+    datasource: '1__table',
+    vizType: 'filter_groupby',
+    nativeFilterId: 'test-filter',
+    defaultValue: [],
+    inputRef: { current: null },
+  },
+};
+
+const renderPlugin = (sortAscending?: boolean) => {
+  const chartProps = new ChartProps({
+    ...baseProps,
+    formData: { ...baseProps.formData, sortAscending },
+    theme: supersetTheme,
+  });
+  return render(
+    <PluginFilterDynamicGroupBy
+      {...(transformProps(chartProps) as unknown as PluginFilterGroupByProps)}
+    />,
+  );
+};
+
+const getOpenedOptionOrder = async () => {
+  userEvent.click(screen.getAllByRole('combobox')[0]);
+  const options = await screen.findAllByRole('option');
+  return options.map(option => option.textContent);
+};
+
+test('sorts display control values A-Z when sortAscending is true', async () => {
+  renderPlugin(true);
+  expect(await getOpenedOptionOrder()).toEqual(['apple', 'banana', 'cherry']);
+});
+
+test('sorts display control values Z-A when sortAscending is false', async () => {
+  renderPlugin(false);
+  expect(await getOpenedOptionOrder()).toEqual(['cherry', 'banana', 'apple']);
+});
+
+test('preserves source order when sorting is disabled', async () => {
+  renderPlugin(undefined);
+  expect(await getOpenedOptionOrder()).toEqual(['banana', 'apple', 'cherry']);
+});
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
index 4919246dac0..cccbcf7af18 100644
--- a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
@@ -18,13 +18,15 @@
  */
 import { t, tn } from '@apache-superset/core/translation';
 import { ensureIsArray, ExtraFormData } from '@superset-ui/core';
-import { useEffect, useState, useMemo } from 'react';
+import { useCallback, useEffect, useState, useMemo } from 'react';
 import {
   FormItem,
   type FormItemProps,
+  LabeledValue,
   Select,
   type SelectValue,
 } from '@superset-ui/core/components';
+import { propertyComparator } from '@superset-ui/core/components/Select/utils';
 import { FilterPluginStyle, StatusMessage } from '../common';
 import { PluginFilterGroupByProps, ColumnOption, ColumnData } from './types';
 
@@ -116,6 +118,20 @@ export default function PluginFilterDynamicGroupBy(
     [data],
   );
 
+  const sortComparator = useCallback(
+    (a: LabeledValue, b: LabeledValue) => {
+      if (formData.sortAscending === undefined) {
+        return 0;
+      }
+      const labelComparator = propertyComparator('label');
+      if (formData.sortAscending) {
+        return labelComparator(a, b);
+      }
+      return labelComparator(b, a);
+    },
+    [formData.sortAscending],
+  );
+
   return (
     <FilterPluginStyle height={height} width={width}>
       <FormItem validateStatus={filterState.validateStatus} {...formItemData}>
@@ -132,6 +148,7 @@ export default function PluginFilterDynamicGroupBy(
             ref={inputRef}
             options={options}
             onOpenChange={setFilterActive}
+            sortComparator={sortComparator}
           />
         </div>
       </FormItem>
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
index 46374249da7..faa1665e347 100644
--- a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
@@ -76,7 +76,6 @@ export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = {
   dataset: null,
   column: null,
   sortFilter: false,
-  sortAscending: true,
   canSelectMultiple: true,
   defaultValue: null,
 };
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
new file mode 100644
index 00000000000..ba128d56dc0
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
@@ -0,0 +1,41 @@
+/**
+ * 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 { LabeledValue } from '@superset-ui/core/components';
+import { createLabelSortComparator } from './GroupByFilterCard';
+
+const apple: LabeledValue = { value: 'a', label: 'Apple' };
+const banana: LabeledValue = { value: 'b', label: 'Banana' };
+
+test('sorts display values A-Z when sortAscending is true', () => {
+  const compare = createLabelSortComparator(true);
+  expect(compare(apple, banana)).toBeLessThan(0);
+  expect(compare(banana, apple)).toBeGreaterThan(0);
+});
+
+test('sorts display values Z-A when sortAscending is false', () => {
+  const compare = createLabelSortComparator(false);
+  expect(compare(apple, banana)).toBeGreaterThan(0);
+  expect(compare(banana, apple)).toBeLessThan(0);
+});
+
+test('preserves source order when sortAscending is unset', () => {
+  const compare = createLabelSortComparator(undefined);
+  expect(compare(apple, banana)).toBe(0);
+  expect(compare(banana, apple)).toBe(0);
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
index 98cc75e53ea..524beddebba 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
@@ -37,12 +37,14 @@ import {
 import {
   Typography,
   Select,
+  type LabeledValue,
   Popover,
   Loading,
   Icons,
   Tooltip,
   FormItem,
 } from '@superset-ui/core/components';
+import { propertyComparator } from '@superset-ui/core/components/Select/utils';
 import { useDispatch, useSelector } from 'react-redux';
 import { RootState } from 'src/dashboard/types';
 import { setPendingChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
@@ -210,6 +212,18 @@ const DescriptionTooltip = ({ description }: { description: string }) => (
   </ToolTipContainer>
 );
 
+// Sort display values by label: ascending when sortAscending is true, descending
+// when false, and source order (no sort) when it is unset.
+export const createLabelSortComparator =
+  (sortAscending?: boolean) =>
+  (a: LabeledValue, b: LabeledValue): number => {
+    if (sortAscending === undefined) {
+      return 0;
+    }
+    const labelComparator = propertyComparator('label');
+    return sortAscending ? labelComparator(a, b) : labelComparator(b, a);
+  };
+
 const GroupByFilterCardContent: FC<{
   customizationItem: ChartCustomization;
   hidePopover: () => void;
@@ -230,14 +244,6 @@ const GroupByFilterCardContent: FC<{
     return t('None');
   }, [dataset, datasetName]);
 
-  const aggregationDisplay = useMemo(() => {
-    const sortMetric = customizationItem.controlValues?.sortMetric;
-    if (sortMetric) {
-      return sortMetric.toUpperCase();
-    }
-    return t('None');
-  }, [customizationItem.controlValues?.sortMetric]);
-
   return (
     <div>
       <Row
@@ -271,11 +277,6 @@ const GroupByFilterCardContent: FC<{
           {typeof datasetLabel === 'string' ? datasetLabel : t('Dataset')}
         </RowValue>
       </Row>
-
-      <Row>
-        <RowLabel>{t('Aggregation')}</RowLabel>
-        <RowValue>{aggregationDisplay}</RowValue>
-      </Row>
     </div>
   );
 };
@@ -342,6 +343,13 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
   const canSelectMultiple =
     customizationItem.controlValues?.canSelectMultiple ?? true;
 
+  const sortAscending = customizationItem.controlValues?.sortAscending;
+
+  const sortComparator = useMemo(
+    () => createLabelSortComparator(sortAscending),
+    [sortAscending],
+  );
+
   const columnDisplayName = useMemo(() => {
     if (customizationItem.name) {
       return customizationItem.name;
@@ -594,6 +602,7 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
                   .toLowerCase()
                   .includes(input.toLowerCase())
               }
+              sortComparator={sortComparator}
               getPopupContainer={triggerNode => triggerNode.parentNode}
               oneLine={isHorizontalLayout}
               className="select-container"
@@ -623,6 +632,7 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
                 .toLowerCase()
                 .includes(input.toLowerCase())
             }
+            sortComparator={sortComparator}
             loading={loading}
           />
         </div>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 3d28036785e..c59fec2483b 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -1418,7 +1418,7 @@ const FiltersConfigForm = (
                                           }}
                                         />
                                       </StyledRowFormItem>
-                                      {hasMetrics && (
+                                      {hasMetrics && !isChartCustomization && (
                                         <StyledRowSubFormItem
                                           expanded={expanded}
                                           name={[
diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
index ed383210314..e0cb579aa34 100644
--- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
+++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts
@@ -287,7 +287,6 @@ function processGroupByCustomizations(
   >,
 ): {
   groupby?: string[];
-  order_by_cols?: string[];
   x_axis?: string;
   series?: string;
   columns?: string[];
@@ -332,7 +331,6 @@ function processGroupByCustomizations(
   const xAxisColumn = chart.form_data?.x_axis;
 
   const groupByColumns: string[] = [];
-  let orderByConfig: string[] | undefined;
   let heatmapColumnAdded = false;
 
   matchingCustomizations.forEach(item => {
@@ -380,12 +378,6 @@ function processGroupByCustomizations(
         }
       });
     }
-
-    const sortMetric = item.controlValues?.sortMetric;
-    const sortAscending = item.controlValues?.sortAscending;
-    if (sortMetric) {
-      orderByConfig = [JSON.stringify([sortMetric, !sortAscending])];
-    }
   });
 
   const groupByFormData = applyChartSpecificGroupBy(
@@ -395,10 +387,6 @@ function processGroupByCustomizations(
     xAxisColumn,
   );
 
-  if (orderByConfig) {
-    groupByFormData.order_by_cols = orderByConfig;
-  }
-
   return groupByFormData;
 }
 
diff --git a/superset-frontend/src/dashboard/util/migrateChartCustomization.test.ts b/superset-frontend/src/dashboard/util/migrateChartCustomization.test.ts
index 3932a417544..282529e40fa 100644
--- a/superset-frontend/src/dashboard/util/migrateChartCustomization.test.ts
+++ b/superset-frontend/src/dashboard/util/migrateChartCustomization.test.ts
@@ -99,9 +99,9 @@ test('migrateChartCustomization handles basic legacy format', () => {
   expect(result.cascadeParentIds).toEqual([]);
   expect(result.controlValues).toEqual({
     sortAscending: true,
-    sortMetric: 'count',
     canSelectMultiple: true,
   });
+  expect(result.controlValues).not.toHaveProperty('sortMetric');
 });
 
 test('migrateChartCustomization handles dataset as string', () => {
@@ -301,11 +301,31 @@ test('migrateChartCustomization merges controlValues', () => {
 
   expect(result.controlValues).toEqual({
     sortAscending: false,
-    sortMetric: undefined,
     canSelectMultiple: undefined,
     enableEmptyFilter: true,
     customSetting: 'value',
   });
+  expect(result.controlValues).not.toHaveProperty('sortMetric');
+});
+
+test('migrateChartCustomization drops sortMetric nested in controlValues', () => {
+  const legacy = {
+    id: 'CUSTOMIZATION-1',
+    customization: {
+      name: 'Test',
+      dataset: 1,
+      column: 'country',
+      controlValues: {
+        sortMetric: 'count',
+        enableEmptyFilter: true,
+      },
+    },
+  };
+
+  const result = migrateChartCustomization(legacy);
+
+  expect(result.controlValues).not.toHaveProperty('sortMetric');
+  expect(result.controlValues.enableEmptyFilter).toBe(true);
 });
 
 test('migrateChartCustomization preserves removed flag', () => {
diff --git a/superset-frontend/src/dashboard/util/migrateChartCustomization.ts b/superset-frontend/src/dashboard/util/migrateChartCustomization.ts
index 9e05495487a..d4af4b1d460 100644
--- a/superset-frontend/src/dashboard/util/migrateChartCustomization.ts
+++ b/superset-frontend/src/dashboard/util/migrateChartCustomization.ts
@@ -78,12 +78,15 @@ export function migrateChartCustomization(
 
   const controlValues: ChartCustomization['controlValues'] = {
     sortAscending: customization.sortAscending,
-    sortMetric: customization.sortMetric,
     canSelectMultiple: customization.canSelectMultiple,
   };
 
   if (customization.controlValues) {
-    Object.assign(controlValues, customization.controlValues);
+    Object.entries(customization.controlValues).forEach(([key, value]) => {
+      if (key !== 'sortMetric') {
+        controlValues[key] = value;
+      }
+    });
   }
 
   let defaultDataMask = customization.defaultDataMask || {