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,