From 07bbe000595a9f88ce2efb90508e69e112f4bd94 Mon Sep 17 00:00:00 2001 From: Robert Dyer <rdyer@unl.edu> Date: Wed, 17 Jul 2024 15:41:50 -0500 Subject: [PATCH] Add additional hotkeys (#3061) * Add account page hotkeys * add release note * fix linter * change shortcut * change hotkey * fix lint * add budget shortcuts * update help page * fix linter * add privacy filter hotkey p * update help modal * fix deps * slash the zero * bound the month picker * change privacy shortcut to ctrl+p * remap keys to ctrl * add select all hotkey * fix linter * change add hotkey to T * update help modal * resize help modal * fix linter * shrink modal size more * change budget reset behavior * change privacy to shift+ctrl+p * move shift to front --- .../src/components/Titlebar.tsx | 12 +++ .../src/components/accounts/Header.jsx | 27 +++++++ .../components/budget/BudgetPageHeader.tsx | 16 +--- .../components/budget/DynamicBudgetTable.tsx | 61 +++++++++++++- .../src/components/filters/FiltersMenu.jsx | 4 + .../modals/KeyboardShortcutModal.tsx | 79 +++++++++++++++---- .../transactions/TransactionsTable.jsx | 11 +++ upcoming-release-notes/3061.md | 6 ++ 8 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 upcoming-release-notes/3061.md diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index 0a1e76dfb..4336ef4e0 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -64,6 +64,18 @@ function PrivacyButton({ style }: PrivacyButtonProps) { const privacyIconStyle = { width: 15, height: 15 }; + useHotkeys( + 'shift+ctrl+p, shift+cmd+p, shift+meta+p', + () => { + setPrivacyEnabledPref(!isPrivacyEnabled); + }, + { + preventDefault: true, + scopes: ['app'], + }, + [setPrivacyEnabledPref, isPrivacyEnabled], + ); + return ( <Button type="bare" diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx index 00e247473..d5591ccbf 100644 --- a/packages/desktop-client/src/components/accounts/Header.jsx +++ b/packages/desktop-client/src/components/accounts/Header.jsx @@ -130,6 +130,33 @@ export function AccountHeader({ }, [searchInput], ); + useHotkeys( + 't', + () => onAddTransaction(), + { + preventDefault: true, + scopes: ['app'], + }, + [onAddTransaction], + ); + useHotkeys( + 'ctrl+i, cmd+i, meta+i', + () => onImport(), + { + scopes: ['app'], + }, + [onImport], + ); + useHotkeys( + 'ctrl+b, cmd+b, meta+b', + () => onSync(), + { + enabled: canSync && !isServerOffline, + preventDefault: true, + scopes: ['app'], + }, + [onSync], + ); return ( <> diff --git a/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx b/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx index 4db9767f5..bd6e61060 100644 --- a/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx +++ b/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx @@ -1,8 +1,6 @@ // @ts-strict-ignore import React, { type ComponentProps, memo } from 'react'; -import * as monthUtils from 'loot-core/src/shared/months'; - import { View } from '../common/View'; import { MonthPicker } from './MonthPicker'; @@ -17,18 +15,6 @@ type BudgetPageHeaderProps = { export const BudgetPageHeader = memo<BudgetPageHeaderProps>( ({ startMonth, onMonthSelect, numMonths, monthBounds }) => { - function getValidMonth(month) { - const start = monthBounds.start; - const end = monthUtils.subMonths(monthBounds.end, numMonths - 1); - - if (month < start) { - return start; - } else if (month > end) { - return end; - } - return month; - } - return ( <View style={{ marginLeft: 200 + 5, flexShrink: 0 }}> <View style={{ marginRight: 5 + getScrollbarWidth() }}> @@ -37,7 +23,7 @@ export const BudgetPageHeader = memo<BudgetPageHeaderProps>( numDisplayed={numMonths} monthBounds={monthBounds} style={{ paddingTop: 5 }} - onSelect={month => onMonthSelect(getValidMonth(month))} + onSelect={month => onMonthSelect(month)} /> </View> </View> diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 60e97052b..7229a0e14 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -1,7 +1,10 @@ // @ts-strict-ignore import React, { useEffect, type ComponentProps } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import AutoSizer from 'react-virtualized-auto-sizer'; +import * as monthUtils from 'loot-core/src/shared/months'; + import { View } from '../common/View'; import { useBudgetMonthCount } from './BudgetMonthCountContext'; @@ -32,6 +35,7 @@ type DynamicBudgetTableInnerProps = { } & DynamicBudgetTableProps; const DynamicBudgetTableInner = ({ + type, width, height, prewarmStartMonth, @@ -51,10 +55,65 @@ const DynamicBudgetTableInner = ({ setDisplayMax(numPossible); }, [numPossible]); + function getValidMonth(month) { + const start = monthBounds.start; + const end = monthUtils.subMonths(monthBounds.end, numMonths - 1); + + if (month < start) { + return start; + } else if (month > end) { + return end; + } + return month; + } + function _onMonthSelect(month) { - onMonthSelect(month, numMonths); + onMonthSelect(getValidMonth(month), numMonths); } + useHotkeys( + 'left', + () => { + _onMonthSelect(monthUtils.prevMonth(startMonth)); + }, + { + preventDefault: true, + scopes: ['app'], + }, + [_onMonthSelect, startMonth], + ); + useHotkeys( + 'right', + () => { + _onMonthSelect(monthUtils.nextMonth(startMonth)); + }, + { + preventDefault: true, + scopes: ['app'], + }, + [_onMonthSelect, startMonth], + ); + useHotkeys( + '0', + () => { + _onMonthSelect( + monthUtils.subMonths( + monthUtils.currentMonth(), + type === 'rollover' + ? Math.floor((numMonths - 1) / 2) + : numMonths === 2 + ? 1 + : Math.max(numMonths - 2, 0), + ), + ); + }, + { + preventDefault: true, + scopes: ['app'], + }, + [_onMonthSelect, startMonth, numMonths], + ); + return ( <View style={{ diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index 4f9b16cd0..fc971664e 100644 --- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx +++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect, useReducer } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { FocusScope } from '@react-aria/focus'; import { @@ -315,6 +316,9 @@ export function FilterButton({ onApply, compact, hover, exclude }) { dispatch({ type: 'close' }); } } + useHotkeys('f', () => dispatch({ type: 'select-field' }), { + scopes: ['app'], + }); return ( <View> diff --git a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx index 0f018ac98..5a4a3a0b1 100644 --- a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx +++ b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx @@ -2,6 +2,7 @@ import { useLocation } from 'react-router-dom'; import * as Platform from 'loot-core/src/client/platform'; +import { type CSSProperties } from '../../style'; import { Modal, type ModalProps } from '../common/Modal'; import { Text } from '../common/Text'; import { View } from '../common/View'; @@ -12,6 +13,7 @@ type KeyboardShortcutsModalProps = { type KeyIconProps = { shortcut: string; + style?: CSSProperties; }; type GroupHeadingProps = { @@ -23,9 +25,10 @@ type ShortcutProps = { description: string; meta?: string; shift?: boolean; + style?: CSSProperties; }; -function KeyIcon({ shortcut }: KeyIconProps) { +function KeyIcon({ shortcut, style }: KeyIconProps) { return ( <div style={{ @@ -37,10 +40,11 @@ function KeyIcon({ shortcut }: KeyIconProps) { color: '#000', border: '1px solid #000', borderRadius: 8, - minWidth: 35, - minHeight: 35, + minWidth: 30, + minHeight: 30, filter: 'drop-shadow(1px 1px)', padding: 5, + ...style, }} > {shortcut} @@ -63,12 +67,18 @@ function GroupHeading({ group }: GroupHeadingProps) { ); } -function Shortcut({ shortcut, description, meta, shift }: ShortcutProps) { +function Shortcut({ + shortcut, + description, + meta, + shift, + style, +}: ShortcutProps) { return ( <div style={{ display: 'flex', - marginBottom: 10, + marginBottom: 5, marginLeft: 20, }} > @@ -85,9 +95,9 @@ function Shortcut({ shortcut, description, meta, shift }: ShortcutProps) { marginRight: 10, }} > - {meta && ( + {shift && ( <> - <KeyIcon shortcut={meta} /> + <KeyIcon shortcut="Shift" /> <Text style={{ display: 'flex', @@ -102,9 +112,9 @@ function Shortcut({ shortcut, description, meta, shift }: ShortcutProps) { </Text> </> )} - {shift && ( + {meta && ( <> - <KeyIcon shortcut="Shift" /> + <KeyIcon shortcut={meta} /> <Text style={{ display: 'flex', @@ -119,7 +129,7 @@ function Shortcut({ shortcut, description, meta, shift }: ShortcutProps) { </Text> </> )} - <KeyIcon shortcut={shortcut} /> + <KeyIcon shortcut={shortcut} style={style} /> </div> <div style={{ @@ -146,6 +156,7 @@ export function KeyboardShortcutModal({ modalProps, }: KeyboardShortcutsModalProps) { const location = useLocation(); + const onBudget = location.pathname.startsWith('/budget'); const onAccounts = location.pathname.startsWith('/accounts'); const ctrl = Platform.OS === 'mac' ? '⌘' : 'Ctrl'; return ( @@ -153,19 +164,45 @@ export function KeyboardShortcutModal({ <View style={{ flexDirection: 'row', + fontSize: 13, }} > <View> + <Shortcut shortcut="?" description="Show this help dialog" /> <Shortcut shortcut="O" description="Close the current budget and open another" meta={ctrl} /> - <Shortcut shortcut="?" description="Show this help dialog" /> + <Shortcut + shortcut="P" + description="Toggle the privacy filter" + meta={ctrl} + shift={true} + /> + {onBudget && ( + <Shortcut + shortcut="0" + description="View current month" + style={{ + fontVariantNumeric: 'slashed-zero', + }} + /> + )} {onAccounts && ( <> <Shortcut shortcut="Enter" description="Move down when editing" /> - <Shortcut shortcut="Tab" description="Move right when editing" /> + <Shortcut + shortcut="Enter" + description="Move up when editing" + shift={true} + /> + <Shortcut + shortcut="I" + description="Import transactions" + meta={ctrl} + /> + <Shortcut shortcut="B" description="Bank sync" meta={ctrl} /> <GroupHeading group="Select a transaction, then" /> <Shortcut shortcut="J" @@ -197,8 +234,7 @@ export function KeyboardShortcutModal({ </View> <View style={{ - marginLeft: 20, - marginRight: 20, + marginRight: 15, }} > <Shortcut @@ -212,18 +248,27 @@ export function KeyboardShortcutModal({ shift={true} meta={ctrl} /> + {onBudget && ( + <> + <Shortcut shortcut="â†" description="View previous month" /> + <Shortcut shortcut="→" description="View next month" /> + </> + )} {onAccounts && ( <> <Shortcut - shortcut="Enter" - description="Move up when editing" - shift={true} + shortcut="A" + description="Select all transactions" + meta={ctrl} /> + <Shortcut shortcut="Tab" description="Move right when editing" /> <Shortcut shortcut="Tab" description="Move left when editing" shift={true} /> + <Shortcut shortcut="T" description="Add a new transaction" /> + <Shortcut shortcut="F" description="Filter transactions" /> <GroupHeading group="With transaction(s) selected" /> <Shortcut shortcut="F" diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index 0f57c59f9..5de3b0abc 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -10,6 +10,7 @@ import React, { useLayoutEffect, useEffect, } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { useDispatch } from 'react-redux'; import { @@ -174,6 +175,16 @@ const TransactionHeader = memo( }) => { const dispatchSelected = useSelectedDispatch(); + useHotkeys( + 'ctrl+a, cmd+a, meta+a', + e => dispatchSelected({ type: 'select-all', event: e }), + { + preventDefault: true, + scopes: ['app'], + }, + [dispatchSelected], + ); + return ( <Row style={{ diff --git a/upcoming-release-notes/3061.md b/upcoming-release-notes/3061.md new file mode 100644 index 000000000..cd8ea967b --- /dev/null +++ b/upcoming-release-notes/3061.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [psybers] +--- + +Add additional keyboard hotkeys. \ No newline at end of file -- GitLab