diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 161d0d688973..38a0bee4ceed 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6403,7 +6403,7 @@ const CONST = { PM: 'PM', }, INDENTS: ' ', - PARENT_CHILD_SEPARATOR: ': ', + PARENT_CHILD_SEPARATOR: ':', DISTANCE_MERCHANT_SEPARATOR: '@', COLON: ':', MAPBOX: { diff --git a/src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx b/src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx index d8b5787aec85..e47f3d84330b 100644 --- a/src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx @@ -4,7 +4,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDecodedCategoryName} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName} from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getCategory, willFieldBeAutomaticallyFilled} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -50,7 +50,7 @@ function CategoryField({ const shouldDisplayCategoryError = formError === 'violations.categoryOutOfPolicy'; const iouCategory = getCategory(transaction); - const decodedCategoryName = getDecodedCategoryName(iouCategory); + const decodedCategoryName = getDecodedLeafCategoryName(iouCategory); const getCategoryRightLabelIcon = () => (willFieldBeAutomaticallyFilled(transaction, 'category') ? icons.Sparkles : undefined); const getCategoryRightLabel = () => { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx index d0cb8db184b3..6f092d9df62d 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getDecodedLeafCategoryName} from '@libs/CategoryUtils'; import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -69,10 +70,9 @@ function MoneyRequestReportGroupHeader({ const {shouldUseNarrowLayout: shouldUseNarrowLayoutHook} = useResponsiveLayoutOnWideRHP(); const shouldUseNarrowLayout = shouldUseNarrowLayoutProp ?? shouldUseNarrowLayoutHook; - const cleanedGroupName = isGroupedByTag && group.groupName ? getCommaSeparatedTagNameWithSanitizedColons(group.groupName) : group.groupName; + const cleanedGroupName = isGroupedByTag && group.groupName ? getCommaSeparatedTagNameWithSanitizedColons(group.groupName) : getDecodedLeafCategoryName(group.groupName); const displayName = cleanedGroupName || translate(isGroupedByTag ? 'reportLayout.noTag' : 'reportLayout.uncategorized'); const formattedAmount = convertToDisplayString(group.subTotalAmount, currency); - const shouldShowCheckbox = isSelectionModeEnabled || !shouldUseNarrowLayout; const textStyle = shouldUseNarrowLayout ? {fontSize: variables.fontSizeLabel, lineHeight: 16} : [styles.labelStrong]; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a0dc940c92ba..f2ac28298175 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -44,7 +44,7 @@ import {updateMoneyRequestBillable, updateMoneyRequestReimbursable, updateMoneyR import initSplitExpense from '@libs/actions/SplitExpenses'; import {enrichAndSortAttendees, getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; import {getBrokenConnectionUrlToFixPersonalCard, getCompanyCardDescription} from '@libs/CardUtils'; -import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getRateFromMerchant} from '@libs/MergeTransactionUtils'; @@ -679,7 +679,7 @@ function MoneyRequestView({ const merchantCopyValue = !canEditMerchant ? updatedMerchantTitle : undefined; const dateCopyValue = !canEditDate ? transactionDate : undefined; const categoryValue = updatedTransaction?.category ?? categoryForDisplay; - const decodedCategoryName = getDecodedCategoryName(categoryValue); + const decodedCategoryName = getDecodedLeafCategoryName(categoryValue); const categoryCopyValue = !canEdit ? decodedCategoryName : undefined; const cardCopyValue = cardProgramName; const taxRateValue = hasTaxValueChanged ? taxValue : (transaction?.taxName ?? taxRateTitle ?? fallbackTaxRateTitle ?? ''); diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index cda98dcec932..9e24adb4845e 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -22,7 +22,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getBrokenConnectionUrlToFixPersonalCard} from '@libs/CardUtils'; -import {getDecodedCategoryName} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName} from '@libs/CategoryUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {calculateAmount} from '@libs/IOUUtils'; import Parser from '@libs/Parser'; @@ -391,7 +391,7 @@ function TransactionPreviewContent({ numberOfLines={1} style={[isDeleted && styles.lineThrough, styles.textMicroSupporting, styles.pre, styles.flexShrink1]} > - {getDecodedCategoryName(category ?? '')} + {getDecodedLeafCategoryName(category ?? '')} )} diff --git a/src/components/SelectionList/ListItem/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx index ea2139e64c02..d71d011e64fd 100644 --- a/src/components/SelectionList/ListItem/SplitListItem.tsx +++ b/src/components/SelectionList/ListItem/SplitListItem.tsx @@ -12,7 +12,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDecodedCategoryName} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName} from '@libs/CategoryUtils'; import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -94,7 +94,7 @@ function SplitListItem({ const textContentAccessibilityLabel = [ splitItem.headerText, splitItem.merchant, - splitItem.category ? getDecodedCategoryName(splitItem.category) : undefined, + splitItem.category ? getDecodedLeafCategoryName(splitItem.category) : undefined, splitItem.tags?.at(0) ? getCommaSeparatedTagNameWithSanitizedColons(splitItem.tags.at(0) ?? '') : undefined, ] .filter(Boolean) @@ -173,7 +173,7 @@ function SplitListItem({ numberOfLines={1} style={[styles.textMicroSupporting, styles.pre, styles.flexShrink1]} > - {getDecodedCategoryName(splitItem.category)} + {getDecodedLeafCategoryName(splitItem.category)} )} diff --git a/src/components/TransactionItemRow/DataCells/CategoryCell.tsx b/src/components/TransactionItemRow/DataCells/CategoryCell.tsx index a9ed438d043e..22e36fa836c2 100644 --- a/src/components/TransactionItemRow/DataCells/CategoryCell.tsx +++ b/src/components/TransactionItemRow/DataCells/CategoryCell.tsx @@ -7,7 +7,7 @@ import type {EditableProps} from '@components/Table/EditableCell'; import TextWithTooltip from '@components/TextWithTooltip'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import type TransactionDataCellProps from './TransactionDataCellProps'; type CategoryCellProps = TransactionDataCellProps & @@ -21,7 +21,7 @@ function CategoryCell({shouldUseNarrowLayout, shouldShowTooltip, transactionItem const {isEditing, anchorRef, isPopoverVisible, popoverPosition, isInverted, startEditing, cancelEditing} = usePopoverEditState({canEdit}); // For display: decoded category name for user-readable text - const categoryForDisplay = isCategoryMissing(transactionItem?.category) ? '' : getDecodedCategoryName(transactionItem?.category ?? ''); + const categoryForDisplay = isCategoryMissing(transactionItem?.category) ? '' : getDecodedLeafCategoryName(transactionItem?.category ?? ''); // For picker comparison: raw category name (empty if missing, matches IOURequestStepCategory) const categoryForComparison = isCategoryMissing(transactionItem?.category) ? '' : (transactionItem?.category ?? ''); diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts index 595d7ad8b9fa..4e8e033ed16c 100644 --- a/src/libs/CategoryOptionListUtils.ts +++ b/src/libs/CategoryOptionListUtils.ts @@ -8,7 +8,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 {getDecodedCategoryName} from './CategoryUtils'; +import {getDecodedCategoryName, processCategoryNameSegments} from './CategoryUtils'; import type {OptionTree} from './OptionsListUtils'; import tokenizedSearch from './tokenizedSearch'; @@ -29,32 +29,11 @@ type Hierarchy = Record | Category[], isOneLine = false, selectedOptions: Category[] = []): OptionTree[] { +function getCategoryOptionTree(options: Record | Category[], selectedOptions: Category[] = []): OptionTree[] { const optionCollection = new Map(); for (const option of Object.values(options)) { - if (isOneLine) { - if (optionCollection.has(option.name)) { - continue; - } - - const decodedCategoryName = getDecodedCategoryName(option.name); - optionCollection.set(option.name, { - text: decodedCategoryName, - keyForList: option.name, - searchText: option.name, - tooltipText: decodedCategoryName, - isDisabled: !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - isSelected: !!option.isSelected, - pendingAction: option.pendingAction, - }); - - continue; - } - - const array = option.name.split(CONST.PARENT_CHILD_SEPARATOR); - + const array = processCategoryNameSegments(option.name); for (let index = 0; index < array.length; index++) { const optionName = array.at(index); if (!optionName) { @@ -63,20 +42,27 @@ function getCategoryOptionTree(options: Record | Category[], i const indents = times(index, () => CONST.INDENTS).join(''); const isChild = array.length - 1 === index; - const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); + + // For leaf categories, use the original full name so it matches the policy. + // For parent categories, build the path from the processed segments. + const searchText = isChild ? option.name : array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); const selectedParentOption = !isChild && Object.values(selectedOptions).find((op) => op.name === searchText); - const isParentOptionDisabled = !selectedParentOption || !selectedParentOption.enabled || selectedParentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const optionParent = !isChild && Object.values(options).find((op) => op.name === searchText); + const parentOption = selectedParentOption ?? optionParent; + + const isParentOptionDisabled = !parentOption || !parentOption.enabled || parentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; if (optionCollection.has(searchText)) { continue; } - - const decodedCategoryName = getDecodedCategoryName(optionName); + const leafName = getDecodedCategoryName(optionName.trim()); + const decodedCategoryName = getDecodedCategoryName(option.name); + const tooltipText = isChild ? decodedCategoryName : getDecodedCategoryName(searchText); optionCollection.set(searchText, { - text: `${indents}${decodedCategoryName}`, + text: `${indents}${leafName}`, keyForList: searchText, searchText, - tooltipText: decodedCategoryName, + tooltipText, isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : isParentOptionDisabled, isSelected: isChild ? !!option.isSelected : !!selectedParentOption, pendingAction: option.pendingAction, @@ -124,7 +110,7 @@ function getCategoryListSections({ } if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState); categorySections.push({ // "Selected" section title: '', @@ -136,14 +122,42 @@ function getCategoryListSections({ } if (searchValue) { + // Step 1: Combine selected and enabled categories for searching const categoriesForSearch = [...selectedOptionsWithDisabledState, ...enabledCategories]; - const searchCategories: Category[] = tokenizedSearch(categoriesForSearch, searchValue, (category) => [category.name]).map((category) => ({ + // Step 2: Get search results using tokenizedSearch + let searchCategories: Category[] = tokenizedSearch(categoriesForSearch, searchValue, (category) => [category.name]).map((category) => ({ + ...category, + // Temporarily store if it was selected + wasSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), + })); + + // Step 3: Deduplicate by name (keep first occurrence, which is likely the selected one if present) + const seen = new Set(); + searchCategories = searchCategories.filter((category) => { + if (seen.has(category.name)) { + return false; + } + seen.add(category.name); + return true; + }); + + // Step 4: Re-sort to restore hierarchical grouping + // Convert back to Record format expected by sortCategories + const categoriesRecord: Record = {}; + for (const category of searchCategories) { + categoriesRecord[category.name] = category; + } + const searchSortedCategories = sortCategories(categoriesRecord, localeCompare); + + // Step 5: Re-apply the isSelected flag (lost during sortCategories) + const finalSearchCategories: Category[] = searchSortedCategories.map((category) => ({ ...category, isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), })); - const data = getCategoryOptionTree(searchCategories, true); + // Step 6: Generate the option tree and push the section + const data = getCategoryOptionTree(finalSearchCategories); categorySections.push({ // "Search" section title: '', @@ -155,7 +169,7 @@ function getCategoryListSections({ } if (selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState); categorySections.push({ // "Selected" section title: '', @@ -168,7 +182,7 @@ function getCategoryListSections({ const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.has(category.name)); if (numberOfEnabledCategories < CONST.STANDARD_LIST_ITEM_LIMIT) { - const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); + const data = getCategoryOptionTree(filteredCategories, selectedOptionsWithDisabledState); categorySections.push({ // "All" section when items amount less than the threshold title: '', @@ -192,7 +206,7 @@ function getCategoryListSections({ if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); - const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); + const data = getCategoryOptionTree(cutRecentlyUsedCategories); categorySections.push({ // "Recent" section title: translate('common.recent'), @@ -201,7 +215,7 @@ function getCategoryListSections({ }); } - const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); + const data = getCategoryOptionTree(filteredCategories, selectedOptionsWithDisabledState); categorySections.push({ // "All" section when items amount more than the threshold title: translate('common.all'), @@ -239,7 +253,7 @@ function sortCategories(categories: Record, localeCompare: Loc * } */ for (const category of sortedCategories) { - const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); + const path = processCategoryNameSegments(category.name); const existedValue = lodashGet(hierarchy, path, {}) as Hierarchy; lodashSet(hierarchy, path, { ...existedValue, diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 772f829bd7a2..fcf329257499 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -147,6 +147,49 @@ function getDecodedCategoryName(categoryName: string) { return Str.htmlDecode(categoryName); } +/** + * Splits a category name on the colon separator, removes empty middle segments, + * and merges a trailing empty segment into the previous part (preserving trailing colons). + * + * Examples: + * "Food: Meat" → ["Food", "Meat"] + * "A: B:" → ["A", "B:"] + * "A: B: :" → ["A", "B:"] + * "A: B::" → ["A", "B:"] + * "A: B:::" → ["A", "B:"] + * ":D" → ["D"] + * "Plain" → ["Plain"] + */ +function processCategoryNameSegments(categoryName: string): string[] { + const parts = categoryName.split(CONST.PARENT_CHILD_SEPARATOR); + const result: string[] = []; + + // Keep only parts that contain at least one non‑whitespace character. + for (let i = 0; i < parts.length; i++) { + const part = parts.at(i); + if (part === undefined) { + continue; + } + if (part.trim() !== '') { + result.push(part); + } + } + + // If the original name ends with a colon (allowing trailing spaces), append a colon to the last segment. + const endsWithColon = categoryName.trim().endsWith(CONST.PARENT_CHILD_SEPARATOR); + if (endsWithColon && result.length > 0) { + result[result.length - 1] = result.at(result.length - 1) + CONST.PARENT_CHILD_SEPARATOR; + } + + return result; +} + +function getDecodedLeafCategoryName(categoryName: string): string { + const segments = processCategoryNameSegments(categoryName); + const leaf = segments.at(segments.length - 1) ?? ''; + return Str.htmlDecode(leaf.trim()); +} + function getAvailableNonPersonalPolicyCategories(policyCategories: OnyxCollection, personalPolicyID: string | undefined) { return Object.fromEntries( Object.entries(policyCategories ?? {}).filter(([key, categories]) => { @@ -171,5 +214,7 @@ export { isCategoryMissing, isCategoryDescriptionRequired, getDecodedCategoryName, + getDecodedLeafCategoryName, + processCategoryNameSegments, getAvailableNonPersonalPolicyCategories, }; diff --git a/src/libs/MergeTransactionUtils.ts b/src/libs/MergeTransactionUtils.ts index 4e8443903e0a..1e5bdf499508 100644 --- a/src/libs/MergeTransactionUtils.ts +++ b/src/libs/MergeTransactionUtils.ts @@ -9,6 +9,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {MergeTransaction, Policy, Report, SearchResults, Transaction} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import SafeString from '@src/utils/SafeString'; +import {getDecodedLeafCategoryName} from './CategoryUtils'; import {convertToBackendAmount} from './CurrencyUtils'; import Parser from './Parser'; import {getCommaSeparatedTagNameWithSanitizedColons} from './PolicyUtils'; @@ -543,6 +544,10 @@ function getDisplayValue( return getTaxName(policy, transaction) ?? transaction.taxValue ?? ''; } + if (field === 'category') { + return getDecodedLeafCategoryName(SafeString(fieldValue)); + } + return SafeString(fieldValue); } /** diff --git a/src/pages/iou/SplitExpenseEditPage.tsx b/src/pages/iou/SplitExpenseEditPage.tsx index e74f0e72060e..c2a74c3f0d30 100644 --- a/src/pages/iou/SplitExpenseEditPage.tsx +++ b/src/pages/iou/SplitExpenseEditPage.tsx @@ -22,7 +22,7 @@ import type {ViolationField} from '@hooks/useViolations'; import {initDraftSplitExpenseDataForEdit, removeSplitExpenseField, updateSplitExpenseField} from '@libs/actions/IOU/SplitExpenseItems'; import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category'; import {openPolicyTagsPage} from '@libs/actions/Policy/Tag'; -import {getDecodedCategoryName, isCategoryDescriptionRequired} from '@libs/CategoryUtils'; +import {getDecodedLeafCategoryName, isCategoryDescriptionRequired} from '@libs/CategoryUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; @@ -260,7 +260,7 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { shouldShowRightIcon key={translate('common.category')} description={translate('common.category')} - title={getDecodedCategoryName(splitExpenseDraftTransactionDetails?.category ?? '')} + title={getDecodedLeafCategoryName(splitExpenseDraftTransactionDetails?.category ?? '')} numberOfLinesTitle={2} rightLabel={isCategoryRequired ? translate('common.required') : ''} onPress={() => { diff --git a/tests/unit/CategoryOptionListUtilsTest.ts b/tests/unit/CategoryOptionListUtilsTest.ts index ec10284f052b..ada043b7510c 100644 --- a/tests/unit/CategoryOptionListUtilsTest.ts +++ b/tests/unit/CategoryOptionListUtilsTest.ts @@ -104,7 +104,7 @@ describe('CategoryOptionListUtils', () => { text: ' Meat', keyForList: 'Food: Meat', searchText: 'Food: Meat', - tooltipText: 'Meat', + tooltipText: 'Food: Meat', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -136,7 +136,7 @@ describe('CategoryOptionListUtils', () => { pendingAction: undefined, }, { - text: 'Food: Meat', + text: ' Meat', keyForList: 'Food: Meat', searchText: 'Food: Meat', tooltipText: 'Food: Meat', @@ -357,7 +357,7 @@ describe('CategoryOptionListUtils', () => { text: ' Audi', keyForList: 'Cars: Audi', searchText: 'Cars: Audi', - tooltipText: 'Audi', + tooltipText: 'Cars: Audi', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -366,7 +366,7 @@ describe('CategoryOptionListUtils', () => { text: ' Mercedes-Benz', keyForList: 'Cars: Mercedes-Benz', searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Mercedes-Benz', + tooltipText: 'Cars: Mercedes-Benz', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -393,7 +393,7 @@ describe('CategoryOptionListUtils', () => { text: ' Meat', keyForList: 'Food: Meat', searchText: 'Food: Meat', - tooltipText: 'Meat', + tooltipText: 'Food: Meat', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -402,7 +402,7 @@ describe('CategoryOptionListUtils', () => { text: ' Milk', keyForList: 'Food: Milk', searchText: 'Food: Milk', - tooltipText: 'Milk', + tooltipText: 'Food: Milk', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -438,7 +438,7 @@ describe('CategoryOptionListUtils', () => { text: ' Meals', keyForList: 'Travel: Meals', searchText: 'Travel: Meals', - tooltipText: 'Meals', + tooltipText: 'Travel: Meals', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -447,7 +447,7 @@ describe('CategoryOptionListUtils', () => { text: ' Breakfast', keyForList: 'Travel: Meals: Breakfast', searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Breakfast', + tooltipText: 'Travel: Meals: Breakfast', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -456,7 +456,7 @@ describe('CategoryOptionListUtils', () => { text: ' Lunch', keyForList: 'Travel: Meals: Lunch', searchText: 'Travel: Meals: Lunch', - tooltipText: 'Lunch', + tooltipText: 'Travel: Meals: Lunch', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -488,7 +488,7 @@ describe('CategoryOptionListUtils', () => { pendingAction: undefined, }, { - text: 'Food: Meat', + text: ' Meat', keyForList: 'Food: Meat', searchText: 'Food: Meat', tooltipText: 'Food: Meat', @@ -497,7 +497,7 @@ describe('CategoryOptionListUtils', () => { pendingAction: undefined, }, { - text: 'Food: Milk', + text: ' Milk', keyForList: 'Food: Milk', searchText: 'Food: Milk', tooltipText: 'Food: Milk', @@ -698,7 +698,7 @@ describe('CategoryOptionListUtils', () => { text: ' Meat', keyForList: 'Food: Meat', searchText: 'Food: Meat', - tooltipText: 'Meat', + tooltipText: 'Food: Meat', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -707,7 +707,7 @@ describe('CategoryOptionListUtils', () => { text: ' Milk', keyForList: 'Food: Milk', searchText: 'Food: Milk', - tooltipText: 'Milk', + tooltipText: 'Food: Milk', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -725,7 +725,7 @@ describe('CategoryOptionListUtils', () => { text: ' Audi', keyForList: 'Cars: Audi', searchText: 'Cars: Audi', - tooltipText: 'Audi', + tooltipText: 'Cars: Audi', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -734,7 +734,7 @@ describe('CategoryOptionListUtils', () => { text: ' Mercedes-Benz', keyForList: 'Cars: Mercedes-Benz', searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Mercedes-Benz', + tooltipText: 'Cars: Mercedes-Benz', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -752,7 +752,7 @@ describe('CategoryOptionListUtils', () => { text: ' Meals', keyForList: 'Travel: Meals', searchText: 'Travel: Meals', - tooltipText: 'Meals', + tooltipText: 'Travel: Meals', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -761,7 +761,7 @@ describe('CategoryOptionListUtils', () => { text: ' Breakfast', keyForList: 'Travel: Meals: Breakfast', searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Breakfast', + tooltipText: 'Travel: Meals: Breakfast', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -770,7 +770,7 @@ describe('CategoryOptionListUtils', () => { text: ' Lunch', keyForList: 'Travel: Meals: Lunch', searchText: 'Travel: Meals: Lunch', - tooltipText: 'Lunch', + tooltipText: 'Travel: Meals: Lunch', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -815,7 +815,7 @@ describe('CategoryOptionListUtils', () => { text: ' B', keyForList: 'A: B', searchText: 'A: B', - tooltipText: 'B', + tooltipText: 'A: B', isDisabled: true, isSelected: false, pendingAction: undefined, @@ -824,7 +824,7 @@ describe('CategoryOptionListUtils', () => { text: ' C', keyForList: 'A: B: C', searchText: 'A: B: C', - tooltipText: 'C', + tooltipText: 'A: B: C', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -833,7 +833,7 @@ describe('CategoryOptionListUtils', () => { text: ' D', keyForList: 'A: B: C: D', searchText: 'A: B: C: D', - tooltipText: 'D', + tooltipText: 'A: B: C: D', isDisabled: true, isSelected: false, pendingAction: undefined, @@ -842,152 +842,13 @@ describe('CategoryOptionListUtils', () => { text: ' E', keyForList: 'A: B: C: D: E', searchText: 'A: B: C: D: E', - tooltipText: 'E', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ]; - const resultOneLine = [ - { - text: 'Meals', - keyForList: 'Meals', - searchText: 'Meals', - tooltipText: 'Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Food: Meat', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Milk', - keyForList: 'Food: Milk', - searchText: 'Food: Milk', - tooltipText: 'Food: Milk', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cars: Audi', - keyForList: 'Cars: Audi', - searchText: 'Cars: Audi', - tooltipText: 'Cars: Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cars: Mercedes-Benz', - keyForList: 'Cars: Mercedes-Benz', - searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Cars: Mercedes-Benz', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals', - keyForList: 'Travel: Meals', - searchText: 'Travel: Meals', - tooltipText: 'Travel: Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals: Breakfast', - keyForList: 'Travel: Meals: Breakfast', - searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Travel: Meals: Breakfast', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals: Lunch', - keyForList: 'Travel: Meals: Lunch', - searchText: 'Travel: Meals: Lunch', - tooltipText: 'Travel: Meals: Lunch', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Plain', - keyForList: 'Plain', - searchText: 'Plain', - tooltipText: 'Plain', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Audi', - keyForList: 'Audi', - searchText: 'Audi', - tooltipText: 'Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Health', - keyForList: 'Health', - searchText: 'Health', - tooltipText: 'Health', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'A: B: C', - keyForList: 'A: B: C', - searchText: 'A: B: C', - tooltipText: 'A: B: C', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'A: B: C: D: E', - keyForList: 'A: B: C: D: E', - searchText: 'A: B: C: D: E', tooltipText: 'A: B: C: D: E', isDisabled: false, isSelected: false, pendingAction: undefined, }, ]; - expect(getCategoryOptionTree(categories)).toStrictEqual(result); - expect(getCategoryOptionTree(categories, true)).toStrictEqual(resultOneLine); }); it('sortCategories', () => {