Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import type {SearchColumnType, SortOrder} from '@components/Search/types';
import type {SortOrder} from '@components/Search/types';
import SortableTableHeader from '@components/SelectionList/SortableTableHeader';
import type {SortableColumnName} from '@components/SelectionList/types';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -52,7 +52,7 @@ const columnConfig: ColumnConfig[] = [
];

type SearchTableHeaderProps = {
sortBy?: SearchColumnType;
sortBy?: SortableColumnName;
sortOrder?: SortOrder;
onSortPress: (column: SortableColumnName, order: SortOrder) => void;
shouldShowSorting: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React from 'react';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import type {TupleToUnion} from 'type-fest';
import type {SortOrder} from '@components/Search/types';
import TransactionItemRow from '@components/TransactionItemRow';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {compareValues} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';
import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader';

Expand All @@ -11,24 +15,81 @@ type MoneyRequestReportTransactionListProps = {
transactions: OnyxTypes.Transaction[];
};

const sortableColumnNames = [
CONST.SEARCH.TABLE_COLUMNS.DATE,
CONST.SEARCH.TABLE_COLUMNS.MERCHANT,
CONST.SEARCH.TABLE_COLUMNS.CATEGORY,
CONST.SEARCH.TABLE_COLUMNS.TAG,
CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT,
];

type SortableColumnName = TupleToUnion<typeof sortableColumnNames>;

type SortedTransactions = {
transactions: OnyxTypes.Transaction[];
sortBy: SortableColumnName;
sortOrder: SortOrder;
};

const isSortableColumnName = (key: unknown): key is SortableColumnName => !!sortableColumnNames.find((val) => val === key);

const getTransactionKey = (transaction: OnyxTypes.Transaction, key: SortableColumnName) => {
const dateKey = transaction.modifiedCreated ? 'modifiedCreated' : 'created';
return key === CONST.SEARCH.TABLE_COLUMNS.DATE ? dateKey : key;
Comment on lines +37 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #69302 checklist: we're missing the case where the TOTAL_AMOUNT column is also based on the modifiedCreated field

};

const areTransactionValuesEqual = (transactions: OnyxTypes.Transaction[], key: SortableColumnName) => {
const firstValidTransaction = transactions.find((transaction) => transaction !== undefined);
if (!firstValidTransaction) {
return true;
}

const keyOfFirstValidTransaction = getTransactionKey(firstValidTransaction, key);
return transactions.every((transaction) => transaction[getTransactionKey(transaction, key)] === firstValidTransaction[keyOfFirstValidTransaction]);
};

function MoneyRequestReportTransactionList({transactions}: MoneyRequestReportTransactionListProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();

const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout;

const [sortedData, setSortedData] = useState<SortedTransactions>({
transactions,
sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE,
sortOrder: CONST.SEARCH.SORT_ORDER.DESC,
});

const {sortBy, sortOrder} = sortedData;

useEffect(() => {
if (areTransactionValuesEqual(transactions, sortBy)) {
return;
}

setSortedData((prevState) => ({
...prevState,
transactions: [...transactions].sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy)),
}));
}, [sortBy, sortOrder, transactions]);

return (
<>
{!displayNarrowVersion && (
<MoneyRequestReportTableHeader
shouldShowSorting
sortBy="date"
sortOrder="desc"
onSortPress={() => {}}
sortBy={sortBy}
sortOrder={sortOrder}
onSortPress={(selectedSortBy, selectedSortOrder) => {
if (!isSortableColumnName(selectedSortBy)) {
return;
}

setSortedData((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder}));
}}
/>
)}
<View style={[styles.pv2, styles.ph5]}>
{transactions.map((transaction) => {
{sortedData.transactions.map((transaction) => {
return (
<View style={[styles.mb2]}>
<TransactionItemRow
Expand Down
39 changes: 26 additions & 13 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,30 @@ function getSortedSections(
return getSortedReportData(data as ReportListItemType[]);
}

/**
* Compares two values based on a specified sorting order and column.
* Handles both string and numeric comparisons, with special handling for absolute values when sorting by total amount.
*/
function compareValues(a: unknown, b: unknown, sortOrder: SortOrder, sortBy: string): number {
const isAsc = sortOrder === CONST.SEARCH.SORT_ORDER.ASC;

if (a === undefined || b === undefined) {
return 0;
}

if (typeof a === 'string' && typeof b === 'string') {
return isAsc ? a.localeCompare(b) : b.localeCompare(a);
}

if (typeof a === 'number' && typeof b === 'number') {
const aValue = sortBy === CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT ? Math.abs(a) : a;
const bValue = sortBy === CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT ? Math.abs(b) : b;
return isAsc ? aValue - bValue : bValue - aValue;
}

return 0;
}

/**
* @private
* Sorts transaction sections based on a specified column and sort order.
Expand All @@ -597,19 +621,7 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear
const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty];
const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty];

if (aValue === undefined || bValue === undefined) {
return 0;
}

// We are guaranteed that both a and b will be string or number at the same time
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
}

const aNum = aValue as number;
const bNum = bValue as number;

return sortOrder === CONST.SEARCH.SORT_ORDER.ASC ? aNum - bNum : bNum - aNum;
return compareValues(aValue, bValue, sortOrder, sortingProperty);
});
}

Expand Down Expand Up @@ -814,5 +826,6 @@ export {
createTypeMenuItems,
createBaseSavedSearchMenuItem,
shouldShowEmptyState,
compareValues,
};
export type {SavedSearchMenuItem, SearchTypeMenuItem};