You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/10/09 16:46:09 UTC
(superset) branch master updated: feat(dashboard): add column allowlist to Group By native filter (#43736)
This is an automated email from the ASF dual-hosted git repository.
eschutho 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 ffc81d92a2c feat(dashboard): add column allowlist to Group By native filter (#43736)
ffc81d92a2c is described below
commit ffc81d92a2c702b20a6410471d18001640df2f2a
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Fri Oct 9 09:45:58 2026 -0700
feat(dashboard): add column allowlist to Group By native filter (#43736)
Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
---
.../DynamicGroupBy/DynamicGroupByPlugin.test.tsx | 56 +++
.../DynamicGroupBy/DynamicGroupByPlugin.tsx | 52 ++-
.../DynamicGroupBy/columnAllowlist.test.ts | 67 ++++
.../components/DynamicGroupBy/columnAllowlist.ts | 105 +++++
.../components/DynamicGroupBy/types.ts | 5 +
.../FilterControls/GroupByFilterCard.test.tsx | 159 ++++++++
.../FilterBar/FilterControls/GroupByFilterCard.tsx | 41 +-
.../FiltersConfigForm/ColumnSelect.test.tsx | 39 ++
.../FiltersConfigForm/ColumnSelect.tsx | 99 ++++-
.../FiltersConfigForm/FiltersConfigForm.tsx | 197 +++++++++-
.../GroupByColumnAllowlist.test.tsx | 429 +++++++++++++++++++++
.../transformers/customizationTransformer.test.ts | 109 ++++++
.../transformers/customizationTransformer.ts | 59 ++-
.../nativeFilters/FiltersConfigModal/types.ts | 4 +
superset/translations/ar/LC_MESSAGES/messages.po | 23 ++
superset/translations/ca/LC_MESSAGES/messages.po | 23 ++
superset/translations/cs/LC_MESSAGES/messages.po | 23 ++
superset/translations/de/LC_MESSAGES/messages.po | 23 ++
superset/translations/en/LC_MESSAGES/messages.po | 23 ++
superset/translations/es/LC_MESSAGES/messages.po | 23 ++
superset/translations/fa/LC_MESSAGES/messages.po | 23 ++
superset/translations/fi/LC_MESSAGES/messages.po | 23 ++
superset/translations/fr/LC_MESSAGES/messages.po | 23 ++
superset/translations/it/LC_MESSAGES/messages.po | 23 ++
superset/translations/ja/LC_MESSAGES/messages.po | 23 ++
superset/translations/ko/LC_MESSAGES/messages.po | 23 ++
superset/translations/lv/LC_MESSAGES/messages.po | 23 ++
superset/translations/messages.pot | 19 +
superset/translations/mi/LC_MESSAGES/messages.po | 23 ++
superset/translations/nl/LC_MESSAGES/messages.po | 23 ++
superset/translations/pl/LC_MESSAGES/messages.po | 23 ++
superset/translations/pt/LC_MESSAGES/messages.po | 23 ++
.../translations/pt_BR/LC_MESSAGES/messages.po | 23 ++
superset/translations/ro/LC_MESSAGES/messages.po | 23 ++
superset/translations/ru/LC_MESSAGES/messages.po | 23 ++
superset/translations/sk/LC_MESSAGES/messages.po | 23 ++
superset/translations/sl/LC_MESSAGES/messages.po | 23 ++
superset/translations/sr/LC_MESSAGES/messages.po | 23 ++
.../translations/sr_Latn/LC_MESSAGES/messages.po | 23 ++
superset/translations/ta/LC_MESSAGES/messages.po | 23 ++
superset/translations/th/LC_MESSAGES/messages.po | 23 ++
superset/translations/tr/LC_MESSAGES/messages.po | 23 ++
superset/translations/uk/LC_MESSAGES/messages.po | 23 ++
superset/translations/zh/LC_MESSAGES/messages.po | 23 ++
.../translations/zh_TW/LC_MESSAGES/messages.po | 23 ++
45 files changed, 2093 insertions(+), 37 deletions(-)
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx
index d1eb0955cbd..862ba9be769 100644
--- a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.test.tsx
@@ -79,3 +79,59 @@ test('preserves source order when sorting is disabled', async () => {
renderPlugin(undefined);
expect(await getOpenedOptionOrder()).toEqual(['banana', 'apple', 'cherry']);
});
+
+const renderDefaultPicker = ({
+ formData = {},
+ filterState = { value: [] as string[] },
+}: {
+ formData?: Record<string, unknown>;
+ filterState?: { value: string[] };
+}) => {
+ const setDataMask = jest.fn();
+ const chartProps = new ChartProps({
+ ...baseProps,
+ filterState,
+ hooks: { setDataMask },
+ formData: { ...baseProps.formData, ...formData },
+ theme: supersetTheme,
+ });
+ render(
+ <PluginFilterDynamicGroupBy
+ {...(transformProps(chartProps) as unknown as PluginFilterGroupByProps)}
+ />,
+ );
+ return setDataMask;
+};
+
+test('default-value picker offers only allowlisted columns', async () => {
+ renderDefaultPicker({ formData: { columnsAllowlist: ['apple', 'cherry'] } });
+ expect(await getOpenedOptionOrder()).toEqual(['apple', 'cherry']);
+});
+
+test('default-value picker offers only groupable columns when no allowlist narrows it', async () => {
+ renderDefaultPicker({ formData: { groupableColumns: ['banana', 'apple'] } });
+ expect(await getOpenedOptionOrder()).toEqual(['banana', 'apple']);
+});
+
+test('drops an existing default that the allowlist excludes', async () => {
+ const setDataMask = renderDefaultPicker({
+ formData: { columnsAllowlist: ['apple'] },
+ filterState: { value: ['apple', 'cherry'] },
+ });
+
+ expect(setDataMask).toHaveBeenLastCalledWith({
+ extraFormData: { custom_form_data: { groupby: ['apple'] } },
+ filterState: { label: 'apple', value: ['apple'] },
+ });
+});
+
+test('leaves a default untouched when nothing restricts the columns', () => {
+ const setDataMask = renderDefaultPicker({
+ filterState: { value: ['apple', 'cherry'] },
+ });
+
+ expect(setDataMask).toHaveBeenLastCalledWith({
+ extraFormData: { custom_form_data: { groupby: ['apple', 'cherry'] } },
+ filterState: { label: 'apple, cherry', value: ['apple', 'cherry'] },
+ });
+});
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
index cccbcf7af18..05910a2068f 100644
--- a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
@@ -29,6 +29,7 @@ import {
import { propertyComparator } from '@superset-ui/core/components/Select/utils';
import { FilterPluginStyle, StatusMessage } from '../common';
import { PluginFilterGroupByProps, ColumnOption, ColumnData } from './types';
+import { getAllowedGroupByColumns } from './columnAllowlist';
const EMPTY_OBJECT = {};
@@ -102,22 +103,49 @@ export default function PluginFilterDynamicGroupBy(
return EMPTY_OBJECT as FormItemProps;
}, [filterState.validateMessage, filterState.validateStatus]);
+ // The columns a default may use: the builder's allowlist, narrowed to the
+ // dataset's groupable columns. Unrestricted (null) when neither is known.
+ const allowedColumns = useMemo(
+ () =>
+ getAllowedGroupByColumns(
+ formData.columnsAllowlist,
+ formData.groupableColumns,
+ ),
+ [formData.columnsAllowlist, formData.groupableColumns],
+ );
+
const options = useMemo(
() =>
- (data || []).map((row: ColumnOption | ColumnData) => {
- const columnName = 'column_name' in row ? row.column_name : row.value;
- const label =
- ('verbose_name' in row && row.verbose_name) ||
- ('label' in row && row.label) ||
- columnName;
- return {
- label,
- value: columnName,
- };
- }),
- [data],
+ (data || [])
+ .map((row: ColumnOption | ColumnData) => {
+ const columnName = 'column_name' in row ? row.column_name : row.value;
+ const label =
+ ('verbose_name' in row && row.verbose_name) ||
+ ('label' in row && row.label) ||
+ columnName;
+ return {
+ label,
+ value: columnName,
+ };
+ })
+ .filter(option => !allowedColumns || allowedColumns.has(option.value)),
+ [data, allowedColumns],
);
+ // A default that the allowlist excludes would put every viewer into a
+ // group-by they cannot choose, so drop excluded columns from the selection
+ // as soon as the options are known (never before, or an unloaded column
+ // list would wipe a valid default).
+ useEffect(() => {
+ if (!allowedColumns || !(data || []).length) {
+ return;
+ }
+ const kept = value.filter(column => allowedColumns.has(column));
+ if (kept.length !== value.length) {
+ handleChange(kept);
+ }
+ }, [allowedColumns, data, value]);
+
const sortComparator = useCallback(
(a: LabeledValue, b: LabeledValue) => {
if (formData.sortAscending === undefined) {
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.test.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.test.ts
new file mode 100644
index 00000000000..3d9447610dc
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.test.ts
@@ -0,0 +1,67 @@
+/**
+ * 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 {
+ getAllowedGroupByColumns,
+ pruneGroupByDataMask,
+} from './columnAllowlist';
+
+test('is unrestricted when neither an allowlist nor the groupable set is known', () => {
+ expect(getAllowedGroupByColumns(undefined, undefined)).toBeNull();
+ expect(getAllowedGroupByColumns([], [])).toBeNull();
+});
+
+test('uses the allowlist, narrowed to groupable columns when known', () => {
+ expect(getAllowedGroupByColumns(['a', 'b'], undefined)).toEqual(
+ new Set(['a', 'b']),
+ );
+ expect(getAllowedGroupByColumns(undefined, ['a', 'c'])).toEqual(
+ new Set(['a', 'c']),
+ );
+ expect(getAllowedGroupByColumns(['a', 'b'], ['a', 'c'])).toEqual(
+ new Set(['a']),
+ );
+});
+
+const mask = (value: string[]) => ({
+ extraFormData: { custom_form_data: { groupby: value } },
+ filterState: { label: value.join(', '), value },
+});
+
+test('drops excluded columns from a Group By data mask', () => {
+ expect(pruneGroupByDataMask(mask(['a', 'x']), new Set(['a']))).toEqual(
+ mask(['a']),
+ );
+});
+
+test('clears the selection when every column is excluded', () => {
+ expect(pruneGroupByDataMask(mask(['x']), new Set(['a']))).toEqual({
+ extraFormData: { custom_form_data: { groupby: [] } },
+ filterState: { label: '', value: null },
+ });
+});
+
+test('returns the same mask when nothing is excluded or unrestricted', () => {
+ const allowedMask = mask(['a']);
+ expect(pruneGroupByDataMask(allowedMask, new Set(['a', 'b']))).toBe(
+ allowedMask,
+ );
+ expect(pruneGroupByDataMask(allowedMask, null)).toBe(allowedMask);
+ const empty = {};
+ expect(pruneGroupByDataMask(empty, new Set(['a']))).toBe(empty);
+});
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.ts
new file mode 100644
index 00000000000..9b1459a45bd
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/columnAllowlist.ts
@@ -0,0 +1,105 @@
+/**
+ * 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 { DataMask, ensureIsArray } from '@superset-ui/core';
+
+/**
+ * Restrict the groupable column options to the builder-configured allowlist.
+ * An unset or empty allowlist means "no restriction" so existing Group By
+ * customizations (which never stored an allowlist) keep offering every
+ * groupable column, preserving backwards compatibility.
+ *
+ * `appliedValues` are the viewer's currently applied group-by columns. They
+ * stay in the options even when a later-narrowed allowlist excludes them, so
+ * an applied selection keeps rendering with its verbose label instead of a
+ * bare column name. The viewer can still clear it, and once cleared it is no
+ * longer offered.
+ */
+export const applyColumnAllowlist = <T extends { value: string }>(
+ options: T[],
+ allowlist?: string[] | null,
+ appliedValues: string[] = [],
+): T[] => {
+ if (!Array.isArray(allowlist) || allowlist.length === 0) {
+ return options;
+ }
+ const allowed = new Set([...allowlist, ...appliedValues]);
+ return options.filter(option => allowed.has(option.value));
+};
+
+/**
+ * The columns a Group By control may group by: the builder's allowlist when
+ * set, narrowed to the dataset's groupable columns when those are known.
+ * Returns null when nothing restricts the choice.
+ */
+export const getAllowedGroupByColumns = (
+ allowlist?: string[] | null,
+ groupableColumns?: string[] | null,
+): Set<string> | null => {
+ const hasAllowlist = Array.isArray(allowlist) && allowlist.length > 0;
+ const hasGroupable =
+ Array.isArray(groupableColumns) && groupableColumns.length > 0;
+ if (!hasAllowlist && !hasGroupable) {
+ return null;
+ }
+ if (!hasAllowlist) {
+ return new Set(groupableColumns);
+ }
+ if (!hasGroupable) {
+ return new Set(allowlist);
+ }
+ const groupable = new Set(groupableColumns);
+ return new Set(
+ ensureIsArray(allowlist).filter(column => groupable.has(column)),
+ );
+};
+
+/**
+ * Drop group-by columns that `allowed` excludes from a Group By data mask
+ * (the shape the Group By control writes: `custom_form_data.groupby` plus a
+ * matching `filterState`). Returns the mask unchanged when nothing is
+ * excluded, so an unrestricted control is never rewritten.
+ */
+export const pruneGroupByDataMask = (
+ mask: DataMask,
+ allowed: Set<string> | null,
+): DataMask => {
+ if (!allowed) {
+ return mask;
+ }
+ const selected = ensureIsArray<string>(mask.filterState?.value);
+ const kept = selected.filter(column => allowed.has(column));
+ if (kept.length === selected.length) {
+ return mask;
+ }
+ return {
+ ...mask,
+ extraFormData: {
+ ...mask.extraFormData,
+ custom_form_data: {
+ ...mask.extraFormData?.custom_form_data,
+ groupby: kept,
+ },
+ },
+ filterState: {
+ ...mask.filterState,
+ label: kept.join(', '),
+ value: kept.length ? kept : null,
+ },
+ };
+};
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
index faa1665e347..95c1b6ab65e 100644
--- a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
@@ -53,6 +53,11 @@ interface PluginFilterGroupByCustomizeProps {
aggregation?: string;
enableEmptyFilter?: boolean;
inputRef?: RefObject<HTMLInputElement>;
+ // Builder allowlist of columns viewers may group by (controlValues).
+ columnsAllowlist?: string[];
+ // Config form only: the dataset's groupable columns, so the default-value
+ // picker never offers a column viewers cannot group by.
+ groupableColumns?: string[];
}
export type PluginFilterGroupByQueryFormData = QueryFormData &
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
index e756a7e03ad..b2947d8fcda 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.test.tsx
@@ -32,6 +32,7 @@ import {
} from 'spec/helpers/testing-library';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import GroupByFilterCard, {
+ applyColumnAllowlist,
createLabelSortComparator,
} from './GroupByFilterCard';
@@ -68,6 +69,40 @@ test('preserves source order when sortAscending is unset', () => {
expect(compare(banana, apple)).toBe(0);
});
+const columnOptions = [
+ { label: 'Country', value: 'country' },
+ { label: 'State', value: 'state' },
+ { label: 'City', value: 'city' },
+];
+
+test('returns all options when the allowlist is unset (backwards compatible)', () => {
+ expect(applyColumnAllowlist(columnOptions, undefined)).toEqual(columnOptions);
+});
+
+test('returns all options when the allowlist is empty (no restriction)', () => {
+ expect(applyColumnAllowlist(columnOptions, [])).toEqual(columnOptions);
+});
+
+test('keeps only allowlisted columns and preserves their order', () => {
+ expect(applyColumnAllowlist(columnOptions, ['city', 'country'])).toEqual([
+ { label: 'Country', value: 'country' },
+ { label: 'City', value: 'city' },
+ ]);
+});
+
+test('keeps an applied value that the allowlist excludes', () => {
+ expect(applyColumnAllowlist(columnOptions, ['country'], ['state'])).toEqual([
+ { label: 'Country', value: 'country' },
+ { label: 'State', value: 'state' },
+ ]);
+});
+
+test('ignores allowlist entries that are not real columns', () => {
+ expect(
+ applyColumnAllowlist(columnOptions, ['state', 'does_not_exist']),
+ ).toEqual([{ label: 'State', value: 'state' }]);
+});
+
/**
* Characterization tests (sc-111089 T002): pin the existing regular-dataset
* behaviour BEFORE the type-aware refactor, so FR-004's "byte-identical"
@@ -373,3 +408,127 @@ test('renders the column-loading spinner small and muted', async () => {
expect(spinner).toHaveClass('inline');
expect(spinner).toHaveStyle({ opacity: 0.25, width: '40px' });
});
+
+/**
+ * sc-119327: the builder-configured column allowlist (stored on
+ * controlValues.columnsAllowlist) restricts which groupable columns a viewer
+ * may pick. Re-expressed in this suite's fetchMock idiom; the pure filtering
+ * logic is proven by the applyColumnAllowlist unit tests above.
+ */
+const allowlistDataset = {
+ result: {
+ table_name: 'cleaned_sales_data',
+ columns: [
+ { column_name: 'country', verbose_name: 'Country', filterable: true },
+ { column_name: 'state', verbose_name: 'State', filterable: true },
+ { column_name: 'city', verbose_name: 'City', filterable: true },
+ ],
+ },
+};
+
+test('only offers allowlisted columns to viewers', async () => {
+ fetchMock.get('glob:*/api/v1/dataset/320', allowlistDataset);
+
+ render(
+ <GroupByFilterCard
+ customizationItem={{
+ ...customization([{ datasetId: 320 }]),
+ controlValues: { columnsAllowlist: ['country', 'city'] },
+ }}
+ />,
+ { useRedux: true, initialState },
+ );
+
+ await userEvent.click(await screen.findByRole('combobox'));
+
+ expect(await screen.findByText('Country')).toBeInTheDocument();
+ expect(screen.getByText('City')).toBeInTheDocument();
+ expect(screen.queryByText('State')).not.toBeInTheDocument();
+});
+
+test('offers every groupable column when no allowlist is configured', async () => {
+ fetchMock.get('glob:*/api/v1/dataset/321', allowlistDataset);
+
+ renderCard([{ datasetId: 321 }]);
+
+ await userEvent.click(await screen.findByRole('combobox'));
+
+ expect(await screen.findByText('Country')).toBeInTheDocument();
+ expect(screen.getByText('State')).toBeInTheDocument();
+ expect(screen.getByText('City')).toBeInTheDocument();
+});
+
+test('keeps an applied selection that a narrowed allowlist excludes, with its label', async () => {
+ fetchMock.get('glob:*/api/v1/dataset/322', allowlistDataset);
+
+ // The viewer grouped by 'state' before the builder narrowed the allowlist to
+ // ['country']. The applied selection is kept (not silently dropped) and
+ // still renders with its verbose label rather than the raw column name.
+ render(
+ <GroupByFilterCard
+ customizationItem={{
+ ...customization([{ datasetId: 322 }]),
+ controlValues: { columnsAllowlist: ['country'] },
+ }}
+ />,
+ {
+ useRedux: true,
+ initialState: {
+ ...initialState,
+ dataMask: {
+ 'cc-1': { id: 'cc-1', filterState: { value: ['state'] } },
+ },
+ },
+ },
+ );
+
+ expect(await screen.findByText('State')).toBeInTheDocument();
+ expect(screen.queryByText('state')).not.toBeInTheDocument();
+
+ // Other excluded columns are still not offered.
+ await userEvent.click(await screen.findByRole('combobox'));
+ expect(await screen.findByText('Country')).toBeInTheDocument();
+ expect(screen.queryByText('City')).not.toBeInTheDocument();
+});
+
+const allowlistGoneMessage =
+ 'The columns allowed for this control are no longer in the dataset. Ask the dashboard owner to update it.';
+
+test('tells viewers when none of the allowed columns remain in the dataset', async () => {
+ fetchMock.get('glob:*/api/v1/dataset/323', allowlistDataset);
+
+ render(
+ <GroupByFilterCard
+ customizationItem={{
+ ...customization([{ datasetId: 323 }]),
+ controlValues: { columnsAllowlist: ['dropped_col'] },
+ }}
+ />,
+ { useRedux: true, initialState },
+ );
+
+ expect(await screen.findByText(allowlistGoneMessage)).toBeInTheDocument();
+ // It does not quietly open the control up to every column instead.
+ await userEvent.click(await screen.findByRole('combobox'));
+ await waitFor(() =>
+ expect(screen.queryByText('Country')).not.toBeInTheDocument(),
+ );
+});
+
+test('shows no message while at least one allowed column remains', async () => {
+ fetchMock.get('glob:*/api/v1/dataset/324', allowlistDataset);
+
+ render(
+ <GroupByFilterCard
+ customizationItem={{
+ ...customization([{ datasetId: 324 }]),
+ controlValues: { columnsAllowlist: ['country', 'dropped_col'] },
+ }}
+ />,
+ { useRedux: true, initialState },
+ );
+
+ await userEvent.click(await screen.findByRole('combobox'));
+ expect(await screen.findByText('Country')).toBeInTheDocument();
+ expect(screen.queryByText(allowlistGoneMessage)).not.toBeInTheDocument();
+});
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 8fc49347e32..a63c3b2ce24 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
@@ -22,6 +22,7 @@ import {
DataMask,
DataMaskStateWithId,
DatasourceType,
+ ensureIsArray,
Filter,
useTruncation,
ChartCustomization,
@@ -49,6 +50,8 @@ import { RootState } from 'src/dashboard/types';
import { setPendingChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
import { TooltipWithTruncation } from 'src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation';
import { addDangerToast } from 'src/components/MessageToasts/actions';
+import { applyColumnAllowlist } from 'src/chartCustomizations/components/DynamicGroupBy/columnAllowlist';
+import { StatusMessage } from 'src/chartCustomizations/components/common';
import { dispatchChartCustomizationHoverAction } from './utils';
import {
displayControlBindingKey,
@@ -414,6 +417,31 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
[sortAscending],
);
+ // Builder-configured allowlist of columns viewers are allowed to group by.
+ const columnsAllowlist = customizationItem.controlValues?.columnsAllowlist as
+ | string[]
+ | undefined;
+
+ const allowedColumnOptions = useMemo(
+ () =>
+ applyColumnAllowlist(
+ columnOptions,
+ columnsAllowlist,
+ ensureIsArray<string>(currentValue),
+ ),
+ [columnOptions, columnsAllowlist, currentValue],
+ );
+
+ // Every column the builder allowed is gone from the dataset (dropped or no
+ // longer groupable). Rather than a silently empty control, viewers get a
+ // message; the control is not opened up to all columns behind the builder's
+ // back.
+ const allowlistUnavailable =
+ !loading &&
+ columnOptions.length > 0 &&
+ ensureIsArray(columnsAllowlist).length > 0 &&
+ applyColumnAllowlist(columnOptions, columnsAllowlist).length === 0;
+
const columnDisplayName = useMemo(() => {
if (customizationItem.name) {
return customizationItem.name;
@@ -591,7 +619,7 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
placeholder={t('Search columns...')}
value={currentValue}
onChange={handleColumnChange}
- options={columnOptions}
+ options={allowedColumnOptions}
showSearch
mode={canSelectMultiple ? 'multiple' : undefined}
filterOption={(input, option) =>
@@ -621,7 +649,7 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
placeholder={t('Search columns...')}
value={currentValue}
onChange={handleColumnChange}
- options={columnOptions}
+ options={allowedColumnOptions}
showSearch
mode={canSelectMultiple ? 'multiple' : undefined}
filterOption={(input, option) =>
@@ -635,6 +663,13 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
</div>
)}
+ {allowlistUnavailable && (
+ <StatusMessage status="warning">
+ {t(
+ 'The columns allowed for this control are no longer in the dataset. Ask the dashboard owner to update it.',
+ )}
+ </StatusMessage>
+ )}
{loading && (
<div style={{ textAlign: 'center', marginTop: 8 }}>
<Loading position="inline" size="s" muted />
@@ -644,4 +679,6 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
);
};
+export { applyColumnAllowlist };
+
export default GroupByFilterCard;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx
index b8aa905e42f..1abf557cc99 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx
@@ -143,3 +143,42 @@ test('Should filter results', async () => {
expect(screen.queryByTitle('column_name_03')).not.toBeInTheDocument();
});
});
+
+test('ignores a slower response for a datasource the user already switched away from', async () => {
+ fetchMock.get(
+ 'glob:*/api/v1/dataset/901?*',
+ { result: { columns: [{ column_name: 'stale_col', filterable: true }] } },
+ { delay: 150 },
+ );
+ fetchMock.get('glob:*/api/v1/dataset/902?*', {
+ result: { columns: [{ column_name: 'current_col', filterable: true }] },
+ });
+ const onColumnsLoaded = jest.fn();
+ const props = createProps({
+ datasetId: 901,
+ value: [],
+ mode: 'multiple',
+ onColumnsLoaded,
+ });
+ const { rerender } = render(<ColumnSelect {...(props as any)} />, {
+ useRedux: true,
+ });
+
+ props.datasetId = 902;
+ rerender(<ColumnSelect {...(props as any)} />);
+
+ await waitFor(() =>
+ expect(onColumnsLoaded).toHaveBeenCalledWith(['current_col'], '902__table'),
+ );
+ // Let the slow 901 response arrive; it must be dropped.
+ await waitFor(() =>
+ expect(
+ fetchMock.callHistory.calls('glob:*/api/v1/dataset/901?*'),
+ ).toHaveLength(1),
+ );
+ await new Promise(resolve => setTimeout(resolve, 250));
+ expect(onColumnsLoaded).toHaveBeenCalledTimes(1);
+ await userEvent.click(screen.getByRole('combobox'));
+ expect(await screen.findByTitle('current_col')).toBeInTheDocument();
+ expect(screen.queryByTitle('stale_col')).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
index cba9f5a3773..27b1d6769f9 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useCallback, useState, useMemo, useEffect } from 'react';
+import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
import rison from 'rison';
import { t } from '@apache-superset/core/translation';
import {
@@ -47,10 +47,28 @@ interface ColumnSelectProps {
value?: string | string[];
onChange?: (value: string) => void;
mode?: 'multiple';
+ // Called with the names of the columns that back the rendered options
+ // (already narrowed by `filterValues`) whenever a dataset's columns load,
+ // plus the datasource key (see getDatasourceKey) they were loaded for. Lets
+ // a parent seed a default selection that matches the options exactly, and
+ // tell which datasource a selection belongs to.
+ onColumnsLoaded?: (columnNames: string[], datasourceKey: string) => void;
+ // Label options (and selected values) with the column's verbose name,
+ // falling back to column_name. Search matches either.
+ showVerboseNames?: boolean;
+ placeholder?: string;
}
+/**
+ * Identifies a datasource across both ID sequences. Datasets and semantic
+ * views have independent IDs, so the type is part of the key.
+ */
+export const getDatasourceKey = (
+ datasetId: number | string | undefined,
+ datasourceType: DatasourceType | undefined,
+) => `${datasetId}__${datasourceType || DatasourceType.Table}`;
+
/** Special purpose AsyncSelect that selects a column from a dataset */
-// eslint-disable-next-line import/prefer-default-export
export function ColumnSelect({
allowClear = false,
filterValues = () => true,
@@ -62,6 +80,9 @@ export function ColumnSelect({
value,
onChange,
mode,
+ onColumnsLoaded,
+ showVerboseNames = false,
+ placeholder = t('Select a column'),
}: ColumnSelectProps) {
const [columns, setColumns] = useState<Column[]>();
const [loading, setLoading] = useState(false);
@@ -72,15 +93,39 @@ export function ColumnSelect({
]);
}, [form, filterId, formField]);
+ // The names backing the rendered options: the loaded columns narrowed by
+ // `filterValues`, the same narrowing the option list applies, so a default
+ // seeded through onColumnsLoaded matches the available options exactly.
+ const filterColumnNames = useCallback(
+ (cols: Column[]) =>
+ ensureIsArray(cols)
+ .filter(filterValues)
+ .map((col: Column) => col.column_name),
+ [filterValues],
+ );
+
const options = useMemo(
() =>
ensureIsArray(columns)
.filter(filterValues)
- .map((col: Column) => col.column_name)
- .map((column: string) => ({ label: column, value: column })),
- [columns, filterValues],
+ .map((col: Column) => ({
+ label: (showVerboseNames && col.verbose_name) || col.column_name,
+ value: col.column_name,
+ })),
+ [columns, filterValues, showVerboseNames],
);
+ // Whether the current selection still matches a loaded column. An empty
+ // selection has nothing to look up and is legitimate (e.g. a cleared
+ // multi-select), so it must not trigger a reset of the form field.
+ const isValueInColumns = (cols: Column[]) => {
+ const lookupValue = ensureIsArray(value);
+ return (
+ lookupValue.length === 0 ||
+ cols.some((column: Column) => lookupValue.includes(column.column_name))
+ );
+ };
+
const currentFilterType =
form.getFieldValue('filters')?.[filterId].filterType;
const currentColumn = useMemo(
@@ -98,8 +143,15 @@ export function ColumnSelect({
// the datasource type changes. Datasets and semantic views have independent
// ID sequences, so switching between them with the same numeric ID must still
// trigger a column re-fetch.
- const datasourceKey = `${datasetId}__${datasourceType || DatasourceType.Table}`;
+ const datasourceKey = getDatasourceKey(datasetId, datasourceType);
+ // The datasource whose columns should be shown. A response for any other
+ // datasource is stale (the user switched while it was in flight) and is
+ // ignored, so it can neither replace the options nor reach onColumnsLoaded.
+ const latestDatasourceKey = useRef(datasourceKey);
+ latestDatasourceKey.current = datasourceKey;
useChangeEffect(datasourceKey, previous => {
+ const requestKey = datasourceKey;
+ const isStale = () => latestDatasourceKey.current !== requestKey;
if (previous != null) {
setColumns([]);
resetColumnField();
@@ -120,17 +172,21 @@ export function ColumnSelect({
if (datasourceType === DatasourceType.SemanticView) {
fetchSemanticViewStructure(datasetId)
.then(({ dimensions }) => {
+ if (isStale()) {
+ return;
+ }
const cols: Column[] = semanticViewDimensionsToColumns(dimensions);
- const lookupValue = Array.isArray(value) ? value : [value];
- const valueExists = cols.some((column: Column) =>
- lookupValue?.includes(column.column_name),
- );
- if (!valueExists) {
+ if (!isValueInColumns(cols)) {
resetColumnField();
}
setColumns(cols);
+ onColumnsLoaded?.(filterColumnNames(cols), requestKey);
}, handleError)
- .finally(() => setLoading(false));
+ .finally(() => {
+ if (!isStale()) {
+ setLoading(false);
+ }
+ });
} else {
cachedSupersetGet({
endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
@@ -139,20 +195,25 @@ export function ColumnSelect({
'columns.is_dttm',
'columns.type_generic',
'columns.filterable',
+ ...(showVerboseNames ? ['columns.verbose_name'] : []),
],
})}`,
})
.then(({ json: { result } }) => {
- const lookupValue = Array.isArray(value) ? value : [value];
- const valueExists = result.columns.some((column: Column) =>
- lookupValue?.includes(column.column_name),
- );
- if (!valueExists) {
+ if (isStale()) {
+ return;
+ }
+ if (!isValueInColumns(result.columns)) {
resetColumnField();
}
setColumns(result.columns);
+ onColumnsLoaded?.(filterColumnNames(result.columns), requestKey);
}, handleError)
- .finally(() => setLoading(false));
+ .finally(() => {
+ if (!isStale()) {
+ setLoading(false);
+ }
+ });
}
}
});
@@ -165,7 +226,7 @@ export function ColumnSelect({
loading={loading}
onChange={onChange}
options={options}
- placeholder={t('Select a column')}
+ placeholder={placeholder}
notFoundContent={t('No compatible columns found')}
showSearch
allowClear={allowClear}
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 1648a2e6477..5479695e3e2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -29,6 +29,7 @@ import {
ChartDataResponseResult,
Column,
DatasourceType,
+ ensureIsArray,
Filter,
ChartCustomization,
ChartCustomizationType,
@@ -98,13 +99,18 @@ import {
mergeExtraFormData,
} from 'src/dashboard/components/nativeFilters/utils';
import { DatasetSelectLabel } from 'src/features/datasets/DatasetSelectLabel';
+import { ChartCustomizationPlugins } from 'src/constants';
import {
filterSupportsDependencies,
getFiltersConfigModalTestId,
} from '../FiltersConfigModal';
-import { FilterRemoval, NativeFiltersForm } from '../types';
+import {
+ ChartCustomizationsFormItem,
+ FilterRemoval,
+ NativeFiltersForm,
+} from '../types';
import { CollapsibleControl } from './CollapsibleControl';
-import { ColumnSelect } from './ColumnSelect';
+import { ColumnSelect, getDatasourceKey } from './ColumnSelect';
import DatasetSelect from './DatasetSelect';
import DefaultValue from './DefaultValue';
import FilterScope from './FilterScope/FilterScope';
@@ -397,6 +403,12 @@ const FiltersConfigForm = (
// @ts-expect-error
!!nativeFilterAndCustomizationItems[itemTypeField]?.value?.datasourceCount;
+ // The Dynamic Group By customization lets builders curate which columns
+ // viewers may group by via a column allowlist (stored in controlValues).
+ const isDynamicGroupBy =
+ isChartCustomization &&
+ itemTypeField === ChartCustomizationPlugins.DynamicGroupBy;
+
const getDatasetId = () => {
if (isChartCustomization) {
if (formFilter?.dataset?.value) {
@@ -576,6 +588,12 @@ const FiltersConfigForm = (
});
newFormData.extra_form_data = dependenciesDefaultValues;
+ if (isDynamicGroupBy) {
+ // Lets the default-value picker offer only columns viewers can group by.
+ newFormData.groupableColumns = (
+ formFilter as Partial<ChartCustomizationsFormItem> | undefined
+ )?.groupableColumns;
+ }
const selectionsReset =
!!formFilter?.semantic_selection_version &&
@@ -600,6 +618,78 @@ const FiltersConfigForm = (
[filterId, form, formChanged],
);
+ // The datasource the allowlist in the form was chosen for. Starts as the
+ // saved control's datasource (undefined for a new control) and follows the
+ // columns ColumnSelect loads. It lets seeding detect a dataset change on its
+ // own instead of relying on the dataset picker's reset, which is skipped when
+ // the builder switches back to the dataset they started with.
+ const allowlistDatasourceKey = useRef<string | undefined>(
+ customizationToEdit?.targets?.[0]?.datasetId != null
+ ? getDatasourceKey(
+ customizationToEdit.targets[0].datasetId,
+ customizationToEdit.targets[0].datasourceType,
+ )
+ : undefined,
+ );
+
+ // Seed a Dynamic Group By control's allowlist with every groupable column so
+ // it defaults to "all selected" (builders can then deselect to restrict).
+ // Seeds when no allowlist is set yet (a freshly created control, or a legacy
+ // control that never stored one) and whenever columns load for a different
+ // datasource than the allowlist was chosen for, because column names are
+ // dataset-specific. An existing selection for the same datasource, including
+ // a deliberately narrowed or emptied one, is never overwritten. The column
+ // names come from the same source that populates the multi-select options,
+ // so the default matches exactly what the builder can choose from.
+ //
+ // The full groupable set is also recorded in the form-only
+ // `groupableColumns` field. On save, an allowlist that still selects every
+ // groupable column collapses back to unset (see
+ // collapseFullColumnsAllowlist), so the seed never freezes a snapshot of
+ // the dataset's columns into the saved config.
+ const seedGroupByAllowlist = useCallback(
+ (columnNames: string[], datasourceKey: string) => {
+ if (!isDynamicGroupBy) {
+ return;
+ }
+ const datasourceChanged =
+ allowlistDatasourceKey.current !== undefined &&
+ allowlistDatasourceKey.current !== datasourceKey;
+ allowlistDatasourceKey.current = datasourceKey;
+ const currentControlValues =
+ form.getFieldValue(['filters', filterId, 'controlValues']) || {};
+ const shouldSeed =
+ columnNames.length > 0 &&
+ (datasourceChanged ||
+ currentControlValues.columnsAllowlist === undefined);
+ setNativeFilterFieldValues(form, filterId, {
+ groupableColumns: columnNames,
+ ...(shouldSeed
+ ? {
+ controlValues: {
+ ...currentControlValues,
+ columnsAllowlist: columnNames,
+ },
+ }
+ : {}),
+ });
+ forceUpdate();
+ },
+ [isDynamicGroupBy, form, filterId, forceUpdate],
+ );
+
+ // Allowlisted columns the loaded dataset can no longer offer (dropped, or no
+ // longer groupable). Viewers never see them, so the builder is warned.
+ const groupableColumns = (
+ formFilter as Partial<ChartCustomizationsFormItem> | undefined
+ )?.groupableColumns;
+ const unavailableAllowlistColumns =
+ isDynamicGroupBy && groupableColumns?.length
+ ? ensureIsArray<string>(
+ formFilter?.controlValues?.columnsAllowlist,
+ ).filter(column => !groupableColumns.includes(column))
+ : [];
+
const hasPreFilter =
!!formFilter?.adhoc_filters?.length ||
!!formFilter?.time_range ||
@@ -1243,6 +1333,21 @@ const FiltersConfigForm = (
defaultDataMask: null,
column: null,
semantic_selection_version: undefined,
+ // Columns are dataset-specific, so clear the
+ // Group By allowlist when the dataset changes to
+ // avoid stale entries. Clearing it to undefined
+ // lets seedGroupByAllowlist re-seed the default
+ // (all groupable columns) once the new dataset's
+ // columns load.
+ ...(isDynamicGroupBy
+ ? {
+ controlValues: {
+ ...formFilter?.controlValues,
+ columnsAllowlist: undefined,
+ },
+ groupableColumns: undefined,
+ }
+ : {}),
});
}
forceUpdate();
@@ -1284,6 +1389,94 @@ const FiltersConfigForm = (
: FilterPanels.configuration.name,
children: (
<>
+ {isDynamicGroupBy &&
+ hasDataset &&
+ showDataset && (
+ <>
+ {/* Registers the form-only groupableColumns
+ field so validateFields returns it to the
+ save transform. */}
+ <FormItem
+ hidden
+ name={[
+ 'filters',
+ filterId,
+ 'groupableColumns',
+ ]}
+ />
+ <StyledRowFormItem
+ expanded={expanded}
+ name={[
+ 'filters',
+ filterId,
+ 'controlValues',
+ 'columnsAllowlist',
+ ]}
+ initialValue={
+ customizationToEdit?.controlValues
+ ?.columnsAllowlist
+ }
+ label={
+ <>
+ <StyledLabel>
+ {t('Groupable columns')}
+ </StyledLabel>
+
+ <InfoTooltip
+ placement="top"
+ tooltip={t(
+ 'Columns viewers can choose to group charts by.',
+ )}
+ />
+ </>
+ }
+ extra={t(
+ 'Leave empty, or keep every column selected, to let viewers group by any groupable column, including columns added to the dataset later.',
+ )}
+ validateStatus={
+ unavailableAllowlistColumns.length
+ ? 'warning'
+ : undefined
+ }
+ help={
+ unavailableAllowlistColumns.length
+ ? t(
+ 'Not in this dataset, so viewers will not see: %s',
+ unavailableAllowlistColumns.join(
+ ', ',
+ ),
+ )
+ : undefined
+ }
+ data-test="groupby-columns-allowlist"
+ >
+ <ColumnSelect
+ mode="multiple"
+ allowClear
+ form={form}
+ filterId={filterId}
+ datasetId={datasetId}
+ datasourceType={datasourceType}
+ // Match the viewer, which offers every
+ // column not explicitly marked
+ // non-filterable (filterable can be
+ // null for legacy columns).
+ filterValues={(column: Column) =>
+ column?.filterable !== false
+ }
+ onColumnsLoaded={seedGroupByAllowlist}
+ showVerboseNames
+ placeholder={t(
+ 'All groupable columns',
+ )}
+ onChange={() => {
+ forceUpdate();
+ formChanged();
+ }}
+ />
+ </StyledRowFormItem>
+ </>
+ )}
{canDependOnOtherFilters &&
(hasAvailableFilters ||
dependencies.length > 0) && (
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/GroupByColumnAllowlist.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/GroupByColumnAllowlist.test.tsx
new file mode 100644
index 00000000000..033ce550d32
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/GroupByColumnAllowlist.test.tsx
@@ -0,0 +1,429 @@
+/**
+ * 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 {
+ Behavior,
+ ChartMetadata,
+ ChartCustomization,
+ ChartCustomizationType,
+ getChartMetadataRegistry,
+} from '@superset-ui/core';
+import { Form, type FormInstance } from '@superset-ui/core/components';
+import fetchMock from 'fetch-mock';
+import {
+ render,
+ screen,
+ userEvent,
+ waitFor,
+ within,
+} from 'spec/helpers/testing-library';
+import { ChartCustomizationPlugins } from 'src/constants';
+import { transformCustomizationForSave } from '../transformers/customizationTransformer';
+import { ChartCustomizationsFormItem, NativeFiltersForm } from '../types';
+import FiltersConfigForm from './FiltersConfigForm';
+
+// Register a minimal Group By customization so the config form treats it as a
+// dataset-backed chart customization (datasourceCount > 0 -> hasDataset).
+getChartMetadataRegistry().registerValue(
+ ChartCustomizationPlugins.DynamicGroupBy,
+ new ChartMetadata({
+ name: 'Group By',
+ datasourceCount: 1,
+ behaviors: [Behavior.ChartCustomization],
+ thumbnail: '',
+ }),
+);
+
+fetchMock.get('glob:*/api/v1/dataset/1?*', {
+ result: {
+ columns: [
+ { column_name: 'country', is_dttm: false, filterable: true },
+ { column_name: 'state', is_dttm: false, filterable: true },
+ ],
+ },
+});
+
+// A column whose `filterable` flag is null (legacy rows; the column is
+// nullable in the metadata DB). The viewer offers it, so the allowlist must
+// too.
+fetchMock.get('glob:*/api/v1/dataset/2?*', {
+ result: {
+ columns: [
+ { column_name: 'country', is_dttm: false, filterable: true },
+ { column_name: 'legacy_col', is_dttm: false, filterable: null },
+ { column_name: 'internal_only', is_dttm: false, filterable: false },
+ ],
+ },
+});
+
+// Columns whose verbose names differ from column_name.
+fetchMock.get('glob:*/api/v1/dataset/3?*', {
+ result: {
+ columns: [
+ {
+ column_name: 'region',
+ verbose_name: 'Sales Region',
+ is_dttm: false,
+ filterable: true,
+ },
+ { column_name: 'country', is_dttm: false, filterable: true },
+ ],
+ },
+});
+
+// The dataset picker's list endpoint (path-matched so it never shadows the
+// per-dataset column endpoints above).
+fetchMock.get('path:/api/v1/dataset/', {
+ result: [
+ {
+ id: 1,
+ table_name: 'sales',
+ schema: 'public',
+ database: { database_name: 'examples' },
+ },
+ {
+ id: 2,
+ table_name: 'legacy_sales',
+ schema: 'public',
+ database: { database_name: 'examples' },
+ },
+ ],
+ count: 2,
+});
+
+const FILTER_ID = 'CHART_CUSTOMIZATION-groupby';
+
+const noop = () => {};
+
+function renderForm({
+ customizationToEdit,
+ initialControlValues,
+ datasetId = 1,
+ onForm,
+}: {
+ customizationToEdit?: ChartCustomization;
+ initialControlValues?: Record<string, unknown>;
+ datasetId?: number;
+ onForm?: (form: FormInstance<NativeFiltersForm>) => void;
+} = {}) {
+ function Harness() {
+ const [form] = Form.useForm<NativeFiltersForm>();
+ onForm?.(form);
+ return (
+ <Form
+ form={form}
+ initialValues={{
+ filters: {
+ [FILTER_ID]: {
+ filterType: ChartCustomizationPlugins.DynamicGroupBy,
+ dataset: { value: datasetId, label: 'sales' },
+ ...(initialControlValues
+ ? { controlValues: initialControlValues }
+ : {}),
+ },
+ },
+ }}
+ >
+ <FiltersConfigForm
+ filterId={FILTER_ID}
+ itemType="chartCustomization"
+ form={form}
+ customizationToEdit={customizationToEdit}
+ expanded
+ removedFilters={{}}
+ restoreFilter={noop}
+ onModifyFilter={noop}
+ getAvailableFilters={() => []}
+ handleActiveFilterPanelChange={noop}
+ activeFilterPanelKeys={[`${FILTER_ID}-configuration`]}
+ isActive
+ setErroredFilters={() => []}
+ validateDependencies={noop}
+ getDependencySuggestion={() => ''}
+ />
+ </Form>
+ );
+ }
+
+ return render(<Harness />, {
+ useRedux: true,
+ initialState: {
+ dashboardInfo: { id: 1 },
+ datasources: {},
+ charts: {},
+ },
+ });
+}
+
+// Scopes queries to the Groupable columns multi-select. antd only renders
+// picked values (not the closed dropdown options) as selection items, so text
+// found inside this Form.Item reflects the current allowlist selection.
+async function getAllowlistScope() {
+ const label = await screen.findByText('Groupable columns');
+ const formItem = label.closest('.ant-form-item') as HTMLElement;
+ return within(formItem);
+}
+
+const customizationWithNarrowedAllowlist: ChartCustomization = {
+ id: FILTER_ID,
+ name: 'Group By',
+ filterType: ChartCustomizationPlugins.DynamicGroupBy,
+ type: ChartCustomizationType.ChartCustomization,
+ targets: [{ datasetId: 1 }],
+ scope: { rootPath: ['ROOT_ID'], excluded: [] },
+ controlValues: { columnsAllowlist: ['country'] },
+ defaultDataMask: {},
+};
+
+afterAll(() => {
+ fetchMock.clearHistory().removeRoutes();
+});
+
+test('shows the Groupable columns allowlist control for the Group By customization', async () => {
+ renderForm({ customizationToEdit: customizationWithNarrowedAllowlist });
+
+ await waitFor(() =>
+ expect(screen.getByText('Groupable columns')).toBeInTheDocument(),
+ );
+
+ // The previously-configured allowlist column is rendered as a selected value,
+ // proving the control reads back its persisted controlValues.columnsAllowlist.
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+});
+
+test('seeds a new control with every groupable column selected by default', async () => {
+ // No customizationToEdit and no configured allowlist: a freshly created
+ // control. Once the dataset's columns load, the allowlist should default to
+ // ALL groupable columns so builders start from "all selected".
+ renderForm();
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+ expect(await allowlist.findByText('state')).toBeInTheDocument();
+});
+
+test('does not overwrite an existing narrowed allowlist when editing', async () => {
+ // Editing a control that already narrowed the allowlist to a single column
+ // must keep that selection; the seeding default only applies to new controls.
+ renderForm({
+ customizationToEdit: customizationWithNarrowedAllowlist,
+ initialControlValues: { columnsAllowlist: ['country'] },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+ // 'state' is a valid option but was deliberately excluded, so it must not be
+ // seeded back in as a selected value.
+ await waitFor(() =>
+ expect(allowlist.queryByText('state')).not.toBeInTheDocument(),
+ );
+});
+
+test('offers and seeds columns whose filterable flag is null, matching the viewer', async () => {
+ renderForm({ datasetId: 2 });
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+ expect(await allowlist.findByText('legacy_col')).toBeInTheDocument();
+ // Explicitly non-filterable columns stay out, as in the viewer.
+ expect(allowlist.queryByText('internal_only')).not.toBeInTheDocument();
+});
+
+const legacyCustomization: ChartCustomization = {
+ ...customizationWithNarrowedAllowlist,
+ // Saved before the allowlist existed: no columnsAllowlist at all.
+ controlValues: { canSelectMultiple: true },
+};
+
+const saveFormValues = async (form: FormInstance<NativeFiltersForm>) => {
+ const values = (await form.validateFields()) as NativeFiltersForm;
+ return transformCustomizationForSave(
+ FILTER_ID,
+ values.filters[FILTER_ID] as unknown as ChartCustomizationsFormItem,
+ ) as ChartCustomization;
+};
+
+test('editing a legacy control leaves columnsAllowlist unset on save unless narrowed', async () => {
+ let form!: FormInstance<NativeFiltersForm>;
+ renderForm({
+ customizationToEdit: legacyCustomization,
+ initialControlValues: { canSelectMultiple: true },
+ onForm: f => {
+ form = f;
+ },
+ });
+
+ // The control shows "all selected" once the columns load...
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('state')).toBeInTheDocument();
+
+ // ...but saving without narrowing does not freeze that snapshot.
+ // (This harness registers no control-panel checkboxes, so only the
+ // allowlist field is part of the validated controlValues.)
+ const saved = await saveFormValues(form);
+ expect(saved.controlValues).not.toHaveProperty('columnsAllowlist');
+});
+
+test('saves a narrowed allowlist as-is', async () => {
+ let form!: FormInstance<NativeFiltersForm>;
+ renderForm({
+ customizationToEdit: customizationWithNarrowedAllowlist,
+ initialControlValues: { columnsAllowlist: ['country'] },
+ onForm: f => {
+ form = f;
+ },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+ await waitFor(() =>
+ expect(
+ (
+ form.getFieldValue('filters')?.[
+ FILTER_ID
+ ] as unknown as ChartCustomizationsFormItem
+ )?.groupableColumns,
+ ).toEqual(['country', 'state']),
+ );
+
+ const saved = await saveFormValues(form);
+ expect(saved.controlValues).toEqual({ columnsAllowlist: ['country'] });
+});
+
+test('loading the allowlist columns does not reset the unrelated column field', async () => {
+ let form!: FormInstance<NativeFiltersForm>;
+ renderForm({
+ customizationToEdit: legacyCustomization,
+ initialControlValues: { canSelectMultiple: true },
+ onForm: f => {
+ form = f;
+ },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('state')).toBeInTheDocument();
+ expect(form.getFieldValue(['filters', FILTER_ID, 'column'])).toBeUndefined();
+});
+
+async function pickDataset(tableName: string) {
+ const datasetSelect = screen.getByRole('combobox', { name: /dataset/i });
+ await userEvent.click(datasetSelect);
+ const option = await waitFor(() => {
+ // eslint-disable-next-line testing-library/no-node-access
+ const list = document.querySelector('.ant-select-dropdown-list');
+ if (!list) throw new Error('dataset list not open');
+ return within(list as HTMLElement).getByText(tableName);
+ });
+ await userEvent.click(option);
+}
+
+test("switching the dataset away and back never saves the other dataset's columns", async () => {
+ let form!: FormInstance<NativeFiltersForm>;
+ renderForm({
+ customizationToEdit: customizationWithNarrowedAllowlist,
+ initialControlValues: { columnsAllowlist: ['country'] },
+ onForm: f => {
+ form = f;
+ },
+ });
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+
+ await pickDataset('legacy_sales');
+ expect(await allowlist.findByText('legacy_col')).toBeInTheDocument();
+ await pickDataset('sales');
+ expect(await allowlist.findByText('state')).toBeInTheDocument();
+
+ const values = form.getFieldValue('filters')?.[
+ FILTER_ID
+ ] as unknown as ChartCustomizationsFormItem;
+ expect(values.controlValues?.columnsAllowlist).toEqual(['country', 'state']);
+ expect(values.groupableColumns).toEqual(['country', 'state']);
+ // "All selected" on the current dataset saves as unrestricted.
+ const saved = await saveFormValues(form);
+ expect(saved.controlValues).not.toHaveProperty('columnsAllowlist');
+});
+
+test('labels and searches allowlist columns by verbose name', async () => {
+ renderForm({ datasetId: 3 });
+
+ const allowlist = await getAllowlistScope();
+ // Seeded tags use the verbose name, falling back to column_name.
+ expect(await allowlist.findByText('Sales Region')).toBeInTheDocument();
+ expect(allowlist.getByText('country')).toBeInTheDocument();
+
+ const combobox = screen.getByRole('combobox', { name: 'Column select' });
+ await userEvent.type(combobox, 'Sales Reg');
+ expect(
+ await screen.findByRole('option', { name: 'Sales Region' }),
+ ).toBeInTheDocument();
+ await userEvent.clear(combobox);
+ await userEvent.type(combobox, 'region');
+ expect(
+ await screen.findByRole('option', { name: 'Sales Region' }),
+ ).toBeInTheDocument();
+});
+
+test('explains that an empty allowlist means every groupable column', async () => {
+ renderForm({
+ customizationToEdit: {
+ ...customizationWithNarrowedAllowlist,
+ controlValues: { columnsAllowlist: [] },
+ },
+ initialControlValues: { columnsAllowlist: [] },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(
+ await allowlist.findByText('All groupable columns'),
+ ).toBeInTheDocument();
+ expect(
+ allowlist.getByText(/including columns added to the dataset later/),
+ ).toBeInTheDocument();
+});
+
+test('warns about allowlisted columns the dataset no longer offers', async () => {
+ renderForm({
+ customizationToEdit: {
+ ...customizationWithNarrowedAllowlist,
+ controlValues: { columnsAllowlist: ['country', 'dropped_col'] },
+ },
+ initialControlValues: { columnsAllowlist: ['country', 'dropped_col'] },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(
+ await allowlist.findByText(
+ 'Not in this dataset, so viewers will not see: dropped_col',
+ ),
+ ).toBeInTheDocument();
+});
+
+test('does not warn when every allowlisted column is available', async () => {
+ renderForm({
+ customizationToEdit: customizationWithNarrowedAllowlist,
+ initialControlValues: { columnsAllowlist: ['country'] },
+ });
+
+ const allowlist = await getAllowlistScope();
+ expect(await allowlist.findByText('country')).toBeInTheDocument();
+ expect(
+ allowlist.queryByText(/so viewers will not see/),
+ ).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.test.ts
index 33f9f4f61ab..fdeac3b5379 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.test.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.test.ts
@@ -73,6 +73,115 @@ test('serializes a dataset-backed customization into a full ChartCustomization',
expect(result).not.toHaveProperty('defaultValueQueriesData');
});
+test('persists the Group By column allowlist from controlValues on save', () => {
+ const formItem = {
+ ...baseFormItem,
+ name: 'Group by',
+ filterType: 'chart_customization_dynamic_groupby',
+ dataset: { value: 42, label: 'sales' },
+ controlValues: {
+ canSelectMultiple: true,
+ columnsAllowlist: ['region', 'country'],
+ },
+ } as unknown as ChartCustomizationsFormItem;
+
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-allowlist',
+ formItem,
+ ) as ChartCustomization;
+
+ // The allowlist rides along in controlValues so it survives save/reload and
+ // dashboard export/import with no extra plumbing.
+ expect(result.controlValues).toEqual({
+ canSelectMultiple: true,
+ columnsAllowlist: ['region', 'country'],
+ });
+});
+
+const groupByFormItem = (
+ controlValues: Record<string, unknown>,
+ groupableColumns?: string[],
+) =>
+ ({
+ ...baseFormItem,
+ name: 'Group by',
+ filterType: 'chart_customization_dynamic_groupby',
+ dataset: { value: 42, label: 'sales' },
+ controlValues,
+ groupableColumns,
+ }) as unknown as ChartCustomizationsFormItem;
+
+test('collapses an allowlist that selects every groupable column back to unset on save', () => {
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-full',
+ groupByFormItem(
+ { canSelectMultiple: true, columnsAllowlist: ['country', 'region'] },
+ ['region', 'country'],
+ ),
+ ) as ChartCustomization;
+
+ // "All selected" is saved as "no restriction", so columns added to the
+ // dataset later stay available to viewers.
+ expect(result.controlValues).toEqual({ canSelectMultiple: true });
+ expect(result).not.toHaveProperty('groupableColumns');
+});
+
+test('keeps a narrowed allowlist on save', () => {
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-narrow',
+ groupByFormItem({ columnsAllowlist: ['country'] }, ['region', 'country']),
+ ) as ChartCustomization;
+
+ expect(result.controlValues).toEqual({ columnsAllowlist: ['country'] });
+ expect(result).not.toHaveProperty('groupableColumns');
+});
+
+test('keeps the allowlist when the groupable columns never loaded', () => {
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-unloaded',
+ groupByFormItem({ columnsAllowlist: ['country'] }),
+ ) as ChartCustomization;
+
+ expect(result.controlValues).toEqual({ columnsAllowlist: ['country'] });
+});
+
+const groupByDefault = (value: string[]) => ({
+ extraFormData: { custom_form_data: { groupby: value } },
+ filterState: { label: value.join(', '), value },
+});
+
+test('never saves a Group By default that the allowlist excludes', () => {
+ const formItem = {
+ ...groupByFormItem({ columnsAllowlist: ['country'] }, [
+ 'region',
+ 'country',
+ ]),
+ defaultDataMask: groupByDefault(['region', 'country']),
+ } as unknown as ChartCustomizationsFormItem;
+
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-default',
+ formItem,
+ ) as ChartCustomization;
+
+ expect(result.defaultDataMask).toEqual(groupByDefault(['country']));
+});
+
+test('never saves a Group By default on a non-groupable column', () => {
+ // No allowlist (all columns), but 'internal' is not in the groupable set.
+ const formItem = {
+ ...groupByFormItem({}, ['region', 'country']),
+ defaultDataMask: groupByDefault(['internal']),
+ } as unknown as ChartCustomizationsFormItem;
+
+ const result = transformCustomizationForSave(
+ 'CHART_CUSTOMIZATION-default-ng',
+ formItem,
+ ) as ChartCustomization;
+
+ expect(result.defaultDataMask?.filterState?.value).toBeNull();
+});
+
test('passes an already-saved ChartCustomization through untouched', () => {
const saved: ChartCustomization = {
id: 'CHART_CUSTOMIZATION-ghi',
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
index 6a7ff78633e..67868911c88 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
@@ -25,6 +25,12 @@ import {
Divider,
NativeFilterType,
} from '@superset-ui/core';
+import { omit } from 'lodash-es';
+import {
+ getAllowedGroupByColumns,
+ pruneGroupByDataMask,
+} from 'src/chartCustomizations/components/DynamicGroupBy/columnAllowlist';
+import { ChartCustomizationPlugins } from 'src/constants';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import {
ChartCustomizationsFormItem,
@@ -96,6 +102,52 @@ function buildCustomizationTarget(
return buildNativeFilterTarget(formInputs);
}
+/**
+ * A Group By allowlist that selects every groupable column is equivalent to
+ * "no restriction". Collapse it back to unset so the saved config does not
+ * freeze a snapshot of the dataset's columns: a column added to the dataset
+ * later then stays available to viewers, exactly like a control that never
+ * stored an allowlist.
+ */
+export function collapseFullColumnsAllowlist(
+ controlValues: ChartCustomizationsFormItem['controlValues'] | undefined,
+ groupableColumns: string[] | undefined,
+): ChartCustomizationsFormItem['controlValues'] {
+ const allowlist = controlValues?.columnsAllowlist;
+ if (
+ !controlValues ||
+ !Array.isArray(allowlist) ||
+ !groupableColumns?.length
+ ) {
+ return controlValues ?? {};
+ }
+ const selected = new Set<string>(allowlist);
+ if (!groupableColumns.every(column => selected.has(column))) {
+ return controlValues;
+ }
+ return omit(controlValues, 'columnsAllowlist');
+}
+
+/**
+ * A Group By default must not group viewers by a column the allowlist (or the
+ * dataset's groupable set) excludes. The default-value picker already prunes
+ * as the builder edits; this re-checks on save so an excluded default can
+ * never be persisted.
+ */
+function pruneGroupByDefault(formInputs: ChartCustomizationsFormItem) {
+ const mask = formInputs.defaultDataMask ?? {};
+ if (formInputs.filterType !== ChartCustomizationPlugins.DynamicGroupBy) {
+ return mask;
+ }
+ return pruneGroupByDataMask(
+ mask,
+ getAllowedGroupByColumns(
+ formInputs.controlValues?.columnsAllowlist,
+ formInputs.groupableColumns,
+ ),
+ );
+}
+
function transformFormInput(
id: string,
formInputs: ChartCustomizationsFormItem,
@@ -113,10 +165,13 @@ function transformFormInput(
description: (formInputs.description || '').trim(),
targets: [buildCustomizationTarget(formInputs)],
scope: formInputs.scope || defaultScope,
- controlValues: formInputs.controlValues ?? {},
+ controlValues: collapseFullColumnsAllowlist(
+ formInputs.controlValues,
+ formInputs.groupableColumns,
+ ),
defaultDataMask: buildNativeFilterDefaultDataMask(
formInputs,
- formInputs.defaultDataMask ?? {},
+ pruneGroupByDefault(formInputs),
),
removed: false,
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
index fb3e8c29b9c..5a27a0006d9 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
@@ -110,6 +110,10 @@ export interface ChartCustomizationsFormItem {
defaultValueQueriesData?: ColumnOption[] | null;
aggregation?: string;
canSelectMultiple?: boolean;
+ // Form-only: every column the Group By allowlist can offer, recorded when
+ // the dataset's columns load. Used on save to collapse an "all selected"
+ // allowlist back to unset; never persisted.
+ groupableColumns?: string[];
}
export interface ChartCustomizationsForm {
diff --git a/superset/translations/ar/LC_MESSAGES/messages.po b/superset/translations/ar/LC_MESSAGES/messages.po
index 3877f2afecb..81412249c57 100644
--- a/superset/translations/ar/LC_MESSAGES/messages.po
+++ b/superset/translations/ar/LC_MESSAGES/messages.po
@@ -2064,6 +2064,9 @@ msgstr "أبعاد"
msgid "All filters"
msgstr "كل عوامل التصفية"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "جميع اللوحات"
@@ -4740,6 +4743,9 @@ msgstr "عرض إجمالي الأعمدة"
msgid "Columns to show in the tooltip."
msgstr "تلميح أداة رأس العمود"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "الجمع بين المقاييس"
@@ -9095,6 +9101,9 @@ msgstr "اسم المجموعة"
msgid "Group remaining as \"Others\""
msgstr "تجميع الباقي كـ \"أخرى\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "تحديد النطاق"
@@ -10145,6 +10154,11 @@ msgstr "الأقل تعديلًا مؤخرًا"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11764,6 +11778,10 @@ msgstr "لم يتم العثور على الرسم البياني"
msgid "Not in"
msgstr "ليس في"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "ليست خالية"
@@ -17057,6 +17075,11 @@ msgstr "العمود الذي سيُستخدم كهدف للحافة."
msgid "The column was deleted or renamed in the database."
msgstr "تم حذف العمود أو إعادة تسميته في قاعدة البيانات."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/ca/LC_MESSAGES/messages.po b/superset/translations/ca/LC_MESSAGES/messages.po
index 187e128cbda..f0a56f96c48 100644
--- a/superset/translations/ca/LC_MESSAGES/messages.po
+++ b/superset/translations/ca/LC_MESSAGES/messages.po
@@ -1903,6 +1903,9 @@ msgstr "Dimensions"
msgid "All filters"
msgstr "Tots els filtres"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Tots els panells"
@@ -4558,6 +4561,9 @@ msgstr "Columnes a llegir"
msgid "Columns to show in the tooltip."
msgstr "Informació d'ajuda de l'encapçalament de columna"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combinar mètriques"
@@ -8740,6 +8746,9 @@ msgstr "Nom del grup"
msgid "Group remaining as \"Others\""
msgstr "Agrupa la resta com a \"Altres\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Àmbit d'aplicació"
@@ -9767,6 +9776,11 @@ msgstr "Menys recentment modificat"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11334,6 +11348,10 @@ msgstr "Gràfic no trobat"
msgid "Not in"
msgstr "No en"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "No nul"
@@ -16521,6 +16539,11 @@ msgstr "La columna a usar com a objectiu de la vora."
msgid "The column was deleted or renamed in the database."
msgstr "La columna va ser eliminada o reanomenada a la base de dades."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "La configuració per les capes del mapa"
diff --git a/superset/translations/cs/LC_MESSAGES/messages.po b/superset/translations/cs/LC_MESSAGES/messages.po
index 4b47fcda8c0..49a8017714f 100644
--- a/superset/translations/cs/LC_MESSAGES/messages.po
+++ b/superset/translations/cs/LC_MESSAGES/messages.po
@@ -1893,6 +1893,9 @@ msgstr "Dimenze"
msgid "All filters"
msgstr "Všechny filtry"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Všechny panely"
@@ -4412,6 +4415,9 @@ msgstr "Sloupce ke čtení"
msgid "Columns to show in the tooltip."
msgstr "Sloupce k zobrazení v tooltipu."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Kombinovat metriky"
@@ -8438,6 +8444,9 @@ msgstr "Název skupiny"
msgid "Group remaining as \"Others\""
msgstr "Zůstávající řádky seskupit do skupiny \"Ostatní\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Seskupení"
@@ -9420,6 +9429,11 @@ msgstr "Nejméně nedávno změněné"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10952,6 +10966,10 @@ msgstr "Graf nenalezen"
msgid "Not in"
msgstr "Není v"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Není null"
@@ -15983,6 +16001,11 @@ msgstr "Sloupec, který se má použít jako cíl hrany."
msgid "The column was deleted or renamed in the database."
msgstr "Sloupec byl v databázi smazán nebo přejmenován."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Konfigurace pro vrstvy mapy"
diff --git a/superset/translations/de/LC_MESSAGES/messages.po b/superset/translations/de/LC_MESSAGES/messages.po
index 6e0207aeb2c..8b6c9cc9eee 100644
--- a/superset/translations/de/LC_MESSAGES/messages.po
+++ b/superset/translations/de/LC_MESSAGES/messages.po
@@ -1836,6 +1836,9 @@ msgstr "Alle Dimensionen"
msgid "All filters"
msgstr "Alle Filter"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Alle Bereiche"
@@ -4334,6 +4337,9 @@ msgstr "Zu lesende Spalten"
msgid "Columns to show in the tooltip."
msgstr "Spalten, die im Tooltip angezeigt werden sollen."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Metriken kombinieren"
@@ -8252,6 +8258,9 @@ msgstr "Gruppenname"
msgid "Group remaining as \"Others\""
msgstr "Gruppe, die als \"Sonstige\" verbleibt"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Gruppierung"
@@ -9206,6 +9215,11 @@ msgstr "Zuletzt geändert"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10679,6 +10693,10 @@ msgstr "Nicht gefunden"
msgid "Not in"
msgstr "Nicht in"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Nicht NULL"
@@ -15571,6 +15589,11 @@ msgstr "Die Spalte, die als Ziel der Kante verwendet werden soll."
msgid "The column was deleted or renamed in the database."
msgstr "Die Spalte wurde in der Datenbank gelöscht oder umbenannt."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Die Konfiguration der Kartenebenen"
diff --git a/superset/translations/en/LC_MESSAGES/messages.po b/superset/translations/en/LC_MESSAGES/messages.po
index ce0b23362cc..02894b9cca2 100644
--- a/superset/translations/en/LC_MESSAGES/messages.po
+++ b/superset/translations/en/LC_MESSAGES/messages.po
@@ -1694,6 +1694,9 @@ msgstr ""
msgid "All filters"
msgstr ""
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr ""
@@ -3982,6 +3985,9 @@ msgstr ""
msgid "Columns to show in the tooltip."
msgstr ""
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr ""
@@ -7634,6 +7640,9 @@ msgstr ""
msgid "Group remaining as \"Others\""
msgstr ""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr ""
@@ -8550,6 +8559,11 @@ msgstr ""
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -9960,6 +9974,10 @@ msgstr ""
msgid "Not in"
msgstr ""
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr ""
@@ -14479,6 +14497,11 @@ msgstr ""
msgid "The column was deleted or renamed in the database."
msgstr ""
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr ""
diff --git a/superset/translations/es/LC_MESSAGES/messages.po b/superset/translations/es/LC_MESSAGES/messages.po
index 183c2839905..fe9d7d9a11a 100644
--- a/superset/translations/es/LC_MESSAGES/messages.po
+++ b/superset/translations/es/LC_MESSAGES/messages.po
@@ -1880,6 +1880,9 @@ msgstr "Todas las dimensiones"
msgid "All filters"
msgstr "Todos los filtros"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Todos los paneles"
@@ -4505,6 +4508,9 @@ msgstr "Columnas a leer"
msgid "Columns to show in the tooltip."
msgstr "Información sobre herramientas del encabezado de columna"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combinar métricas"
@@ -8638,6 +8644,9 @@ msgstr "Nombre del grupo"
msgid "Group remaining as \"Others\""
msgstr "Agrupar restantes como \"Otros\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Ejecutando alcance"
@@ -9659,6 +9668,11 @@ msgstr "Modificado menos recientemente"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11223,6 +11237,10 @@ msgstr "No se ha encontrado ningún gráfico"
msgid "Not in"
msgstr "No está en"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "No es nulo"
@@ -16382,6 +16400,11 @@ msgstr ""
"La columna se ha eliminado o se le ha cambiado el nombre en la base de "
"datos."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "La configuración de las capas del mapa"
diff --git a/superset/translations/fa/LC_MESSAGES/messages.po b/superset/translations/fa/LC_MESSAGES/messages.po
index 51e475045f2..f2b808ebf4f 100644
--- a/superset/translations/fa/LC_MESSAGES/messages.po
+++ b/superset/translations/fa/LC_MESSAGES/messages.po
@@ -1917,6 +1917,9 @@ msgstr "ابعاد"
msgid "All filters"
msgstr "تمام فیلترها"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "همه پنلها"
@@ -4558,6 +4561,9 @@ msgstr "ستونها را برای خواندن انتخاب کنید"
msgid "Columns to show in the tooltip."
msgstr "راهنمای تیتر ستون"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "ترکیب معیارها"
@@ -8776,6 +8782,9 @@ msgstr "نام گروه"
msgid "Group remaining as \"Others\""
msgstr "گروهبندی باقیمانده به عنوان \"سایرین\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "دامنهگذاری"
@@ -9821,6 +9830,11 @@ msgstr "آخرین تغییرات کمتر شده"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11413,6 +11427,10 @@ msgstr "نمودار یافت نشد"
msgid "Not in"
msgstr "نه در"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "غیر خالی"
@@ -16628,6 +16646,11 @@ msgstr "ستونی که قرار است به عنوان هدف لبه استفا
msgid "The column was deleted or renamed in the database."
msgstr "ستون در پایگاه داده حذف یا تغییر نام داده شده است."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/fi/LC_MESSAGES/messages.po b/superset/translations/fi/LC_MESSAGES/messages.po
index 242c23711cf..c49067a8fe0 100644
--- a/superset/translations/fi/LC_MESSAGES/messages.po
+++ b/superset/translations/fi/LC_MESSAGES/messages.po
@@ -2835,6 +2835,9 @@ msgstr "Dimensiot"
msgid "All filters"
msgstr "Kaikki suodattimet"
+msgid "All groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk, zh, zh_TW]
#, fuzzy
@@ -7020,6 +7023,9 @@ msgstr "Luettavat sarakkeet"
msgid "Columns to show in the tooltip."
msgstr "Työkaluvinkissä näytettävät sarakkeet."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13673,6 +13679,9 @@ msgstr "Ryhmän nimi"
msgid "Group remaining as \"Others\""
msgstr "Ryhmittele loput \"Muut\"-ryhmäksi"
+msgid "Groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: fr, ja, lv,
# ru, uk]
#, fuzzy
@@ -15352,6 +15361,11 @@ msgstr "Vähiten äskettäin muokattu"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -17905,6 +17919,10 @@ msgstr "Kaaviota ei löydy"
msgid "Not in"
msgstr "Ei sisälly"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -26113,6 +26131,11 @@ msgstr "Sarake, jota käytetään reunan kohteena."
msgid "The column was deleted or renamed in the database."
msgstr "Sarake poistettiin tai nimettiin uudelleen tietokannassa."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, es,
# fr, ja, lv, mi, ru, sk, uk]
#, fuzzy
diff --git a/superset/translations/fr/LC_MESSAGES/messages.po b/superset/translations/fr/LC_MESSAGES/messages.po
index cc426e82f30..a1587245af2 100644
--- a/superset/translations/fr/LC_MESSAGES/messages.po
+++ b/superset/translations/fr/LC_MESSAGES/messages.po
@@ -1853,6 +1853,9 @@ msgstr "Toutes les dimensions"
msgid "All filters"
msgstr "Tous les filtres"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Tous les panneaux"
@@ -4417,6 +4420,9 @@ msgstr "Colonnes à lire"
msgid "Columns to show in the tooltip."
msgstr "Colonnes à afficher dans l'infobulle"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combiner les mesures"
@@ -8385,6 +8391,9 @@ msgstr "Nom du groupe"
msgid "Group remaining as \"Others\""
msgstr "Grouper le reste sous « Autres »"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Grouper"
@@ -9354,6 +9363,11 @@ msgstr "Dernière modification en date"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10858,6 +10872,10 @@ msgstr "Non trouvé"
msgid "Not in"
msgstr "Pas dans"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Non nul"
@@ -15852,6 +15870,11 @@ msgstr "La colonne à utiliser comme cible de l'arête."
msgid "The column was deleted or renamed in the database."
msgstr "La colonne a été supprimée ou renommée dans la base de données."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "La configuration des couches de la carte"
diff --git a/superset/translations/it/LC_MESSAGES/messages.po b/superset/translations/it/LC_MESSAGES/messages.po
index 9793d833aea..d8faea190e0 100644
--- a/superset/translations/it/LC_MESSAGES/messages.po
+++ b/superset/translations/it/LC_MESSAGES/messages.po
@@ -2248,6 +2248,9 @@ msgstr "Tutte le dimensioni"
msgid "All filters"
msgstr "Tutti i filtri"
+msgid "All groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk,
# zh, zh_TW]
@@ -5347,6 +5350,9 @@ msgstr "Colonne da leggere"
msgid "Columns to show in the tooltip."
msgstr "Colonne da mostrare nel suggerimento."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combina metriche"
@@ -10235,6 +10241,9 @@ msgstr "Nome del gruppo"
msgid "Group remaining as \"Others\""
msgstr "Raggruppa i rimanenti come \"Altri\""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Raggruppamento"
@@ -11541,6 +11550,11 @@ msgstr "Modificato più di recente"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -13419,6 +13433,10 @@ msgstr "Non trovato"
msgid "Not in"
msgstr "Non in"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Non null"
@@ -19584,6 +19602,11 @@ msgstr "La colonna da usare come destinazione del bordo."
msgid "The column was deleted or renamed in the database."
msgstr "La colonna è stata cancellata o rinominata nel database."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "La configurazione dei livelli della mappa"
diff --git a/superset/translations/ja/LC_MESSAGES/messages.po b/superset/translations/ja/LC_MESSAGES/messages.po
index aa73a11235c..798a12871c2 100644
--- a/superset/translations/ja/LC_MESSAGES/messages.po
+++ b/superset/translations/ja/LC_MESSAGES/messages.po
@@ -1714,6 +1714,9 @@ msgstr "すべてのディメンション"
msgid "All filters"
msgstr "すべてのフィルター"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "すべてのパネル"
@@ -4007,6 +4010,9 @@ msgstr "読み込む列"
msgid "Columns to show in the tooltip."
msgstr "ツールチップに表示する列"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "メトリックを結合"
@@ -7652,6 +7658,9 @@ msgstr "グループ名"
msgid "Group remaining as \"Others\""
msgstr "残りの項目を「その他」にまとめる"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "グループ化"
@@ -8574,6 +8583,11 @@ msgstr "更新日が古い順"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -9982,6 +9996,10 @@ msgstr "見つかりません"
msgid "Not in"
msgstr "次を含まない (Not in)"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "NULLではない"
@@ -14536,6 +14554,11 @@ msgstr "エッジのターゲット(終点)として使用する列。"
msgid "The column was deleted or renamed in the database."
msgstr "データベース内で列が削除されたか、名前が変更されました。"
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "マップレイヤーの設定"
diff --git a/superset/translations/ko/LC_MESSAGES/messages.po b/superset/translations/ko/LC_MESSAGES/messages.po
index 367fdd9c7a7..ffd7b3f98af 100644
--- a/superset/translations/ko/LC_MESSAGES/messages.po
+++ b/superset/translations/ko/LC_MESSAGES/messages.po
@@ -2517,6 +2517,9 @@ msgstr "대시보드 저장"
msgid "All filters"
msgstr "필터"
+msgid "All groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk,
# zh, zh_TW]
@@ -6270,6 +6273,9 @@ msgstr "컬럼 보기"
msgid "Columns to show in the tooltip."
msgstr "툴팁에 표시할 열."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
#, fuzzy
msgid "Combine metrics"
msgstr "메트릭"
@@ -12180,6 +12186,9 @@ msgstr "그룹 이름"
msgid "Group remaining as \"Others\""
msgstr "나머지를 \"기타\"로 그룹화"
+msgid "Groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, fr, ja,
# lv, ru, sr, sr_Latn, uk]
#, fuzzy
@@ -13715,6 +13724,11 @@ msgstr "최근 수정 순서가 낮은"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -16090,6 +16104,10 @@ msgstr "데이터베이스를 찾을 수 없습니다."
msgid "Not in"
msgstr "주석"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -23520,6 +23538,11 @@ msgstr "에지의 대상으로 사용할 열입니다."
msgid "The column was deleted or renamed in the database."
msgstr "데이터베이스에서 열이 삭제되거나 이름이 변경되었습니다."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/lv/LC_MESSAGES/messages.po b/superset/translations/lv/LC_MESSAGES/messages.po
index a00a1b94942..7cb7bdaa04d 100644
--- a/superset/translations/lv/LC_MESSAGES/messages.po
+++ b/superset/translations/lv/LC_MESSAGES/messages.po
@@ -1882,6 +1882,9 @@ msgstr "Dimensijas"
msgid "All filters"
msgstr "Visi filtri"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Visi paneļi"
@@ -4365,6 +4368,9 @@ msgstr "Lasāmās kolonnas"
msgid "Columns to show in the tooltip."
msgstr "Kolonnas, kas jāparāda paskaidrē."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Apvienot rādītājus"
@@ -8287,6 +8293,9 @@ msgstr "Grupas nosaukums"
msgid "Group remaining as \"Others\""
msgstr "Grupēt atlikušos kā \"Others\""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Grupēšana"
@@ -9239,6 +9248,11 @@ msgstr "Visvecāk modificēts"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10744,6 +10758,10 @@ msgstr "Diagramma nav atrasta"
msgid "Not in"
msgstr "Neietilpst"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Nav tukšs"
@@ -15675,6 +15693,11 @@ msgstr "Kolonna, kuru izmantot kā malas mērķi."
msgid "The column was deleted or renamed in the database."
msgstr "Kolonna datubāzē tika dzēsta vai pārdēvēta."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Kartes slāņu konfigurācija"
diff --git a/superset/translations/messages.pot b/superset/translations/messages.pot
index 52211f75a29..26205a23420 100644
--- a/superset/translations/messages.pot
+++ b/superset/translations/messages.pot
@@ -1597,6 +1597,9 @@ msgstr ""
msgid "All filters"
msgstr ""
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr ""
@@ -3747,6 +3750,9 @@ msgstr ""
msgid "Columns to show in the tooltip."
msgstr ""
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr ""
@@ -7192,6 +7198,9 @@ msgstr ""
msgid "Group remaining as \"Others\""
msgstr ""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr ""
@@ -8073,6 +8082,9 @@ msgstr ""
msgid "Leave blank to keep the current password"
msgstr ""
+msgid "Leave empty, or keep every column selected, to let viewers group by any groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid "Leave on the deployment default unless you need to force asynchronous loading on or off for this dashboard."
msgstr ""
@@ -9412,6 +9424,10 @@ msgstr ""
msgid "Not in"
msgstr ""
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr ""
@@ -13628,6 +13644,9 @@ msgstr ""
msgid "The column was deleted or renamed in the database."
msgstr ""
+msgid "The columns allowed for this control are no longer in the dataset. Ask the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr ""
diff --git a/superset/translations/mi/LC_MESSAGES/messages.po b/superset/translations/mi/LC_MESSAGES/messages.po
index 2b2052a9e5e..1fa6c7027af 100644
--- a/superset/translations/mi/LC_MESSAGES/messages.po
+++ b/superset/translations/mi/LC_MESSAGES/messages.po
@@ -1903,6 +1903,9 @@ msgstr "Āhuahanga"
msgid "All filters"
msgstr "Tātari katoa"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Papa katoa"
@@ -4530,6 +4533,9 @@ msgstr "Tīwae hei pānui"
msgid "Columns to show in the tooltip."
msgstr "Kītukutuku pane tīwae"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Whakakotahi ine"
@@ -8677,6 +8683,9 @@ msgstr "Ingoa rōpū"
msgid "Group remaining as \"Others\""
msgstr "Whakaropu i ngā toenga hei \"Ētahi Atu\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Korahi"
@@ -9699,6 +9708,11 @@ msgstr "I whakarerekeahia tata nei iti rawa"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11257,6 +11271,10 @@ msgstr "Kāore i kitea te kauwhata"
msgid "Not in"
msgstr "Kāore i roto i"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Ehara i te kore"
@@ -16430,6 +16448,11 @@ msgstr "Te tīwae ka whakamahia hei whāinga o te tapa."
msgid "The column was deleted or renamed in the database."
msgstr "I mukua, i whakaingoahia-anō rānei te tīwae i te pātengi raraunga."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Te whirihora mō ngā papa mahere"
diff --git a/superset/translations/nl/LC_MESSAGES/messages.po b/superset/translations/nl/LC_MESSAGES/messages.po
index dbcd18bfec9..7d6a544d9f6 100644
--- a/superset/translations/nl/LC_MESSAGES/messages.po
+++ b/superset/translations/nl/LC_MESSAGES/messages.po
@@ -1934,6 +1934,9 @@ msgstr "Dimensies"
msgid "All filters"
msgstr "Alle filters"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Alle panelen"
@@ -4657,6 +4660,9 @@ msgstr "Kolommen om te Lezen"
msgid "Columns to show in the tooltip."
msgstr "Kolomkop tooltip"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combineer metrieken"
@@ -8975,6 +8981,9 @@ msgstr "Groepsnaam"
msgid "Group remaining as \"Others\""
msgstr "Resterende groeperen als \"Overige\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Scoping"
@@ -10039,6 +10048,11 @@ msgstr "Meest recente wijziging"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11659,6 +11673,10 @@ msgstr "Grafiek niet gevonden"
msgid "Not in"
msgstr "Niet in"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Niet null"
@@ -16979,6 +16997,11 @@ msgstr "De kolom die als doel van de verbinding moet worden gebruikt."
msgid "The column was deleted or renamed in the database."
msgstr "De kolom werd verwijderd of hernoemd in de database."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/pl/LC_MESSAGES/messages.po b/superset/translations/pl/LC_MESSAGES/messages.po
index 69900875636..195d296d10b 100644
--- a/superset/translations/pl/LC_MESSAGES/messages.po
+++ b/superset/translations/pl/LC_MESSAGES/messages.po
@@ -1986,6 +1986,9 @@ msgstr "Wymiary"
msgid "All filters"
msgstr "Wszystkie filtry"
+msgid "All groupable columns"
+msgstr ""
+
#, fuzzy
msgid "All panels"
msgstr "Wszystkie panele"
@@ -4693,6 +4696,9 @@ msgstr "Kolumny do odczytu"
msgid "Columns to show in the tooltip."
msgstr "Podpowiedź nagłówka kolumny"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
#, fuzzy
msgid "Combine metrics"
msgstr "Łączenie metryk"
@@ -9131,6 +9137,9 @@ msgstr "Nazwa grupy"
msgid "Group remaining as \"Others\""
msgstr "Grupuj pozostałe jako \"Inne\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Określanie zakresu"
@@ -10195,6 +10204,11 @@ msgstr "Najmniej ostatnio zmodyfikowany"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11825,6 +11839,10 @@ msgstr "Nie znaleziono wykresu"
msgid "Not in"
msgstr "Nie w"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Nie puste"
@@ -17313,6 +17331,11 @@ msgstr "Kolumna używana jako cel krawędzi."
msgid "The column was deleted or renamed in the database."
msgstr "Kolumna została usunięta lub przemianowana w bazie danych."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/pt/LC_MESSAGES/messages.po b/superset/translations/pt/LC_MESSAGES/messages.po
index c2659ee423e..65ac0b488fa 100644
--- a/superset/translations/pt/LC_MESSAGES/messages.po
+++ b/superset/translations/pt/LC_MESSAGES/messages.po
@@ -2324,6 +2324,9 @@ msgstr "Todas as dimensões"
msgid "All filters"
msgstr "Todos os filtros"
+msgid "All groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk,
# zh, zh_TW]
@@ -5549,6 +5552,9 @@ msgstr "Colunas para ler"
msgid "Columns to show in the tooltip."
msgstr "Colunas a serem mostradas na dica de ferramenta."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combinar métricas"
@@ -10607,6 +10613,9 @@ msgstr "Nome do grupo"
msgid "Group remaining as \"Others\""
msgstr "Grupo permanecendo como \"Outros\""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Agrupamento"
@@ -11901,6 +11910,11 @@ msgstr "Modificação mais recente"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -13909,6 +13923,10 @@ msgstr "Não encontrado"
msgid "Not in"
msgstr "Não está em"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -20293,6 +20311,11 @@ msgstr "A coluna a ser usada como alvo da ponte."
msgid "The column was deleted or renamed in the database."
msgstr "A coluna foi excluída ou renomeada no banco de dados."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
msgid "The configuration for the map layers"
diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.po b/superset/translations/pt_BR/LC_MESSAGES/messages.po
index 5bb60af1f2d..aa8d7aae101 100644
--- a/superset/translations/pt_BR/LC_MESSAGES/messages.po
+++ b/superset/translations/pt_BR/LC_MESSAGES/messages.po
@@ -1927,6 +1927,9 @@ msgstr "Todas as dimensões"
msgid "All filters"
msgstr "Todos os filtros"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Todos os painéis"
@@ -4650,6 +4653,9 @@ msgstr "Colunas para ler"
msgid "Columns to show in the tooltip."
msgstr "Colunas a serem mostradas na dica de ferramenta."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combinar métricas"
@@ -9001,6 +9007,9 @@ msgstr "Nome do grupo"
msgid "Group remaining as \"Others\""
msgstr "Grupo permanecendo como \"Outros\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Agrupamento"
@@ -10100,6 +10109,11 @@ msgstr "Modificação mais recente"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11720,6 +11734,10 @@ msgstr "Não encontrado"
msgid "Not in"
msgstr "Não está em"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Não nulo"
@@ -17122,6 +17140,11 @@ msgstr "A coluna a ser usada como alvo da ponte."
msgid "The column was deleted or renamed in the database."
msgstr "A coluna foi excluída ou renomeada no banco de dados."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/ro/LC_MESSAGES/messages.po b/superset/translations/ro/LC_MESSAGES/messages.po
index ec809aeaf8d..ceaf449964d 100644
--- a/superset/translations/ro/LC_MESSAGES/messages.po
+++ b/superset/translations/ro/LC_MESSAGES/messages.po
@@ -1992,6 +1992,9 @@ msgstr "Toate dimensiunile"
msgid "All filters"
msgstr "Toate filtrele"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Toate panourile"
@@ -4633,6 +4636,9 @@ msgstr "Coloane de citit"
msgid "Columns to show in the tooltip."
msgstr "Coloane de afișat în tooltip."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Combină indicatori"
@@ -8848,6 +8854,9 @@ msgstr "Numele grupului"
msgid "Group remaining as \"Others\""
msgstr "Grupează restul ca \"Altele\""
+msgid "Groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, fr, it,
# ja, lv, ru, sr, sr_Latn, uk]
#, fuzzy
@@ -9894,6 +9903,11 @@ msgstr "Cel mai puțin modificat recent"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11490,6 +11504,10 @@ msgstr "Negăsit"
msgid "Not in"
msgstr "Nu în"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Nu null"
@@ -16715,6 +16733,11 @@ msgstr "Coloana de folosit ca țintă a muchiei."
msgid "The column was deleted or renamed in the database."
msgstr "Coloana a fost ștearsă sau redenumită în baza de date."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Configurația pentru straturile hărții"
diff --git a/superset/translations/ru/LC_MESSAGES/messages.po b/superset/translations/ru/LC_MESSAGES/messages.po
index 1fe14864ae7..b998f821486 100644
--- a/superset/translations/ru/LC_MESSAGES/messages.po
+++ b/superset/translations/ru/LC_MESSAGES/messages.po
@@ -1843,6 +1843,9 @@ msgstr "Все измерения"
msgid "All filters"
msgstr "Все фильтры"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Все панели"
@@ -4302,6 +4305,9 @@ msgstr "Столбцы для чтения"
msgid "Columns to show in the tooltip."
msgstr "Столбцы для отображения во всплывающей подсказке."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Объединить меры"
@@ -8159,6 +8165,9 @@ msgstr "Имя группы"
msgid "Group remaining as \"Others\""
msgstr "Группировать остальное в \"Другое\""
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Группировка"
@@ -9106,6 +9115,11 @@ msgstr "Измененные давно"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10582,6 +10596,10 @@ msgstr "Не найдено"
msgid "Not in"
msgstr "Not in"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Не пусто"
@@ -15375,6 +15393,11 @@ msgstr "Целевой столбец для ребра графа."
msgid "The column was deleted or renamed in the database."
msgstr "Столбец был удален или переименован в базе данных"
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Настройки слоев карты"
diff --git a/superset/translations/sk/LC_MESSAGES/messages.po b/superset/translations/sk/LC_MESSAGES/messages.po
index dddfac498e6..2d2245be718 100644
--- a/superset/translations/sk/LC_MESSAGES/messages.po
+++ b/superset/translations/sk/LC_MESSAGES/messages.po
@@ -1887,6 +1887,9 @@ msgstr "Dimenze"
msgid "All filters"
msgstr "Všetky filtre"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Všetky panely"
@@ -4415,6 +4418,9 @@ msgstr "Stĺpce ke čtenie"
msgid "Columns to show in the tooltip."
msgstr "Stĺpce k zobrazenie v tooltipu."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Kombinovať metriky"
@@ -8441,6 +8447,9 @@ msgstr "Názov skupiny"
msgid "Group remaining as \"Others\""
msgstr "Zostávajúce riadky zoskupiť do skupiny \"Ostatné\""
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "Zoskupenie"
@@ -9425,6 +9434,11 @@ msgstr "Nejméne nedávno zmenenýé"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10961,6 +10975,10 @@ msgstr "Graf nenalezen"
msgid "Not in"
msgstr "Nie je v"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Nie je null"
@@ -15982,6 +16000,11 @@ msgstr "Stĺpec, ktorý sa má použít ako cieľ hrany."
msgid "The column was deleted or renamed in the database."
msgstr "Stĺpec bol v databázy zmazaný alebo přejmenován."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Konfigurácia pre vrstvy mapy"
diff --git a/superset/translations/sl/LC_MESSAGES/messages.po b/superset/translations/sl/LC_MESSAGES/messages.po
index 9ee197e0d84..bbd43fcd03e 100644
--- a/superset/translations/sl/LC_MESSAGES/messages.po
+++ b/superset/translations/sl/LC_MESSAGES/messages.po
@@ -1903,6 +1903,9 @@ msgstr "Vse dimenzije"
msgid "All filters"
msgstr "Vsi filtri"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Vsi paneli"
@@ -4419,6 +4422,9 @@ msgstr "Stolpci za branje"
msgid "Columns to show in the tooltip."
msgstr "Stolpci, prikazani v opisu orodja."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Združuj mere"
@@ -8415,6 +8421,9 @@ msgstr "Ime skupine"
msgid "Group remaining as \"Others\""
msgstr "Skupina ostane kot »Drugi«"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Združevanje v skupine"
@@ -9383,6 +9392,11 @@ msgstr "Zadnje spremenjeno"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10897,6 +10911,10 @@ msgstr "Ni najden"
msgid "Not in"
msgstr "Ne vsebuje (NOT IN)"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Ni null (IS NOT NULL)"
@@ -15838,6 +15856,11 @@ msgstr "Stolpec, ki bo uporabljen kot cilj povezave."
msgid "The column was deleted or renamed in the database."
msgstr "Stolpec je bil izbrisan ali preimenovan v podatkovni bazi."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
msgid "The configuration for the map layers"
diff --git a/superset/translations/sr/LC_MESSAGES/messages.po b/superset/translations/sr/LC_MESSAGES/messages.po
index 10856e39d35..8c9f2b94ded 100644
--- a/superset/translations/sr/LC_MESSAGES/messages.po
+++ b/superset/translations/sr/LC_MESSAGES/messages.po
@@ -2198,6 +2198,9 @@ msgstr "Све димензије"
msgid "All filters"
msgstr "Сви филтери"
+msgid "All groupable columns"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk, zh, zh_TW]
msgid "All panels"
msgstr "Сви панели"
@@ -5237,6 +5240,9 @@ msgstr "Колоне за читање"
msgid "Columns to show in the tooltip."
msgstr "Колоне за приказ у облачићу."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Combine metrics"
msgstr "Комбинуј метрике"
@@ -10054,6 +10060,9 @@ msgstr "Назив групе"
msgid "Group remaining as \"Others\""
msgstr "Групиши преостале као \"Остало\""
+msgid "Groupable columns"
+msgstr ""
+
# ru, uk]
msgid "Grouping"
msgstr "Груписање"
@@ -11242,6 +11251,11 @@ msgstr "Најмање недавно измењено"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -13049,6 +13063,10 @@ msgstr "Није пронађено"
msgid "Not in"
msgstr "Није у"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Not null"
msgstr "Није null"
@@ -18973,6 +18991,11 @@ msgstr "Колона која се користи као одредиште гр
msgid "The column was deleted or renamed in the database."
msgstr "Колона је обрисана или преименована у бази података."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# fr, ja, lv, mi, ru, sk, uk]
msgid "The configuration for the map layers"
msgstr "Конфигурација слојева мапе"
diff --git a/superset/translations/sr_Latn/LC_MESSAGES/messages.po b/superset/translations/sr_Latn/LC_MESSAGES/messages.po
index e63ff52ba77..0de3b92cf28 100644
--- a/superset/translations/sr_Latn/LC_MESSAGES/messages.po
+++ b/superset/translations/sr_Latn/LC_MESSAGES/messages.po
@@ -2199,6 +2199,9 @@ msgstr "Sve dimenzije"
msgid "All filters"
msgstr "Svi filteri"
+msgid "All groupable columns"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk, zh, zh_TW]
msgid "All panels"
msgstr "Svi paneli"
@@ -5240,6 +5243,9 @@ msgstr "Kolone za čitanje"
msgid "Columns to show in the tooltip."
msgstr "Kolone za prikaz u oblačiću."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Combine metrics"
msgstr "Kombinuj metrike"
@@ -10061,6 +10067,9 @@ msgstr "Naziv grupe"
msgid "Group remaining as \"Others\""
msgstr "Grupiši preostale kao \"Ostalo\""
+msgid "Groupable columns"
+msgstr ""
+
# ru, uk]
msgid "Grouping"
msgstr "Grupisanje"
@@ -11249,6 +11258,11 @@ msgstr "Najmanje nedavno izmenjeno"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -13056,6 +13070,10 @@ msgstr "Nije pronađeno"
msgid "Not in"
msgstr "Nije u"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Not null"
msgstr "Nije null"
@@ -18981,6 +18999,11 @@ msgstr "Kolona koja se koristi kao odredište grane."
msgid "The column was deleted or renamed in the database."
msgstr "Kolona je obrisana ili preimenovana u bazi podataka."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# fr, ja, lv, mi, ru, sk, uk]
msgid "The configuration for the map layers"
msgstr "Konfiguracija slojeva mape"
diff --git a/superset/translations/ta/LC_MESSAGES/messages.po b/superset/translations/ta/LC_MESSAGES/messages.po
index ee68ee82d0c..c10dcaa1dcf 100644
--- a/superset/translations/ta/LC_MESSAGES/messages.po
+++ b/superset/translations/ta/LC_MESSAGES/messages.po
@@ -1838,6 +1838,9 @@ msgstr "பரிமாணமா (Is dimension)"
msgid "All filters"
msgstr "அனைத்து வடிகட்டிகள்"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "அனைத்து பேனல்கள் (panels)"
@@ -4323,6 +4326,9 @@ msgstr "வாசிக்க வேண்டிய நெடுவரிசை
msgid "Columns to show in the tooltip."
msgstr "உதவிக்குறிப்பில் காட்ட வேண்டிய நெடுவரிசைகள்."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "அளவீடுகளை இணைக்கவும்"
@@ -8209,6 +8215,9 @@ msgstr "குழுவின் பெயர்"
msgid "Group remaining as \"Others\""
msgstr "மீதமுள்ளவற்றை \"ஏனையவை\" என குழுவாக்கு"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "குழுவாக்கம்"
@@ -9148,6 +9157,11 @@ msgstr "மிகக் குறைவாக மாற்றப்பட்ட
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10590,6 +10604,10 @@ msgstr "முடிவுகள் எதுவும் காணப்பட
msgid "Not in"
msgstr "இதில் இல்லை"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "காலியாக இல்லை (Not null)"
@@ -15300,6 +15318,11 @@ msgstr "விளிம்பின் இலக்காகப் பயன்
msgid "The column was deleted or renamed in the database."
msgstr "தரவுத்தளத்தில் நெடுவரிசை நீக்கப்பட்டது அல்லது மறுபெயரிடப்பட்டது."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "வரைபட அடுக்குகளுக்கான உள்ளமைப்பு"
diff --git a/superset/translations/th/LC_MESSAGES/messages.po b/superset/translations/th/LC_MESSAGES/messages.po
index 1529ff70028..caf48df59f8 100644
--- a/superset/translations/th/LC_MESSAGES/messages.po
+++ b/superset/translations/th/LC_MESSAGES/messages.po
@@ -2758,6 +2758,9 @@ msgstr "มิติข้อมูล"
msgid "All filters"
msgstr "ตัวกรองทั้งหมด"
+msgid "All groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk, zh, zh_TW]
#, fuzzy
@@ -6883,6 +6886,9 @@ msgstr "คอลัมน์ที่จะอ่าน"
msgid "Columns to show in the tooltip."
msgstr "คอลัมน์ที่จะแสดงในคำอธิบาย"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13478,6 +13484,9 @@ msgstr "ชื่อกลุ่ม"
msgid "Group remaining as \"Others\""
msgstr "จัดกลุ่มที่เหลือเป็น \"อื่นๆ\""
+msgid "Groupable columns"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: fr, ja, lv,
# ru, uk]
#, fuzzy
@@ -15151,6 +15160,11 @@ msgstr "แก้ไขเมื่อนานที่สุด"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -17686,6 +17700,10 @@ msgstr "ไม่พบแผนภูมิ"
msgid "Not in"
msgstr "ไม่อยู่ใน"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -25839,6 +25857,11 @@ msgstr "คอลัมน์ที่จะใช้เป็นปลายท
msgid "The column was deleted or renamed in the database."
msgstr "คอลัมน์ถูกลบหรือเปลี่ยนชื่อในฐานข้อมูล"
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, es,
# fr, ja, lv, mi, ru, sk, uk]
#, fuzzy
diff --git a/superset/translations/tr/LC_MESSAGES/messages.po b/superset/translations/tr/LC_MESSAGES/messages.po
index 1077ae31d73..3a0b8b64e42 100644
--- a/superset/translations/tr/LC_MESSAGES/messages.po
+++ b/superset/translations/tr/LC_MESSAGES/messages.po
@@ -1888,6 +1888,9 @@ msgstr "Tüm boyutlar"
msgid "All filters"
msgstr "Tüm filtreler"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Tüm paneller"
@@ -4509,6 +4512,9 @@ msgstr "Kolonlar bulunamadı"
msgid "Columns to show in the tooltip."
msgstr "Takımda göstermek için sütunlar."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Ekle metrics"
@@ -8650,6 +8656,9 @@ msgstr "Grup adı"
msgid "Group remaining as \"Others\""
msgstr "Grup \"Diğerleri\" olarak kaldı"
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "çalışıyor"
@@ -9657,6 +9666,11 @@ msgstr "Least son zamanlarda değiştirilmiş"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11240,6 +11254,10 @@ msgstr "Bulunamadı"
msgid "Not in"
msgstr "Değil"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Çıplak Değil"
@@ -16402,6 +16420,11 @@ msgstr "Köşe kenar hedefi olarak kullanılır."
msgid "The column was deleted or renamed in the database."
msgstr "sütun veritabanında silindi veya değiştirildi."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Harita katmanları için yapılandırma"
diff --git a/superset/translations/uk/LC_MESSAGES/messages.po b/superset/translations/uk/LC_MESSAGES/messages.po
index cd301468c63..bcf2469fe8e 100644
--- a/superset/translations/uk/LC_MESSAGES/messages.po
+++ b/superset/translations/uk/LC_MESSAGES/messages.po
@@ -1900,6 +1900,9 @@ msgstr "Виміри"
msgid "All filters"
msgstr "Усі фільтри"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "Усі панелі"
@@ -4371,6 +4374,9 @@ msgstr "Стовпці, які читати"
msgid "Columns to show in the tooltip."
msgstr "Стовпці, які показувати у підказках."
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "Поєднати показники"
@@ -8299,6 +8305,9 @@ msgstr "Назва групи"
msgid "Group remaining as \"Others\""
msgstr "Згрупувати решту як «Інші»"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "Групування"
@@ -9247,6 +9256,11 @@ msgstr "Давно без змін"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -10748,6 +10762,10 @@ msgstr "Діаграму не знайдено"
msgid "Not in"
msgstr "Не в"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "Не порожній"
@@ -15682,6 +15700,11 @@ msgstr "Стовпець, який використовується як кін
msgid "The column was deleted or renamed in the database."
msgstr "Стовпець вилучено або перейменовано у базі даних."
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
msgid "The configuration for the map layers"
msgstr "Налаштування шарів карти"
diff --git a/superset/translations/zh/LC_MESSAGES/messages.po b/superset/translations/zh/LC_MESSAGES/messages.po
index 391e6a0621d..89f7aa9310b 100644
--- a/superset/translations/zh/LC_MESSAGES/messages.po
+++ b/superset/translations/zh/LC_MESSAGES/messages.po
@@ -1892,6 +1892,9 @@ msgstr "所有维度"
msgid "All filters"
msgstr "所有过滤器"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "所有面板"
@@ -4422,6 +4425,9 @@ msgstr "要读取的列"
msgid "Columns to show in the tooltip."
msgstr "工具提示中要显示的列。"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "整合指标"
@@ -8504,6 +8510,9 @@ msgstr "分组名称"
msgid "Group remaining as \"Others\""
msgstr "将剩余分组保留为“其他”"
+msgid "Groupable columns"
+msgstr ""
+
msgid "Grouping"
msgstr "分组"
@@ -9527,6 +9536,11 @@ msgstr "最近修改时间最久"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11119,6 +11133,10 @@ msgstr "未找到"
msgid "Not in"
msgstr "不在"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "非空"
@@ -16170,6 +16188,11 @@ msgstr "用作边目标的列。"
msgid "The column was deleted or renamed in the database."
msgstr "该列已在数据库中删除或重命名。"
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/zh_TW/LC_MESSAGES/messages.po b/superset/translations/zh_TW/LC_MESSAGES/messages.po
index 058e6e1b72c..b54c15ca252 100644
--- a/superset/translations/zh_TW/LC_MESSAGES/messages.po
+++ b/superset/translations/zh_TW/LC_MESSAGES/messages.po
@@ -1965,6 +1965,9 @@ msgstr "维度"
msgid "All filters"
msgstr "所有過濾器"
+msgid "All groupable columns"
+msgstr ""
+
msgid "All panels"
msgstr "應用於所有面板"
@@ -4673,6 +4676,9 @@ msgstr "要讀取的列"
msgid "Columns to show in the tooltip."
msgstr "列標題提示"
+msgid "Columns viewers can choose to group charts by."
+msgstr ""
+
msgid "Combine metrics"
msgstr "整合指標"
@@ -9054,6 +9060,9 @@ msgstr "群組名稱"
msgid "Group remaining as \"Others\""
msgstr "將剩餘項目分組為「其他」"
+msgid "Groupable columns"
+msgstr ""
+
#, fuzzy
msgid "Grouping"
msgstr "範圍"
@@ -10160,6 +10169,11 @@ msgstr "最近未經常修改"
msgid "Leave blank to keep the current password"
msgstr ""
+msgid ""
+"Leave empty, or keep every column selected, to let viewers group by any "
+"groupable column, including columns added to the dataset later."
+msgstr ""
+
msgid ""
"Leave on the deployment default unless you need to force asynchronous "
"loading on or off for this dashboard."
@@ -11851,6 +11865,10 @@ msgstr ""
msgid "Not in"
msgstr "Not In"
+#, python-format
+msgid "Not in this dataset, so viewers will not see: %s"
+msgstr ""
+
msgid "Not null"
msgstr "非空"
@@ -17241,6 +17259,11 @@ msgstr "要作為邊的目標的欄位。"
msgid "The column was deleted or renamed in the database."
msgstr "該列已在資料庫中删除或重命名。"
+msgid ""
+"The columns allowed for this control are no longer in the dataset. Ask "
+"the dashboard owner to update it."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ca, cs, de,
# es, fr, ja, lv, mi, ro, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy