From ae320479d47a7c8d8ed442952c31d34159ecca66 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 30 Jul 2025 17:07:43 +0530 Subject: [PATCH] Refactored localeCompare in SearchUIUtils --- .../MoneyRequestReportTransactionList.tsx | 6 ++-- src/components/Search/index.tsx | 6 ++-- src/libs/SearchUIUtils.ts | 27 +++++++------- tests/unit/Search/SearchUIUtilsTest.ts | 35 +++++++++++-------- tests/utils/TestHelper.ts | 11 ++++++ 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 5fb04f36f229..fbf3fee49bef 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -98,7 +98,7 @@ function MoneyRequestReportTransactionList({ useCopySelectionHelper(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {translate} = useLocalize(); + const {translate, localeCompare} = useLocalize(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -152,12 +152,12 @@ function MoneyRequestReportTransactionList({ const sortedTransactions: TransactionWithOptionalHighlight[] = useMemo(() => { return [...transactions] - .sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy)) + .sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy, localeCompare)) .map((transaction) => ({ ...transaction, shouldBeHighlighted: newTransactions?.includes(transaction), })); - }, [newTransactions, sortBy, sortOrder, transactions]); + }, [newTransactions, sortBy, sortOrder, transactions, localeCompare]); const navigateToTransaction = useCallback( (activeTransactionID: string) => { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 8baaa47143ef..4c44a522e09e 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -207,7 +207,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS .flatMap((filteredReportActions) => Object.values(filteredReportActions ?? {})), [reportActions], ); - const {translate} = useLocalize(); + const {translate, localeCompare} = useLocalize(); const searchListRef = useRef(null); useFocusEffect( @@ -546,7 +546,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const ListItem = getListItem(type, status, groupBy); const sortedSelectedData = useMemo( () => - getSortedSections(type, status, data, sortBy, sortOrder, groupBy).map((item) => { + getSortedSections(type, status, data, localeCompare, sortBy, sortOrder, groupBy).map((item) => { const baseKey = isChat ? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}` : `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; @@ -567,7 +567,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return mapToItemWithAdditionalInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight); }), - [type, status, data, sortBy, sortOrder, groupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple], + [type, status, data, sortBy, sortOrder, groupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple, localeCompare], ); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 4cf370746263..3dfc5e3b5929 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2,6 +2,7 @@ import type {TextStyle, ViewStyle} from 'react-native'; import Onyx from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import DotLottieAnimations from '@components/LottieAnimations'; import type DotLottieAnimation from '@components/LottieAnimations/types'; import type {MenuItemWithLink} from '@components/MenuItemList'; @@ -51,7 +52,6 @@ import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {isDevelopment} from './Environment/Environment'; import interceptAnonymousUser from './interceptAnonymousUser'; -import localeCompare from './LocaleCompare'; import {formatPhoneNumber} from './LocalePhoneNumber'; import {translateLocal} from './Localize'; import Navigation from './Navigation/Navigation'; @@ -1292,15 +1292,16 @@ function getSortedSections( type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, + localeCompare: LocaleContextProps['localeCompare'], sortBy?: SearchColumnType, sortOrder?: SortOrder, groupBy?: SearchGroupBy, ) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { - return getSortedReportActionData(data as ReportActionListItemType[]); + return getSortedReportActionData(data as ReportActionListItemType[], localeCompare); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { - return getSortedTaskData(data as TaskListItemType[], sortBy, sortOrder); + return getSortedTaskData(data as TaskListItemType[], localeCompare, sortBy, sortOrder); } if (groupBy) { @@ -1308,7 +1309,7 @@ function getSortedSections( // eslint-disable-next-line default-case switch (groupBy) { case CONST.SEARCH.GROUP_BY.REPORTS: - return getSortedReportData(data as TransactionReportGroupListItemType[]); + return getSortedReportData(data as TransactionReportGroupListItemType[], localeCompare); case CONST.SEARCH.GROUP_BY.MEMBERS: return getSortedMemberData(data as TransactionMemberGroupListItemType[]); case CONST.SEARCH.GROUP_BY.CARDS: @@ -1316,14 +1317,14 @@ function getSortedSections( } } - return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder); + return getSortedTransactionData(data as TransactionListItemType[], localeCompare, sortBy, sortOrder); } /** * 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 { +function compareValues(a: unknown, b: unknown, sortOrder: SortOrder, sortBy: string, localeCompare: LocaleContextProps['localeCompare']): number { const isAsc = sortOrder === CONST.SEARCH.SORT_ORDER.ASC; if (a === undefined || b === undefined) { @@ -1347,7 +1348,7 @@ function compareValues(a: unknown, b: unknown, sortOrder: SortOrder, sortBy: str * @private * Sorts transaction sections based on a specified column and sort order. */ -function getSortedTransactionData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { +function getSortedTransactionData(data: TransactionListItemType[], localeCompare: LocaleContextProps['localeCompare'], sortBy?: SearchColumnType, sortOrder?: SortOrder) { if (!sortBy || !sortOrder) { return data; } @@ -1362,11 +1363,11 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty as keyof TransactionListItemType]; const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty as keyof TransactionListItemType]; - return compareValues(aValue, bValue, sortOrder, sortingProperty); + return compareValues(aValue, bValue, sortOrder, sortingProperty, localeCompare); }); } -function getSortedTaskData(data: TaskListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { +function getSortedTaskData(data: TaskListItemType[], localeCompare: LocaleContextProps['localeCompare'], sortBy?: SearchColumnType, sortOrder?: SortOrder) { if (!sortBy || !sortOrder) { return data; } @@ -1381,7 +1382,7 @@ function getSortedTaskData(data: TaskListItemType[], sortBy?: SearchColumnType, const aValue = a[sortingProperty as keyof TaskListItemType]; const bValue = b[sortingProperty as keyof TaskListItemType]; - return compareValues(aValue, bValue, sortOrder, sortingProperty); + return compareValues(aValue, bValue, sortOrder, sortingProperty, localeCompare); }); } @@ -1389,9 +1390,9 @@ function getSortedTaskData(data: TaskListItemType[], sortBy?: SearchColumnType, * @private * Sorts report sections based on a specified column and sort order. */ -function getSortedReportData(data: TransactionReportGroupListItemType[]) { +function getSortedReportData(data: TransactionReportGroupListItemType[], localeCompare: LocaleContextProps['localeCompare']) { for (const report of data) { - report.transactions = getSortedTransactionData(report.transactions, CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.SORT_ORDER.DESC); + report.transactions = getSortedTransactionData(report.transactions, localeCompare, CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.SORT_ORDER.DESC); } return data.sort((a, b) => { const aNewestTransaction = a.transactions?.at(0)?.modifiedCreated ? a.transactions?.at(0)?.modifiedCreated : a.transactions?.at(0)?.created; @@ -1425,7 +1426,7 @@ function getSortedCardData(data: TransactionCardGroupListItemType[]) { * @private * Sorts report actions sections based on a specified column and sort order. */ -function getSortedReportActionData(data: ReportActionListItemType[]) { +function getSortedReportActionData(data: ReportActionListItemType[], localeCompare: LocaleContextProps['localeCompare']) { return data.sort((a, b) => { const aValue = a?.created; const bValue = b?.created; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 2088f6d17f57..3275c2117a62 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -10,6 +10,7 @@ import * as SearchUIUtils from '@src/libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import {localeCompare} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; jest.mock('@src/components/ConfirmedRoute.tsx'); @@ -1265,7 +1266,7 @@ describe('SearchUIUtils', () => { describe('Test getSortedSections', () => { it('should return getSortedReportActionData result when type is CHAT', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.EXPENSE.ALL, reportActionListItems)).toStrictEqual([ + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.EXPENSE.ALL, reportActionListItems, localeCompare)).toStrictEqual([ { accountID: 18439984, actionName: 'ADDCOMMENT', @@ -1300,37 +1301,43 @@ describe('SearchUIUtils', () => { }); it('should return getSortedTransactionData result when groupBy is undefined', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionsListItems, 'date', 'asc', undefined)).toStrictEqual(transactionsListItems); + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionsListItems, localeCompare, 'date', 'asc', undefined)).toStrictEqual(transactionsListItems); }); it('should return getSortedReportData result when type is EXPENSE and groupBy is report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS)).toStrictEqual( - transactionReportGroupListItems, - ); + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS), + ).toStrictEqual(transactionReportGroupListItems); }); it('should return getSortedReportData result when type is TRIP and groupBy is report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS)).toStrictEqual( - transactionReportGroupListItems, - ); + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS), + ).toStrictEqual(transactionReportGroupListItems); }); it('should return getSortedReportData result when type is INVOICE and groupBy is report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS)).toStrictEqual( - transactionReportGroupListItems, - ); + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.REPORTS), + ).toStrictEqual(transactionReportGroupListItems); }); it('should return getSortedMemberData result when type is EXPENSE and groupBy is member', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS)).toStrictEqual([]); // s77rt update test + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS), + ).toStrictEqual([]); // s77rt update test }); it('should return getSortedMemberData result when type is TRIP and groupBy is member', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS)).toStrictEqual([]); // s77rt update test + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS), + ).toStrictEqual([]); // s77rt update test }); it('should return getSortedMemberData result when type is INVOICE and groupBy is member', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, '', transactionReportGroupListItems, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS)).toStrictEqual([]); // s77rt update test + expect( + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, '', transactionReportGroupListItems, localeCompare, 'date', 'asc', CONST.SEARCH.GROUP_BY.MEMBERS), + ).toStrictEqual([]); // s77rt update test }); // s77rt add test for group by card diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 549b3ea97851..9e62dbe495e1 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -346,6 +346,16 @@ async function navigateToSidebarOption(index: number): Promise { await waitForBatchedUpdatesWithAct(); } +/** + * @private + * This is a custom collator only for testing purposes. + */ +const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}); + +function localeCompare(a: string, b: string): number { + return customCollator.compare(a, b); +} + export type {MockFetch, FormData}; export { assertFormDataMatchesObject, @@ -363,4 +373,5 @@ export { navigateToSidebarOption, getOnyxData, getNavigateToChatHintRegex, + localeCompare, };