diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts index a420bee2ade03..87bc4f5a17165 100644 --- a/src/libs/CategoryOptionListUtils.ts +++ b/src/libs/CategoryOptionListUtils.ts @@ -6,6 +6,7 @@ import type {PolicyCategories} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; +import localeCompare from './LocaleCompare'; import {translateLocal} from './Localize'; import type {OptionTree, SectionBase} from './OptionsListUtils'; @@ -218,7 +219,7 @@ function getCategoryListSections({ */ function sortCategories(categories: Record): Category[] { // Sorts categories alphabetically by name. - const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); + const sortedCategories = Object.values(categories).sort((a, b) => localeCompare(a.name, b.name)); // An object that respects nesting of categories. Also, can contain only uniq categories. const hierarchy: Hierarchy = {}; diff --git a/src/libs/LocaleCompare.ts b/src/libs/LocaleCompare.ts index b2c48b410d32a..817f617523f3a 100644 --- a/src/libs/LocaleCompare.ts +++ b/src/libs/LocaleCompare.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'base'}; +const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'variant', numeric: true, caseFirst: 'upper'}; let collator = new Intl.Collator(CONST.LOCALES.DEFAULT, COLLATOR_OPTIONS); diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts index e67c4378b245a..3b5f6adfcaafe 100644 --- a/src/libs/TagsOptionsListUtils.ts +++ b/src/libs/TagsOptionsListUtils.ts @@ -1,12 +1,11 @@ -import lodashSortBy from 'lodash/sortBy'; import CONST from '@src/CONST'; import type {PolicyTag, PolicyTagLists, PolicyTags} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import localeCompare from './LocaleCompare'; -import * as Localize from './Localize'; +import {translateLocal} from './Localize'; +import {hasEnabledOptions} from './OptionsListUtils'; import type {Option} from './OptionsListUtils'; -import * as OptionsListUtils from './OptionsListUtils'; -import * as PolicyUtils from './PolicyUtils'; +import {getCleanedTagName} from './PolicyUtils'; type SelectedTagOption = { name: string; @@ -24,7 +23,7 @@ type SelectedTagOption = { function getTagsOptions(tags: Array>, selectedOptions?: SelectedTagOption[]): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. - const cleanedName = PolicyUtils.getCleanedTagName(tag.name); + const cleanedName = getCleanedTagName(tag.name); return { text: cleanedName, keyForList: tag.name, @@ -84,8 +83,8 @@ function getTagListSections({ } if (searchValue) { - const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchValue.toLowerCase())); - const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchValue.toLowerCase())); + const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => getCleanedTagName(tag.name.toLowerCase()).includes(searchValue.toLowerCase())); + const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => getCleanedTagName(tag.name.toLowerCase()).includes(searchValue.toLowerCase())); const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; tagSections.push({ @@ -130,7 +129,7 @@ function getTagListSections({ tagSections.push({ // "Recent" section - title: Localize.translateLocal('common.recent'), + title: translateLocal('common.recent'), shouldShow: true, data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); @@ -138,7 +137,7 @@ function getTagListSections({ tagSections.push({ // "All" section when items amount more than the threshold - title: Localize.translateLocal('common.all'), + title: translateLocal('common.all'), shouldShow: true, data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), }); @@ -155,15 +154,14 @@ function hasEnabledTags(policyTagList: Array Object.values(tags)) .flat(); - return OptionsListUtils.hasEnabledOptions(policyTagValueList); + return hasEnabledOptions(policyTagValueList); } /** * Sorts tags alphabetically by name. */ function sortTags(tags: Record | Array) { - // Use lodash's sortBy to ensure consistency with oldDot. - return lodashSortBy(tags, 'name', localeCompare) as PolicyTag[]; + return Object.values(tags ?? {}).sort((a, b) => localeCompare(a.name, b.name)) as PolicyTag[]; } export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags}; diff --git a/src/libs/TaxOptionsListUtils.ts b/src/libs/TaxOptionsListUtils.ts index 99dfcd9551f34..223a4f6ffa405 100644 --- a/src/libs/TaxOptionsListUtils.ts +++ b/src/libs/TaxOptionsListUtils.ts @@ -1,8 +1,8 @@ -import lodashSortBy from 'lodash/sortBy'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {Policy, TaxRate, TaxRates, Transaction} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import localeCompare from './LocaleCompare'; import {transformedTaxRates} from './TransactionUtils'; type TaxRatesOption = { @@ -32,8 +32,8 @@ type TaxSection = { * Sorts tax rates alphabetically by name. */ function sortTaxRates(taxRates: TaxRates): TaxRate[] { - const sortedTaxRates = lodashSortBy(taxRates, (taxRate) => taxRate.name); - return sortedTaxRates; + const sortedtaxRates = Object.values(taxRates).sort((a, b) => localeCompare(a.name, b.name)); + return sortedtaxRates; } /** diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 2569066fbc5e1..ad76f102c7159 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -1,4 +1,3 @@ -import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -164,7 +163,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return categoryText.includes(normalizedSearchInput) || alternateText.includes(normalizedSearchInput); }, []); const sortCategories = useCallback((data: PolicyOption[]) => { - return lodashSortBy(data, 'text', localeCompare) as PolicyOption[]; + return data.sort((a, b) => localeCompare(a.text ?? '', b?.text ?? '')); }, []); const [inputValue, setInputValue, filteredCategoryList] = useSearchResults(categoryList, filterCategory, sortCategories); diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 87f06ad16c2e1..2ce0f08c6323f 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -1,5 +1,4 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -200,7 +199,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const normalizedSearchInput = StringUtils.normalize(searchInput.toLowerCase()); return rateText.includes(normalizedSearchInput); }, []); - const sortRates = useCallback((rates: PolicyOption[]) => lodashSortBy(rates, 'text', localeCompare) as PolicyOption[], []); + const sortRates = useCallback((rates: PolicyOption[]) => rates.sort((a, b) => localeCompare(a.text ?? '', b.text ?? '')), []); const [inputValue, setInputValue, filteredSubRatesList] = useSearchResults(subRatesList, filterRate, sortRates); const toggleSubRate = (subRate: PolicyOption) => { diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 70d13c3d95ebd..24311cdb0d512 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -1,4 +1,3 @@ -import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -241,7 +240,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const normalizeSearchInput = StringUtils.normalize(searchInput.toLowerCase()); return tagText.includes(normalizeSearchInput) || tagValue.includes(normalizeSearchInput); }, []); - const sortTags = useCallback((tags: TagListItem[]) => lodashSortBy(tags, 'value', localeCompare) as TagListItem[], []); + const sortTags = useCallback((tags: TagListItem[]) => tags.sort((a, b) => localeCompare(a.value, b.value)), []); const [inputValue, setInputValue, filteredTagList] = useSearchResults(tagList, filterTag, sortTags); const filteredTagListKeyedByName = useMemo( diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 0f29d05be2401..f0007c631db11 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -32,6 +32,7 @@ import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {clearTaxRateError, deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getLatestErrorFieldForAnyField} from '@libs/ErrorUtils'; +import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import { @@ -192,7 +193,7 @@ function WorkspaceTaxesPage({ return taxes.sort((a, b) => { const aText = a.text ?? a.keyForList ?? ''; const bText = b.text ?? b.keyForList ?? ''; - return aText.localeCompare(bText); + return localeCompare(aText, bText); }); }, []); const [inputValue, setInputValue, filteredTaxesList] = useSearchResults(taxesList, filterTax, sortTaxes); diff --git a/tests/unit/LocaleCompareTest.ts b/tests/unit/LocaleCompareTest.ts index 3c709675f31d1..f362d8949cb16 100644 --- a/tests/unit/LocaleCompareTest.ts +++ b/tests/unit/LocaleCompareTest.ts @@ -33,10 +33,10 @@ describe('localeCompare', () => { expect(result).toBe(0); }); - it('should discard sensitivity differences', () => { + it('should put uppercase letters first', () => { const result = localeCompare('apple', 'Apple'); - expect(result).toBe(0); + expect(result).toBe(1); }); it('distinguishes spanish diacritic characters', async () => { diff --git a/tests/unit/TagsOptionsListUtilsTest.ts b/tests/unit/TagsOptionsListUtilsTest.ts index a3928ba56d08e..6033221c597da 100644 --- a/tests/unit/TagsOptionsListUtilsTest.ts +++ b/tests/unit/TagsOptionsListUtilsTest.ts @@ -321,13 +321,13 @@ describe('TagsOptionsListUtils', () => { const createTagObjects = (names: string[]) => names.map((name) => ({name, enabled: true})); const unorderedTagNames = ['10bc', 'b', '0a', '1', '中国', 'b10', '!', '2', '0', '@', 'a1', 'a', '3', 'b1', '日本', '$', '20', '20a', '#', 'a20', 'c', '10']; - const expectedOrderNames = ['!', '#', '$', '0', '0a', '1', '10', '10bc', '2', '20', '20a', '3', '@', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c', '中国', '日本']; + const expectedOrderNames = ['!', '@', '#', '$', '0', '0a', '1', '2', '3', '10', '10bc', '20', '20a', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c', '中国', '日本']; const unorderedTags = createTagObjects(unorderedTagNames); const expectedOrder = createTagObjects(expectedOrderNames); expect(sortTags(unorderedTags)).toStrictEqual(expectedOrder); const unorderedTagNames2 = ['0', 'a1', '1', 'b1', '3', '10', 'b10', 'a', '2', 'c', '20', 'a20', 'b']; - const expectedOrderNames2 = ['0', '1', '10', '2', '20', '3', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c']; + const expectedOrderNames2 = ['0', '1', '2', '3', '10', '20', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c']; const unorderedTags2 = createTagObjects(unorderedTagNames2); const expectedOrder2 = createTagObjects(expectedOrderNames2); expect(sortTags(unorderedTags2)).toStrictEqual(expectedOrder2); @@ -436,8 +436,15 @@ describe('TagsOptionsListUtils', () => { ]; const expectedOrderNames3 = [ '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', '10', - '100', '11', '12', '13', @@ -447,7 +454,6 @@ describe('TagsOptionsListUtils', () => { '17', '18', '19', - '2', '20', '21', '22', @@ -458,7 +464,6 @@ describe('TagsOptionsListUtils', () => { '27', '28', '29', - '3', '30', '31', '32', @@ -469,7 +474,6 @@ describe('TagsOptionsListUtils', () => { '37', '38', '39', - '4', '40', '41', '42', @@ -480,7 +484,6 @@ describe('TagsOptionsListUtils', () => { '47', '48', '49', - '5', '50', '51', '52', @@ -491,7 +494,6 @@ describe('TagsOptionsListUtils', () => { '57', '58', '59', - '6', '60', '61', '62', @@ -502,7 +504,6 @@ describe('TagsOptionsListUtils', () => { '67', '68', '69', - '7', '70', '71', '72', @@ -513,7 +514,6 @@ describe('TagsOptionsListUtils', () => { '77', '78', '79', - '8', '80', '81', '82', @@ -524,7 +524,6 @@ describe('TagsOptionsListUtils', () => { '87', '88', '89', - '9', '90', '91', '92', @@ -535,6 +534,7 @@ describe('TagsOptionsListUtils', () => { '97', '98', '99', + '100', ]; const unorderedTags3 = createTagObjects(unorderedTagNames3); const expectedOrder3 = createTagObjects(expectedOrderNames3); diff --git a/tests/unit/compareUserInListTest.ts b/tests/unit/compareUserInListTest.ts index ce8b27ddf2268..bbba68f56dbd7 100644 --- a/tests/unit/compareUserInListTest.ts +++ b/tests/unit/compareUserInListTest.ts @@ -22,9 +22,16 @@ describe('compareUserInList', () => { }); it('Should compare the accountID if both the weight and displayName are the same', () => { - const first = {login: 'águero', weight: 2, accountID: 6}; + const first = {login: 'aguero', weight: 2, accountID: 6}; const second = {login: 'aguero', weight: 2, accountID: 7}; expect(compareUserInList(first, second)).toBe(-1); expect(compareUserInList(second, first)).toBe(1); }); + + it('Should compare the displayName with different diacritics if the weight is the same', () => { + const first = {login: 'águero', weight: 2, accountID: 8}; + const second = {login: 'aguero', weight: 2, accountID: 8}; + expect(compareUserInList(first, second)).toBe(1); + expect(compareUserInList(second, first)).toBe(-1); + }); });