feat: Dynamic currency (#36416)

This commit is contained in:
Richard Fogaca Nienkotter
2026-01-17 02:58:41 -03:00
committed by GitHub
parent 896947c787
commit f4474b2e3e
72 changed files with 3068 additions and 173 deletions

View File

@@ -22,15 +22,18 @@ import { t } from '@apache-superset/core';
import {
AdhocMetric,
BinaryQueryObjectFilterClause,
Currency,
CurrencyFormatter,
DataRecordValue,
FeatureFlag,
getColumnLabel,
getNumberFormatter,
getSelectedText,
hasMixedCurrencies,
isAdhocColumn,
isFeatureEnabled,
isPhysicalColumn,
normalizeCurrency,
NumberFormatter,
} from '@superset-ui/core';
import { styled, useTheme } from '@apache-superset/core/ui';
@@ -101,6 +104,71 @@ const StyledMinusSquareOutlined = styled(MinusSquareOutlined)`
stroke-width: 16px;
`;
/** Aggregator with currency tracking support */
interface CurrencyTrackingAggregator {
getCurrencies?: () => string[];
}
type BaseFormatter = NumberFormatter | CurrencyFormatter;
/** Create formatter that handles AUTO mode with per-cell currency detection */
const createCurrencyAwareFormatter = (
baseFormatter: BaseFormatter,
currencyConfig: Currency | undefined,
d3Format: string,
fallbackCurrency?: string,
): ((value: number, aggregator?: CurrencyTrackingAggregator) => string) => {
const isAutoMode = currencyConfig?.symbol === 'AUTO';
return (value: number, aggregator?: CurrencyTrackingAggregator): string => {
// If not AUTO mode, use base formatter directly
if (!isAutoMode) {
return baseFormatter(value);
}
// AUTO mode: check aggregator for currency tracking
if (aggregator && typeof aggregator.getCurrencies === 'function') {
const currencies = aggregator.getCurrencies();
if (currencies && currencies.length > 0) {
if (hasMixedCurrencies(currencies)) {
return getNumberFormatter(d3Format)(value);
}
const detectedCurrency = normalizeCurrency(currencies[0]);
if (detectedCurrency && currencyConfig) {
const cellFormatter = new CurrencyFormatter({
currency: {
symbol: detectedCurrency,
symbolPosition: currencyConfig.symbolPosition,
},
d3Format,
});
return cellFormatter(value);
}
}
}
// Fallback: use detected_currency from API response if available
if (fallbackCurrency && currencyConfig) {
const normalizedFallback = normalizeCurrency(fallbackCurrency);
if (normalizedFallback) {
const fallbackFormatter = new CurrencyFormatter({
currency: {
symbol: normalizedFallback,
symbolPosition: currencyConfig.symbolPosition,
},
d3Format,
});
return fallbackFormatter(value);
}
}
// Final fallback to neutral format
return getNumberFormatter(d3Format)(value);
};
};
const aggregatorsFactory = (formatter: NumberFormatter) => ({
Count: aggregatorTemplates.count(formatter),
'Count Unique Values': aggregatorTemplates.countUnique(formatter),
@@ -171,6 +239,8 @@ export default function PivotTableChart(props: PivotTableProps) {
rowSubTotals,
valueFormat,
currencyFormat,
currencyCodeColumn,
detectedCurrency,
emitCrossFilters,
setDataMask,
selectedFilters,
@@ -186,9 +256,11 @@ export default function PivotTableChart(props: PivotTableProps) {
} = props;
const theme = useTheme();
const defaultFormatter = useMemo(
// Base formatter without currency-awareness (for non-AUTO mode or as fallback)
const baseFormatter = useMemo(
() =>
currencyFormat?.symbol
currencyFormat?.symbol && currencyFormat.symbol !== 'AUTO'
? new CurrencyFormatter({
currency: currencyFormat,
d3Format: valueFormat,
@@ -196,6 +268,18 @@ export default function PivotTableChart(props: PivotTableProps) {
: getNumberFormatter(valueFormat),
[valueFormat, currencyFormat],
);
// Currency-aware formatter for AUTO mode support
const defaultFormatter = useMemo(
() =>
createCurrencyAwareFormatter(
baseFormatter,
currencyFormat,
valueFormat,
detectedCurrency ?? undefined,
),
[baseFormatter, currencyFormat, valueFormat, detectedCurrency],
);
const customFormatsArray = useMemo(
() =>
Array.from(
@@ -216,19 +300,31 @@ export default function PivotTableChart(props: PivotTableProps) {
hasCustomMetricFormatters
? {
[METRIC_KEY]: Object.fromEntries(
customFormatsArray.map(([metric, d3Format, currency]) => [
metric,
currency
? new CurrencyFormatter({
currency,
d3Format,
})
: getNumberFormatter(d3Format),
]),
customFormatsArray.map(([metric, d3Format, currency]) => {
// Create base formatter
const metricBaseFormatter =
currency && (currency as Currency).symbol !== 'AUTO'
? new CurrencyFormatter({
currency: currency as Currency,
d3Format: d3Format as string,
})
: getNumberFormatter(d3Format as string);
// Wrap with currency-aware formatter for AUTO mode support
return [
metric,
createCurrencyAwareFormatter(
metricBaseFormatter,
currency as Currency | undefined,
d3Format as string,
detectedCurrency ?? undefined,
),
];
}),
),
}
: undefined,
[customFormatsArray, hasCustomMetricFormatters],
[customFormatsArray, hasCustomMetricFormatters, detectedCurrency],
);
const metricNames = useMemo(
@@ -249,12 +345,14 @@ export default function PivotTableChart(props: PivotTableProps) {
...record,
[METRIC_KEY]: name,
value: record[name],
// Mark currency column for per-cell currency detection in aggregators
__currencyColumn: currencyCodeColumn,
}))
.filter(record => record.value !== null),
],
[],
),
[data, metricNames],
[data, metricNames, currencyCodeColumn],
);
const groupbyRows = useMemo(
() => groupbyRowsRaw.map(getColumnLabel),