diff --git a/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx b/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx new file mode 100644 index 000000000..5dacd08ac --- /dev/null +++ b/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx @@ -0,0 +1,53 @@ +import { FC, useEffect } from 'react'; +import { getSupportedCurrencyList } from './currencies'; +import { BasicSelect } from '../../components/forms/BasicSelect'; +import { Resource, dataBrowser, useString } from '@tomic/react'; + +interface CurrencyPickerProps { + resource: Resource; +} + +const supportedCurrencies = getSupportedCurrencyList(); + +const getSymbol = (code: string) => { + return new Intl.NumberFormat('default', { + style: 'currency', + currency: code, + currencyDisplay: 'narrowSymbol', + }) + .formatToParts(0) + .find(part => part.type === 'currency')?.value; +}; + +const CurrencyPicker: FC = ({ resource }) => { + const [currency, setCurrency] = useString( + resource, + dataBrowser.properties.currency, + ); + + const handleChange = (e: React.ChangeEvent) => { + setCurrency(e.target.value); + }; + + useEffect(() => { + if (currency === undefined) { + setCurrency('EUR'); + } + }, []); + + return ( + + {supportedCurrencies.map(c => ( + + ))} + + ); +}; + +export default CurrencyPicker; diff --git a/browser/data-browser/src/chunks/CurrencyPicker/currencies.ts b/browser/data-browser/src/chunks/CurrencyPicker/currencies.ts new file mode 100644 index 000000000..0f716e88b --- /dev/null +++ b/browser/data-browser/src/chunks/CurrencyPicker/currencies.ts @@ -0,0 +1,11 @@ +// Data taken from https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml +const currencyNames = JSON.parse( + `{"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"}`, +); + +export function getSupportedCurrencyList() { + return Intl.supportedValuesOf('currency').map(code => ({ + code, + name: currencyNames[code] as string, + })); +} diff --git a/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts b/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts new file mode 100644 index 000000000..39ac38fca --- /dev/null +++ b/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts @@ -0,0 +1,29 @@ +/** + * 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 + * Used to update the string in currencies.ts. + * Only works in the browser. + * + * To use, move the file out of the chunks folder + * @param xmlStr XML String with ISO 4217 data + */ +export const processCurrencyFile = (xmlStr: string): string => { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlStr, 'text/xml'); + const currencyNodes = xmlDoc.getElementsByTagName('CcyNtry'); + const currencyMap = {}; + + for (let i = 0; i < currencyNodes.length; i++) { + const currencyNode = currencyNodes[i]; + const code = currencyNode.getElementsByTagName('Ccy')[0]?.textContent; + + if (!code) { + continue; + } + + const currencyName = + currencyNode.getElementsByTagName('CcyNm')[0]?.textContent; + currencyMap[code] = currencyName; + } + + return JSON.stringify(currencyMap); +}; diff --git a/browser/data-browser/src/components/forms/AtomicSelectInput.tsx b/browser/data-browser/src/components/forms/AtomicSelectInput.tsx index f0e8d89f6..b3c00dcea 100644 --- a/browser/data-browser/src/components/forms/AtomicSelectInput.tsx +++ b/browser/data-browser/src/components/forms/AtomicSelectInput.tsx @@ -1,7 +1,6 @@ import { Resource, useValue } from '@tomic/react'; -import { InputWrapper } from './InputStyles'; -import { styled } from 'styled-components'; +import { BasicSelect } from './BasicSelect'; interface AtomicSelectInputProps { resource: Resource; @@ -33,41 +32,12 @@ export function AtomicSelectInput({ }; return ( - - - - - + + {options.map(option => ( + + ))} + ); } - -const StyledInputWrapper = styled(InputWrapper)` - min-width: 15ch; -`; - -const SelectWrapper = styled.span<{ disabled: boolean }>` - width: 100%; - padding-inline: 0.2rem; - background-color: ${p => - p.disabled ? p.theme.colors.bg1 : p.theme.colors.bg}; -`; - -const Select = styled.select` - cursor: pointer; - width: 100%; - border: none; - outline: none; - height: 2rem; - background-color: transparent; - color: ${p => p.theme.colors.text}; - &:disabled { - color: ${props => props.theme.colors.textLight}; - background-color: transparent; - } -`; diff --git a/browser/data-browser/src/components/forms/BasicSelect.tsx b/browser/data-browser/src/components/forms/BasicSelect.tsx new file mode 100644 index 000000000..012b2ce41 --- /dev/null +++ b/browser/data-browser/src/components/forms/BasicSelect.tsx @@ -0,0 +1,43 @@ +import { styled } from 'styled-components'; +import { InputWrapper } from './InputStyles'; +import { FC, PropsWithChildren } from 'react'; + +type Props = React.SelectHTMLAttributes; + +export const BasicSelect: FC> = ({ + children, + ...props +}) => { + return ( + + + + + + ); +}; + +const StyledInputWrapper = styled(InputWrapper)` + min-width: 15ch; +`; + +const SelectWrapper = styled.span<{ disabled: boolean }>` + width: 100%; + padding-inline: 0.2rem; + background-color: ${p => + p.disabled ? p.theme.colors.bg1 : p.theme.colors.bg}; +`; + +const Select = styled.select` + cursor: pointer; + width: 100%; + border: none; + outline: none; + height: 2rem; + background-color: transparent; + color: ${p => p.theme.colors.text}; + &:disabled { + color: ${props => props.theme.colors.textLight}; + background-color: transparent; + } +`; diff --git a/browser/data-browser/src/views/TablePage/EditorCells/FloatCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/FloatCell.tsx index cf7267a0c..dd9bc8787 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/FloatCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/FloatCell.tsx @@ -1,5 +1,6 @@ import { JSONValue, + dataBrowser, urls, useNumber, useResource, @@ -50,12 +51,18 @@ function FloatCellDisplay({ urls.properties.constraints.decimalPlaces, ); + const [currency] = useString( + propertyResource, + dataBrowser.properties.currency, + ); + const isPercentage = numberFormatting === numberFormats.percentage; const formattedValue = formatNumber( value as number | undefined, decimalPlaces, numberFormatting, + currency, ); return ( diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/NumberPropertyForm.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/NumberPropertyForm.tsx index 5453db0db..44ae3413f 100644 --- a/browser/data-browser/src/views/TablePage/PropertyForm/NumberPropertyForm.tsx +++ b/browser/data-browser/src/views/TablePage/PropertyForm/NumberPropertyForm.tsx @@ -1,5 +1,12 @@ -import { urls, useNumber, useStore, useString } from '@tomic/react'; -import { useEffect } from 'react'; +import { + core, + dataBrowser, + urls, + useNumber, + useStore, + useString, +} from '@tomic/react'; +import { Suspense, lazy, useEffect } from 'react'; import { RadioGroup, RadioInput } from '../../../components/forms/RadioInput'; import { FormGroupHeading } from './FormGroupHeading'; import { DecimalPlacesInput } from './Inputs/DecimalPlacesInput'; @@ -7,6 +14,9 @@ import { TableRangeInput } from './Inputs/TableRangeInput'; import { PropertyCategoryFormProps } from './PropertyCategoryFormProps'; const { numberFormats } = urls.instances; +const CurrencyPicker = lazy( + () => import('../../../chunks/CurrencyPicker/CurrencyPicker'), +); export const NumberPropertyForm = ({ resource, @@ -14,27 +24,36 @@ export const NumberPropertyForm = ({ const store = useStore(); const [numberFormatting, setNumberFormatting] = useString( resource, - urls.properties.constraints.numberFormatting, + dataBrowser.properties.numberFormatting, ); const [decimalPlaces] = useNumber( resource, - urls.properties.constraints.decimalPlaces, + dataBrowser.properties.decimalPlaces, ); - const handleNumberFormatChange = (e: React.ChangeEvent) => { + const [_, setDataType] = useString(resource, core.properties.datatype); + + const handleNumberFormatChange = async ( + e: React.ChangeEvent, + ) => { setNumberFormatting(e.target.value); + + if (e.target.value === numberFormats.currency) { + await resource.addClasses(store, dataBrowser.classes.currencyProperty); + await setDataType(urls.datatypes.float); + } else { + await resource.removeClasses(store, dataBrowser.classes.currencyProperty); + resource.removePropVal(dataBrowser.properties.currency); + } }; useEffect(() => { - resource.addClasses( - store, - urls.classes.constraintProperties.formattedNumber, - ); + resource.addClasses(store, dataBrowser.classes.formattedNumber); // If decimal places is not set yet we assume it is a new property and should default to float. if (decimalPlaces === undefined) { - resource.set(urls.properties.datatype, urls.datatypes.float, store); + resource.set(core.properties.datatype, urls.datatypes.float, store); } if (numberFormatting === undefined) { @@ -43,7 +62,7 @@ export const NumberPropertyForm = ({ }, []); return ( - <> + loading...}> Number Format Percentage + + Currency + - + {resource.hasClasses(dataBrowser.classes.currencyProperty) ? ( + + ) : ( + + )} Range - + ); }; diff --git a/browser/data-browser/src/views/TablePage/helpers/formatNumber.ts b/browser/data-browser/src/views/TablePage/helpers/formatNumber.ts index 4a0689025..d79c7de12 100644 --- a/browser/data-browser/src/views/TablePage/helpers/formatNumber.ts +++ b/browser/data-browser/src/views/TablePage/helpers/formatNumber.ts @@ -3,7 +3,8 @@ import { urls } from '@tomic/react'; export function formatNumber( value: number | undefined, fractionDigits: number | undefined, - formatting?: string, + formatting: string | undefined, + currency?: string, ): string { if (value === undefined) { return ''; @@ -21,6 +22,22 @@ export function formatNumber( return formatter.format(value / 100); } + if (formatting === urls.instances.numberFormats.currency) { + try { + const formatter = new Intl.NumberFormat('default', { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + }); + + return formatter.format(value); + } catch (e) { + console.error(e); + + return value.toString(); + } + } + const formatter = new Intl.NumberFormat('default', { style: 'decimal', minimumFractionDigits: fixedFractionDigits, diff --git a/browser/lib/src/ontologies/dataBrowser.ts b/browser/lib/src/ontologies/dataBrowser.ts index 00732f83f..12dd67d71 100644 --- a/browser/lib/src/ontologies/dataBrowser.ts +++ b/browser/lib/src/ontologies/dataBrowser.ts @@ -25,6 +25,8 @@ export const dataBrowser = { formattedDate: 'https://atomicdata.dev/classes/FormattedDate', table: 'https://atomicdata.dev/classes/Table', tag: 'https://atomicdata.dev/classes/Tag', + currencyProperty: + 'https://atomicdata.dev/ontology/data-browser/class/currency-property', }, properties: { subResources: 'https://atomicdata.dev/properties/subresources', @@ -50,6 +52,7 @@ export const dataBrowser = { color: 'https://atomicdata.dev/properties/color', emoji: 'https://atomicdata.dev/properties/emoji', tags: 'https://atomicdata.dev/properties/tags', + currency: 'https://atomicdata.dev/ontology/data-browser/property/currency', }, } as const; @@ -74,6 +77,7 @@ export namespace DataBrowser { export type FormattedDate = typeof dataBrowser.classes.formattedDate; export type Table = typeof dataBrowser.classes.table; export type Tag = typeof dataBrowser.classes.tag; + export type CurrencyProperty = typeof dataBrowser.classes.currencyProperty; } declare module '../index.js' { @@ -142,7 +146,7 @@ declare module '../index.js' { }; [dataBrowser.classes.numberFormat]: { requires: BaseProps | 'https://atomicdata.dev/properties/shortname'; - recommends: typeof dataBrowser.properties.decimalPlaces; + recommends: never; }; [dataBrowser.classes.rangeProperty]: { requires: BaseProps; @@ -181,6 +185,10 @@ declare module '../index.js' { | typeof dataBrowser.properties.color | typeof dataBrowser.properties.emoji; }; + [dataBrowser.classes.currencyProperty]: { + requires: BaseProps | typeof dataBrowser.properties.currency; + recommends: never; + }; } interface PropTypeMapping { @@ -206,6 +214,7 @@ declare module '../index.js' { [dataBrowser.properties.color]: string; [dataBrowser.properties.emoji]: string; [dataBrowser.properties.tags]: string[]; + [dataBrowser.properties.currency]: string; } interface PropSubjectToNameMapping { @@ -231,5 +240,6 @@ declare module '../index.js' { [dataBrowser.properties.color]: 'color'; [dataBrowser.properties.emoji]: 'emoji'; [dataBrowser.properties.tags]: 'tags'; + [dataBrowser.properties.currency]: 'currency'; } } diff --git a/browser/lib/src/urls.ts b/browser/lib/src/urls.ts index 0948ad746..4f936fa3e 100644 --- a/browser/lib/src/urls.ts +++ b/browser/lib/src/urls.ts @@ -177,6 +177,8 @@ export const instances = { numberFormats: { number: 'https://atomicdata.dev/classes/NumberFormat/number', percentage: 'https://atomicdata.dev/classes/NumberFormat/Percentage', + currency: + 'https://atomicdata.dev/ontology/data-browser/number-format/vAikhI3z', }, dateFormats: { localNumeric: 'https://atomicdata.dev/classes/DateFormat/localNumeric',