mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: expand sidebar once open form editor page.
feat: rounding money amount. feat: optimize page form structure. feat: refactoring make journal and expense form with FastField component.
This commit is contained in:
@@ -16,7 +16,8 @@ export default function AccountsSelectList({
|
||||
popoverFill = false,
|
||||
filterByRootTypes = [],
|
||||
filterByTypes = [],
|
||||
filterByNormal
|
||||
filterByNormal,
|
||||
buttonProps = {}
|
||||
}) {
|
||||
// Filters accounts based on filter props.
|
||||
const filteredAccounts = useMemo(() => {
|
||||
@@ -113,6 +114,7 @@ export default function AccountsSelectList({
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={selectedAccount ? selectedAccount.name : defaultSelectText}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { ListSelect } from 'components';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
@@ -8,6 +9,7 @@ export default function CurrencySelectList({
|
||||
selectedCurrencyCode,
|
||||
defaultSelectText = <T id={'select_currency_code'} />,
|
||||
onCurrencySelected,
|
||||
className,
|
||||
...restProps
|
||||
}) {
|
||||
const [selectedCurrency, setSelectedCurrency] = useState(null);
|
||||
@@ -54,6 +56,7 @@ export default function CurrencySelectList({
|
||||
itemPredicate={filterCurrencies}
|
||||
itemRenderer={currencyCodeRenderer}
|
||||
popoverProps={{ minimal: true }}
|
||||
className={classNames('form-group--select-list', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,47 +12,39 @@ import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
|
||||
import Search from 'containers/GeneralSearch/Search';
|
||||
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
|
||||
|
||||
|
||||
import withSettingsActions from 'containers/Settings/withSettingsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
function Dashboard({
|
||||
// #withSettings
|
||||
requestFetchOptions,
|
||||
}) {
|
||||
const fetchOptions = useQuery(
|
||||
['options'], () => requestFetchOptions(),
|
||||
);
|
||||
const fetchOptions = useQuery(['options'], () => requestFetchOptions());
|
||||
|
||||
return (
|
||||
|
||||
<DashboardLoadingIndicator isLoading={fetchOptions.isFetching}>
|
||||
<Switch>
|
||||
<Route path="/preferences">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesSidebar />
|
||||
</DashboardSplitPane>
|
||||
<PreferencesContent />
|
||||
</Route>
|
||||
<DashboardLoadingIndicator isLoading={fetchOptions.isFetching}>
|
||||
<Switch>
|
||||
<Route path="/preferences">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesSidebar />
|
||||
</DashboardSplitPane>
|
||||
<PreferencesContent />
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<DashboardContent />
|
||||
</DashboardSplitPane>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Route path="/">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<DashboardContent />
|
||||
</DashboardSplitPane>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Search />
|
||||
<DialogsContainer />
|
||||
</DashboardLoadingIndicator>
|
||||
|
||||
<Search />
|
||||
<DialogsContainer />
|
||||
</DashboardLoadingIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withSettingsActions,
|
||||
)(Dashboard);
|
||||
export default compose(withSettingsActions)(Dashboard);
|
||||
|
||||
@@ -27,7 +27,7 @@ function DashboardSplitPane({
|
||||
<SplitPane
|
||||
allowResize={sidebarExpended}
|
||||
split="vertical"
|
||||
minSize={180}
|
||||
minSize={190}
|
||||
maxSize={300}
|
||||
defaultSize={sidebarExpended ? defaultSize : 50}
|
||||
size={sidebarExpended ? defaultSize : 50}
|
||||
|
||||
@@ -29,6 +29,10 @@ function DashboardTopbar({
|
||||
|
||||
// #withDashboardActions
|
||||
toggleSidebarExpend,
|
||||
recordSidebarPreviousExpand,
|
||||
|
||||
// #withDashboard
|
||||
sidebarExpended,
|
||||
|
||||
// #withGlobalSearch
|
||||
openGlobalSearch,
|
||||
@@ -41,13 +45,20 @@ function DashboardTopbar({
|
||||
|
||||
const handleSidebarToggleBtn = () => {
|
||||
toggleSidebarExpend();
|
||||
recordSidebarPreviousExpand();
|
||||
};
|
||||
return (
|
||||
<div class="dashboard__topbar">
|
||||
<div class="dashboard__topbar-left">
|
||||
<div class="dashboard__topbar-sidebar-toggle">
|
||||
<Tooltip
|
||||
content={<T id={'close_sidebar'} />}
|
||||
content={
|
||||
!sidebarExpended ? (
|
||||
<T id={'open_sidebar'} />
|
||||
) : (
|
||||
<T id={'close_sidebar'} />
|
||||
)
|
||||
}
|
||||
position={Position.RIGHT}
|
||||
>
|
||||
<Button minimal={true} onClick={handleSidebarToggleBtn}>
|
||||
@@ -147,10 +158,11 @@ function DashboardTopbar({
|
||||
|
||||
export default compose(
|
||||
withSearch,
|
||||
withDashboard(({ pageTitle, pageSubtitle, editViewId }) => ({
|
||||
withDashboard(({ pageTitle, pageSubtitle, editViewId, sidebarExpended }) => ({
|
||||
pageTitle,
|
||||
pageSubtitle,
|
||||
editViewId,
|
||||
sidebarExpended
|
||||
})),
|
||||
withDashboardActions,
|
||||
)(DashboardTopbar);
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||
import { MoneyInputGroup } from 'components';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
// Input form cell renderer.
|
||||
const MoneyFieldCellRenderer = ({
|
||||
row: { index },
|
||||
row: { index, moneyInputGroupProps = {} },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload: { errors, updateData },
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const handleFieldChange = useCallback((e, value) => {
|
||||
const handleFieldChange = useCallback((value) => {
|
||||
setValue(value);
|
||||
}, []);
|
||||
}, [setValue]);
|
||||
|
||||
function isNumeric(data) {
|
||||
return (
|
||||
@@ -21,7 +22,7 @@ const MoneyFieldCellRenderer = ({
|
||||
);
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
const handleFieldBlur = () => {
|
||||
const updateValue = isNumeric(value) ? parseFloat(value) : value;
|
||||
updateData(index, id, updateValue);
|
||||
};
|
||||
@@ -34,15 +35,14 @@ const MoneyFieldCellRenderer = ({
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}>
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={CLASSES.FILL}>
|
||||
<MoneyInputGroup
|
||||
value={value}
|
||||
prefix={'$'}
|
||||
onChange={handleFieldChange}
|
||||
inputGroupProps={{
|
||||
fill: true,
|
||||
onBlur,
|
||||
}}
|
||||
onBlur={handleFieldBlur}
|
||||
{...moneyInputGroupProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||
import { MoneyInputGroup } from 'components';
|
||||
|
||||
const PercentFieldCell = ({
|
||||
cell: { value: initialValue },
|
||||
@@ -10,14 +10,15 @@ const PercentFieldCell = ({
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onBlur = (e) => {
|
||||
updateData(index, id, parseInt(e.target.value, 10));
|
||||
const handleBlurChange = (newValue) => {
|
||||
const parsedValue = newValue === '' || newValue === undefined
|
||||
? '' : parseInt(newValue, 10);
|
||||
updateData(index, id, parsedValue);
|
||||
};
|
||||
|
||||
const onChange = useCallback((e) => {
|
||||
setValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback((value) => {
|
||||
setValue(value);
|
||||
}, [setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
@@ -29,12 +30,8 @@ const PercentFieldCell = ({
|
||||
<FormGroup intent={error ? Intent.DANGER : null}>
|
||||
<MoneyInputGroup
|
||||
value={value}
|
||||
suffix={'%'}
|
||||
onChange={onChange}
|
||||
inputGroupProps={{
|
||||
fill: true,
|
||||
onBlur,
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlurValue={handleBlurChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import ContactsListFieldCell from './ContactsListFieldCell';
|
||||
import ItemsListCell from './ItemsListCell';
|
||||
import PercentFieldCell from './PercentFieldCell';
|
||||
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
|
||||
|
||||
export {
|
||||
AccountsListFieldCell,
|
||||
MoneyFieldCell,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Checkbox as BPCheckbox,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
export default function CheckboxComponent(props) {
|
||||
export default function CheckboxComponent(props: any) {
|
||||
const { field, form, ...rest } = props;
|
||||
const [value, setValue] = useState(field.value || false);
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
|
||||
|
||||
export type Separator = ',' | '.';
|
||||
|
||||
export type CurrencyInputProps = Overwrite<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
{
|
||||
/**
|
||||
* Allow decimals
|
||||
*
|
||||
* Default = true
|
||||
*/
|
||||
allowDecimals?: boolean;
|
||||
|
||||
/**
|
||||
* Allow user to enter negative value
|
||||
*
|
||||
* Default = true
|
||||
*/
|
||||
allowNegativeValue?: boolean;
|
||||
|
||||
/**
|
||||
* Component id
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Maximum characters the user can enter
|
||||
*/
|
||||
maxLength?: number;
|
||||
|
||||
/**
|
||||
* Class names
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Limit length of decimals allowed
|
||||
*
|
||||
* Default = 2
|
||||
*/
|
||||
decimalsLimit?: number;
|
||||
|
||||
/**
|
||||
* Default value
|
||||
*/
|
||||
defaultValue?: number | string;
|
||||
|
||||
/**
|
||||
* Disabled
|
||||
*
|
||||
* Default = false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* Value will always have the specified length of decimals
|
||||
*/
|
||||
fixedDecimalLength?: number;
|
||||
|
||||
/**
|
||||
* Handle change in value
|
||||
*/
|
||||
onChange?: (value: string | undefined, name?: string) => void;
|
||||
|
||||
/**
|
||||
* Handle value onBlur
|
||||
*
|
||||
*/
|
||||
onBlurValue?: (value: string | undefined, name?: string) => void;
|
||||
|
||||
/**
|
||||
* Placeholder
|
||||
*/
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* Specify decimal precision for padding/trimming
|
||||
*/
|
||||
precision?: number;
|
||||
|
||||
/**
|
||||
* Include a prefix eg. £
|
||||
*/
|
||||
prefix?: string;
|
||||
|
||||
/**
|
||||
* Incremental value change on arrow down and arrow up key press
|
||||
*/
|
||||
step?: number;
|
||||
|
||||
/**
|
||||
* Separator between integer part and fractional part of value. Cannot be a number
|
||||
*
|
||||
* Default = "."
|
||||
*/
|
||||
decimalSeparator?: string;
|
||||
|
||||
/**
|
||||
* Separator between thousand, million and billion. Cannot be a number
|
||||
*
|
||||
* Default = ","
|
||||
*/
|
||||
groupSeparator?: string;
|
||||
|
||||
/**
|
||||
* Disable auto adding separator between values eg. 1000 > 1,000
|
||||
*
|
||||
* Default = false
|
||||
*/
|
||||
turnOffSeparators?: boolean;
|
||||
|
||||
/**
|
||||
* Disable abbreviations eg. 1k > 1,000, 2m > 2,000,000
|
||||
*
|
||||
* Default = false
|
||||
*/
|
||||
turnOffAbbreviations?: boolean;
|
||||
}
|
||||
>;
|
||||
190
client/src/components/Forms/MoneyInputGroup/index.tsx
Normal file
190
client/src/components/Forms/MoneyInputGroup/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React, { FC, useState, useEffect, useRef } from 'react';
|
||||
import { InputGroup } from '@blueprintjs/core';
|
||||
import { CurrencyInputProps } from './CurrencyInputProps';
|
||||
import {
|
||||
isNumber,
|
||||
cleanValue,
|
||||
fixedDecimalValue,
|
||||
formatValue,
|
||||
padTrimValue,
|
||||
CleanValueOptions,
|
||||
} from './utils';
|
||||
|
||||
export const CurrencyInput: FC<CurrencyInputProps> = ({
|
||||
allowDecimals = true,
|
||||
allowNegativeValue = true,
|
||||
id,
|
||||
name,
|
||||
className,
|
||||
decimalsLimit,
|
||||
defaultValue,
|
||||
disabled = false,
|
||||
maxLength: userMaxLength,
|
||||
value: userValue,
|
||||
onChange,
|
||||
onBlurValue,
|
||||
fixedDecimalLength,
|
||||
placeholder,
|
||||
precision,
|
||||
prefix,
|
||||
step,
|
||||
decimalSeparator = '.',
|
||||
groupSeparator = ',',
|
||||
turnOffSeparators = false,
|
||||
turnOffAbbreviations = false,
|
||||
...props
|
||||
}: CurrencyInputProps) => {
|
||||
if (decimalSeparator === groupSeparator) {
|
||||
throw new Error('decimalSeparator cannot be the same as groupSeparator');
|
||||
}
|
||||
|
||||
if (isNumber(decimalSeparator)) {
|
||||
throw new Error('decimalSeparator cannot be a number');
|
||||
}
|
||||
|
||||
if (isNumber(groupSeparator)) {
|
||||
throw new Error('groupSeparator cannot be a number');
|
||||
}
|
||||
|
||||
const formatValueOptions = {
|
||||
decimalSeparator,
|
||||
groupSeparator,
|
||||
turnOffSeparators,
|
||||
prefix,
|
||||
};
|
||||
|
||||
const cleanValueOptions: Partial<CleanValueOptions> = {
|
||||
decimalSeparator,
|
||||
groupSeparator,
|
||||
allowDecimals,
|
||||
decimalsLimit: decimalsLimit || fixedDecimalLength || 2,
|
||||
allowNegativeValue,
|
||||
turnOffAbbreviations,
|
||||
prefix,
|
||||
};
|
||||
|
||||
const _defaultValue =
|
||||
defaultValue !== undefined
|
||||
? formatValue({ value: String(defaultValue), ...formatValueOptions })
|
||||
: '';
|
||||
const [stateValue, setStateValue] = useState(_defaultValue);
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onFocus = (): number => (stateValue ? stateValue.length : 0);
|
||||
|
||||
const processChange = (value: string, selectionStart?: number | null): void => {
|
||||
const valueOnly = cleanValue({ value, ...cleanValueOptions });
|
||||
|
||||
if (!valueOnly) {
|
||||
onChange && onChange(undefined, name);
|
||||
setStateValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (userMaxLength && valueOnly.replace(/-/g, '').length > userMaxLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueOnly === '-') {
|
||||
onChange && onChange(undefined, name);
|
||||
setStateValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedValue = formatValue({ value: valueOnly, ...formatValueOptions });
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (selectionStart !== undefined && selectionStart !== null) {
|
||||
const cursor = selectionStart + (formattedValue.length - value.length) || 1;
|
||||
setCursor(cursor);
|
||||
}
|
||||
|
||||
setStateValue(formattedValue);
|
||||
|
||||
onChange && onChange(valueOnly, name);
|
||||
};
|
||||
|
||||
const handleOnChange = ({
|
||||
target: { value, selectionStart },
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
processChange(value, selectionStart);
|
||||
};
|
||||
|
||||
const handleOnBlur = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const valueOnly = cleanValue({ value, ...cleanValueOptions });
|
||||
|
||||
if (valueOnly === '-' || !valueOnly) {
|
||||
onBlurValue && onBlurValue(undefined, name);
|
||||
setStateValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength);
|
||||
|
||||
// Add padding or trim value to precision
|
||||
const newValue = padTrimValue(fixedDecimals, decimalSeparator, precision || fixedDecimalLength);
|
||||
onChange && onChange(newValue, name);
|
||||
onBlurValue && onBlurValue(newValue, name);
|
||||
|
||||
const formattedValue = formatValue({ value: newValue, ...formatValueOptions });
|
||||
setStateValue(formattedValue);
|
||||
};
|
||||
|
||||
const handleOnKeyDown = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (step && (key === 'ArrowUp' || key === 'ArrowDown')) {
|
||||
const currentValue =
|
||||
Number(
|
||||
userValue !== undefined
|
||||
? userValue
|
||||
: cleanValue({ value: stateValue, ...cleanValueOptions })
|
||||
) || 0;
|
||||
const newValue =
|
||||
key === 'ArrowUp'
|
||||
? String(currentValue + Number(step))
|
||||
: String(currentValue - Number(step));
|
||||
|
||||
processChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.setSelectionRange(cursor, cursor);
|
||||
}
|
||||
}, [cursor, inputRef]);
|
||||
|
||||
const formattedPropsValue =
|
||||
userValue !== undefined
|
||||
? formatValue({ value: String(userValue), ...formatValueOptions })
|
||||
: undefined;
|
||||
|
||||
const handleInputRef = (ref: HTMLInputElement | null) => {
|
||||
inputRef.current = ref;
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
id={id}
|
||||
name={name}
|
||||
className={className}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={handleOnKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
value={
|
||||
formattedPropsValue !== undefined && stateValue !== '-' ? formattedPropsValue : stateValue
|
||||
}
|
||||
inputRef={handleInputRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrencyInput;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { addSeparators } from '../addSeparators';
|
||||
|
||||
describe('Separators', () => {
|
||||
it('should add separator in string', () => {
|
||||
expect(addSeparators('1000000')).toEqual('1,000,000');
|
||||
});
|
||||
|
||||
it('should use custom separator when provided', () => {
|
||||
expect(addSeparators('1000000', '.')).toEqual('1.000.000');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import { cleanValue } from '../cleanValue';
|
||||
|
||||
describe('cleanValue', () => {
|
||||
it('should remove group separator in string', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '1,000,000',
|
||||
})
|
||||
).toEqual('1000000');
|
||||
});
|
||||
|
||||
it('should handle period decimal separator in string', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '1.000.000,12',
|
||||
decimalSeparator: ',',
|
||||
groupSeparator: '.',
|
||||
})
|
||||
).toEqual('1000000,12');
|
||||
});
|
||||
|
||||
it('should remove prefix', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '£1000000',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('1000000');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '$5.5',
|
||||
prefix: '$',
|
||||
})
|
||||
).toEqual('5.5');
|
||||
});
|
||||
|
||||
it('should remove extra decimals', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '100.0000',
|
||||
})
|
||||
).toEqual('100.00');
|
||||
});
|
||||
|
||||
it('should remove decimals if not allowed', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '100.0000',
|
||||
allowDecimals: false,
|
||||
decimalsLimit: 0,
|
||||
})
|
||||
).toEqual('100');
|
||||
});
|
||||
|
||||
it('should include decimals if allowed', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '100.123',
|
||||
allowDecimals: true,
|
||||
decimalsLimit: 0,
|
||||
})
|
||||
).toEqual('100.123');
|
||||
});
|
||||
|
||||
it('should format value', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '£1,234,567.89',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('1234567.89');
|
||||
});
|
||||
|
||||
describe('negative values', () => {
|
||||
it('should handle negative value', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '-£1,000',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowDecimals: true,
|
||||
decimalsLimit: 2,
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-1000');
|
||||
});
|
||||
|
||||
it('should handle negative value with decimal', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '-£99,999.99',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowDecimals: true,
|
||||
decimalsLimit: 2,
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-99999.99');
|
||||
});
|
||||
|
||||
it('should handle not allow negative value if allowNegativeValue is false', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '-£1,000',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowDecimals: true,
|
||||
decimalsLimit: 2,
|
||||
allowNegativeValue: false,
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('1000');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle values placed before prefix', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '2£1',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('12');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '-2£1',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-12');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '2-£1',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-12');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '2-£1.99',
|
||||
prefix: '£',
|
||||
decimalsLimit: 5,
|
||||
})
|
||||
).toEqual('-1.992');
|
||||
});
|
||||
|
||||
describe('abbreviations', () => {
|
||||
it('should return empty string if abbreviation only', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: 'k',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: 'm',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: 'b',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('should return empty string if prefix and abbreviation only', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '$k',
|
||||
prefix: '$',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '£m',
|
||||
prefix: '£',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('should ignore abbreviations if turnOffAbbreviations is true', () => {
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '1k',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('1');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '-2k',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('-2');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '25.6m',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('25.6');
|
||||
|
||||
expect(
|
||||
cleanValue({
|
||||
value: '9b',
|
||||
turnOffAbbreviations: true,
|
||||
})
|
||||
).toEqual('9');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { fixedDecimalValue } from '../fixedDecimalValue';
|
||||
|
||||
describe('fixedDecimalValue', () => {
|
||||
it('should return original value if no match', () => {
|
||||
expect(fixedDecimalValue('abc', '.', 2)).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should work with 2 fixed decimal length', () => {
|
||||
expect(fixedDecimalValue('1', '.', 2)).toEqual('1');
|
||||
expect(fixedDecimalValue('12', '.', 2)).toEqual('1.2');
|
||||
expect(fixedDecimalValue('123', '.', 2)).toEqual('1.23');
|
||||
expect(fixedDecimalValue('12345', '.', 2)).toEqual('123.45');
|
||||
expect(fixedDecimalValue('123.4567', '.', 2)).toEqual('123.45');
|
||||
});
|
||||
|
||||
it('should work with 4 fixed decimal length', () => {
|
||||
expect(fixedDecimalValue('12', ',', 4)).toEqual('1,2');
|
||||
expect(fixedDecimalValue('123', ',', 4)).toEqual('1,23');
|
||||
expect(fixedDecimalValue('1234', ',', 4)).toEqual('1,234');
|
||||
expect(fixedDecimalValue('12345', ',', 4)).toEqual('1,2345');
|
||||
});
|
||||
|
||||
it('should trim decimals if too long', () => {
|
||||
expect(fixedDecimalValue('1.23', '.', 2)).toEqual('1.23');
|
||||
expect(fixedDecimalValue('1.2345', '.', 2)).toEqual('1.23');
|
||||
expect(fixedDecimalValue('1,2345678', ',', 3)).toEqual('1,234');
|
||||
expect(fixedDecimalValue('123,45678', ',', 3)).toEqual('123,456');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { formatValue } from '../formatValue';
|
||||
|
||||
describe('formatValue', () => {
|
||||
it('should return empty if blank value', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '',
|
||||
})
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('should add separator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567',
|
||||
})
|
||||
).toEqual('1,234,567');
|
||||
});
|
||||
|
||||
it('should handle period separator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: '.',
|
||||
})
|
||||
).toEqual('1.234.567');
|
||||
});
|
||||
|
||||
it('should handle comma separator for decimals', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567,89',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: '.',
|
||||
})
|
||||
).toEqual('1.234.567,89');
|
||||
});
|
||||
|
||||
it('should handle - as separator for decimals', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567-89',
|
||||
decimalSeparator: '-',
|
||||
groupSeparator: '.',
|
||||
})
|
||||
).toEqual('1.234.567-89');
|
||||
});
|
||||
|
||||
it('should handle empty decimal separator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567-89',
|
||||
decimalSeparator: '',
|
||||
groupSeparator: '.',
|
||||
})
|
||||
).toEqual('1.234.567-89');
|
||||
});
|
||||
|
||||
it('should NOT add separator if "turnOffSeparators" is true', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567',
|
||||
turnOffSeparators: true,
|
||||
})
|
||||
).toEqual('1234567');
|
||||
});
|
||||
|
||||
it('should NOT add separator if "turnOffSeparators" is true even if decimal and group separators specified', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
turnOffSeparators: true,
|
||||
})
|
||||
).toEqual('1234567');
|
||||
});
|
||||
|
||||
it('should add prefix', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '123',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('£123');
|
||||
});
|
||||
|
||||
it('should include "."', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567.',
|
||||
})
|
||||
).toEqual('1,234,567.');
|
||||
});
|
||||
|
||||
it('should include decimals', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234.567',
|
||||
})
|
||||
).toEqual('1,234.567');
|
||||
});
|
||||
|
||||
it('should format value', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '1234567.89',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('£1,234,567.89');
|
||||
});
|
||||
|
||||
it('should handle 0 value', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '0',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('£0');
|
||||
});
|
||||
|
||||
describe('negative values', () => {
|
||||
it('should handle negative values', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '-1234',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-£1,234');
|
||||
});
|
||||
|
||||
it('should return negative sign if only negative sign', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '-',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle negative value and "-" as groupSeparator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '-1234',
|
||||
groupSeparator: '-',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-£1-234');
|
||||
});
|
||||
|
||||
it('should handle negative value and "-" as decimalSeparator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '-12-34',
|
||||
decimalSeparator: '-',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-£12-34');
|
||||
});
|
||||
|
||||
it('should handle negative value and "-" as groupSeparator', () => {
|
||||
expect(
|
||||
formatValue({
|
||||
value: '-123456',
|
||||
groupSeparator: '-',
|
||||
prefix: '£',
|
||||
})
|
||||
).toEqual('-£123-456');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isNumber } from '../isNumber';
|
||||
|
||||
describe('isNumber', () => {
|
||||
it('should return true for 0', () => {
|
||||
expect(isNumber('0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for -3', () => {
|
||||
expect(isNumber('-3')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for 9', () => {
|
||||
expect(isNumber('9')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for abc1', () => {
|
||||
expect(isNumber('abc1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a.1', () => {
|
||||
expect(isNumber('a.1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for space', () => {
|
||||
expect(isNumber(' ')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for comma', () => {
|
||||
expect(isNumber(',')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for period', () => {
|
||||
expect(isNumber('.')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for -', () => {
|
||||
expect(isNumber('-')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for +', () => {
|
||||
expect(isNumber('+')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { padTrimValue } from '../padTrimValue';
|
||||
|
||||
describe('padTrimValue', () => {
|
||||
it('should return original value if no precision', () => {
|
||||
const value = padTrimValue('1000000');
|
||||
expect(value).toEqual('1000000');
|
||||
});
|
||||
|
||||
it('should return blank value if no value', () => {
|
||||
const value = padTrimValue('', '.', 2);
|
||||
expect(value).toEqual('');
|
||||
});
|
||||
|
||||
it('should return blank value if no only negative', () => {
|
||||
const value = padTrimValue('-', '.', 2);
|
||||
expect(value).toEqual('');
|
||||
});
|
||||
|
||||
it('should pad with 0 if no decimals', () => {
|
||||
const value = padTrimValue('99', '.', 3);
|
||||
expect(value).toEqual('99.000');
|
||||
});
|
||||
|
||||
it('should pad with 0 if decimal length is less than precision', () => {
|
||||
const value = padTrimValue('10.5', '.', 5);
|
||||
expect(value).toEqual('10.50000');
|
||||
});
|
||||
|
||||
it('should trim if decimal length is larger than precision', () => {
|
||||
const value = padTrimValue('10.599', '.', 2);
|
||||
expect(value).toEqual('10.59');
|
||||
});
|
||||
|
||||
it('should trim handle comma as decimal separator', () => {
|
||||
const value = padTrimValue('9,9', ',', 3);
|
||||
expect(value).toEqual('9,900');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { abbrValue, parseAbbrValue } from '../parseAbbrValue';
|
||||
|
||||
describe('abbrValue', () => {
|
||||
it('should not convert value under 1000', () => {
|
||||
expect(abbrValue(999)).toEqual('999');
|
||||
});
|
||||
|
||||
it('should convert thousand to k', () => {
|
||||
expect(abbrValue(1000)).toEqual('1k');
|
||||
expect(abbrValue(1500)).toEqual('1.5k');
|
||||
expect(abbrValue(10000)).toEqual('10k');
|
||||
});
|
||||
|
||||
it('should work with comma as decimal separator', () => {
|
||||
expect(abbrValue(1500, ',')).toEqual('1,5k');
|
||||
});
|
||||
|
||||
it('should work with decimal places option', () => {
|
||||
expect(abbrValue(123456, '.')).toEqual('0.123456M');
|
||||
expect(abbrValue(123456, '.', 2)).toEqual('0.12M');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAbbrValue', () => {
|
||||
it('should return undefined if cannot parse', () => {
|
||||
expect(parseAbbrValue('1km')).toEqual(undefined);
|
||||
expect(parseAbbrValue('2mb')).toEqual(undefined);
|
||||
expect(parseAbbrValue('3a')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if no abbreviation', () => {
|
||||
expect(parseAbbrValue('1.23')).toEqual(undefined);
|
||||
expect(parseAbbrValue('100')).toEqual(undefined);
|
||||
expect(parseAbbrValue('20000')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for only letter', () => {
|
||||
expect(parseAbbrValue('k')).toBeUndefined();
|
||||
expect(parseAbbrValue('m')).toBeUndefined();
|
||||
expect(parseAbbrValue('b')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 0 for 0', () => {
|
||||
expect(parseAbbrValue('0k')).toEqual(0);
|
||||
expect(parseAbbrValue('0m')).toEqual(0);
|
||||
expect(parseAbbrValue('0b')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should parse k', () => {
|
||||
expect(parseAbbrValue('1k')).toEqual(1000);
|
||||
expect(parseAbbrValue('2K')).toEqual(2000);
|
||||
expect(parseAbbrValue('1.1239999k')).toEqual(1123.9999);
|
||||
expect(parseAbbrValue('1.5k')).toEqual(1500);
|
||||
expect(parseAbbrValue('50.12K')).toEqual(50120);
|
||||
expect(parseAbbrValue('100K')).toEqual(100000);
|
||||
});
|
||||
|
||||
it('should parse m', () => {
|
||||
expect(parseAbbrValue('1m')).toEqual(1000000);
|
||||
expect(parseAbbrValue('1.5m')).toEqual(1500000);
|
||||
expect(parseAbbrValue('45.123456m')).toEqual(45123456);
|
||||
expect(parseAbbrValue('83.5m')).toEqual(83500000);
|
||||
expect(parseAbbrValue('100M')).toEqual(100000000);
|
||||
});
|
||||
|
||||
it('should parse b', () => {
|
||||
expect(parseAbbrValue('1b')).toEqual(1000000000);
|
||||
expect(parseAbbrValue('1.5b')).toEqual(1500000000);
|
||||
expect(parseAbbrValue('65.5513b')).toEqual(65551300000);
|
||||
expect(parseAbbrValue('100B')).toEqual(100000000000);
|
||||
});
|
||||
|
||||
it('should work with comma as decimal separator', () => {
|
||||
expect(parseAbbrValue('1,2k', ',')).toEqual(1200);
|
||||
expect(parseAbbrValue('2,3m', ',')).toEqual(2300000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { removeInvalidChars } from '../removeInvalidChars';
|
||||
|
||||
describe('removeInvalidChars', () => {
|
||||
it('should remove letters in string', () => {
|
||||
expect(removeInvalidChars('1,000ab,0cd00.99', [',', '.'])).toEqual('1,000,000.99');
|
||||
});
|
||||
|
||||
it('should remove special characters in string', () => {
|
||||
expect(removeInvalidChars('1.00ji0.0*&0^0', ['.'])).toEqual('1.000.000');
|
||||
});
|
||||
|
||||
it('should keep abbreviations', () => {
|
||||
expect(removeInvalidChars('9k', ['k'])).toEqual('9k');
|
||||
expect(removeInvalidChars('1m', ['m'])).toEqual('1m');
|
||||
expect(removeInvalidChars('5b', ['b'])).toEqual('5b');
|
||||
});
|
||||
|
||||
it('should keep abbreviations (case insensitive)', () => {
|
||||
expect(removeInvalidChars('9K', ['k'])).toEqual('9K');
|
||||
expect(removeInvalidChars('1M', ['m'])).toEqual('1M');
|
||||
expect(removeInvalidChars('5B', ['b'])).toEqual('5B');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { removeSeparators } from '../removeSeparators';
|
||||
|
||||
describe('removeSeparators', () => {
|
||||
it('should remove separators in string', () => {
|
||||
expect(removeSeparators('1,000,000')).toEqual('1000000');
|
||||
});
|
||||
|
||||
it('should use custom separator when provided', () => {
|
||||
expect(removeSeparators('1.000.000', '.')).toEqual('1000000');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Add group separator to value eg. 1000 > 1,000
|
||||
*/
|
||||
export const addSeparators = (value: string, separator = ','): string => {
|
||||
return value.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { parseAbbrValue } from './parseAbbrValue';
|
||||
import { removeSeparators } from './removeSeparators';
|
||||
import { removeInvalidChars } from './removeInvalidChars';
|
||||
import { escapeRegExp } from './escapeRegExp';
|
||||
|
||||
export type CleanValueOptions = {
|
||||
value: string;
|
||||
decimalSeparator?: string;
|
||||
groupSeparator?: string;
|
||||
allowDecimals?: boolean;
|
||||
decimalsLimit?: number;
|
||||
allowNegativeValue?: boolean;
|
||||
turnOffAbbreviations?: boolean;
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove prefix, separators and extra decimals from value
|
||||
*/
|
||||
export const cleanValue = ({
|
||||
value,
|
||||
groupSeparator = ',',
|
||||
decimalSeparator = '.',
|
||||
allowDecimals = true,
|
||||
decimalsLimit = 2,
|
||||
allowNegativeValue = true,
|
||||
turnOffAbbreviations = false,
|
||||
prefix = '',
|
||||
}: CleanValueOptions): string => {
|
||||
const abbreviations = turnOffAbbreviations ? [] : ['k', 'm', 'b'];
|
||||
const isNegative = value.includes('-');
|
||||
|
||||
const [prefixWithValue, preValue] = RegExp(`(\\d+)-?${escapeRegExp(prefix)}`).exec(value) || [];
|
||||
const withoutPrefix = prefix ? value.replace(prefixWithValue, '').concat(preValue) : value;
|
||||
const withoutSeparators = removeSeparators(withoutPrefix, groupSeparator);
|
||||
const withoutInvalidChars = removeInvalidChars(withoutSeparators, [
|
||||
groupSeparator,
|
||||
decimalSeparator,
|
||||
...abbreviations,
|
||||
]);
|
||||
|
||||
let valueOnly = withoutInvalidChars;
|
||||
|
||||
if (!turnOffAbbreviations) {
|
||||
// disallow letter without number
|
||||
if (abbreviations.some((letter) => letter === withoutInvalidChars.toLowerCase())) {
|
||||
return '';
|
||||
}
|
||||
const parsed = parseAbbrValue(withoutInvalidChars, decimalSeparator);
|
||||
if (parsed) {
|
||||
valueOnly = String(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
const includeNegative = isNegative && allowNegativeValue ? '-' : '';
|
||||
|
||||
if (String(valueOnly).includes(decimalSeparator)) {
|
||||
const [int, decimals] = withoutInvalidChars.split(decimalSeparator);
|
||||
const trimmedDecimals = decimalsLimit ? decimals.slice(0, decimalsLimit) : decimals;
|
||||
const includeDecimals = allowDecimals ? `${decimalSeparator}${trimmedDecimals}` : '';
|
||||
|
||||
return `${includeNegative}${int}${includeDecimals}`;
|
||||
}
|
||||
|
||||
return `${includeNegative}${valueOnly}`;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Escape regex char
|
||||
*
|
||||
* See: https://stackoverflow.com/questions/17885855/use-dynamic-variable-string-as-regex-pattern-in-javascript
|
||||
*/
|
||||
export const escapeRegExp = (stringToGoIntoTheRegex: string): string => {
|
||||
return stringToGoIntoTheRegex.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export const fixedDecimalValue = (
|
||||
value: string,
|
||||
decimalSeparator: string,
|
||||
fixedDecimalLength?: number
|
||||
): string => {
|
||||
if (fixedDecimalLength && value.length > 1) {
|
||||
if (value.includes(decimalSeparator)) {
|
||||
const [int, decimals] = value.split(decimalSeparator);
|
||||
if (decimals.length > fixedDecimalLength) {
|
||||
return `${int}${decimalSeparator}${decimals.slice(0, fixedDecimalLength)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const reg =
|
||||
value.length > fixedDecimalLength
|
||||
? new RegExp(`(\\d+)(\\d{${fixedDecimalLength}})`)
|
||||
: new RegExp(`(\\d)(\\d+)`);
|
||||
|
||||
const match = value.match(reg);
|
||||
if (match) {
|
||||
const [, int, decimals] = match;
|
||||
return `${int}${decimalSeparator}${decimals}`;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { addSeparators } from './addSeparators';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Value to format
|
||||
*/
|
||||
value: number | string | undefined;
|
||||
|
||||
/**
|
||||
* Decimal separator
|
||||
*
|
||||
* Default = '.'
|
||||
*/
|
||||
decimalSeparator?: string;
|
||||
|
||||
/**
|
||||
* Group separator
|
||||
*
|
||||
* Default = ','
|
||||
*/
|
||||
groupSeparator?: string;
|
||||
|
||||
/**
|
||||
* Turn off separators
|
||||
*
|
||||
* This will override Group separators
|
||||
*
|
||||
* Default = false
|
||||
*/
|
||||
turnOffSeparators?: boolean;
|
||||
|
||||
/**
|
||||
* Prefix
|
||||
*/
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format value with decimal separator, group separator and prefix
|
||||
*/
|
||||
export const formatValue = (props: Props): string => {
|
||||
const {
|
||||
value: _value,
|
||||
groupSeparator = ',',
|
||||
decimalSeparator = '.',
|
||||
turnOffSeparators = false,
|
||||
prefix,
|
||||
} = props;
|
||||
|
||||
if (_value === '' || _value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const value = String(_value);
|
||||
|
||||
if (value === '-') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const isNegative = RegExp('^-\\d+').test(value);
|
||||
const hasDecimalSeparator = decimalSeparator && value.includes(decimalSeparator);
|
||||
|
||||
const valueOnly = isNegative ? value.replace('-', '') : value;
|
||||
const [int, decimals] = hasDecimalSeparator ? valueOnly.split(decimalSeparator) : [valueOnly];
|
||||
|
||||
const formattedInt = turnOffSeparators ? int : addSeparators(int, groupSeparator);
|
||||
|
||||
const includePrefix = prefix ? prefix : '';
|
||||
const includeNegative = isNegative ? '-' : '';
|
||||
const includeDecimals =
|
||||
hasDecimalSeparator && decimals
|
||||
? `${decimalSeparator}${decimals}`
|
||||
: hasDecimalSeparator
|
||||
? `${decimalSeparator}`
|
||||
: '';
|
||||
|
||||
return `${includeNegative}${includePrefix}${formattedInt}${includeDecimals}`;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './cleanValue';
|
||||
export * from './fixedDecimalValue';
|
||||
export * from './formatValue';
|
||||
export * from './isNumber';
|
||||
export * from './padTrimValue';
|
||||
@@ -0,0 +1 @@
|
||||
export const isNumber = (input: string): boolean => RegExp(/\d/, 'gi').test(input);
|
||||
@@ -0,0 +1,22 @@
|
||||
export const padTrimValue = (value: string, decimalSeparator = '.', precision?: number): string => {
|
||||
if (!precision || value === '' || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (!value.match(/\d/g)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const [int, decimals] = value.split(decimalSeparator);
|
||||
let newValue = decimals || '';
|
||||
|
||||
if (newValue.length < precision) {
|
||||
while (newValue.length < precision) {
|
||||
newValue += '0';
|
||||
}
|
||||
} else {
|
||||
newValue = newValue.slice(0, precision);
|
||||
}
|
||||
|
||||
return `${int}${decimalSeparator}${newValue}`;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { escapeRegExp } from './escapeRegExp';
|
||||
|
||||
/**
|
||||
* Abbreviate number eg. 1000 = 1k
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9345181
|
||||
*/
|
||||
export const abbrValue = (value: number, decimalSeparator = '.', _decimalPlaces = 10): string => {
|
||||
if (value > 999) {
|
||||
let valueLength = ('' + value).length;
|
||||
const p = Math.pow;
|
||||
const d = p(10, _decimalPlaces);
|
||||
valueLength -= valueLength % 3;
|
||||
|
||||
const abbrValue = Math.round((value * d) / p(10, valueLength)) / d + ' kMGTPE'[valueLength / 3];
|
||||
return abbrValue.replace('.', decimalSeparator);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
type AbbrMap = { [key: string]: number };
|
||||
|
||||
const abbrMap: AbbrMap = { k: 1000, m: 1000000, b: 1000000000 };
|
||||
|
||||
/**
|
||||
* Parse a value with abbreviation e.g 1k = 1000
|
||||
*/
|
||||
export const parseAbbrValue = (value: string, decimalSeparator = '.'): number | undefined => {
|
||||
const reg = new RegExp(`(\\d+(${escapeRegExp(decimalSeparator)}\\d+)?)([kmb])$`, 'i');
|
||||
const match = value.match(reg);
|
||||
|
||||
if (match) {
|
||||
const [, digits, , abbr] = match;
|
||||
const multiplier = abbrMap[abbr.toLowerCase()];
|
||||
if (digits && multiplier) {
|
||||
return Number(digits.replace(decimalSeparator, '.')) * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { escapeRegExp } from './escapeRegExp';
|
||||
|
||||
/**
|
||||
* Remove invalid characters
|
||||
*/
|
||||
export const removeInvalidChars = (value: string, validChars: ReadonlyArray<string>): string => {
|
||||
const chars = escapeRegExp(validChars.join(''));
|
||||
const reg = new RegExp(`[^\\d${chars}]`, 'gi');
|
||||
return value.replace(reg, '');
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { escapeRegExp } from './escapeRegExp';
|
||||
|
||||
/**
|
||||
* Remove group separator from value eg. 1,000 > 1000
|
||||
*/
|
||||
export const removeSeparators = (value: string, separator = ','): string => {
|
||||
const reg = new RegExp(escapeRegExp(separator), 'g');
|
||||
return value.replace(reg, '');
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {formattedAmount} from 'utils';
|
||||
import { formattedAmount } from 'utils';
|
||||
|
||||
export default function Money({ amount, currency }) {
|
||||
return (
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { InputGroup } from '@blueprintjs/core';
|
||||
|
||||
const joinIntegerAndDecimal = (integer, decimal, separator) => {
|
||||
let output = `${integer}`;
|
||||
|
||||
if (separator) {
|
||||
output += separator;
|
||||
output += decimal ? decimal : '';
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const hasSeparator = (input, separator) => {
|
||||
return -1 !== input.indexOf(separator);
|
||||
};
|
||||
|
||||
const addThousandSeparator = (integer, separator) => {
|
||||
return integer.replace(/(\d)(?=(?:\d{3})+\b)/gm, `$1${separator}`);
|
||||
};
|
||||
|
||||
const toString = (number) => `${number}`;
|
||||
|
||||
const onlyNumbers = (input) => {
|
||||
return toString(input).replace(/\D+/g, '') || '0';
|
||||
};
|
||||
|
||||
const formatter = (value, options) => {
|
||||
const input = toString(value);
|
||||
const navigate = input.indexOf('-') >= 0 ? '-' : '';
|
||||
const parts = toString(input).split(options.decimal);
|
||||
const integer = parseInt(onlyNumbers(parts[0]), 10);
|
||||
const decimal = parts[1] ? onlyNumbers(parts[1]) : null;
|
||||
const integerThousand = addThousandSeparator(
|
||||
toString(integer),
|
||||
options.thousands,
|
||||
);
|
||||
const separator = hasSeparator(input, options.decimal)
|
||||
? options.decimal
|
||||
: false;
|
||||
|
||||
return `${navigate}${options.prefix}${joinIntegerAndDecimal(
|
||||
integerThousand,
|
||||
decimal,
|
||||
separator,
|
||||
)}${options.suffix}`;
|
||||
};
|
||||
|
||||
const unformatter = (input, options) => {
|
||||
const navigate = input.indexOf('-') >= 0 ? '-' : '';
|
||||
const parts = toString(input).split(options.decimal);
|
||||
const integer = parseInt(onlyNumbers(parts[0]), 10);
|
||||
const decimal = parts[1] ? onlyNumbers(parts[1]) : null;
|
||||
const separator = hasSeparator(input, options.decimal)
|
||||
? options.decimal
|
||||
: false;
|
||||
|
||||
return `${navigate}${joinIntegerAndDecimal(integer, decimal, separator)}`;
|
||||
};
|
||||
|
||||
export default function MoneyFieldGroup({
|
||||
value,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
thousands = ',',
|
||||
decimal = '.',
|
||||
precision = 2,
|
||||
inputGroupProps,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) {
|
||||
const [state, setState] = useState(value);
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
prefix,
|
||||
suffix,
|
||||
thousands,
|
||||
decimal,
|
||||
precision,
|
||||
}),
|
||||
[prefix, suffix, thousands, decimal, precision],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
const formatted = formatter(event.target.value, options);
|
||||
const value = unformatter(event.target.value, options);
|
||||
|
||||
setState(formatted);
|
||||
onChange && onChange(event, value);
|
||||
},
|
||||
[onChange, options],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const formatted = formatter(value, options);
|
||||
setState(formatted);
|
||||
}, [value, options, setState]);
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
{...inputGroupProps}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
client/src/components/PageFormBigNumber.js
Normal file
20
client/src/components/PageFormBigNumber.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import {
|
||||
Money,
|
||||
} from 'components';
|
||||
|
||||
export default function PageFormBigNumber({ label, amount, currencyCode }) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
|
||||
<div class="big-amount">
|
||||
<span class="big-amount__label">{ label }</span>
|
||||
|
||||
<h1 class="big-amount__number">
|
||||
<Money amount={0} currency={'LYD'} />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,9 @@ export default function SidebarMenu() {
|
||||
) : null;
|
||||
|
||||
return item.spacer ? (
|
||||
<div class="bp3-menu-spacer"></div>
|
||||
<div class="bp3-menu-spacer" style={{
|
||||
'height': `${item.spacer}px`,
|
||||
}}></div>
|
||||
) : item.divider ? (
|
||||
<MenuDivider key={index} title={item.title} />
|
||||
) : item.label ? (
|
||||
|
||||
@@ -35,11 +35,12 @@ import ContactSelecetList from './ContactSelecetList';
|
||||
import CurrencySelectList from './CurrencySelectList'
|
||||
import SalutationList from './SalutationList';
|
||||
import DisplayNameList from './DisplayNameList';
|
||||
import MoneyInputGroup from './MoneyInputGroup';
|
||||
import MoneyInputGroup from './Forms/MoneyInputGroup';
|
||||
import Dragzone from './Dragzone';
|
||||
import EmptyStatus from './EmptyStatus';
|
||||
import DashboardCard from './Dashboard/DashboardCard';
|
||||
import InputPrependText from './Forms/InputPrependText';
|
||||
import PageFormBigNumber from './PageFormBigNumber';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
@@ -86,4 +87,5 @@ export {
|
||||
EmptyStatus,
|
||||
DashboardCard,
|
||||
InputPrependText,
|
||||
PageFormBigNumber
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user