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:
Ahmed Bouhuolia
2020-11-29 00:06:59 +02:00
parent 53dd447540
commit 011542e2a3
118 changed files with 3883 additions and 2660 deletions

View File

@@ -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>
);

View File

@@ -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}
/>
);

View File

@@ -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);

View File

@@ -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}

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -5,6 +5,7 @@ import ContactsListFieldCell from './ContactsListFieldCell';
import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
export {
AccountsListFieldCell,
MoneyFieldCell,

View File

@@ -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);

View File

@@ -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;
}
>;

View 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;

View File

@@ -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');
});
});

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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);
};

View File

@@ -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}`;
};

View File

@@ -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, '\\$&');
};

View File

@@ -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;
};

View File

@@ -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}`;
};

View File

@@ -0,0 +1,5 @@
export * from './cleanValue';
export * from './fixedDecimalValue';
export * from './formatValue';
export * from './isNumber';
export * from './padTrimValue';

View File

@@ -0,0 +1 @@
export const isNumber = (input: string): boolean => RegExp(/\d/, 'gi').test(input);

View File

@@ -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}`;
};

View File

@@ -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;
};

View File

@@ -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, '');
};

View File

@@ -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, '');
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import {formattedAmount} from 'utils';
import { formattedAmount } from 'utils';
export default function Money({ amount, currency }) {
return (

View File

@@ -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}
/>
);
}

View 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>
);
}

View File

@@ -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 ? (

View File

@@ -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
};