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 (
);
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.
|