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

@@ -82,6 +82,7 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
});
}
const theme = useTheme();
const columnConfigs = useMemo(() => {
const configs: Record<string, ColumnConfigInfo> = {};
colnames?.forEach((col, idx) => {
@@ -100,6 +101,7 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
const [showAllColumns, setShowAllColumns] = useState(false);
const getColumnInfo = (col: string) => columnConfigs[col] || {};
const setColumnConfig = (col: string, config: T) => {
if (onChange) {
// Only keep configs for known columns

View File

@@ -168,7 +168,7 @@ const currencyFormat: ControlFormItemSpec<'CurrencyControl'> = {
controlType: 'CurrencyControl',
label: t('Currency format'),
description: t(
'Customize chart metrics or columns with currency symbols as prefixes or suffixes. Choose a symbol from dropdown or type your own.',
"Format metrics or columns with currency symbols as prefixes or suffixes. Choose a symbol manually or use 'Auto-detect' to apply the correct symbol based on the dataset's currency code column. When multiple currencies are present, formatting falls back to neutral numbers.",
),
debounceDelay: 200,
};

View File

@@ -0,0 +1,38 @@
/**
* 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 { render } from 'spec/helpers/testing-library';
import { CurrencyControl } from './CurrencyControl';
test('CurrencyControl renders position and symbol selects', () => {
const { container } = render(
<CurrencyControl onChange={jest.fn()} value={{}} />,
{
useRedux: true,
initialState: {
common: { currencies: ['USD', 'EUR'] },
explore: { datasource: {} },
},
},
);
expect(
container.querySelector('[data-test="currency-control-container"]'),
).toBeInTheDocument();
expect(container.querySelectorAll('.ant-select')).toHaveLength(2);
});

View File

@@ -16,14 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { t } from '@apache-superset/core';
import { Currency, ensureIsArray, getCurrencySymbol } from '@superset-ui/core';
import { css, styled } from '@apache-superset/core/ui';
import { css, styled, useTheme } from '@apache-superset/core/ui';
import { CSSObject } from '@emotion/react';
import { Select, type SelectProps } from '@superset-ui/core/components';
import { ViewState } from 'src/views/types';
import { ExplorePageState } from 'src/explore/types';
import ControlHeader from '../../ControlHeader';
export interface CurrencyControlProps {
@@ -67,19 +68,74 @@ export const CurrencyControl = ({
currencySelectAdditionalStyles,
...props
}: CurrencyControlProps) => {
const theme = useTheme();
const currencies = useSelector<ViewState, string[]>(
state => state.common?.currencies,
);
const currenciesOptions = useMemo(
() =>
ensureIsArray(currencies).map(currencyCode => ({
value: currencyCode,
label: `${getCurrencySymbol({
symbol: currencyCode,
})} (${currencyCode})`,
})),
[currencies],
const currencyCodeColumn = useSelector<ExplorePageState, string | undefined>(
state => state?.explore?.datasource?.currency_code_column,
);
const currenciesOptions = useMemo(() => {
const options = ensureIsArray(currencies).map(currencyCode => ({
value: currencyCode,
label: `${getCurrencySymbol({
symbol: currencyCode,
})} (${currencyCode})`,
}));
const autoDetectOption = currencyCodeColumn
? [
{
value: 'AUTO',
label: t('Auto-detect'),
className: 'currency-auto-detect-option',
},
]
: [];
return [
...autoDetectOption,
...options,
{ value: '', label: t('Custom...') },
];
}, [currencies, currencyCodeColumn]);
const currencySortComparator = useCallback(
(
a: { value?: string | number },
b: { value?: string | number },
): number => {
if (a.value === 'AUTO') return -1;
if (b.value === 'AUTO') return 1;
if (a.value === '') return 1;
if (b.value === '') return -1;
const labelA = String(a.value ?? '');
const labelB = String(b.value ?? '');
return labelA.localeCompare(labelB);
},
[],
);
const renderCurrencyPopup = useMemo(
() =>
currencyCodeColumn
? (menu: React.ReactNode) => (
<div
css={css`
.currency-auto-detect-option {
border-bottom: 1px solid ${theme.colorBorderSecondary};
margin-bottom: ${theme.sizeUnit}px;
}
`}
>
{menu}
</div>
)
: undefined,
[currencyCodeColumn, theme],
);
return (
<>
<ControlHeader {...props} />
@@ -92,7 +148,7 @@ export const CurrencyControl = ({
${currencySelectAdditionalStyles};
}
`}
className="currency-control-container"
data-test="currency-control-container"
>
<Select
ariaLabel={t('Currency prefix or suffix')}
@@ -117,6 +173,8 @@ export const CurrencyControl = ({
value={currency?.symbol}
allowClear
allowNewOptions
sortComparator={currencySortComparator}
popupRender={renderCurrencyPopup}
{...currencySelectOverrideProps}
/>
</CurrencyControlContainer>