diff --git a/src/App.tsx b/src/App.tsx index a3a9f7a3f3b6..61874dc72fb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; +import OptionsListContextProvider from './components/OptionListContextProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; @@ -82,6 +83,7 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, + OptionsListContextProvider, ]} > diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 3033bf118e8f..0307b67114e5 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -47,8 +47,8 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( - {}, - {}, + [], + [], [], debouncedSearchValue, selectedOptions, diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx new file mode 100644 index 000000000000..43c5906d4900 --- /dev/null +++ b/src/components/OptionListContextProvider.tsx @@ -0,0 +1,142 @@ +import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionList} from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import {usePersonalDetails} from './OnyxProvider'; + +type OptionsListContextProps = { + /** List of options for reports and personal details */ + options: OptionList; + /** Function to initialize the options */ + initializeOptions: () => void; + /** Flag to check if the options are initialized */ + areOptionsInitialized: boolean; +}; + +type OptionsListProviderOnyxProps = { + /** Collection of reports */ + reports: OnyxCollection; +}; + +type OptionsListProviderProps = OptionsListProviderOnyxProps & { + /** Actual content wrapped by this component */ + children: React.ReactNode; +}; + +const OptionsListContext = createContext({ + options: { + reports: [], + personalDetails: [], + }, + initializeOptions: () => {}, + areOptionsInitialized: false, +}); + +function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { + const areOptionsInitialized = useRef(false); + const [options, setOptions] = useState({ + reports: [], + personalDetails: [], + }); + const personalDetails = usePersonalDetails(); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + const lastUpdatedReport = ReportUtils.getLastUpdatedReport(); + + if (!lastUpdatedReport) { + return; + } + + const newOption = OptionsListUtils.createOptionFromReport(lastUpdatedReport, personalDetails); + const replaceIndex = options.reports.findIndex((option) => option.reportID === lastUpdatedReport.reportID); + + if (replaceIndex === -1) { + return; + } + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.reports[replaceIndex] = newOption; + return newOptions; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reports]); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + // since personal details are not a collection, we need to recreate the whole list from scratch + const newPersonalDetailsOptions = OptionsListUtils.createOptionList(personalDetails).personalDetails; + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.personalDetails = newPersonalDetailsOptions; + return newOptions; + }); + }, [personalDetails]); + + const loadOptions = useCallback(() => { + const optionLists = OptionsListUtils.createOptionList(personalDetails, reports); + setOptions({ + reports: optionLists.reports, + personalDetails: optionLists.personalDetails, + }); + }, [personalDetails, reports]); + + const initializeOptions = useCallback(() => { + if (areOptionsInitialized.current) { + return; + } + + loadOptions(); + areOptionsInitialized.current = true; + }, [loadOptions]); + + return ( + ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current}), [options, initializeOptions])}> + {children} + + ); +} + +const useOptionsListContext = () => useContext(OptionsListContext); + +// Hook to use the OptionsListContext with an initializer to load the options +const useOptionsList = (options?: {shouldInitialize: boolean}) => { + const {shouldInitialize = true} = options ?? {}; + const {initializeOptions, options: optionsList, areOptionsInitialized} = useOptionsListContext(); + + useEffect(() => { + if (!shouldInitialize || areOptionsInitialized) { + return; + } + + initializeOptions(); + }, [shouldInitialize, initializeOptions, areOptionsInitialized]); + + return { + initializeOptions, + options: optionsList, + areOptionsInitialized, + }; +}; + +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, +})(OptionsListContextProvider); + +export {useOptionsListContext, useOptionsList, OptionsListContext}; diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index af8acd19e8c4..54ad016173b7 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -91,7 +91,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, + () => OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ca44931e7e8e..9e1320e83abb 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -56,6 +56,15 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type SearchOption = ReportUtils.OptionData & { + item: T; +}; + +type OptionList = { + reports: Array>; + personalDetails: Array>; +}; + type Option = Partial; /** @@ -161,7 +170,7 @@ type GetOptions = { taxRatesOptions: CategorySection[]; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -509,6 +518,28 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } +/** + * Update alternate text for the option when applicable + */ +function getAlternateText( + option: ReportUtils.OptionData, + {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, +) { + if (!!option.isThread || !!option.isMoneyRequestReport) { + return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { + return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; + } + if (option.isTaskReport) { + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + + return showChatPreviewLine && option.lastMessageText + ? option.lastMessageText + : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); +} + /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -588,8 +619,9 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, + config?: PreviewConfig, ): ReportUtils.OptionData { + const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -622,6 +654,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -630,10 +663,8 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; - result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; - if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -675,16 +706,15 @@ function createOption( lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (result.isChatRoom || result.isPolicyExpenseChat) { - result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; - } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); - } - reportName = ReportUtils.getReportName(report); + result.lastMessageText = lastMessageText; + + // If displaying chat preview line is needed, let's overwrite the default alternate text + result.alternateText = + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + + reportName = showPersonalDetails + ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') + : ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -1320,12 +1350,63 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { + const reportMapForAccountIDs: Record = {}; + const allReportOptions: Array> = []; + + if (reports) { + Object.values(reports).forEach((report) => { + if (!report) { + return; + } + + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // Save the report in the map if this is a single participant so we can associate the reportID with the + // personal detail option later. Individuals should not be associated with single participant + // policyExpenseChats or chatRooms since those are not people. + if (accountIDs.length <= 1) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push({ + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }); + }); + } + + const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ + item: personalDetail, + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), + })); + + return { + reports: allReportOptions, + personalDetails: allPersonalDetailsOptions as Array>, + }; +} + +function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { + const accountIDs = report.participantAccountIDs ?? []; + + return { + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }; +} + /** - * Build the options + * filter options based on specific conditions */ function getOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + options: OptionList, { reportActions = {}, betas = [], @@ -1403,26 +1484,14 @@ function getOptions( }; } - if (!isPersonalDetailsReady(personalDetails)) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - }; - } - - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports ?? {}).filter((report) => { + const filteredReportOptions = options.reports.filter((option) => { + const report = option.item; + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1431,7 +1500,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId() ?? '', + currentReportId: topmostReportId, betas, policies, doesReportHaveViolations, @@ -1444,27 +1513,28 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { + const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { + const report = option.item; + if (option.isArchivedRoom) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReports.reverse(); + orderedReportOptions.reverse(); + + const allReportOptions = orderedReportOptions.filter((option) => { + const report = option.item; - const allReportOptions: ReportUtils.OptionData[] = []; - orderedReports.forEach((report) => { if (!report) { return; } - const isThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); - const isTaskReport = ReportUtils.isTaskReport(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const isSelfDM = ReportUtils.isSelfDM(report); + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; @@ -1502,33 +1572,11 @@ function getOptions( return; } - // Save the report in the map if this is a single participant so we can associate the reportID with the - // personal detail option later. Individuals should not be associated with single participant - // policyExpenseChats or chatRooms since those are not people. - if (accountIDs.length <= 1 && !isPolicyExpenseChat && !isChatRoom) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + return option; }); - // We're only picking personal details that have logins set - // This is a temporary fix for all the logic that's been breaking because of the new privacy changes - // See https://github.com/Expensify/Expensify/issues/293465 for more context - // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - const havingLoginPersonalDetails = !includeP2P - ? {} - : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + + const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); + let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1549,8 +1597,17 @@ function getOptions( optionsToExclude.push({login}); }); + let recentReportOptions = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + if (includeRecentReports) { for (const reportOption of allReportOptions) { + /** + * By default, generated options does not have the chat preview line enabled. + * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. + */ + reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); + // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1646,7 +1703,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...personalDetails, + ...allPersonalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1714,10 +1771,10 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const options = getOptions(reports, personalDetails, { + const optionList = getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1736,11 +1793,11 @@ function getSearchOptions(reports: OnyxCollection, personalDetails: Onyx Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return options; + return optionList; } -function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(reports, personalDetails, { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { + return getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1791,8 +1848,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Array, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1811,28 +1868,31 @@ function getFilteredOptions( taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - includeRecentReports: true, - includePersonalDetails: true, - maxRecentReportsToShow: 5, - excludeLogins, - includeOwnedWorkspaceChats, - includeP2P, - includeCategories, - categories, - recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, - canInviteUser, - includeSelectedOptions, - includeTaxRates, - taxRates, - includeSelfDM, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + includeRecentReports: true, + includePersonalDetails: true, + maxRecentReportsToShow: 5, + excludeLogins, + includeOwnedWorkspaceChats, + includeP2P, + includeCategories, + categories, + recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, + canInviteUser, + includeSelectedOptions, + includeTaxRates, + taxRates, + includeSelfDM, + }, + ); } /** @@ -1840,8 +1900,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1849,24 +1909,27 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }, + ); } /** @@ -1899,20 +1962,23 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { * Build the options for the Workspace Member Invite view */ function getMemberInviteOptions( - personalDetails: OnyxEntry, + personalDetails: Array>, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, ): GetOptions { - return getOptions({}, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }); + return getOptions( + {reports: [], personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + }, + ); } /** @@ -2052,8 +2118,10 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + createOptionList, + createOptionFromReport, getReportOption, getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption, PayeePersonalDetails, Category}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8296e38411be..c75a9ba507e0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -523,6 +523,23 @@ Onyx.connect({ }, }); +let lastUpdatedReport: OnyxEntry; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (value) => { + if (!value) { + return; + } + + lastUpdatedReport = value; + }, +}); + +function getLastUpdatedReport(): OnyxEntry { + return lastUpdatedReport; +} + function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource { return currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID); } @@ -5905,6 +5922,7 @@ export { isJoinRequestInAdminRoom, canAddOrDeleteTransactions, shouldCreateNewMoneyRequestReport, + getLastUpdatedReport, isGroupChat, isTrackExpenseReport, hasActionsWithErrors, diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index c1c4717a295b..751813d1d3cf 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,9 +1,10 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -18,7 +19,6 @@ import doInteractionTask from '@libs/DoInteractionTask'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; @@ -29,9 +29,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; type NewChatPageWithOnyxProps = { - /** All reports shared with the user */ - reports: OnyxCollection; - /** New group chat draft data */ newGroupDraft: OnyxEntry; @@ -53,8 +50,9 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { +function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { const {translate} = useLocalize(); + const styles = useThemeStyles(); const personalData = useCurrentUserPersonalDetails(); @@ -72,13 +70,16 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF }; const [searchTerm, setSearchTerm] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(); + const [filteredRecentReports, setFilteredRecentReports] = useState([]); + const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); + const [filteredUserToInvite, setFilteredUserToInvite] = useState(); const [selectedOptions, setSelectedOptions] = useState(getGroupParticipants); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -91,8 +92,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), ); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; @@ -145,8 +144,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, newSelectedOptions, @@ -206,8 +205,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, selectedOptions, @@ -228,7 +227,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF setFilteredUserToInvite(userToInvite); // props.betas is not added as dependency since it doesn't change during the component lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reports, personalDetails, searchTerm]); + }, [options, searchTerm]); useEffect(() => { const interactionTask = doInteractionTask(() => { @@ -290,7 +289,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} + shouldShowOptions={areOptionsInitialized} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} @@ -319,9 +318,6 @@ export default withOnyx({ newGroupDraft: { key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 77b5c48d8a72..49e53381e040 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,11 +2,10 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; @@ -25,30 +24,25 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; import SearchInputManager from './workspace/SearchInputManager'; -type RoomInvitePageOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; -}; - -type RoomInvitePageProps = RoomInvitePageOnyxProps & WithReportOrNotFoundProps & WithNavigationTransitionEndProps; +type RoomInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; type Sections = Array>>; -function RoomInvitePage({betas, personalDetails, report, policies, didScreenTransitionEnd}: RoomInvitePageProps) { +function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); + const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -64,7 +58,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetails, betas ?? [], searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -83,12 +77,12 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran setInvitePersonalDetails(inviteOptions.personalDetails); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, searchTerm, excludedUsers]); + }, [betas, searchTerm, excludedUsers, options.personalDetails]); const sections = useMemo(() => { const sectionsArr: Sections = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -130,7 +124,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran } return sectionsArr; - }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); + }, [areOptionsInitialized, selectedOptions, searchTerm, invitePersonalDetails, userToInvite, translate]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -193,6 +187,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, Boolean(userToInvite), searchValue); }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + return ( ({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - })(RoomInvitePage), - ), -); +export default withNavigationTransitionEnd(withReportOrNotFound()(RoomInvitePage)); diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index b1555fd1cab8..c072bfd56913 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -1,10 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -17,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -30,9 +30,6 @@ type SearchPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; - /** All reports shared with the user */ - reports: OnyxCollection; - /** Whether or not we are searching for reports on the server */ isSearchingForReports: OnyxEntry; }; @@ -40,7 +37,7 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; type SearchPageSectionItem = { - data: ReportUtils.OptionData[]; + data: OptionData[]; shouldShow: boolean; }; @@ -53,12 +50,14 @@ const setPerformanceTimersEnd = () => { const SearchPageFooterInstance = ; -function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchPageProps) { +function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); - const personalDetails = usePersonalDetails(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -79,7 +78,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP userToInvite, headerMessage, } = useMemo(() => { - if (!isScreenTransitionEnd) { + if (!areOptionsInitialized) { return { recentReports: [], personalDetails: [], @@ -87,10 +86,10 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP headerMessage: '', }; } - const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas ?? []); - const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); - return {...options, headerMessage: header}; - }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); + const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); + return {...optionList, headerMessage: header}; + }, [areOptionsInitialized, options, debouncedSearchValue, betas]); const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; @@ -119,7 +118,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP return newSections; }, [localPersonalDetails, recentReports, userToInvite]); - const selectReport = (option: ReportUtils.OptionData) => { + const selectReport = (option: OptionData) => { if (!option) { return; } @@ -136,8 +135,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP setIsScreenTransitionEnd(true); }; - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({safeAreaPaddingBottomStyle}) => ( <> - - sections={didScreenTransitionEnd && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} + + sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -164,7 +161,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} onLayout={setPerformanceTimersEnd} onSelectRow={selectReport} - showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} + showLoadingPlaceholder={!areOptionsInitialized} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> @@ -178,9 +175,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP SearchPage.displayName = 'SearchPage'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 4870d39002ac..a05167d5cedf 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -19,8 +20,6 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -49,9 +48,6 @@ const propTypes = { }), ), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -68,7 +64,6 @@ const propTypes = { const defaultProps = { participants: [], safeAreaPaddingBottomStyle: {}, - reports: {}, betas: [], dismissedReferralBanners: {}, didScreenTransitionEnd: false, @@ -77,7 +72,6 @@ const defaultProps = { function MoneyTemporaryForRefactorRequestParticipantsSelector({ betas, participants, - reports, onFinish, onParticipantsAdded, safeAreaPaddingBottomStyle, @@ -93,6 +87,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -109,12 +106,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return [newSections, {}]; } const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, debouncedSearchTerm, participants, @@ -175,7 +172,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, reports, personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, translate]); + }, [ + areOptionsInitialized, + options.reports, + options.personalDetails, + betas, + debouncedSearchTerm, + participants, + iouType, + canUseP2PDistanceRequests, + iouRequestType, + maxParticipantsReached, + personalDetails, + translate, + ]); /** * Adds a single participant to the request @@ -342,13 +352,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( 0 ? safeAreaPaddingBottomStyle : {}]}> diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 16608ba13de8..05ef5baa8432 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -19,7 +20,6 @@ import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,9 +50,6 @@ const propTypes = { }), ), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -61,33 +58,26 @@ const propTypes = { /** Whether the money request is a distance request or not */ isDistanceRequest: PropTypes.bool, - - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, }; const defaultProps = { dismissedReferralBanners: {}, participants: [], safeAreaPaddingBottomStyle: {}, - reports: {}, betas: [], isDistanceRequest: false, - isSearchingForReports: false, }; function MoneyRequestParticipantsSelector({ betas, dismissedReferralBanners, participants, - reports, navigateToRequest, navigateToSplit, onAddParticipants, safeAreaPaddingBottomStyle, iouType, isDistanceRequest, - isSearchingForReports, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -96,6 +86,7 @@ function MoneyRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); + const {options, areOptionsInitialized} = useOptionsList(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -104,8 +95,8 @@ function MoneyRequestParticipantsSelector({ const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, searchTerm, participants, @@ -132,7 +123,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); + }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); /** * Returns the sections needed for the OptionsSelector @@ -365,7 +356,7 @@ function MoneyRequestParticipantsSelector({ onSelectRow={addSingleParticipant} footerContent={footerContent} headerMessage={headerMessage} - showLoadingPlaceholder={isSearchingForReports} + showLoadingPlaceholder={!areOptionsInitialized} rightHandSideComponent={itemRightSideComponent} /> @@ -380,14 +371,7 @@ export default withOnyx({ dismissedReferralBanners: { key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, })(MoneyRequestParticipantsSelector); diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 70c2d301b9ac..cee62380a011 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -11,46 +11,45 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; -function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogListProps) { +function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState>({ recentReports: [], personalDetails: [], userToInvite: null, }); - const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); const isMounted = useRef(false); - const personalDetails = usePersonalDetails(); - + const {options, areOptionsInitialized} = useOptionsList(); const updateOptions = useCallback(() => { const { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - } = OptionsListUtils.getShareLogOptions(reports, personalDetails, searchValue.trim(), betas ?? []); + } = OptionsListUtils.getShareLogOptions(options, searchValue.trim(), betas ?? []); setSearchOptions({ recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, }); - }, [betas, personalDetails, reports, searchValue]); - - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + }, [betas, options, searchValue]); useEffect(() => { + if (!areOptionsInitialized) { + return; + } + updateOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [options, areOptionsInitialized]); useEffect(() => { if (!isMounted.current) { @@ -126,7 +125,7 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis value={searchValue} headerMessage={headerMessage} showTitleTooltip - shouldShowOptions={isOptionsDataReady} + shouldShowOptions={areOptionsInitialized} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index bb199ddc905f..7a6ff74087de 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -7,7 +7,8 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; +import {useBetas, useSession} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; @@ -39,22 +40,18 @@ type TaskAssigneeSelectorModalOnyxProps = { task: OnyxEntry; }; -type UseOptions = { - reports: OnyxCollection; -}; - type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps & WithNavigationTransitionEndProps; -function useOptions({reports}: UseOptions) { - const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; +function useOptions() { const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - reports, - allPersonalDetails, + optionsList.reports, + optionsList.personalDetails, betas, debouncedSearchValue.trim(), [], @@ -87,18 +84,18 @@ function useOptions({reports}: UseOptions) { currentUserOption, headerMessage, }; - }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); + }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); - return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } -function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: TaskAssigneeSelectorModalProps) { +function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); + const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); @@ -215,14 +212,14 @@ function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: Task /> diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 5b56e58752ac..b4b8f9084a57 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,9 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -22,8 +22,6 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; type TaskShareDestinationSelectorModalOnyxProps = { - reports: OnyxCollection; - isSearchingForReports: OnyxEntry; }; @@ -40,29 +38,36 @@ const selectReportHandler = (option: unknown) => { Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports: OnyxCollection) => - Object.keys(reports ?? {}).reduce((filtered, reportKey) => { - const report: OnyxEntry = reports?.[reportKey] ?? null; +const reportFilter = (reportOptions: Array>) => + (reportOptions ?? []).reduce((filtered: Array>, option) => { + const report = option.item; if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; + filtered.push(option); } return filtered; - }, {}); + }, []); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { +function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails(); const {isOffline} = useNetwork(); + const {options: optionList, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); const options = useMemo(() => { - const filteredReports = reportFilter(reports); - - const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); - + if (!areOptionsInitialized) { + return { + sections: [], + headerMessage: '', + }; + } + const filteredReports = reportFilter(optionList.reports); + const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); const sections = @@ -84,7 +89,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas : []; return {sections, headerMessage}; - }, [personalDetails, reports, debouncedSearchValue]); + }, [areOptionsInitialized, optionList.reports, optionList.personalDetails, debouncedSearchValue]); useEffect(() => { ReportActions.searchInServer(debouncedSearchValue); @@ -94,29 +99,28 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas setDidScreenTransitionEnd(true)} > - {({didScreenTransitionEnd}) => ( - <> - Navigation.goBack(ROUTES.NEW_TASK)} + <> + Navigation.goBack(ROUTES.NEW_TASK)} + /> + + - - - - - )} + + ); } @@ -124,9 +128,6 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, isSearchingForReports: { key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 014097cd019c..3f95c3e02a5b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -7,6 +7,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; @@ -75,6 +76,9 @@ function WorkspaceInvitePage({ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -98,8 +102,7 @@ function WorkspaceInvitePage({ const newPersonalDetailsDict: Record = {}; const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); - + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -150,12 +153,12 @@ function WorkspaceInvitePage({ setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); + }, [options.personalDetails, policyMembers, betas, searchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -203,7 +206,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); + }, [areOptionsInitialized, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -304,7 +307,7 @@ function WorkspaceInvitePage({ onSelectRow={toggleOption} onConfirm={inviteUser} showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)} + showLoadingPlaceholder={!areOptionsInitialized} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} /> diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index d81be3165919..5041e919e7c1 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -44,35 +44,37 @@ jest.mock('@react-navigation/native', () => { } as typeof NativeNavigation; }); +const options = OptionsListUtils.createOptionList(personalDetails, reports); + /* GetOption is the private function and is never called directly, we are testing the functions which call getOption with different params */ describe('OptionsListUtils', () => { /* Testing getSearchOptions */ test('[OptionsListUtils] getSearchOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getSearchOptions(reports, personalDetails, SEARCH_VALUE, mockedBetas)); + await measureFunction(() => OptionsListUtils.getSearchOptions(options, SEARCH_VALUE, mockedBetas)); }); /* Testing getShareLogOptions */ test('[OptionsListUtils] getShareLogOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getShareLogOptions(reports, personalDetails, SEARCH_VALUE, mockedBetas)); + await measureFunction(() => OptionsListUtils.getShareLogOptions(options, SEARCH_VALUE, mockedBetas)); }); /* Testing getFilteredOptions */ test('[OptionsListUtils] getFilteredOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getFilteredOptions(reports, personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getFilteredOptions(options.reports, options.personalDetails, mockedBetas, SEARCH_VALUE)); }); /* Testing getShareDestinationOptions */ test('[OptionsListUtils] getShareDestinationOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getShareDestinationOptions(reports, personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getShareDestinationOptions(options.reports, options.personalDetails, mockedBetas, SEARCH_VALUE)); }); /* Testing getMemberInviteOptions */ test('[OptionsListUtils] getMemberInviteOptions with search value', async () => { await waitForBatchedUpdates(); - await measureFunction(() => OptionsListUtils.getMemberInviteOptions(personalDetails, mockedBetas, SEARCH_VALUE)); + await measureFunction(() => OptionsListUtils.getMemberInviteOptions(options.personalDetails, mockedBetas, SEARCH_VALUE)); }); }); diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index 5f2dd3800e0f..95f5630e3fe9 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -2,14 +2,16 @@ import type * as NativeNavigation from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import {fireEvent, screen, waitFor} from '@testing-library/react-native'; import type {TextMatch} from '@testing-library/react-native/build/matches'; -import React from 'react'; +import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import type {RootStackParamList} from '@libs/Navigation/types'; +import {createOptionList} from '@libs/OptionsListUtils'; import SearchPage from '@pages/SearchPage'; import ComposeProviders from '@src/components/ComposeProviders'; import OnyxProvider from '@src/components/OnyxProvider'; @@ -94,6 +96,7 @@ const getMockedPersonalDetails = (length = 100) => const mockedReports = getMockedReports(600); const mockedBetas = Object.values(CONST.BETAS); const mockedPersonalDetails = getMockedPersonalDetails(100); +const mockedOptions = createOptionList(mockedPersonalDetails, mockedReports); beforeAll(() => Onyx.init({ @@ -124,7 +127,7 @@ type SearchPageProps = StackScreenProps + + ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> + + + + ); +} + +test('[Search Page] should render list with cached options', async () => { + const {addListener} = TestHelper.createAddListenerMock(); + + const scenario = async () => { + await screen.findByTestId('SearchPage'); + }; + + const navigation = {addListener}; + + return ( + waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(() => measurePerformance(, {scenario})) + ); +}); + test('[Search Page] should interact when text input changes', async () => { const {addListener} = TestHelper.createAddListenerMock(); diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 49ad848fe466..f95fe3e484e9 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -311,25 +311,38 @@ describe('OptionsListUtils', () => { return waitForBatchedUpdates().then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS)); }); + let OPTIONS = {}; + let OPTIONS_WITH_CONCIERGE = {}; + let OPTIONS_WITH_CHRONOS = {}; + let OPTIONS_WITH_RECEIPTS = {}; + let OPTIONS_WITH_WORKSPACES = {}; + + beforeEach(() => { + OPTIONS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS); + OPTIONS_WITH_CONCIERGE = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CONCIERGE, REPORTS_WITH_CONCIERGE); + OPTIONS_WITH_CHRONOS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, REPORTS_WITH_CHRONOS); + OPTIONS_WITH_RECEIPTS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, REPORTS_WITH_RECEIPTS); + OPTIONS_WITH_WORKSPACES = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); + }); + it('getSearchOptions()', () => { // When we filter in the Search view without providing a searchValue - let results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); - + let results = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); // Then the 2 personalDetails that don't have reports should be returned expect(results.personalDetails.length).toBe(2); // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(_.size(REPORTS)); + expect(results.recentReports.length).toBe(_.size(OPTIONS.reports)); // When we filter again but provide a searchValue - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'spider'); + results = OptionsListUtils.getSearchOptions(OPTIONS, 'spider'); // Then only one option should be returned and it's the one matching the search value expect(results.recentReports.length).toBe(1); expect(results.recentReports[0].login).toBe('peterparker@expensify.com'); // When we filter again but provide a searchValue that should match multiple times - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'fantastic'); + results = OptionsListUtils.getSearchOptions(OPTIONS, 'fantastic'); // Value with latest lastVisibleActionCreated should be at the top. expect(results.recentReports.length).toBe(2); @@ -339,9 +352,9 @@ describe('OptionsListUtils', () => { return waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, PERSONAL_DETAILS_WITH_PERIODS)) .then(() => { + const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); // When we filter again but provide a searchValue that should match with periods - results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, 'barry.allen@expensify.com'); - + results = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS, 'barry.allen@expensify.com'); // Then we expect to have the personal detail with period filtered expect(results.recentReports.length).toBe(1); expect(results.recentReports[0].text).toBe('The Flash'); @@ -353,14 +366,14 @@ describe('OptionsListUtils', () => { const MAX_RECENT_REPORTS = 5; // When we call getFilteredOptions() with no search value - let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], ''); // We should expect maximimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS); // We should expect all personalDetails to be returned, // minus the currently logged in user and recent reports count - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS.personalDetails) - 1 - MAX_RECENT_REPORTS); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -373,7 +386,7 @@ describe('OptionsListUtils', () => { expect(personalDetailWithExistingReport.reportID).toBe(2); // When we only pass personal details - results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], ''); + results = OptionsListUtils.getFilteredOptions([], OPTIONS.personalDetails, [], ''); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -382,13 +395,13 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('Invisible Woman'); // When we provide a search value that does not match any personal details - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'magneto'); // Then no options will be returned expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches an email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'peterparker@expensify.com'); // Then one recentReports will be returned and it will be the correct option // personalDetails should be empty array @@ -397,7 +410,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches a partial display name or email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '.com'); // Then several options will be returned and they will be each have the search string in their email or name // even though the currently logged in user matches they should not show. @@ -410,45 +423,46 @@ describe('OptionsListUtils', () => { expect(results.recentReports[2].text).toBe('Black Panther'); // Test for Concierge's existence in chat options - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 1 - MAX_RECENT_REPORTS); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails, [], '', [], [CONST.EMAIL.CONCIERGE]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CHRONOS.reports, OPTIONS_WITH_CHRONOS.personalDetails, [], '', [], [CONST.EMAIL.CHRONOS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CHRONOS.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_RECEIPTS.reports, OPTIONS_WITH_RECEIPTS.personalDetails, [], '', [], [CONST.EMAIL.RECEIPTS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_RECEIPTS.personalDetails) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getFilteredOptions() for group Chat', () => { // When we call getFilteredOptions() with no search value - let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], ''); // Then we should expect only a maxmimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(5); // And we should expect all the personalDetails to show (minus the 5 that are already // showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 6); + expect(results.personalDetails.length).toBe(_.size(OPTIONS.personalDetails) - 6); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -462,7 +476,7 @@ describe('OptionsListUtils', () => { expect(personalDetailsOverlapWithReports).toBe(false); // When we search for an option that is only in a personalDetail with no existing report - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'hulk'); // Then reports should return no results expect(results.recentReports.length).toBe(0); @@ -472,7 +486,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].login).toBe('brucebanner@expensify.com'); // When we search for an option that matches things in both personalDetails and reports - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '.com'); // Then all single participant reports that match will show up in the recentReports array, Recently used contact should be at the top expect(results.recentReports.length).toBe(5); @@ -483,7 +497,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com'); // When we provide no selected options to getFilteredOptions() - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', []); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '', []); // Then one of our older report options (not in our five most recent) should appear in the personalDetails // but not in recentReports @@ -491,7 +505,7 @@ describe('OptionsListUtils', () => { expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false); // When we provide a "selected" option to getFilteredOptions() - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '', [{login: 'peterparker@expensify.com'}]); // Then the option should not appear anywhere in either list expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); @@ -499,7 +513,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is not a potential email or phone - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'marc@expensify'); // Then we should have no options or personal details at all and also that there is no userToInvite expect(results.recentReports.length).toBe(0); @@ -508,7 +522,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential email - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'marc@expensify.com'); // Then we should have no options or personal details at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -516,7 +530,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).not.toBe(null); // When we add a search term with a period, with options for it that don't contain the period - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], 'peter.parker@expensify.com'); // Then we should have no options at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -524,7 +538,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number without country code added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '5005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -535,7 +549,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with country code added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '+15005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -546,7 +560,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with special characters added - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '+1 (800)324-3233'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -556,7 +570,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite.login).toBe('+18003243233'); // When we use a search term for contact number that contains alphabet characters - results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); + results = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], '998243aaaa'); // Then we shouldn't have any results or user to invite expect(results.recentReports.length).toBe(0); @@ -564,93 +578,100 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).toBe(null); // Test Concierge's existence in new group options - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 6); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 6); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CONCIERGE.reports, OPTIONS_WITH_CONCIERGE.personalDetails, [], '', [], [CONST.EMAIL.CONCIERGE]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CONCIERGE.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_CHRONOS.reports, OPTIONS_WITH_CHRONOS.personalDetails, [], '', [], [CONST.EMAIL.CHRONOS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_CHRONOS.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(OPTIONS_WITH_RECEIPTS.reports, OPTIONS_WITH_RECEIPTS.personalDetails, [], '', [], [CONST.EMAIL.RECEIPTS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 7); + expect(results.personalDetails.length).toBe(_.size(OPTIONS_WITH_RECEIPTS.personalDetails) - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = {}; - _.keys(REPORTS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS[reportKey])) { - return; - } - filteredReports[reportKey] = REPORTS[reportKey]; - }); + const filteredReports = _.reduce( + OPTIONS.reports, + (filtered, option) => { + const report = option.item; + if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { + filtered.push(option); + } + return filtered; + }, + [], + ); // When we pass an empty search value - let results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], ''); // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS) - 1); + expect(results.recentReports.length).toBe(_.size(OPTIONS.reports) - 1); // When we pass a search value that doesn't match the group chat name - results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'mutants'); + results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], 'mutants'); // Then we should expect no recent reports to show expect(results.recentReports.length).toBe(0); // When we pass a search value that matches the group chat name - results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'Iron Man, Fantastic'); + results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], 'Iron Man, Fantastic'); // Then we should expect the group chat to show along with the contacts matching the search expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = {}; - _.keys(REPORTS_WITH_WORKSPACE_ROOMS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS_WITH_WORKSPACE_ROOMS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS_WITH_WORKSPACE_ROOMS[reportKey])) { - return; - } - filteredReportsWithWorkspaceRooms[reportKey] = REPORTS_WITH_WORKSPACE_ROOMS[reportKey]; - }); + const filteredReportsWithWorkspaceRooms = _.reduce( + OPTIONS_WITH_WORKSPACES.reports, + (filtered, option) => { + const report = option.item; + if (ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + filtered.push(option); + } + return filtered; + }, + [], + ); // When we also have a policy to return rooms in the results - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], ''); - + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS_WITH_WORKSPACE_ROOMS) - 1); + expect(results.recentReports.length).toBe(_.size(OPTIONS_WITH_WORKSPACES.reports) - 1); // When we search for a workspace room - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Avengers Room'); + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], 'Avengers Room'); // Then we should expect only the workspace room to show expect(results.recentReports.length).toBe(1); // When we search for a workspace room that doesn't exist - results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Mutants Lair'); + results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], 'Mutants Lair'); // Then we should expect no results to show expect(results.recentReports.length).toBe(0); @@ -658,7 +679,7 @@ describe('OptionsListUtils', () => { it('getMemberInviteOptions()', () => { // When we only pass personal details - let results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], ''); + let results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], ''); // We should expect personal details to be sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -667,13 +688,13 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('Invisible Woman'); // When we provide a search value that does not match any personal details - results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], 'magneto'); + results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], 'magneto'); // Then no options will be returned expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches an email - results = OptionsListUtils.getMemberInviteOptions(PERSONAL_DETAILS, [], 'peterparker@expensify.com'); + results = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, [], 'peterparker@expensify.com'); // Then one personal should be in personalDetails list expect(results.personalDetails.length).toBe(1); @@ -1011,18 +1032,18 @@ describe('OptionsListUtils', () => { }, ]; - const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList); + const smallResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], emptySearch, [], [], false, false, true, smallCategoriesList); expect(smallResult.categoryOptions).toStrictEqual(smallResultList); - const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList); + const smallSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, [], [], false, false, true, smallCategoriesList); expect(smallSearchResult.categoryOptions).toStrictEqual(smallSearchResultList); - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], wrongSearch, [], [], false, false, true, smallCategoriesList); expect(smallWrongSearchResult.categoryOptions).toStrictEqual(smallWrongSearchResultList); const largeResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], emptySearch, selectedOptions, @@ -1036,8 +1057,8 @@ describe('OptionsListUtils', () => { expect(largeResult.categoryOptions).toStrictEqual(largeResultList); const largeSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], search, selectedOptions, @@ -1051,8 +1072,8 @@ describe('OptionsListUtils', () => { expect(largeSearchResult.categoryOptions).toStrictEqual(largeSearchResultList); const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], wrongSearch, selectedOptions, @@ -1065,7 +1086,7 @@ describe('OptionsListUtils', () => { ); expect(largeWrongSearchResult.categoryOptions).toStrictEqual(largeWrongSearchResultList); - const emptyResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, selectedOptions, [], false, false, true, emptyCategoriesList); + const emptyResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, selectedOptions, [], false, false, true, emptyCategoriesList); expect(emptyResult.categoryOptions).toStrictEqual(emptySelectedResultList); }); @@ -1310,18 +1331,32 @@ describe('OptionsListUtils', () => { }, ]; - const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList); + const smallResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList); expect(smallResult.tagOptions).toStrictEqual(smallResultList); - const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, false, {}, [], true, smallTagsList); + const smallSearchResult = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], search, [], [], false, false, false, {}, [], true, smallTagsList); expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, false, {}, [], true, smallTagsList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions( + OPTIONS.reports, + OPTIONS.personalDetails, + [], + wrongSearch, + [], + [], + false, + false, + false, + {}, + [], + true, + smallTagsList, + ); expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); const largeResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], emptySearch, selectedOptions, @@ -1338,8 +1373,8 @@ describe('OptionsListUtils', () => { expect(largeResult.tagOptions).toStrictEqual(largeResultList); const largeSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], search, selectedOptions, @@ -1356,8 +1391,8 @@ describe('OptionsListUtils', () => { expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( - REPORTS, - PERSONAL_DETAILS, + OPTIONS.reports, + OPTIONS.personalDetails, [], wrongSearch, selectedOptions,