diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index 999fe3fbd72..040b8da5528 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -119,6 +119,23 @@ function AllTransactions({ account = {}, transactions, filtered, children }) { return children(allTransactions); } +function getField(field) { + switch (field) { + case 'account': + return 'account.name'; + case 'payee': + return 'payee.name'; + case 'category': + return 'category.name'; + case 'payment': + return 'amount'; + case 'deposit': + return 'amount'; + default: + return field; + } +} + class AccountInternal extends PureComponent { constructor(props) { super(props); @@ -142,6 +159,7 @@ class AccountInternal extends PureComponent { latestDate: null, filterId: [], conditionsOp: 'and', + sort: [], }; } @@ -232,6 +250,11 @@ class AccountInternal extends PureComponent { this.refetchTransactions(); }, 100); } + + //Resest sort/filter/search on account change + if (this.props.accountId !== prevProps.accountId) { + this.setState({ sort: [], search: '', filters: [] }); + } } componentWillUnmount() { @@ -450,7 +473,12 @@ class AccountInternal extends PureComponent { let accountId = this.props.accountId; let account = this.props.accounts.find(account => account.id === accountId); return ( - account && this.state.search === '' && this.state.filters.length === 0 + account && + this.state.search === '' && + this.state.filters.length === 0 && + (this.state.sort.length === 0 || + (this.state.sort.field === 'date' && + this.state.sort.ascDesc === 'desc')) ); }; @@ -523,11 +551,32 @@ class AccountInternal extends PureComponent { this.props.savePrefs({ ['show-balances-' + accountId]: false }); this.setState({ showBalances: false, balances: [] }); } else { + this.setState({ + transactions: [], + transactionCount: 0, + filters: [], + search: '', + sort: [], + showBalances: true, + }); + this.fetchTransactions(); this.props.savePrefs({ ['show-balances-' + accountId]: true }); - this.setState({ showBalances: true }); this.calculateBalances(); } break; + case 'remove-sorting': { + let filters = this.state.filters; + this.setState({ sort: [] }); + if (filters.length > 0) { + this.applyFilters([...filters]); + } else { + this.fetchTransactions(); + } + if (this.state.search !== '') { + this.onSearch(this.state.search); + } + break; + } case 'toggle-cleared': if (this.state.showCleared) { this.props.savePrefs({ ['hide-cleared-' + accountId]: true }); @@ -815,6 +864,9 @@ class AccountInternal extends PureComponent { this.setState({ conditionsOp: value }); this.setState({ filterId: { ...this.state.filterId, status: 'changed' } }); this.applyFilters([...filters]); + if (this.state.search !== '') { + this.onSearch(this.state.search); + } }; onReloadSavedFilter = (savedFilter, item) => { @@ -837,6 +889,9 @@ class AccountInternal extends PureComponent { this.setState({ conditionsOp: 'and' }); this.setState({ filterId: [] }); this.applyFilters([]); + if (this.state.search !== '') { + this.onSearch(this.state.search); + } }; onUpdateFilter = (oldFilter, updatedFilter) => { @@ -849,6 +904,9 @@ class AccountInternal extends PureComponent { status: this.state.filterId && 'changed', }, }); + if (this.state.search !== '') { + this.onSearch(this.state.search); + } }; onDeleteFilter = filter => { @@ -864,6 +922,9 @@ class AccountInternal extends PureComponent { }, }); } + if (this.state.search !== '') { + this.onSearch(this.state.search); + } }; onApplyFilter = async cond => { @@ -884,6 +945,9 @@ class AccountInternal extends PureComponent { }); this.applyFilters([...filters, cond]); } + if (this.state.search !== '') { + this.onSearch(this.state.search); + } }; onScheduleAction = async (name, ids) => { @@ -918,11 +982,91 @@ class AccountInternal extends PureComponent { [conditionsOpKey]: [...filters, ...customFilters], }); this.updateQuery(this.currentQuery, true); - this.setState({ filters: conditions, search: '' }); + this.setState({ filters: conditions }); } else { this.setState({ transactions: [], transactionCount: 0 }); this.fetchTransactions(); - this.setState({ filters: conditions, search: '' }); + this.setState({ filters: conditions }); + } + + if (this.state.sort.length !== 0) { + this.applySort(); + } + }; + + applySort = (field, ascDesc, prevField, prevAscDesc) => { + let filters = this.state.filters; + let sortField = getField(!field ? this.state.sort.field : field); + let sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc; + let sortPrevField = getField( + !prevField ? this.state.sort.prevField : prevField, + ); + let sortPrevAscDesc = !prevField + ? this.state.sort.prevAscDesc + : prevAscDesc; + + if (!field) { + //no sort was made (called by applyFilters) + this.currentQuery = this.currentQuery.orderBy({ + [sortField]: sortAscDesc, + }); + } else { + //sort called directly + if (filters.length > 0) { + //if filters already exist then apply them + this.applyFilters([...filters]); + this.currentQuery = this.currentQuery.orderBy({ + [sortField]: sortAscDesc, + }); + } else { + //no filters exist make new rootquery + this.currentQuery = this.rootQuery.orderBy({ + [sortField]: sortAscDesc, + }); + } + } + if (sortPrevField) { + //apply previos sort if it exists + this.currentQuery = this.currentQuery.orderBy({ + [sortPrevField]: sortPrevAscDesc, + }); + } + + this.updateQuery(this.currentQuery, this.state.filters.length > 0); + }; + + onSort = (headerClicked, ascDesc) => { + let prevField; + let prevAscDesc; + //if staying on same column but switching asc/desc + //then keep prev the same + if (headerClicked === this.state.sort.field) { + prevField = this.state.sort.prevField; + prevAscDesc = this.state.sort.prevAscDesc; + this.setState({ + sort: { + ...this.state.sort, + ascDesc: ascDesc, + }, + }); + } else { + //if switching to new column then capture state + //of current sort column as prev + prevField = this.state.sort.field; + prevAscDesc = this.state.sort.ascDesc; + this.setState({ + sort: { + field: headerClicked, + ascDesc: ascDesc, + prevField: this.state.sort.field, + prevAscDesc: this.state.sort.ascDesc, + }, + }); + } + + this.applySort(headerClicked, ascDesc, prevField, prevAscDesc); + if (this.state.search !== '') { + this.onSearch(this.state.search); } }; @@ -1010,6 +1154,7 @@ class AccountInternal extends PureComponent { showEmptyMessage={showEmptyMessage} balanceQuery={balanceQuery} canCalculateBalance={this.canCalculateBalance} + isSorted={this.state.sort.length !== 0} reconcileAmount={reconcileAmount} search={this.state.search} filters={this.state.filters} @@ -1095,6 +1240,9 @@ class AccountInternal extends PureComponent { ) : null } + onSort={this.onSort} + sortField={this.state.sort.field} + ascDesc={this.state.sort.ascDesc} onChange={this.onTransactionsChange} onRefetch={this.refetchTransactions} onRefetchUpToRow={row => diff --git a/packages/desktop-client/src/components/accounts/Header.js b/packages/desktop-client/src/components/accounts/Header.js index 1a9033832c1..e3bd351f1a0 100644 --- a/packages/desktop-client/src/components/accounts/Header.js +++ b/packages/desktop-client/src/components/accounts/Header.js @@ -51,6 +51,7 @@ export function AccountHeader({ balanceQuery, reconcileAmount, canCalculateBalance, + isSorted, search, filters, conditionsOp, @@ -350,6 +351,7 @@ export function AccountHeader({ account={account} canSync={canSync} canShowBalances={canCalculateBalance()} + isSorted={isSorted} showBalances={showBalances} showCleared={showCleared} onMenuSelect={item => { @@ -372,6 +374,7 @@ export function AccountHeader({ onMenuSelect(item); }} onClose={() => setMenuOpen(false)} + isSorted={isSorted} /> )} @@ -411,6 +414,7 @@ function AccountMenu({ canShowBalances, showCleared, onClose, + isSorted, onReconcile, onMenuSelect, }) { @@ -434,6 +438,10 @@ function AccountMenu({ } }} items={[ + isSorted && { + name: 'remove-sorting', + text: 'Remove all Sorting', + }, canShowBalances && { name: 'toggle-balance', text: (showBalances ? 'Hide' : 'Show') + ' Running Balance', @@ -464,14 +472,20 @@ function AccountMenu({ ); } -function CategoryMenu({ onClose, onMenuSelect }) { +function CategoryMenu({ onClose, onMenuSelect, isSorted }) { return ( { onMenuSelect(item); }} - items={[{ name: 'export', text: 'Export' }]} + items={[ + isSorted && { + name: 'remove-sorting', + text: 'Remove all Sorting', + }, + { name: 'export', text: 'Export' }, + ]} /> ); diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx index de369995883..5c1a4aff44f 100644 --- a/packages/desktop-client/src/components/table.tsx +++ b/packages/desktop-client/src/components/table.tsx @@ -184,6 +184,7 @@ type CellProps = Omit, 'children' | 'value'> & { formatter?: (value: string, type?: unknown) => string; focused?: boolean; textAlign?: string; + alignItems?: string; borderColor?: string; plain?: boolean; exposed?: boolean; @@ -201,6 +202,7 @@ export function Cell({ value, formatter, textAlign, + alignItems, onExpose, borderColor: oldBorderColor, children, @@ -232,6 +234,7 @@ export function Cell({ borderBottomWidth: borderColor ? 1 : 0, borderColor, backgroundColor, + alignItems: alignItems, }; return ( @@ -905,7 +908,9 @@ type TableProps = { animated?: boolean; allowPopupsEscape?: boolean; isSelected?: (id: TableItem['id']) => boolean; + saveScrollWidth: (parent, child) => void; }; + export const Table = forwardRef( ( { @@ -928,6 +933,7 @@ export const Table = forwardRef( animated, allowPopupsEscape, isSelected, + saveScrollWidth, ...props }, ref, @@ -1016,6 +1022,15 @@ export const Table = forwardRef( let editing = editingId === item.id; let selected = isSelected && isSelected(item.id); + if (scrollContainer.current && saveScrollWidth) { + saveScrollWidth( + scrollContainer.current.offsetParent + ? scrollContainer.current.offsetParent.clientWidth + : 0, + scrollContainer.current ? scrollContainer.current.clientWidth : 0, + ); + } + let row = renderItem({ item, editing, diff --git a/packages/desktop-client/src/components/transactions/TransactionList.js b/packages/desktop-client/src/components/transactions/TransactionList.js index 0ad01b0fb96..2e9b20b4968 100644 --- a/packages/desktop-client/src/components/transactions/TransactionList.js +++ b/packages/desktop-client/src/components/transactions/TransactionList.js @@ -77,6 +77,9 @@ export default function TransactionList({ hideFraction, addNotification, renderEmpty, + onSort, + sortField, + ascDesc, onChange, onRefetch, onCloseAddTransaction, @@ -196,6 +199,9 @@ export default function TransactionList({ style={{ backgroundColor: 'white' }} onNavigateToTransferAccount={onNavigateToTransferAccount} onNavigateToSchedule={onNavigateToSchedule} + onSort={onSort} + sortField={sortField} + ascDesc={ascDesc} /> ); } diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.js b/packages/desktop-client/src/components/transactions/TransactionsTable.js index 16c2dd18cf1..6b3b17b3e32 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.js +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.js @@ -47,6 +47,8 @@ import usePrevious from '../../hooks/usePrevious'; import { useSelectedDispatch, useSelectedItems } from '../../hooks/useSelected'; import LeftArrow2 from '../../icons/v0/LeftArrow2'; import RightArrow2 from '../../icons/v0/RightArrow2'; +import ArrowDown from '../../icons/v1/ArrowDown'; +import ArrowUp from '../../icons/v1/ArrowUp'; import CheveronDown from '../../icons/v1/CheveronDown'; import ArrowsSynchronize from '../../icons/v2/ArrowsSynchronize'; import CalendarIcon from '../../icons/v2/Calendar'; @@ -237,8 +239,26 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { ); } +function selectAscDesc(field, ascDesc, clicked, defaultAscDesc = 'asc') { + return field === clicked + ? ascDesc === 'asc' + ? 'desc' + : 'asc' + : defaultAscDesc; +} + const TransactionHeader = memo( - ({ hasSelected, showAccount, showCategory, showBalance, showCleared }) => { + ({ + hasSelected, + showAccount, + showCategory, + showBalance, + showCleared, + scrollWidth, + onSort, + ascDesc, + field, + }) => { let dispatchSelected = useSelectedDispatch(); return ( @@ -258,16 +278,93 @@ const TransactionHeader = memo( width={20} onSelect={e => dispatchSelected({ type: 'select-all', event: e })} /> - - {showAccount && } - - - {showCategory && } - - + + onSort('date', selectAscDesc(field, ascDesc, 'date', 'desc')) + } + /> + {showAccount && ( + + onSort('account', selectAscDesc(field, ascDesc, 'account', 'asc')) + } + /> + )} + + onSort('payee', selectAscDesc(field, ascDesc, 'payee', 'asc')) + } + /> + + onSort('notes', selectAscDesc(field, ascDesc, 'notes', 'asc')) + } + /> + {showCategory && ( + + onSort( + 'category', + selectAscDesc(field, ascDesc, 'category', 'asc'), + ) + } + /> + )} + + onSort('payment', selectAscDesc(field, ascDesc, 'payment', 'asc')) + } + /> + + onSort('deposit', selectAscDesc(field, ascDesc, 'deposit', 'desc')) + } + /> {showBalance && } - {showCleared && } - + {showCleared && } + ); }, @@ -340,7 +437,7 @@ function StatusCell({ return ( + + {icon === 'asc' && ( + + )} + {icon === 'desc' && ( + + )} + + } + /> + ); +} + function PayeeCell({ id, payeeId, @@ -406,6 +553,7 @@ function PayeeCell({ { let acct = acctId && getAccountsById(accounts)[acctId]; @@ -866,6 +1016,7 @@ const Transaction = memo(function Transaction(props) { value @@ -1041,7 +1197,7 @@ const Transaction = memo(function Transaction(props) { )} - + ); }); @@ -1299,6 +1455,13 @@ function TransactionTableInner({ }) { const containerRef = createRef(); const isAddingPrev = usePrevious(props.isAdding); + let [scrollWidth, setScrollWidth] = useState(0); + + function saveScrollWidth(parent, child) { + let width = parent > 0 && child > 0 && parent - child; + + setScrollWidth(!width ? 0 : width); + } let onNavigateToTransferAccount = useCallback( accountId => { @@ -1427,6 +1590,10 @@ function TransactionTableInner({ showCategory={props.showCategory} showBalance={!!props.balances} showCleared={props.showCleared} + scrollWidth={scrollWidth} + onSort={props.onSort} + ascDesc={props.ascDesc} + field={props.sortField} /> {props.isAdding && ( @@ -1483,6 +1650,7 @@ function TransactionTableInner({ isSelected={id => props.selectedItems.has(id)} onKeyDown={e => props.onCheckEnter(e)} onScroll={onScroll} + saveScrollWidth={saveScrollWidth} /> {props.isAdding && ( @@ -1574,7 +1742,6 @@ export let TransactionTable = forwardRef((props, ref) => { let savePending = useRef(false); let afterSaveFunc = useRef(false); let [_, forceRerender] = useState({}); - let selectedItems = useSelectedItems(); useLayoutEffect(() => { diff --git a/upcoming-release-notes/1232.md b/upcoming-release-notes/1232.md new file mode 100644 index 00000000000..a9e0650261d --- /dev/null +++ b/upcoming-release-notes/1232.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Added transaction sorting on the Account page. Uses current action as well as previous action to sort. Also adjusted the functionality and interactions of filters and searches with the sorting.