From d5e8710e2fa3125e1fb273ce63493caeca15e263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 8 Nov 2024 17:34:09 +0100 Subject: [PATCH] cleanup: refactor getTagListSections to TagsOptionsListUtils --- .../MoneyRequestConfirmationListFooter.tsx | 3 +- .../ReportActionItem/MoneyRequestView.tsx | 3 +- src/components/TagPicker/index.tsx | 18 +- src/libs/OptionsListUtils.ts | 188 ------ src/libs/TagsOptionsListUtils.ts | 176 ++++++ .../iou/request/step/IOURequestStepTag.tsx | 4 +- tests/unit/OptionsListUtilsTest.ts | 538 ----------------- tests/unit/TagsOptionsListUtilsTest.ts | 543 ++++++++++++++++++ 8 files changed, 729 insertions(+), 744 deletions(-) create mode 100644 src/libs/TagsOptionsListUtils.ts create mode 100644 tests/unit/TagsOptionsListUtilsTest.ts diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index fe04fea81e71..01756e11aadf 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -17,6 +17,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; +import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -224,7 +225,7 @@ function MoneyRequestConfirmationListFooter({ // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); + const shouldShowTags = useMemo(() => isPolicyExpenseChat && TagsOptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]); const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ee87f8f12c7d..b31baede3906 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -28,6 +28,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; +import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; @@ -182,7 +183,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); // transactionTag can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTag = isPolicyExpenseChat && (transactionTag || TagsOptionsListUtils.hasEnabledTags(policyTagLists)); const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable); const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 9d3a70d4d50c..8df25a94c8a4 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -7,18 +7,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import type * as ReportUtils from '@libs/ReportUtils'; +import type {SelectedTagOption} from '@libs/TagsOptionsListUtils'; +import * as TagOptionListUtils from '@libs/TagsOptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTag, PolicyTags} from '@src/types/onyx'; -import type {PendingAction} from '@src/types/onyx/OnyxCommon'; - -type SelectedTagOption = { - name: string; - enabled: boolean; - isSelected?: boolean; - accountID: number | undefined; - pendingAction?: PendingAction; -}; type TagPickerProps = { /** The policyID we are getting tags for */ @@ -81,15 +74,12 @@ function TagPicker({selectedTag, tagListName, policyID, tagListIndex, shouldShow const sections = useMemo( () => - OptionsListUtils.getFilteredOptions({ + TagOptionListUtils.getTagListSections({ searchValue, selectedOptions, - includeP2P: false, - includeTags: true, tags: enabledTags, recentlyUsedTags: policyRecentlyUsedTagsList, - canInviteUser: false, - }).tagOptions, + }), [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1296a64e571d..0d722fdf866c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -9,7 +9,6 @@ import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; import {FallbackAvatar} from '@components/Icon/Expensicons'; -import type {SelectedTagOption} from '@components/TagPicker'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -24,8 +23,6 @@ import type { PolicyCategories, PolicyCategory, PolicyTag, - PolicyTagLists, - PolicyTags, Report, ReportAction, ReportActions, @@ -42,7 +39,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; import filterArrayByMatch from './filterArrayByMatch'; -import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; @@ -162,9 +158,6 @@ type GetOptionsConfig = { includeCategories?: boolean; categories?: PolicyCategories; recentlyUsedCategories?: string[]; - includeTags?: boolean; - tags?: PolicyTags | Array; - recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; includeTaxRates?: boolean; @@ -215,7 +208,6 @@ type Options = { userToInvite: ReportUtils.OptionData | null; currentUserOption: ReportUtils.OptionData | null | undefined; categoryOptions: CategoryTreeSection[]; - tagOptions: CategorySection[]; taxRatesOptions: CategorySection[]; policyReportFieldOptions?: CategorySection[] | null; }; @@ -982,16 +974,6 @@ function sortCategories(categories: Record): Category[] { return flatHierarchy(hierarchy); } -/** - * Sorts tags alphabetically by name. - */ -function sortTags(tags: Record | Array) { - const sortedTags = Array.isArray(tags) ? tags : Object.values(tags); - - // Use lodash's sortBy to ensure consistency with oldDot. - return lodashSortBy(sortedTags, 'name', localeCompare); -} - /** * Builds the options for the category tree hierarchy via indents * @@ -1174,138 +1156,6 @@ function getCategoryListSections( return categorySections; } -/** - * Transforms the provided tags into option objects. - * - * @param tags - an initial tag array - */ -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); - return { - text: cleanedName, - keyForList: tag.name, - searchText: tag.name, - tooltipText: cleanedName, - isDisabled: !tag.enabled || tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), - pendingAction: tag.pendingAction, - }; - }); -} - -/** - * Build the section list for tags - */ -function getTagListSections( - tags: Array, - recentlyUsedTags: string[], - selectedOptions: SelectedTagOption[], - searchInputValue: string, - maxRecentReportsToShow: number, -) { - const tagSections = []; - const sortedTags = sortTags(tags) as PolicyTag[]; - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = sortedTags.filter((tag) => tag.enabled); - const enabledTagsNames = enabledTags.map((tag) => tag.name); - const enabledTagsWithoutSelectedOptions = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); - const selectedTagsWithDisabledState: SelectedTagOption[] = []; - const numberOfTags = enabledTags.length; - - selectedOptions.forEach((tag) => { - if (enabledTagsNames.includes(tag.name)) { - selectedTagsWithDisabledState.push({...tag, enabled: true}); - return; - } - selectedTagsWithDisabledState.push({...tag, enabled: false}); - }); - - // If all tags are disabled but there's a previously selected tag, show only the selected tag - if (numberOfTags === 0 && selectedOptions.length > 0) { - tagSections.push({ - // "Selected" section - title: '', - shouldShow: false, - data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), - }); - - return tagSections; - } - - if (searchInputValue) { - const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); - const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); - const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; - - tagSections.push({ - // "Search" section - title: '', - shouldShow: true, - data: getTagsOptions(tagsForSearch, selectedOptions), - }); - - return tagSections; - } - - if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { - tagSections.push({ - // "All" section when items amount less than the threshold - title: '', - shouldShow: false, - data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), - }); - - return tagSections; - } - - const filteredRecentlyUsedTags = recentlyUsedTags - .filter((recentlyUsedTag) => { - const tagObject = tags.find((tag) => tag.name === recentlyUsedTag); - return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag) && tagObject?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - }) - .map((tag) => ({name: tag, enabled: true})); - - if (selectedOptions.length) { - tagSections.push({ - // "Selected" section - title: '', - shouldShow: true, - data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), - }); - } - - if (filteredRecentlyUsedTags.length > 0) { - const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow); - - tagSections.push({ - // "Recent" section - title: Localize.translateLocal('common.recent'), - shouldShow: true, - data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), - }); - } - - tagSections.push({ - // "All" section when items amount more than the threshold - title: Localize.translateLocal('common.all'), - shouldShow: true, - data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), - }); - - return tagSections; -} - -/** - * Verifies that there is at least one enabled tag - */ -function hasEnabledTags(policyTagList: Array) { - const policyTagValueList = policyTagList.map(({tags}) => Object.values(tags)).flat(); - - return hasEnabledOptions(policyTagValueList); -} - /** * Transforms the provided report field options into option objects. * @@ -1714,9 +1564,6 @@ function getOptions( includeCategories = false, categories = {}, recentlyUsedCategories = [], - includeTags = false, - tags = {}, - recentlyUsedTags = [], canInviteUser = true, includeSelectedOptions = false, transactionViolations = {}, @@ -1743,21 +1590,6 @@ function getOptions( userToInvite: null, currentUserOption: null, categoryOptions, - tagOptions: [], - taxRatesOptions: [], - }; - } - - if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); - - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions, taxRatesOptions: [], }; } @@ -1771,7 +1603,6 @@ function getOptions( userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], taxRatesOptions, }; } @@ -1784,7 +1615,6 @@ function getOptions( userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], taxRatesOptions: [], policyReportFieldOptions: transformedPolicyReportFieldOptions, }; @@ -2046,7 +1876,6 @@ function getOptions( userToInvite: canInviteUser ? userToInvite : null, currentUserOption, categoryOptions: [], - tagOptions: [], taxRatesOptions: [], }; } @@ -2133,9 +1962,6 @@ type FilteredOptionsParams = { includeCategories?: boolean; categories?: PolicyCategories; recentlyUsedCategories?: string[]; - includeTags?: boolean; - tags?: PolicyTags | Array; - recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; includeTaxRates?: boolean; @@ -2174,9 +2000,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue includeCategories = false, categories = {}, recentlyUsedCategories = [], - includeTags = false, - tags = {}, - recentlyUsedTags = [], canInviteUser = true, includeSelectedOptions = false, includeTaxRates = false, @@ -2204,9 +2027,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue includeCategories, categories, recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, canInviteUser, includeSelectedOptions, includeTaxRates, @@ -2248,9 +2068,6 @@ function getAttendeeOptions( includeCategories: false, categories: {}, recentlyUsedCategories: [], - includeTags: false, - tags: {}, - recentlyUsedTags: [], canInviteUser, includeSelectedOptions: false, includeTaxRates: false, @@ -2550,7 +2367,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt userToInvite: null, currentUserOption, categoryOptions: [], - tagOptions: [], taxRatesOptions: [], }; }, options); @@ -2586,7 +2402,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt userToInvite, currentUserOption: matchResults.currentUserOption, categoryOptions: [], - tagOptions: [], taxRatesOptions: [], }; } @@ -2602,7 +2417,6 @@ function getEmptyOptions(): Options { userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], taxRatesOptions: [], }; } @@ -2637,9 +2451,7 @@ export { hasEnabledOptions, sortCategories, sortAlphabetically, - sortTags, getCategoryOptionTree, - hasEnabledTags, formatMemberForList, formatSectionsFromSearchTerm, getShareLogOptions, diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts new file mode 100644 index 000000000000..5fc3c25bc6f1 --- /dev/null +++ b/src/libs/TagsOptionsListUtils.ts @@ -0,0 +1,176 @@ +import lodashSortBy from 'lodash/sortBy'; +import CONST from '@src/CONST'; +import type {PolicyCategories, PolicyCategory, 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 type {Option} from './OptionsListUtils'; +import * as PolicyUtils from './PolicyUtils'; + +type SelectedTagOption = { + name: string; + enabled: boolean; + isSelected?: boolean; + accountID: number | undefined; + pendingAction?: PendingAction; +}; + +/** + * Transforms the provided tags into option objects. + * + * @param tags - an initial tag array + */ +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); + return { + text: cleanedName, + keyForList: tag.name, + searchText: tag.name, + tooltipText: cleanedName, + isDisabled: !tag.enabled || tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), + pendingAction: tag.pendingAction, + }; + }); +} + +/** + * Build the section list for tags + */ +function getTagListSections({ + tags, + recentlyUsedTags = [], + selectedOptions = [], + searchValue = '', + maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, +}: { + // TODO: can we be sure that this is always an array?! + tags: PolicyTags | Array; + recentlyUsedTags?: string[]; + selectedOptions?: SelectedTagOption[]; + searchValue?: string; + maxRecentReportsToShow?: number; +}) { + const tagSections = []; + const sortedTags = sortTags(tags); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const enabledTags = sortedTags.filter((tag) => tag.enabled); + const enabledTagsNames = enabledTags.map((tag) => tag.name); + const enabledTagsWithoutSelectedOptions = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); + const selectedTagsWithDisabledState: SelectedTagOption[] = []; + const numberOfTags = enabledTags.length; + + selectedOptions.forEach((tag) => { + if (enabledTagsNames.includes(tag.name)) { + selectedTagsWithDisabledState.push({...tag, enabled: true}); + return; + } + selectedTagsWithDisabledState.push({...tag, enabled: false}); + }); + + // If all tags are disabled but there's a previously selected tag, show only the selected tag + if (numberOfTags === 0 && selectedOptions.length > 0) { + tagSections.push({ + // "Selected" section + title: '', + shouldShow: false, + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), + }); + + return tagSections; + } + + 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 tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; + + tagSections.push({ + // "Search" section + title: '', + shouldShow: true, + data: getTagsOptions(tagsForSearch, selectedOptions), + }); + + return tagSections; + } + + if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { + tagSections.push({ + // "All" section when items amount less than the threshold + title: '', + shouldShow: false, + data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), + }); + + return tagSections; + } + + const filteredRecentlyUsedTags = recentlyUsedTags + .filter((recentlyUsedTag) => { + const tagObject = sortedTags.find((tag) => tag.name === recentlyUsedTag); + return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag) && tagObject?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + }) + .map((tag) => ({name: tag, enabled: true})); + + if (selectedOptions.length) { + tagSections.push({ + // "Selected" section + title: '', + shouldShow: true, + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), + }); + } + + if (filteredRecentlyUsedTags.length > 0) { + const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow); + + tagSections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), + }); + } + + tagSections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), + }); + + return tagSections; +} + +/** + * Verifies that there is at least one enabled tag + */ +function hasEnabledTags(policyTagList: Array) { + const policyTagValueList = policyTagList.map(({tags}) => Object.values(tags)).flat(); + + return hasEnabledOptions(policyTagValueList); +} + +/** + * Sorts tags alphabetically by name. + */ +function sortTags(tags: Record | Array): Array { + const sortedTags = Array.isArray(tags) ? tags : Object.values(tags); + + // TODO: fix the error here, might be fixable by enforcing array + // Use lodash's sortBy to ensure consistency with oldDot. + return lodashSortBy(sortedTags, 'name', localeCompare); +} + +/** + * Verifies that there is at least one enabled option + */ +function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { + return Object.values(options).some((option: PolicyTag | PolicyCategory) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +} + +export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags, hasEnabledOptions}; +export type {SelectedTagOption}; diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx index 6c999d7a7f70..6080a4fa3d8d 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.tsx +++ b/src/pages/iou/request/step/IOURequestStepTag.tsx @@ -12,10 +12,10 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -64,7 +64,7 @@ function IOURequestStepTag({ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - const shouldShowTag = transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists); + const shouldShowTag = transactionTag || TagsOptionsListUtils.hasEnabledTags(policyTagLists); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 5a0cd6638a07..31567591f7ea 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1146,320 +1146,6 @@ describe('OptionsListUtils', () => { expect(emptyResult.categoryOptions).toStrictEqual(emptySelectedResultList); }); - it('getFilteredOptions() for tags', () => { - const search = 'ing'; - const emptySearch = ''; - const wrongSearch = 'bla bla'; - const recentlyUsedTags = ['Engineering', 'HR']; - - const selectedOptions = [ - { - name: 'Medical', - }, - ]; - const smallTagsList: Record = { - Engineering: { - enabled: false, - name: 'Engineering', - accountID: undefined, - }, - Medical: { - enabled: true, - name: 'Medical', - accountID: undefined, - }, - Accounting: { - enabled: true, - name: 'Accounting', - accountID: undefined, - }, - HR: { - enabled: true, - name: 'HR', - accountID: undefined, - pendingAction: 'delete', - }, - }; - const smallResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: false, - // data sorted alphabetically by name - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: true, - isSelected: false, - pendingAction: 'delete', - }, - { - text: 'Medical', - keyForList: 'Medical', - searchText: 'Medical', - tooltipText: 'Medical', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const smallSearchResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ - { - title: '', - shouldShow: true, - data: [], - }, - ]; - const largeTagsList: Record = { - Engineering: { - enabled: false, - name: 'Engineering', - accountID: undefined, - }, - Medical: { - enabled: true, - name: 'Medical', - accountID: undefined, - }, - Accounting: { - enabled: true, - name: 'Accounting', - accountID: undefined, - }, - HR: { - enabled: true, - name: 'HR', - accountID: undefined, - }, - Food: { - enabled: true, - name: 'Food', - accountID: undefined, - }, - Traveling: { - enabled: false, - name: 'Traveling', - accountID: undefined, - }, - Cleaning: { - enabled: true, - name: 'Cleaning', - accountID: undefined, - }, - Software: { - enabled: true, - name: 'Software', - accountID: undefined, - }, - OfficeSupplies: { - enabled: false, - name: 'Office Supplies', - accountID: undefined, - }, - Taxes: { - enabled: true, - name: 'Taxes', - accountID: undefined, - pendingAction: 'delete', - }, - Benefits: { - enabled: true, - name: 'Benefits', - accountID: undefined, - }, - }; - const largeResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Medical', - keyForList: 'Medical', - searchText: 'Medical', - tooltipText: 'Medical', - isDisabled: false, - isSelected: true, - pendingAction: undefined, - }, - ], - }, - { - title: 'Recent', - shouldShow: true, - data: [ - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - { - title: 'All', - shouldShow: true, - // data sorted alphabetically by name - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Benefits', - keyForList: 'Benefits', - searchText: 'Benefits', - tooltipText: 'Benefits', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cleaning', - keyForList: 'Cleaning', - searchText: 'Cleaning', - tooltipText: 'Cleaning', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Software', - keyForList: 'Software', - searchText: 'Software', - tooltipText: 'Software', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Taxes', - keyForList: 'Taxes', - searchText: 'Taxes', - tooltipText: 'Taxes', - isDisabled: true, - isSelected: false, - pendingAction: 'delete', - }, - ], - }, - ]; - const largeSearchResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cleaning', - keyForList: 'Cleaning', - searchText: 'Cleaning', - tooltipText: 'Cleaning', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ - { - title: '', - shouldShow: true, - data: [], - }, - ]; - - const smallResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallResult.tagOptions).toStrictEqual(smallResultList); - - const smallSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); - - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions({searchValue: wrongSearch, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); - - const largeResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); - expect(largeResult.tagOptions).toStrictEqual(largeResultList); - - const largeSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); - expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); - - const largeWrongSearchResult = OptionsListUtils.getFilteredOptions({ - searchValue: wrongSearch, - selectedOptions, - includeP2P: false, - includeTags: true, - tags: largeTagsList, - recentlyUsedTags, - }); - expect(largeWrongSearchResult.tagOptions).toStrictEqual(largeWrongSearchResultList); - }); - it('getCategoryOptionTree()', () => { const categories = { Meals: { @@ -2200,230 +1886,6 @@ describe('OptionsListUtils', () => { expect(OptionsListUtils.sortCategories(categoriesIncorrectOrdering3)).toStrictEqual(result3); }); - it('sortTags', () => { - 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 unorderedTags = createTagObjects(unorderedTagNames); - const expectedOrder = createTagObjects(expectedOrderNames); - expect(OptionsListUtils.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 unorderedTags2 = createTagObjects(unorderedTagNames2); - const expectedOrder2 = createTagObjects(expectedOrderNames2); - expect(OptionsListUtils.sortTags(unorderedTags2)).toStrictEqual(expectedOrder2); - - const unorderedTagNames3 = [ - '61', - '39', - '97', - '93', - '77', - '71', - '22', - '27', - '30', - '64', - '91', - '24', - '33', - '60', - '21', - '85', - '59', - '76', - '42', - '67', - '13', - '96', - '84', - '44', - '68', - '31', - '62', - '87', - '50', - '4', - '100', - '12', - '28', - '49', - '53', - '5', - '45', - '14', - '55', - '78', - '11', - '35', - '75', - '18', - '9', - '80', - '54', - '2', - '34', - '48', - '81', - '6', - '73', - '15', - '98', - '25', - '8', - '99', - '17', - '90', - '47', - '1', - '10', - '38', - '66', - '57', - '23', - '86', - '29', - '3', - '65', - '74', - '19', - '56', - '63', - '20', - '7', - '32', - '46', - '70', - '26', - '16', - '83', - '37', - '58', - '43', - '36', - '69', - '79', - '72', - '41', - '94', - '95', - '82', - '51', - '52', - '89', - '88', - '40', - '92', - ]; - const expectedOrderNames3 = [ - '1', - '10', - '100', - '11', - '12', - '13', - '14', - '15', - '16', - '17', - '18', - '19', - '2', - '20', - '21', - '22', - '23', - '24', - '25', - '26', - '27', - '28', - '29', - '3', - '30', - '31', - '32', - '33', - '34', - '35', - '36', - '37', - '38', - '39', - '4', - '40', - '41', - '42', - '43', - '44', - '45', - '46', - '47', - '48', - '49', - '5', - '50', - '51', - '52', - '53', - '54', - '55', - '56', - '57', - '58', - '59', - '6', - '60', - '61', - '62', - '63', - '64', - '65', - '66', - '67', - '68', - '69', - '7', - '70', - '71', - '72', - '73', - '74', - '75', - '76', - '77', - '78', - '79', - '8', - '80', - '81', - '82', - '83', - '84', - '85', - '86', - '87', - '88', - '89', - '9', - '90', - '91', - '92', - '93', - '94', - '95', - '96', - '97', - '98', - '99', - ]; - const unorderedTags3 = createTagObjects(unorderedTagNames3); - const expectedOrder3 = createTagObjects(expectedOrderNames3); - expect(OptionsListUtils.sortTags(unorderedTags3)).toStrictEqual(expectedOrder3); - }); - it('getFilteredOptions() for taxRate', () => { const search = 'rate'; const emptySearch = ''; diff --git a/tests/unit/TagsOptionsListUtilsTest.ts b/tests/unit/TagsOptionsListUtilsTest.ts new file mode 100644 index 000000000000..5105ef030391 --- /dev/null +++ b/tests/unit/TagsOptionsListUtilsTest.ts @@ -0,0 +1,543 @@ +import type * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {SelectedTagOption} from '@libs/TagsOptionsListUtils'; +import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; + +describe('TagsOptionsListUtils', () => { + it('getTagListSections()', () => { + const search = 'ing'; + const emptySearch = ''; + const wrongSearch = 'bla bla'; + const recentlyUsedTags = ['Engineering', 'HR']; + + const selectedOptions: SelectedTagOption[] = [ + { + name: 'Medical', + enabled: true, + accountID: undefined, + }, + ]; + const smallTagsList: Record = { + Engineering: { + enabled: false, + name: 'Engineering', + accountID: undefined, + }, + Medical: { + enabled: true, + name: 'Medical', + accountID: undefined, + }, + Accounting: { + enabled: true, + name: 'Accounting', + accountID: undefined, + }, + HR: { + enabled: true, + name: 'HR', + accountID: undefined, + pendingAction: 'delete', + }, + }; + const smallResultList: OptionsListUtils.CategorySection[] = [ + { + title: '', + shouldShow: false, + // data sorted alphabetically by name + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: true, + isSelected: false, + pendingAction: 'delete', + }, + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const smallSearchResultList: OptionsListUtils.CategorySection[] = [ + { + title: '', + shouldShow: true, + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + data: [], + }, + ]; + const largeTagsList: Record = { + Engineering: { + enabled: false, + name: 'Engineering', + accountID: undefined, + }, + Medical: { + enabled: true, + name: 'Medical', + accountID: undefined, + }, + Accounting: { + enabled: true, + name: 'Accounting', + accountID: undefined, + }, + HR: { + enabled: true, + name: 'HR', + accountID: undefined, + }, + Food: { + enabled: true, + name: 'Food', + accountID: undefined, + }, + Traveling: { + enabled: false, + name: 'Traveling', + accountID: undefined, + }, + Cleaning: { + enabled: true, + name: 'Cleaning', + accountID: undefined, + }, + Software: { + enabled: true, + name: 'Software', + accountID: undefined, + }, + OfficeSupplies: { + enabled: false, + name: 'Office Supplies', + accountID: undefined, + }, + Taxes: { + enabled: true, + name: 'Taxes', + accountID: undefined, + pendingAction: 'delete', + }, + Benefits: { + enabled: true, + name: 'Benefits', + accountID: undefined, + }, + }; + const largeResultList: OptionsListUtils.CategorySection[] = [ + { + title: '', + shouldShow: true, + data: [ + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: false, + isSelected: true, + pendingAction: undefined, + }, + ], + }, + { + title: 'Recent', + shouldShow: true, + data: [ + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + { + title: 'All', + shouldShow: true, + // data sorted alphabetically by name + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Benefits', + keyForList: 'Benefits', + searchText: 'Benefits', + tooltipText: 'Benefits', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Software', + keyForList: 'Software', + searchText: 'Software', + tooltipText: 'Software', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Taxes', + keyForList: 'Taxes', + searchText: 'Taxes', + tooltipText: 'Taxes', + isDisabled: true, + isSelected: false, + pendingAction: 'delete', + }, + ], + }, + ]; + const largeSearchResultList: OptionsListUtils.CategorySection[] = [ + { + title: '', + shouldShow: true, + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + data: [], + }, + ]; + + const smallResult = TagsOptionsListUtils.getTagListSections({searchValue: emptySearch, tags: smallTagsList}); + expect(smallResult).toStrictEqual(smallResultList); + + const smallSearchResult = TagsOptionsListUtils.getTagListSections({searchValue: search, tags: smallTagsList}); + expect(smallSearchResult).toStrictEqual(smallSearchResultList); + + const smallWrongSearchResult = TagsOptionsListUtils.getTagListSections({searchValue: wrongSearch, tags: smallTagsList}); + expect(smallWrongSearchResult).toStrictEqual(smallWrongSearchResultList); + + const largeResult = TagsOptionsListUtils.getTagListSections({searchValue: emptySearch, selectedOptions, tags: largeTagsList, recentlyUsedTags}); + expect(largeResult).toStrictEqual(largeResultList); + + const largeSearchResult = TagsOptionsListUtils.getTagListSections({searchValue: search, selectedOptions, tags: largeTagsList, recentlyUsedTags}); + expect(largeSearchResult).toStrictEqual(largeSearchResultList); + + const largeWrongSearchResult = TagsOptionsListUtils.getTagListSections({ + searchValue: wrongSearch, + selectedOptions, + tags: largeTagsList, + recentlyUsedTags, + }); + expect(largeWrongSearchResult).toStrictEqual(largeWrongSearchResultList); + }); + + it('sortTags', () => { + 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 unorderedTags = createTagObjects(unorderedTagNames); + const expectedOrder = createTagObjects(expectedOrderNames); + expect(TagsOptionsListUtils.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 unorderedTags2 = createTagObjects(unorderedTagNames2); + const expectedOrder2 = createTagObjects(expectedOrderNames2); + expect(TagsOptionsListUtils.sortTags(unorderedTags2)).toStrictEqual(expectedOrder2); + + const unorderedTagNames3 = [ + '61', + '39', + '97', + '93', + '77', + '71', + '22', + '27', + '30', + '64', + '91', + '24', + '33', + '60', + '21', + '85', + '59', + '76', + '42', + '67', + '13', + '96', + '84', + '44', + '68', + '31', + '62', + '87', + '50', + '4', + '100', + '12', + '28', + '49', + '53', + '5', + '45', + '14', + '55', + '78', + '11', + '35', + '75', + '18', + '9', + '80', + '54', + '2', + '34', + '48', + '81', + '6', + '73', + '15', + '98', + '25', + '8', + '99', + '17', + '90', + '47', + '1', + '10', + '38', + '66', + '57', + '23', + '86', + '29', + '3', + '65', + '74', + '19', + '56', + '63', + '20', + '7', + '32', + '46', + '70', + '26', + '16', + '83', + '37', + '58', + '43', + '36', + '69', + '79', + '72', + '41', + '94', + '95', + '82', + '51', + '52', + '89', + '88', + '40', + '92', + ]; + const expectedOrderNames3 = [ + '1', + '10', + '100', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '2', + '20', + '21', + '22', + '23', + '24', + '25', + '26', + '27', + '28', + '29', + '3', + '30', + '31', + '32', + '33', + '34', + '35', + '36', + '37', + '38', + '39', + '4', + '40', + '41', + '42', + '43', + '44', + '45', + '46', + '47', + '48', + '49', + '5', + '50', + '51', + '52', + '53', + '54', + '55', + '56', + '57', + '58', + '59', + '6', + '60', + '61', + '62', + '63', + '64', + '65', + '66', + '67', + '68', + '69', + '7', + '70', + '71', + '72', + '73', + '74', + '75', + '76', + '77', + '78', + '79', + '8', + '80', + '81', + '82', + '83', + '84', + '85', + '86', + '87', + '88', + '89', + '9', + '90', + '91', + '92', + '93', + '94', + '95', + '96', + '97', + '98', + '99', + ]; + const unorderedTags3 = createTagObjects(unorderedTagNames3); + const expectedOrder3 = createTagObjects(expectedOrderNames3); + expect(TagsOptionsListUtils.sortTags(unorderedTags3)).toStrictEqual(expectedOrder3); + }); +});