From 77ef86413d2abbf385389199e272ade2a50847fb Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Tue, 18 Jul 2023 20:21:44 +0100
Subject: [PATCH] Sort Transactions on Accounts Page (#1232)

---
 .../src/components/accounts/Account.js        | 156 +++++++++++++-
 .../src/components/accounts/Header.js         |  18 +-
 .../desktop-client/src/components/table.tsx   |  15 ++
 .../transactions/TransactionList.js           |   6 +
 .../transactions/TransactionsTable.js         | 199 ++++++++++++++++--
 upcoming-release-notes/1232.md                |   6 +
 6 files changed, 378 insertions(+), 22 deletions(-)
 create mode 100644 upcoming-release-notes/1232.md

diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js
index 999fe3fbd..040b8da55 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 {
                         </View>
                       ) : 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 1a9033832..e3bd351f1 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}
                 />
               )}
             </View>
@@ -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 (
     <MenuTooltip width={200} onClose={onClose}>
       <Menu
         onMenuSelect={item => {
           onMenuSelect(item);
         }}
-        items={[{ name: 'export', text: 'Export' }]}
+        items={[
+          isSorted && {
+            name: 'remove-sorting',
+            text: 'Remove all Sorting',
+          },
+          { name: 'export', text: 'Export' },
+        ]}
       />
     </MenuTooltip>
   );
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index de3699958..5c1a4aff4 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -184,6 +184,7 @@ type CellProps = Omit<ComponentProps<typeof View>, '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<TableHandleRef, TableProps>(
   (
     {
@@ -928,6 +933,7 @@ export const Table = forwardRef<TableHandleRef, TableProps>(
       animated,
       allowPopupsEscape,
       isSelected,
+      saveScrollWidth,
       ...props
     },
     ref,
@@ -1016,6 +1022,15 @@ export const Table = forwardRef<TableHandleRef, TableProps>(
       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 0ad01b0fb..2e9b20b49 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 16c2dd18c..6b3b17b3e 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 })}
         />
-        <Cell value="Date" width={110} />
-        {showAccount && <Cell value="Account" width="flex" />}
-        <Cell value="Payee" width="flex" />
-        <Cell value="Notes" width="flex" />
-        {showCategory && <Cell value="Category" width="flex" />}
-        <Cell value="Payment" width={80} textAlign="right" />
-        <Cell value="Deposit" width={80} textAlign="right" />
+        <HeaderCell
+          value="Date"
+          width={110}
+          alignItems="flex"
+          marginLeft={-5}
+          id="date"
+          icon={field === 'date' ? ascDesc : 'clickable'}
+          onClick={() =>
+            onSort('date', selectAscDesc(field, ascDesc, 'date', 'desc'))
+          }
+        />
+        {showAccount && (
+          <HeaderCell
+            value="Account"
+            width="flex"
+            alignItems="flex"
+            marginLeft={-5}
+            id="account"
+            icon={field === 'account' ? ascDesc : 'clickable'}
+            onClick={() =>
+              onSort('account', selectAscDesc(field, ascDesc, 'account', 'asc'))
+            }
+          />
+        )}
+        <HeaderCell
+          value="Payee"
+          width="flex"
+          alignItems="flex"
+          marginLeft={-5}
+          id="payee"
+          icon={field === 'payee' ? ascDesc : 'clickable'}
+          onClick={() =>
+            onSort('payee', selectAscDesc(field, ascDesc, 'payee', 'asc'))
+          }
+        />
+        <HeaderCell
+          value="Notes"
+          width="flex"
+          alignItems="flex"
+          marginLeft={-5}
+          id="notes"
+          icon={field === 'notes' ? ascDesc : 'clickable'}
+          onClick={() =>
+            onSort('notes', selectAscDesc(field, ascDesc, 'notes', 'asc'))
+          }
+        />
+        {showCategory && (
+          <HeaderCell
+            value="Category"
+            width="flex"
+            alignItems="flex"
+            marginLeft={-5}
+            id="category"
+            icon={field === 'category' ? ascDesc : 'clickable'}
+            onClick={() =>
+              onSort(
+                'category',
+                selectAscDesc(field, ascDesc, 'category', 'asc'),
+              )
+            }
+          />
+        )}
+        <HeaderCell
+          value="Payment"
+          width={90}
+          alignItems="flex-end"
+          marginRight={-5}
+          id="payment"
+          icon={field === 'payment' ? ascDesc : 'clickable'}
+          onClick={() =>
+            onSort('payment', selectAscDesc(field, ascDesc, 'payment', 'asc'))
+          }
+        />
+        <HeaderCell
+          value="Deposit"
+          width={85}
+          alignItems="flex-end"
+          marginRight={-5}
+          id="deposit"
+          icon={field === 'deposit' ? ascDesc : 'clickable'}
+          onClick={() =>
+            onSort('deposit', selectAscDesc(field, ascDesc, 'deposit', 'desc'))
+          }
+        />
         {showBalance && <Cell value="Balance" width={88} textAlign="right" />}
-        {showCleared && <Field width={21} truncate={false} />}
-        <Cell value="" width={15 + styles.scrollbarWidth} />
+        {showCleared && <Field width={30} truncate={false} />}
+        <Cell value="" width={5 + scrollWidth ?? 0} />
       </Row>
     );
   },
@@ -340,7 +437,7 @@ function StatusCell({
   return (
     <Cell
       name="cleared"
-      width="auto"
+      width={30}
       focused={focused}
       style={{ padding: 1 }}
       plain
@@ -376,6 +473,56 @@ function StatusCell({
   );
 }
 
+function HeaderCell({
+  value,
+  id,
+  width,
+  alignItems,
+  marginLeft,
+  marginRight,
+  icon,
+  onClick,
+}) {
+  return (
+    <CustomCell
+      width={width}
+      name={id}
+      alignItems={alignItems}
+      unexposedContent={
+        <Button
+          bare
+          onClick={onClick}
+          style={{
+            whiteSpace: 'nowrap',
+            overflow: 'hidden',
+            textOverflow: 'ellipsis',
+            color: colors.n4,
+            fontWeight: 300,
+            marginLeft: marginLeft,
+            marginRight: marginRight,
+          }}
+        >
+          <UnexposedCellContent value={value} />
+          {icon === 'asc' && (
+            <ArrowDown
+              width={10}
+              height={10}
+              style={{ marginLeft: 5, color: colors.n4 }}
+            />
+          )}
+          {icon === 'desc' && (
+            <ArrowUp
+              width={10}
+              height={10}
+              style={{ marginLeft: 5, color: colors.n4 }}
+            />
+          )}
+        </Button>
+      }
+    />
+  );
+}
+
 function PayeeCell({
   id,
   payeeId,
@@ -406,6 +553,7 @@ function PayeeCell({
     <CustomCell
       width="flex"
       name="payee"
+      textAlign="flex"
       value={payeeId}
       valueStyle={[valueStyle, inherited && { color: colors.n8 }]}
       exposed={focused}
@@ -759,6 +907,7 @@ const Transaction = memo(function Transaction(props) {
         <CustomCell
           name="date"
           width={110}
+          textAlign="flex"
           exposed={focusedField === 'date'}
           value={date}
           valueStyle={valueStyle}
@@ -795,6 +944,7 @@ const Transaction = memo(function Transaction(props) {
         <CustomCell
           name="account"
           width="flex"
+          textAlign="flex"
           value={accountId}
           formatter={acctId => {
             let acct = acctId && getAccountsById(accounts)[acctId];
@@ -866,6 +1016,7 @@ const Transaction = memo(function Transaction(props) {
         <InputCell
           width="flex"
           name="notes"
+          textAlign="flex"
           exposed={focusedField === 'notes'}
           focused={focusedField === 'notes'}
           value={notes || ''}
@@ -975,7 +1126,11 @@ const Transaction = memo(function Transaction(props) {
               : ''
           }
           valueStyle={valueStyle}
-          style={{ fontStyle: 'italic', color: '#c0c0c0', fontWeight: 300 }}
+          style={{
+            fontStyle: 'italic',
+            color: '#c0c0c0',
+            fontWeight: 300,
+          }}
           inputProps={{
             readOnly: true,
             style: { fontStyle: 'italic' },
@@ -985,6 +1140,7 @@ const Transaction = memo(function Transaction(props) {
         <CustomCell
           name="category"
           width="flex"
+          textAlign="flex"
           value={categoryId}
           formatter={value =>
             value
@@ -1041,7 +1197,7 @@ const Transaction = memo(function Transaction(props) {
 
       <InputCell
         type="input"
-        width={80}
+        width={90}
         name="debit"
         exposed={focusedField === 'debit'}
         focused={focusedField === 'debit'}
@@ -1059,7 +1215,7 @@ const Transaction = memo(function Transaction(props) {
 
       <InputCell
         type="input"
-        width={80}
+        width={85}
         name="credit"
         exposed={focusedField === 'credit'}
         focused={focusedField === 'credit'}
@@ -1103,7 +1259,7 @@ const Transaction = memo(function Transaction(props) {
         />
       )}
 
-      <Cell width={15} />
+      <Cell width={5} />
     </Row>
   );
 });
@@ -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 000000000..a9e065026
--- /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.
-- 
GitLab