Skip to content

Commit d11726e

Browse files
Pollepsjoepio
authored andcommitted
#764 Add option to format numbers as currency in tables
1 parent 1abdc43 commit d11726e

File tree

11 files changed

+230
-56
lines changed

11 files changed

+230
-56
lines changed

browser/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This changelog covers all three packages, as they are (for now) updated as a who
1212
- [#758](https://github.com/atomicdata-dev/atomic-server/issues/758) Fix Relation column forms to close when clicking on the searchbox
1313
- [#780](https://github.com/atomicdata-dev/atomic-server/issues/780) Use tags in ontology editor to create enum properties.
1414
- [#810](https://github.com/atomicdata-dev/atomic-server/issues/810) Add button to resource selectors to navigate to the selected resource.
15+
- [#764](https://github.com/atomicdata-dev/atomic-server/issues/764) Add option to format numbers as currency in tables.
1516
- Fix server not rebuilding client when files changed.
1617
- Added persistent scrollbar to table
1718
- Improved table header UX
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { FC, useEffect } from 'react';
2+
import { getSupportedCurrencyList } from './currencies';
3+
import { BasicSelect } from '../../components/forms/BasicSelect';
4+
import { Resource, dataBrowser, useString } from '@tomic/react';
5+
6+
interface CurrencyPickerProps {
7+
resource: Resource;
8+
}
9+
10+
const supportedCurrencies = getSupportedCurrencyList();
11+
12+
const getSymbol = (code: string) => {
13+
return new Intl.NumberFormat('default', {
14+
style: 'currency',
15+
currency: code,
16+
currencyDisplay: 'narrowSymbol',
17+
})
18+
.formatToParts(0)
19+
.find(part => part.type === 'currency')?.value;
20+
};
21+
22+
const CurrencyPicker: FC<CurrencyPickerProps> = ({ resource }) => {
23+
const [currency, setCurrency] = useString(
24+
resource,
25+
dataBrowser.properties.currency,
26+
);
27+
28+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
29+
setCurrency(e.target.value);
30+
};
31+
32+
useEffect(() => {
33+
if (currency === undefined) {
34+
setCurrency('EUR');
35+
}
36+
}, []);
37+
38+
return (
39+
<BasicSelect defaultValue={currency ?? 'EUR'} onChange={handleChange}>
40+
{supportedCurrencies.map(c => (
41+
<option
42+
key={c.code}
43+
value={c.code}
44+
label={`${c.code} ${c.name ?? ''} (${getSymbol(c.code)})`}
45+
>
46+
{c.code}
47+
</option>
48+
))}
49+
</BasicSelect>
50+
);
51+
};
52+
53+
export default CurrencyPicker;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Data taken from https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml
2+
const currencyNames = JSON.parse(
3+
`{"AFN":"Afghani","EUR":"Euro","ALL":"Lek","DZD":"Algerian Dinar","USD":"US Dollar","AOA":"Kwanza","XCD":"East Caribbean Dollar","ARS":"Argentine Peso","AMD":"Armenian Dram","AWG":"Aruban Florin","AUD":"Australian Dollar","AZN":"Azerbaijan Manat","BSD":"Bahamian Dollar","BHD":"Bahraini Dinar","BDT":"Taka","BBD":"Barbados Dollar","BYN":"Belarusian Ruble","BZD":"Belize Dollar","XOF":"CFA Franc BCEAO","BMD":"Bermudian Dollar","INR":"Indian Rupee","BTN":"Ngultrum","BOB":"Boliviano","BOV":"Mvdol","BAM":"Convertible Mark","BWP":"Pula","NOK":"Norwegian Krone","BRL":"Brazilian Real","BND":"Brunei Dollar","BGN":"Bulgarian Lev","BIF":"Burundi Franc","CVE":"Cabo Verde Escudo","KHR":"Riel","XAF":"CFA Franc BEAC","CAD":"Canadian Dollar","KYD":"Cayman Islands Dollar","CLP":"Chilean Peso","CLF":"Unidad de Fomento","CNY":"Yuan Renminbi","COP":"Colombian Peso","COU":"Unidad de Valor Real","KMF":"Comorian Franc ","CDF":"Congolese Franc","NZD":"New Zealand Dollar","CRC":"Costa Rican Colon","CUP":"Cuban Peso","CUC":"Peso Convertible","ANG":"Netherlands Antillean Guilder","CZK":"Czech Koruna","DKK":"Danish Krone","DJF":"Djibouti Franc","DOP":"Dominican Peso","EGP":"Egyptian Pound","SVC":"El Salvador Colon","ERN":"Nakfa","SZL":"Lilangeni","ETB":"Ethiopian Birr","FKP":"Falkland Islands Pound","FJD":"Fiji Dollar","XPF":"CFP Franc","GMD":"Dalasi","GEL":"Lari","GHS":"Ghana Cedi","GIP":"Gibraltar Pound","GTQ":"Quetzal","GBP":"Pound Sterling","GNF":"Guinean Franc","GYD":"Guyana Dollar","HTG":"Gourde","HNL":"Lempira","HKD":"Hong Kong Dollar","HUF":"Forint","ISK":"Iceland Krona","IDR":"Rupiah","XDR":"SDR (Special Drawing Right)","IRR":"Iranian Rial","IQD":"Iraqi Dinar","ILS":"New Israeli Sheqel","JMD":"Jamaican Dollar","JPY":"Yen","JOD":"Jordanian Dinar","KZT":"Tenge","KES":"Kenyan Shilling","KPW":"North Korean Won","KRW":"Won","KWD":"Kuwaiti Dinar","KGS":"Som","LAK":"Lao Kip","LBP":"Lebanese Pound","LSL":"Loti","ZAR":"Rand","LRD":"Liberian Dollar","LYD":"Libyan Dinar","CHF":"Swiss Franc","MOP":"Pataca","MKD":"Denar","MGA":"Malagasy Ariary","MWK":"Malawi Kwacha","MYR":"Malaysian Ringgit","MVR":"Rufiyaa","MRU":"Ouguiya","MUR":"Mauritius Rupee","XUA":"ADB Unit of Account","MXN":"Mexican Peso","MXV":"Mexican Unidad de Inversion (UDI)","MDL":"Moldovan Leu","MNT":"Tugrik","MAD":"Moroccan Dirham","MZN":"Mozambique Metical","MMK":"Kyat","NAD":"Namibia Dollar","NPR":"Nepalese Rupee","NIO":"Cordoba Oro","NGN":"Naira","OMR":"Rial Omani","PKR":"Pakistan Rupee","PAB":"Balboa","PGK":"Kina","PYG":"Guarani","PEN":"Sol","PHP":"Philippine Peso","PLN":"Zloty","QAR":"Qatari Rial","RON":"Romanian Leu","RUB":"Russian Ruble","RWF":"Rwanda Franc","SHP":"Saint Helena Pound","WST":"Tala","STN":"Dobra","SAR":"Saudi Riyal","RSD":"Serbian Dinar","SCR":"Seychelles Rupee","SLE":"Leone","SGD":"Singapore Dollar","XSU":"Sucre","SBD":"Solomon Islands Dollar","SOS":"Somali Shilling","SSP":"South Sudanese Pound","LKR":"Sri Lanka Rupee","SDG":"Sudanese Pound","SRD":"Surinam Dollar","SEK":"Swedish Krona","CHE":"WIR Euro","CHW":"WIR Franc","SYP":"Syrian Pound","TWD":"New Taiwan Dollar","TJS":"Somoni","TZS":"Tanzanian Shilling","THB":"Baht","TOP":"Pa’anga","TTD":"Trinidad and Tobago Dollar","TND":"Tunisian Dinar","TRY":"Turkish Lira","TMT":"Turkmenistan New Manat","UGX":"Uganda Shilling","UAH":"Hryvnia","AED":"UAE Dirham","USN":"US Dollar (Next day)","UYU":"Peso Uruguayo","UYI":"Uruguay Peso en Unidades Indexadas (UI)","UYW":"Unidad Previsional","UZS":"Uzbekistan Sum","VUV":"Vatu","VES":"Bolívar Soberano","VED":"Bolívar Soberano","VND":"Dong","YER":"Yemeni Rial","ZMW":"Zambian Kwacha","ZWL":"Zimbabwe Dollar","XBA":"Bond Markets Unit European Composite Unit (EURCO)","XBB":"Bond Markets Unit European Monetary Unit (E.M.U.-6)","XBC":"Bond Markets Unit European Unit of Account 9 (E.U.A.-9)","XBD":"Bond Markets Unit European Unit of Account 17 (E.U.A.-17)","XTS":"Codes specifically reserved for testing purposes","XXX":"The codes assigned for transactions where no currency is involved","XAU":"Gold","XPD":"Palladium","XPT":"Platinum","XAG":"Silver"}`,
4+
);
5+
6+
export function getSupportedCurrencyList() {
7+
return Intl.supportedValuesOf('currency').map(code => ({
8+
code,
9+
name: currencyNames[code] as string,
10+
}));
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Function to map currency codes to names using this list: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml
3+
* Used to update the string in currencies.ts.
4+
* Only works in the browser.
5+
*
6+
* To use, move the file out of the chunks folder
7+
* @param xmlStr XML String with ISO 4217 data
8+
*/
9+
export const processCurrencyFile = (xmlStr: string): string => {
10+
const parser = new DOMParser();
11+
const xmlDoc = parser.parseFromString(xmlStr, 'text/xml');
12+
const currencyNodes = xmlDoc.getElementsByTagName('CcyNtry');
13+
const currencyMap = {};
14+
15+
for (let i = 0; i < currencyNodes.length; i++) {
16+
const currencyNode = currencyNodes[i];
17+
const code = currencyNode.getElementsByTagName('Ccy')[0]?.textContent;
18+
19+
if (!code) {
20+
continue;
21+
}
22+
23+
const currencyName =
24+
currencyNode.getElementsByTagName('CcyNm')[0]?.textContent;
25+
currencyMap[code] = currencyName;
26+
}
27+
28+
return JSON.stringify(currencyMap);
29+
};
Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Resource, useValue } from '@tomic/react';
22

3-
import { InputWrapper } from './InputStyles';
4-
import { styled } from 'styled-components';
3+
import { BasicSelect } from './BasicSelect';
54

65
interface AtomicSelectInputProps {
76
resource: Resource;
@@ -33,41 +32,12 @@ export function AtomicSelectInput({
3332
};
3433

3534
return (
36-
<StyledInputWrapper>
37-
<SelectWrapper disabled={!!props.disabled}>
38-
<Select {...props} onChange={handleChange} value={value as string}>
39-
{options.map(option => (
40-
<option key={option.value} value={option.value}>
41-
{option.label}
42-
</option>
43-
))}
44-
</Select>
45-
</SelectWrapper>
46-
</StyledInputWrapper>
35+
<BasicSelect {...props} onChange={handleChange} value={value as string}>
36+
{options.map(option => (
37+
<option key={option.value} value={option.value}>
38+
{option.label}
39+
</option>
40+
))}
41+
</BasicSelect>
4742
);
4843
}
49-
50-
const StyledInputWrapper = styled(InputWrapper)`
51-
min-width: 15ch;
52-
`;
53-
54-
const SelectWrapper = styled.span<{ disabled: boolean }>`
55-
width: 100%;
56-
padding-inline: 0.2rem;
57-
background-color: ${p =>
58-
p.disabled ? p.theme.colors.bg1 : p.theme.colors.bg};
59-
`;
60-
61-
const Select = styled.select`
62-
cursor: pointer;
63-
width: 100%;
64-
border: none;
65-
outline: none;
66-
height: 2rem;
67-
background-color: transparent;
68-
color: ${p => p.theme.colors.text};
69-
&:disabled {
70-
color: ${props => props.theme.colors.textLight};
71-
background-color: transparent;
72-
}
73-
`;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { styled } from 'styled-components';
2+
import { InputWrapper } from './InputStyles';
3+
import { FC, PropsWithChildren } from 'react';
4+
5+
type Props = React.SelectHTMLAttributes<HTMLSelectElement>;
6+
7+
export const BasicSelect: FC<PropsWithChildren<Props>> = ({
8+
children,
9+
...props
10+
}) => {
11+
return (
12+
<StyledInputWrapper>
13+
<SelectWrapper disabled={!!props.disabled}>
14+
<Select {...props}>{children}</Select>
15+
</SelectWrapper>
16+
</StyledInputWrapper>
17+
);
18+
};
19+
20+
const StyledInputWrapper = styled(InputWrapper)`
21+
min-width: 15ch;
22+
`;
23+
24+
const SelectWrapper = styled.span<{ disabled: boolean }>`
25+
width: 100%;
26+
padding-inline: 0.2rem;
27+
background-color: ${p =>
28+
p.disabled ? p.theme.colors.bg1 : p.theme.colors.bg};
29+
`;
30+
31+
const Select = styled.select`
32+
cursor: pointer;
33+
width: 100%;
34+
border: none;
35+
outline: none;
36+
height: 2rem;
37+
background-color: transparent;
38+
color: ${p => p.theme.colors.text};
39+
&:disabled {
40+
color: ${props => props.theme.colors.textLight};
41+
background-color: transparent;
42+
}
43+
`;

browser/data-browser/src/views/TablePage/EditorCells/FloatCell.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
JSONValue,
3+
dataBrowser,
34
urls,
45
useNumber,
56
useResource,
@@ -50,12 +51,18 @@ function FloatCellDisplay({
5051
urls.properties.constraints.decimalPlaces,
5152
);
5253

54+
const [currency] = useString(
55+
propertyResource,
56+
dataBrowser.properties.currency,
57+
);
58+
5359
const isPercentage = numberFormatting === numberFormats.percentage;
5460

5561
const formattedValue = formatNumber(
5662
value as number | undefined,
5763
decimalPlaces,
5864
numberFormatting,
65+
currency,
5966
);
6067

6168
return (
Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,59 @@
1-
import { urls, useNumber, useStore, useString } from '@tomic/react';
2-
import { useEffect } from 'react';
1+
import {
2+
core,
3+
dataBrowser,
4+
urls,
5+
useNumber,
6+
useStore,
7+
useString,
8+
} from '@tomic/react';
9+
import { Suspense, lazy, useEffect } from 'react';
310
import { RadioGroup, RadioInput } from '../../../components/forms/RadioInput';
411
import { FormGroupHeading } from './FormGroupHeading';
512
import { DecimalPlacesInput } from './Inputs/DecimalPlacesInput';
613
import { TableRangeInput } from './Inputs/TableRangeInput';
714
import { PropertyCategoryFormProps } from './PropertyCategoryFormProps';
815

916
const { numberFormats } = urls.instances;
17+
const CurrencyPicker = lazy(
18+
() => import('../../../chunks/CurrencyPicker/CurrencyPicker'),
19+
);
1020

1121
export const NumberPropertyForm = ({
1222
resource,
1323
}: PropertyCategoryFormProps): JSX.Element => {
1424
const store = useStore();
1525
const [numberFormatting, setNumberFormatting] = useString(
1626
resource,
17-
urls.properties.constraints.numberFormatting,
27+
dataBrowser.properties.numberFormatting,
1828
);
1929

2030
const [decimalPlaces] = useNumber(
2131
resource,
22-
urls.properties.constraints.decimalPlaces,
32+
dataBrowser.properties.decimalPlaces,
2333
);
2434

25-
const handleNumberFormatChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35+
const [_, setDataType] = useString(resource, core.properties.datatype);
36+
37+
const handleNumberFormatChange = async (
38+
e: React.ChangeEvent<HTMLInputElement>,
39+
) => {
2640
setNumberFormatting(e.target.value);
41+
42+
if (e.target.value === numberFormats.currency) {
43+
await resource.addClasses(store, dataBrowser.classes.currencyProperty);
44+
await setDataType(urls.datatypes.float);
45+
} else {
46+
await resource.removeClasses(store, dataBrowser.classes.currencyProperty);
47+
resource.removePropVal(dataBrowser.properties.currency);
48+
}
2749
};
2850

2951
useEffect(() => {
30-
resource.addClasses(
31-
store,
32-
urls.classes.constraintProperties.formattedNumber,
33-
);
52+
resource.addClasses(store, dataBrowser.classes.formattedNumber);
3453

3554
// If decimal places is not set yet we assume it is a new property and should default to float.
3655
if (decimalPlaces === undefined) {
37-
resource.set(urls.properties.datatype, urls.datatypes.float, store);
56+
resource.set(core.properties.datatype, urls.datatypes.float, store);
3857
}
3958

4059
if (numberFormatting === undefined) {
@@ -43,7 +62,7 @@ export const NumberPropertyForm = ({
4362
}, []);
4463

4564
return (
46-
<>
65+
<Suspense fallback={<div>loading...</div>}>
4766
<FormGroupHeading>Number Format</FormGroupHeading>
4867
<RadioGroup>
4968
<RadioInput
@@ -62,15 +81,27 @@ export const NumberPropertyForm = ({
6281
>
6382
Percentage
6483
</RadioInput>
84+
<RadioInput
85+
name='number-format'
86+
value={numberFormats.currency}
87+
checked={numberFormatting === numberFormats.currency}
88+
onChange={handleNumberFormatChange}
89+
>
90+
Currency
91+
</RadioInput>
6592
</RadioGroup>
66-
<DecimalPlacesInput resource={resource} />
93+
{resource.hasClasses(dataBrowser.classes.currencyProperty) ? (
94+
<CurrencyPicker resource={resource} />
95+
) : (
96+
<DecimalPlacesInput resource={resource} />
97+
)}
6798
<FormGroupHeading>Range</FormGroupHeading>
6899
<TableRangeInput
69100
resource={resource}
70-
minProp={urls.properties.constraints.min}
71-
maxProp={urls.properties.constraints.max}
72-
constraintClass={urls.classes.constraintProperties.rangeProperty}
101+
minProp={dataBrowser.properties.min}
102+
maxProp={dataBrowser.properties.max}
103+
constraintClass={dataBrowser.classes.rangeProperty}
73104
/>
74-
</>
105+
</Suspense>
75106
);
76107
};

browser/data-browser/src/views/TablePage/helpers/formatNumber.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { urls } from '@tomic/react';
33
export function formatNumber(
44
value: number | undefined,
55
fractionDigits: number | undefined,
6-
formatting?: string,
6+
formatting: string | undefined,
7+
currency?: string,
78
): string {
89
if (value === undefined) {
910
return '';
@@ -21,6 +22,22 @@ export function formatNumber(
2122
return formatter.format(value / 100);
2223
}
2324

25+
if (formatting === urls.instances.numberFormats.currency) {
26+
try {
27+
const formatter = new Intl.NumberFormat('default', {
28+
style: 'currency',
29+
currency,
30+
currencyDisplay: 'narrowSymbol',
31+
});
32+
33+
return formatter.format(value);
34+
} catch (e) {
35+
console.error(e);
36+
37+
return value.toString();
38+
}
39+
}
40+
2441
const formatter = new Intl.NumberFormat('default', {
2542
style: 'decimal',
2643
minimumFractionDigits: fixedFractionDigits,

0 commit comments

Comments
 (0)