Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This changelog covers all three packages, as they are (for now) updated as a who
- [#758](https://github.com/atomicdata-dev/atomic-server/issues/758) Fix Relation column forms to close when clicking on the searchbox
- [#780](https://github.com/atomicdata-dev/atomic-server/issues/780) Use tags in ontology editor to create enum properties.
- [#810](https://github.com/atomicdata-dev/atomic-server/issues/810) Add button to resource selectors to navigate to the selected resource.
- [#764](https://github.com/atomicdata-dev/atomic-server/issues/764) Add option to format numbers as currency in tables.
- Fix server not rebuilding client when files changed.
- Added persistent scrollbar to table
- Improved table header UX
Expand Down
53 changes: 53 additions & 0 deletions browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx
Original file line number Diff line number Diff line change
@@ -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<CurrencyPickerProps> = ({ resource }) => {
const [currency, setCurrency] = useString(
resource,
dataBrowser.properties.currency,
);

const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setCurrency(e.target.value);
};

useEffect(() => {
if (currency === undefined) {
setCurrency('EUR');
}
}, []);

return (
<BasicSelect defaultValue={currency ?? 'EUR'} onChange={handleChange}>
{supportedCurrencies.map(c => (
<option
key={c.code}
value={c.code}
label={`${c.code} ${c.name ?? ''} (${getSymbol(c.code)})`}
>
{c.code}
</option>
))}
</BasicSelect>
);
};

export default CurrencyPicker;
11 changes: 11 additions & 0 deletions browser/data-browser/src/chunks/CurrencyPicker/currencies.ts
Original file line number Diff line number Diff line change
@@ -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,
}));
}
Original file line number Diff line number Diff line change
@@ -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);
};
46 changes: 8 additions & 38 deletions browser/data-browser/src/components/forms/AtomicSelectInput.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -33,41 +32,12 @@ export function AtomicSelectInput({
};

return (
<StyledInputWrapper>
<SelectWrapper disabled={!!props.disabled}>
<Select {...props} onChange={handleChange} value={value as string}>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</SelectWrapper>
</StyledInputWrapper>
<BasicSelect {...props} onChange={handleChange} value={value as string}>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</BasicSelect>
);
}

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;
}
`;
43 changes: 43 additions & 0 deletions browser/data-browser/src/components/forms/BasicSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { styled } from 'styled-components';
import { InputWrapper } from './InputStyles';
import { FC, PropsWithChildren } from 'react';

type Props = React.SelectHTMLAttributes<HTMLSelectElement>;

export const BasicSelect: FC<PropsWithChildren<Props>> = ({
children,
...props
}) => {
return (
<StyledInputWrapper>
<SelectWrapper disabled={!!props.disabled}>
<Select {...props}>{children}</Select>
</SelectWrapper>
</StyledInputWrapper>
);
};

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;
}
`;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
JSONValue,
dataBrowser,
urls,
useNumber,
useResource,
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
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';
import { TableRangeInput } from './Inputs/TableRangeInput';
import { PropertyCategoryFormProps } from './PropertyCategoryFormProps';

const { numberFormats } = urls.instances;
const CurrencyPicker = lazy(
() => import('../../../chunks/CurrencyPicker/CurrencyPicker'),
);

export const NumberPropertyForm = ({
resource,
}: PropertyCategoryFormProps): JSX.Element => {
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<HTMLInputElement>) => {
const [_, setDataType] = useString(resource, core.properties.datatype);

const handleNumberFormatChange = async (
e: React.ChangeEvent<HTMLInputElement>,
) => {
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) {
Expand All @@ -43,7 +62,7 @@ export const NumberPropertyForm = ({
}, []);

return (
<>
<Suspense fallback={<div>loading...</div>}>
<FormGroupHeading>Number Format</FormGroupHeading>
<RadioGroup>
<RadioInput
Expand All @@ -62,15 +81,27 @@ export const NumberPropertyForm = ({
>
Percentage
</RadioInput>
<RadioInput
name='number-format'
value={numberFormats.currency}
checked={numberFormatting === numberFormats.currency}
onChange={handleNumberFormatChange}
>
Currency
</RadioInput>
</RadioGroup>
<DecimalPlacesInput resource={resource} />
{resource.hasClasses(dataBrowser.classes.currencyProperty) ? (
<CurrencyPicker resource={resource} />
) : (
<DecimalPlacesInput resource={resource} />
)}
<FormGroupHeading>Range</FormGroupHeading>
<TableRangeInput
resource={resource}
minProp={urls.properties.constraints.min}
maxProp={urls.properties.constraints.max}
constraintClass={urls.classes.constraintProperties.rangeProperty}
minProp={dataBrowser.properties.min}
maxProp={dataBrowser.properties.max}
constraintClass={dataBrowser.classes.rangeProperty}
/>
</>
</Suspense>
);
};
19 changes: 18 additions & 1 deletion browser/data-browser/src/views/TablePage/helpers/formatNumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handleError

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this catch should actually do nothing and just let the formatter continue to format the value as if it were just a number as this only triggers when the currency is not supported by the browser.


return value.toString();
}
}

const formatter = new Intl.NumberFormat('default', {
style: 'decimal',
minimumFractionDigits: fixedFractionDigits,
Expand Down
Loading