diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index e3db756577405cbe1cbff28a32813d770fd0f74e..32bccd20c4a8908a04c40c697aed42f3cc9606d9 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -1728,6 +1728,7 @@ class AccountInternal extends React.PureComponent { payees, syncEnabled, dateFormat, + hideFraction, addNotification, accountsSyncing, replaceModal, @@ -1856,6 +1857,7 @@ class AccountInternal extends React.PureComponent { this.state.search !== '' || this.state.filters.length > 0 } dateFormat={dateFormat} + hideFraction={hideFraction} addNotification={addNotification} renderEmpty={() => showEmptyMessage ? ( @@ -1920,6 +1922,7 @@ export default function Account(props) { categoryGroups: state.queries.categories.grouped, syncEnabled: state.prefs.local['flags.syncAccount'], dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', + hideFraction: state.prefs.local.hideFraction || false, expandSplits: props.match && state.prefs.local['expand-splits'], showBalances: props.match && diff --git a/packages/desktop-client/src/components/accounts/MobileAccount.js b/packages/desktop-client/src/components/accounts/MobileAccount.js index 772c4db187ce53bf70b2b8ddfd52d6baa8805aa0..cab2574d4c45824acb3042bea7763b1cfc8ca287 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccount.js +++ b/packages/desktop-client/src/components/accounts/MobileAccount.js @@ -231,6 +231,7 @@ function Account(props) { let balance = queries.accountBalance(account); let numberFormat = state.prefs.numberFormat || 'comma-dot'; + let hideFraction = state.prefs.hideFraction || false; return ( <SyncRefresh onSync={onRefresh}> @@ -246,7 +247,7 @@ function Account(props) { // format changes {...state} {...actionCreators} - key={numberFormat} + key={numberFormat + hideFraction} account={account} accounts={props.accounts} categories={state.categories} diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.js b/packages/desktop-client/src/components/accounts/MobileAccounts.js index f655274d1224b480754e65931c47d18d75ab1dc2..b2c80aeb89a9bd3b6a9424840fad0f2a6aae9d3e 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.js +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.js @@ -306,13 +306,14 @@ function Accounts(props) { let { accounts, categories, newTransactions, updatedAccounts, prefs } = props; let numberFormat = prefs.numberFormat || 'comma-dot'; + let hideFraction = prefs.hideFraction || false; return ( <View style={{ flex: 1 }}> <AccountList // This key forces the whole table rerender when the number // format changes - key={numberFormat} + key={numberFormat + hideFraction} accounts={accounts.filter(account => !account.closed)} categories={categories} transactions={transactions || []} diff --git a/packages/desktop-client/src/components/accounts/TransactionList.js b/packages/desktop-client/src/components/accounts/TransactionList.js index e3050fa2a875f719f0e1b2e3ce32e7e6b9f8f7ca..96c1e30968d12bb0e27565e2145a488af2997e61 100644 --- a/packages/desktop-client/src/components/accounts/TransactionList.js +++ b/packages/desktop-client/src/components/accounts/TransactionList.js @@ -71,6 +71,7 @@ export default function TransactionList({ isMatched, isFiltered, dateFormat, + hideFraction, addNotification, renderEmpty, onChange, @@ -170,6 +171,7 @@ export default function TransactionList({ isMatched={isMatched} isFiltered={isFiltered} dateFormat={dateFormat} + hideFraction={hideFraction} addNotification={addNotification} headerContent={headerContent} renderEmpty={renderEmpty} diff --git a/packages/desktop-client/src/components/accounts/TransactionsTable.js b/packages/desktop-client/src/components/accounts/TransactionsTable.js index 61fa795fc8a1e34d26387a0e3bd28fd9c01393c7..3c93f8aff4844f2fbf927e5893f93546713d710f 100644 --- a/packages/desktop-client/src/components/accounts/TransactionsTable.js +++ b/packages/desktop-client/src/components/accounts/TransactionsTable.js @@ -264,7 +264,7 @@ export const TransactionHeader = React.memo( {showCategory && <Cell value="Category" width="flex" />} <Cell value="Payment" width={80} textAlign="right" /> <Cell value="Deposit" width={80} textAlign="right" /> - {showBalance && <Cell value="Balance" width={85} textAlign="right" />} + {showBalance && <Cell value="Balance" width={88} textAlign="right" />} {showCleared && <Field width={21} truncate={false} />} <Cell value="" width={15 + styles.scrollbarWidth} /> </Row> @@ -517,6 +517,7 @@ export const Transaction = React.memo(function Transaction(props) { accounts, balance, dateFormat = 'MM/dd/yyyy', + hideFraction, onSave, onEdit, onHover, @@ -621,6 +622,7 @@ export const Transaction = React.memo(function Transaction(props) { let valueStyle = added ? { fontWeight: 600 } : null; let backgroundFocus = hovered || focusedField === 'select'; + let amountStyle = hideFraction ? { letterSpacing: -0.5 } : null; return ( <Row @@ -992,7 +994,7 @@ export const Transaction = React.memo(function Transaction(props) { textAlign="right" title={debit} onExpose={!isPreview && (name => onEdit(id, name))} - style={[isParent && { fontStyle: 'italic' }, styles.tnum]} + style={[isParent && { fontStyle: 'italic' }, styles.tnum, amountStyle]} inputProps={{ value: debit, onUpdate: onUpdate.bind(null, 'debit'), @@ -1010,7 +1012,7 @@ export const Transaction = React.memo(function Transaction(props) { textAlign="right" title={credit} onExpose={!isPreview && (name => onEdit(id, name))} - style={[isParent && { fontStyle: 'italic' }, styles.tnum]} + style={[isParent && { fontStyle: 'italic' }, styles.tnum, amountStyle]} inputProps={{ value: credit, onUpdate: onUpdate.bind(null, 'credit'), @@ -1026,8 +1028,8 @@ export const Transaction = React.memo(function Transaction(props) { : integerToCurrency(balance) } valueStyle={{ color: balance < 0 ? colors.r4 : colors.g4 }} - style={styles.tnum} - width={85} + style={[styles.tnum, amountStyle]} + width={88} textAlign="right" /> )} @@ -1124,6 +1126,7 @@ function NewTransaction({ showBalance, showCleared, dateFormat, + hideFraction, onHover, onClose, onSplit, @@ -1169,6 +1172,7 @@ function NewTransaction({ categoryGroups={categoryGroups} payees={payees} dateFormat={dateFormat} + hideFraction={hideFraction} expanded={true} onHover={onHover} onEdit={onEdit} @@ -1249,6 +1253,7 @@ function TransactionTableInner({ showAccount, showCategory, balances, + hideFraction, isNew, isMatched, isExpanded, @@ -1313,7 +1318,8 @@ function TransactionTableInner({ : new Set() } dateFormat={dateFormat} - onHover={onHover} + hideFraction={hideFraction} + onHover={props.onHover} onEdit={tableNavigator.onEdit} onSave={props.onSave} onDelete={props.onDelete} @@ -1359,6 +1365,7 @@ function TransactionTableInner({ showBalance={!!props.balances} showCleared={props.showCleared} dateFormat={dateFormat} + hideFraction={props.hideFraction} onClose={props.onCloseAddTransaction} onAdd={props.onAddTemporary} onAddSplit={props.onAddSplit} diff --git a/packages/desktop-client/src/components/budget/MobileBudget.js b/packages/desktop-client/src/components/budget/MobileBudget.js index f2f4799acb24262a016bbadaf252f3e0d291cdd8..02bbaeb89ea16bcec0dce38182da46f157f5f303 100644 --- a/packages/desktop-client/src/components/budget/MobileBudget.js +++ b/packages/desktop-client/src/components/budget/MobileBudget.js @@ -241,6 +241,7 @@ class Budget extends React.Component { applyBudgetAction, } = this.props; let numberFormat = prefs.numberFormat || 'comma-dot'; + let hideFraction = prefs.hideFraction || false; if (!categoryGroups || !initialized) { return ( @@ -264,7 +265,7 @@ class Budget extends React.Component { <BudgetTable // This key forces the whole table rerender when the number // format changes - key={numberFormat} + key={numberFormat + hideFraction} categories={categories} categoryGroups={categoryGroups} type={budgetType} diff --git a/packages/desktop-client/src/components/settings/Format.js b/packages/desktop-client/src/components/settings/Format.js index 62818239d40b2955155c93438497ce53b798096f..621412bb0ce49ac46687629bd20361594e092e6e 100644 --- a/packages/desktop-client/src/components/settings/Format.js +++ b/packages/desktop-client/src/components/settings/Format.js @@ -1,11 +1,16 @@ import React from 'react'; -import { css } from 'glamor'; - import { numberFormats } from 'loot-core/src/shared/util'; -import { Text } from 'loot-design/src/components/common'; +import { + Button, + CustomSelect, + Text, + View, +} from 'loot-design/src/components/common'; +import { Checkbox } from 'loot-design/src/components/forms'; +import tokens from 'loot-design/src/tokens'; -import { Section } from './UI'; +import { Setting } from './UI'; let dateFormats = [ { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' }, @@ -15,53 +20,95 @@ let dateFormats = [ { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' }, ]; +function Column({ title, children }) { + return ( + <View + style={{ + alignItems: 'flex-start', + gap: '0.5em', + flexGrow: 1, + [`@media (max-width: ${tokens.breakpoint_xs})`]: { + width: '100%', + }, + }} + > + <Text style={{ fontWeight: 500 }}>{title}</Text> + <View style={{ alignItems: 'flex-start', gap: '1em' }}>{children}</View> + </View> + ); +} + export default function FormatSettings({ prefs, savePrefs }) { - function onDateFormat(e) { - let format = e.target.value; + function onDateFormat(format) { savePrefs({ dateFormat: format }); } - function onNumberFormat(e) { - let format = e.target.value; + function onNumberFormat(format) { savePrefs({ numberFormat: format }); } + function onHideFraction(e) { + let hideFraction = e.target.checked; + savePrefs({ hideFraction }); + } + let dateFormat = prefs.dateFormat || 'MM/dd/yyyy'; let numberFormat = prefs.numberFormat || 'comma-dot'; return ( - <Section title="Formatting"> - <Text> - <label htmlFor="settings-numberFormat">Number format: </label> - <select - defaultValue={numberFormat} - id="settings-numberFormat" - {...css({ marginLeft: 5, fontSize: 14 })} - onChange={onNumberFormat} + <Setting + primaryAction={ + <View + style={{ + flexDirection: 'row', + gap: '1em', + width: '100%', + [`@media (max-width: ${tokens.breakpoint_xs})`]: { + flexDirection: 'column', + }, + }} > - {numberFormats.map(f => ( - <option key={f.value} value={f.value}> - {f.label} - </option> - ))} - </select> - </Text> + <Column title="Numbers"> + <Button bounce={false} style={{ padding: 0 }}> + <CustomSelect + key={prefs.hideFraction} // needed because label does not update + value={numberFormat} + onChange={onNumberFormat} + options={numberFormats.map(f => [ + f.value, + prefs.hideFraction ? f.labelNoFraction : f.label, + ])} + style={{ padding: '5px 10px', fontSize: 15 }} + /> + </Button> + <Text style={{ display: 'flex' }}> + <Checkbox + id="settings-textDecimal" + checked={prefs.hideFraction} + onChange={onHideFraction} + /> + <label htmlFor="settings-textDecimal">Hide decimal places</label> + </Text> + </Column> + + <Column title="Dates"> + <Button bounce={false} style={{ padding: 0 }}> + <CustomSelect + value={dateFormat} + onChange={onDateFormat} + options={dateFormats.map(f => [f.value, f.label])} + style={{ padding: '5px 10px', fontSize: 15 }} + /> + </Button> + </Column> + </View> + } + > <Text> - <label htmlFor="settings-dateFormat">Date format: </label> - <select - defaultValue={dateFormat} - id="settings-dateFormat" - {...css({ marginLeft: 5, fontSize: 14 })} - onChange={onDateFormat} - > - {dateFormats.map(f => ( - <option key={f.value} value={f.value}> - {f.label} - </option> - ))} - </select> + <strong>Formatting</strong> does not affect how budget data is stored, + and can be changed at any time. </Text> - </Section> + </Setting> ); } diff --git a/packages/loot-core/src/client/reducers/prefs.js b/packages/loot-core/src/client/reducers/prefs.js index 56010e5d37e0ba12eb2c4e76a0c7f253f045b6db..3513f87dac22bb253522f9f95f8945651685d290 100644 --- a/packages/loot-core/src/client/reducers/prefs.js +++ b/packages/loot-core/src/client/reducers/prefs.js @@ -10,12 +10,21 @@ export default function update(state = initialState, action) { switch (action.type) { case constants.SET_PREFS: if (action.prefs) { - setNumberFormat(action.prefs.numberFormat || 'comma-dot'); + setNumberFormat({ + format: action.prefs.numberFormat || 'comma-dot', + hideFraction: action.prefs.hideFraction, + }); } return { local: action.prefs, global: action.globalPrefs }; case constants.MERGE_LOCAL_PREFS: - if (action.prefs.numberFormat) { - setNumberFormat(action.prefs.numberFormat); + if (action.prefs.numberFormat || action.prefs.hideFraction != null) { + setNumberFormat({ + format: action.prefs.numberFormat || state.local.numberFormat, + hideFraction: + action.prefs.hideFraction != null + ? action.prefs.hideFraction + : state.local.hideFraction, + }); } return { diff --git a/packages/loot-core/src/shared/util.js b/packages/loot-core/src/shared/util.js index 0e7671466a4612a5e12c557d1b9d142db695e318..12745e479c846cebf51f19d66ef0267c1fa67401 100644 --- a/packages/loot-core/src/shared/util.js +++ b/packages/loot-core/src/shared/util.js @@ -249,9 +249,9 @@ export function titleFirst(str) { } export let numberFormats = [ - { value: 'comma-dot', label: '1,000.33' }, - { value: 'dot-comma', label: '1.000,33' }, - { value: 'space-comma', label: '1 000,33' }, + { value: 'comma-dot', label: '1,000.33', labelNoFraction: '1,000' }, + { value: 'dot-comma', label: '1.000,33', labelNoFraction: '1.000' }, + { value: 'space-comma', label: '1 000,33', labelNoFraction: '1 000' }, ]; let numberFormat = { @@ -260,7 +260,7 @@ let numberFormat = { regex: null, }; -export function setNumberFormat(format) { +export function setNumberFormat({ format, hideFraction }) { let locale, regex, separator; switch (format) { @@ -285,8 +285,8 @@ export function setNumberFormat(format) { value: format, separator, formatter: new Intl.NumberFormat(locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, + minimumFractionDigits: hideFraction ? 0 : 2, + maximumFractionDigits: hideFraction ? 0 : 2, }), regex, }; @@ -296,7 +296,7 @@ export function getNumberFormat() { return numberFormat; } -setNumberFormat('comma-dot'); +setNumberFormat({ format: 'comma-dot', hideFraction: false }); // Number utilities diff --git a/packages/loot-core/src/shared/util.test.js b/packages/loot-core/src/shared/util.test.js index a5a2df902273f72b922f849c9bf086f93435fa64..ebd52e0c02ae6cf18c7770ef6a50e5c195b0655b 100644 --- a/packages/loot-core/src/shared/util.test.js +++ b/packages/loot-core/src/shared/util.test.js @@ -30,24 +30,33 @@ describe('utility functions', () => { }); test('number formatting works with comma-dot format', () => { - setNumberFormat('comma-dot'); - const formatter = getNumberFormat().formatter; - + setNumberFormat({ format: 'comma-dot', hideFraction: false }); + let formatter = getNumberFormat().formatter; expect(formatter.format('1234.56')).toBe('1,234.56'); + + setNumberFormat({ format: 'comma-dot', hideFraction: true }); + formatter = getNumberFormat().formatter; + expect(formatter.format('1234.56')).toBe('1,235'); }); test('number formatting works with dot-comma format', () => { - setNumberFormat('dot-comma'); - const formatter = getNumberFormat().formatter; - + setNumberFormat({ format: 'dot-comma', hideFraction: false }); + let formatter = getNumberFormat().formatter; expect(formatter.format('1234.56')).toBe('1.234,56'); + + setNumberFormat({ format: 'dot-comma', hideFraction: true }); + formatter = getNumberFormat().formatter; + expect(formatter.format('1234.56')).toBe('1.235'); }); test('number formatting works with space-comma format', () => { - setNumberFormat('space-comma'); - const formatter = getNumberFormat().formatter; - + setNumberFormat({ format: 'space-comma', hideFraction: false }); + let formatter = getNumberFormat().formatter; // grouping separator space char is a non-breaking space, or UTF-16 \xa0 expect(formatter.format('1234.56')).toBe('1\xa0234,56'); + + setNumberFormat({ format: 'space-comma', hideFraction: true }); + formatter = getNumberFormat().formatter; + expect(formatter.format('1234.56')).toBe('1\xa0235'); }); }); diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js index c6bad8b6c489b53a0a91c56e03f11b400ee6becd..1ada04125e6497f2f635ddead5e94dd4f8455fe1 100644 --- a/packages/loot-design/src/components/common.js +++ b/packages/loot-design/src/components/common.js @@ -199,6 +199,7 @@ export const Button = React.forwardRef( disabled, hoveredStyle, activeStyle, + bounce = true, as = 'button', ...nativeProps }, @@ -214,7 +215,7 @@ export const Button = React.forwardRef( bare ? { backgroundColor: 'rgba(100, 100, 100, .25)' } : { - transform: 'translateY(1px)', + transform: bounce && 'translateY(1px)', boxShadow: !bare && (primary diff --git a/packages/loot-design/src/style.js b/packages/loot-design/src/style.js index 7ca66f8356b72c1b26a6dbb98c021f9b2cc167f4..3a159eccc6aefe62c2dc45a6a95cadaa8cbfa8f0 100644 --- a/packages/loot-design/src/style.js +++ b/packages/loot-design/src/style.js @@ -112,8 +112,10 @@ export const styles = { page: { // This is the height of the titlebar paddingTop: 8, - minWidth: 360, flex: 1, + [`@media (min-width: ${tokens.breakpoint_xs})`]: { + minWidth: 360, + }, [`@media (min-width: ${tokens.breakpoint_medium})`]: { minWidth: 500, paddingTop: 36, diff --git a/packages/loot-design/src/tokens.js b/packages/loot-design/src/tokens.js index 5521b9a38b023161129f540c842f3e1d64a73100..96fca17498fe5b9d93aea922605e0bb50d6882b9 100644 --- a/packages/loot-design/src/tokens.js +++ b/packages/loot-design/src/tokens.js @@ -1,4 +1,5 @@ const tokens = { + breakpoint_xs: '350px', breakpoint_narrow: '512px', breakpoint_medium: '768px', breakpoint_wide: '1024px', diff --git a/upcoming-release-notes/725.md b/upcoming-release-notes/725.md new file mode 100644 index 0000000000000000000000000000000000000000..39383c12bee278514e043a9265a29924e3cf4d62 --- /dev/null +++ b/upcoming-release-notes/725.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [j-f1] +--- + +A “hide decimal places†option has been added to improve readability for currencies that typically have large values.