diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d24042c76f450..396c1acd94aca 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -647,6 +647,9 @@ const ONYXKEYS = { /** Keeps track of whether the "Confirm Navigate to Expensify Classic" modal is opened */ IS_OPEN_CONFIRM_NAVIGATE_EXPENSIFY_CLASSIC_MODAL_OPEN: 'IsOpenConfirmNavigateExpensifyClassicModalOpen', + /** The transaction IDs to be highlighted when opening the Expenses search route page */ + TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE: 'transactionIdsHighlightOnSearchRoute', + /** Collection Keys */ COLLECTION: { DOMAIN: 'domain_', @@ -1391,6 +1394,7 @@ type OnyxValuesMapping = { [ONYXKEYS.HAS_DENIED_CONTACT_IMPORT_PROMPT]: boolean | undefined; [ONYXKEYS.IS_OPEN_CONFIRM_NAVIGATE_EXPENSIFY_CLASSIC_MODAL_OPEN]: boolean; [ONYXKEYS.PERSONAL_POLICY_ID]: string; + [ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE]: Record>; }; type OnyxDerivedValuesMapping = { diff --git a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx index f3004e162dec3..2e57a0a3bc378 100644 --- a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx +++ b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx @@ -60,7 +60,7 @@ function BaseFloatingCameraButton({icon}: BaseFloatingCameraButtonProps) { const quickActionReportID = policyChatForActivePolicy?.reportID ?? reportID; Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); - startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts, true); }); }; diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index a66f5e43ef60e..3008393129899 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -1,16 +1,19 @@ import {useIsFocused} from '@react-navigation/native'; -import {useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchQueryJSON} from '@components/Search/types'; import type {SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionListWithSections/types'; import {search} from '@libs/actions/Search'; +import {mergeTransactionIdsHighlightOnSearchRoute} from '@libs/actions/Transaction'; import {isReportActionEntry} from '@libs/SearchUIUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; import usePrevious from './usePrevious'; type UseSearchHighlightAndScroll = { @@ -53,6 +56,12 @@ function useSearchHighlightAndScroll({ const initializedRef = useRef(false); const hasPendingSearchRef = useRef(false); const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT; + + const transactionIDsToHighlightSelector = useCallback((allTransactionIDs: OnyxEntry>>) => allTransactionIDs?.[queryJSON.type], [queryJSON.type]); + const [transactionIDsToHighlight] = useOnyx(ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, { + canBeMissing: true, + selector: transactionIDsToHighlightSelector, + }); const searchResultsData = searchResults?.data; const prevTransactionsIDs = Object.keys(previousTransactions ?? {}); @@ -195,11 +204,20 @@ function useSearchHighlightAndScroll({ } else { const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); + const manualHighlightTransactionIDs = new Set(Object.keys(transactionIDsToHighlight ?? {}).filter((id) => !!transactionIDsToHighlight?.[id])); // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted - const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id)); + const newTransactionIDs = currentTransactionIDs.filter((id) => { + if (manualHighlightTransactionIDs.has(id)) { + return true; + } + if (!triggeredByHookRef.current || !hasNewItemsRef.current) { + return false; + } + return !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id); + }); - if (!triggeredByHookRef.current || newTransactionIDs.length === 0 || !hasNewItemsRef.current) { + if (newTransactionIDs.length === 0) { return; } @@ -211,7 +229,41 @@ function useSearchHighlightAndScroll({ } setNewSearchResultKeys(newKeys); } - }, [searchResults?.data, previousSearchResults, isChat]); + }, [searchResults?.data, previousSearchResults, isChat, transactionIDsToHighlight]); + + // Reset transactionIDsToHighlight after they have been highlighted + useEffect(() => { + if (isEmptyObject(transactionIDsToHighlight) || newSearchResultKeys === null) { + return; + } + + const highlightedTransactionIDs = Object.keys(transactionIDsToHighlight).filter( + (id) => transactionIDsToHighlight[id] && newSearchResultKeys?.has(`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`), + ); + + // We need to use requestAnimationFrame here to ensure that setTimeout actually starts + // only after the user has navigated to the "Reports > Expenses" page. + // Otherwise, there is still a chance we might miss the timing because setTimeout runs too early, + // causing the highlight not to appear. + let timer: NodeJS.Timeout; + const animation = requestAnimationFrame(() => { + timer = setTimeout(() => { + mergeTransactionIdsHighlightOnSearchRoute(queryJSON.type, Object.fromEntries(highlightedTransactionIDs.map((id) => [id, false]))); + }, CONST.ANIMATED_HIGHLIGHT_START_DURATION); + }); + return () => { + clearTimeout(timer); + cancelAnimationFrame(animation); + }; + }, [transactionIDsToHighlight, queryJSON.type, newSearchResultKeys]); + + // Remove transactionIDsToHighlight when the user leaves the current search type + useEffect( + () => () => { + mergeTransactionIdsHighlightOnSearchRoute(queryJSON.type, null); + }, + [queryJSON.type], + ); // Reset newSearchResultKey after it's been used useEffect(() => { diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 8d7af62b6891c..183ff5891626b 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -976,6 +976,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) case 'reimbursable': case 'participantsAutoAssigned': case 'isFromGlobalCreate': + case 'isFromFloatingActionButton': case 'hasEReceipt': case 'shouldShowOriginalAmount': case 'managedCard': @@ -1085,6 +1086,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) tag: CONST.RED_BRICK_ROAD_PENDING_ACTION, transactionType: CONST.RED_BRICK_ROAD_PENDING_ACTION, isFromGlobalCreate: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isFromFloatingActionButton: CONST.RED_BRICK_ROAD_PENDING_ACTION, taxRate: CONST.RED_BRICK_ROAD_PENDING_ACTION, parentTransactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, reimbursable: CONST.RED_BRICK_ROAD_PENDING_ACTION, diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index bea275ab32b4a..5d6c4ef368f3a 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -8,8 +8,6 @@ import DateUtils from '@libs/DateUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; -import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; -import Navigation from '@libs/Navigation/Navigation'; import {getReportActionHtml, getReportActionText} from '@libs/ReportActionsUtils'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction} from '@libs/ReportUtils'; import { @@ -34,7 +32,7 @@ import type {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report' import type {OnyxData} from '@src/types/onyx/Request'; import type {Receipt} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getAllPersonalDetails, getReceiptError, getSearchOnyxUpdate, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies} from '.'; +import {getAllPersonalDetails, getReceiptError, getSearchOnyxUpdate, handleNavigateAfterExpenseCreate, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies} from '.'; import type {BasePolicyParams} from '.'; type SendInvoiceInformation = { @@ -77,6 +75,7 @@ type SendInvoiceOptions = { companyWebsite?: string; policyRecentlyUsedCategories?: OnyxEntry; policyRecentlyUsedTags?: OnyxEntry; + isFromGlobalCreate?: boolean; }; type BuildOnyxDataForInvoiceParams = { @@ -722,6 +721,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, + isFromGlobalCreate, }: SendInvoiceOptions) { const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { @@ -786,11 +786,12 @@ function sendInvoice({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - if (isSearchTopmostFullScreenRoute()) { - Navigation.dismissModal(); - } else { - Navigation.dismissModalWithReport({reportID: invoiceRoom.reportID}); - } + handleNavigateAfterExpenseCreate({ + activeReportID: invoiceRoom.reportID, + transactionID, + isFromGlobalCreate, + isInvoice: true, + }); notifyNewAction(invoiceRoom.reportID, currentUserAccountID); } diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 4b7c0a079b07f..307f9a748e4a4 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -60,6 +60,7 @@ import Log from '@libs/Log'; import {validateAmount} from '@libs/MoneyRequestUtils'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import isReportOpenInSuperWideRHP from '@libs/Navigation/helpers/isReportOpenInSuperWideRHP'; +import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import {isOffline} from '@libs/Network/NetworkStore'; @@ -185,7 +186,7 @@ import { shouldEnableNegative, updateReportPreview, } from '@libs/ReportUtils'; -import {getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {buildCannedSearchQuery, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; import {getSuggestedSearches} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; @@ -229,7 +230,7 @@ import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from '@userAct import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, getCurrentUserAccountID, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions'; -import {sanitizeRecentWaypoints} from '@userActions/Transaction'; +import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeRecentWaypoints} from '@userActions/Transaction'; import {removeDraftTransaction, removeDraftTransactions} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize} from '@userActions/Welcome/OnboardingFlow'; @@ -271,6 +272,7 @@ type BaseTransactionParams = { billable?: boolean; reimbursable?: boolean; customUnitRateID?: string; + isFromGlobalCreate?: boolean; }; type InitMoneyRequestParams = { @@ -278,6 +280,7 @@ type InitMoneyRequestParams = { policy?: OnyxEntry; personalPolicy: Pick | undefined; isFromGlobalCreate?: boolean; + isFromFloatingActionButton?: boolean; currentIouRequestType?: IOURequestType | undefined; newIouRequestType: IOURequestType | undefined; report: OnyxEntry; @@ -690,6 +693,7 @@ type TrackExpenseTransactionParams = { isLinkedTrackedExpenseReportArchived?: boolean; odometerStart?: number; odometerEnd?: number; + isFromGlobalCreate?: boolean; gpsCoordinates?: string; }; @@ -978,9 +982,9 @@ function getUserAccountID(): number { * If the action is done from the report RHP, then we just want to dismiss the money request flow screens. * It is a helper function used only in this file. */ -function dismissModalAndOpenReportInInboxTab(reportID?: string) { +function dismissModalAndOpenReportInInboxTab(reportID?: string, isInvoice?: boolean) { const rootState = navigationRef.getRootState(); - if (isReportOpenInRHP(rootState)) { + if (!isInvoice && isReportOpenInRHP(rootState)) { const rhpKey = rootState.routes.at(-1)?.state?.key; if (rhpKey) { const hasMultipleTransactions = Object.values(allTransactions).filter((transaction) => transaction?.reportID === reportID).length > 0; @@ -1008,6 +1012,52 @@ function dismissModalAndOpenReportInInboxTab(reportID?: string) { Navigation.dismissModalWithReport({reportID}); } +/** + * Helper to navigate after an expense is created in order to standardize the post‑creation experience + * when creating an expense from the global create button. + * If the expense is created from the global create button then: + * - If it is created on the inbox tab, it will open the chat report containing that expense. + * - If it is created elsewhere, it will navigate to Reports > Expense and highlight the newly created expense. + */ +function handleNavigateAfterExpenseCreate({ + activeReportID, + transactionID, + isFromGlobalCreate, + isInvoice, + shouldHandleNavigation = true, +}: { + activeReportID?: string; + transactionID?: string; + isFromGlobalCreate?: boolean; + isInvoice?: boolean; + shouldHandleNavigation?: boolean; +}) { + const isUserOnInbox = isReportTopmostSplitNavigator(); + + // If the expense is not created from global create or is currently on the inbox tab, + // we just need to dismiss the money request flow screens + // and open the report chat containing the IOU report + if (!isFromGlobalCreate || isUserOnInbox || !transactionID) { + if (shouldHandleNavigation) { + dismissModalAndOpenReportInInboxTab(activeReportID, isInvoice); + } + return; + } + + const type = isInvoice ? CONST.SEARCH.DATA_TYPES.INVOICE : CONST.SEARCH.DATA_TYPES.EXPENSE; + // We mark this transaction to be highlighted when opening the expense search route page + mergeTransactionIdsHighlightOnSearchRoute(type, {[transactionID]: true}); + + if (!shouldHandleNavigation) { + return; + } + const queryString = buildCannedSearchQuery({type}); + Navigation.dismissModal(); + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: queryString})); + }); +} + /** * Build a minimal transaction record for formula computation in buildOptimisticExpenseReport. * This allows formulas like {report:startdate}, {report:expensescount} to work correctly. @@ -1061,6 +1111,7 @@ function initMoneyRequest({ policy, personalPolicy, isFromGlobalCreate, + isFromFloatingActionButton, currentIouRequestType, newIouRequestType, report, @@ -1088,6 +1139,7 @@ function initMoneyRequest({ Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${newTransactionID}`, { reportID, isFromGlobalCreate, + isFromFloatingActionButton, created, currency, transactionID: newTransactionID, @@ -1161,6 +1213,7 @@ function initMoneyRequest({ reportID, transactionID: newTransactionID, isFromGlobalCreate, + isFromFloatingActionButton, merchant: defaultMerchant, }; @@ -1195,6 +1248,7 @@ function startMoneyRequest( skipConfirmation = false, backToReport?: string, draftTransactions?: OnyxCollection, + isFromFloatingActionButton?: boolean, ) { Performance.markStart(CONST.TIMING.OPEN_CREATE_EXPENSE); const sourceRoute = Navigation.getActiveRoute(); @@ -1209,6 +1263,9 @@ function startMoneyRequest( }, }); clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID, skipConfirmation, draftTransactions); + if (isFromFloatingActionButton) { + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, {isFromFloatingActionButton}); + } switch (requestType) { case CONST.IOU.REQUEST_TYPE.MANUAL: Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID, backToReport)); @@ -1227,8 +1284,18 @@ function startMoneyRequest( } } -function startDistanceRequest(iouType: ValueOf, reportID: string, requestType?: IOURequestType, skipConfirmation = false, backToReport?: string) { +function startDistanceRequest( + iouType: ValueOf, + reportID: string, + requestType?: IOURequestType, + skipConfirmation = false, + backToReport?: string, + isFromFloatingActionButton?: boolean, +) { clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID, skipConfirmation); + if (isFromFloatingActionButton) { + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, {isFromFloatingActionButton}); + } switch (requestType) { case CONST.IOU.REQUEST_TYPE.DISTANCE_MAP: Navigation.navigate(ROUTES.DISTANCE_REQUEST_CREATE_TAB_MAP.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID, backToReport)); @@ -6162,6 +6229,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep count, rate, unit, + isFromGlobalCreate, } = transactionParams; const testDriveCommentReportActionID = isTestDrive ? NumberUtils.rand64() : undefined; @@ -6359,9 +6427,6 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep if (shouldHandleNavigation) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransactions()); - if (!requestMoneyInformation.isRetry) { - dismissModalAndOpenReportInInboxTab(backToReport ?? activeReportID); - } const trackReport = Navigation.getReportRouteByID(linkedTrackedExpenseReportAction?.childReportID); if (trackReport?.key) { @@ -6369,6 +6434,15 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep } } + if (!requestMoneyInformation.isRetry) { + handleNavigateAfterExpenseCreate({ + activeReportID: backToReport ?? activeReportID, + transactionID: transaction.transactionID, + isFromGlobalCreate, + shouldHandleNavigation, + }); + } + if (activeReportID && !isMoneyRequestReport) { Navigation.setNavigationActionToMicrotaskQueue(() => setTimeout(() => { @@ -6398,7 +6472,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf policyRecentlyUsedCurrencies, } = submitPerDiemExpenseInformation; const {payeeAccountID} = participantParams; - const {currency, comment = '', category, tag, created, customUnit, attendees} = transactionParams; + const {currency, comment = '', category, tag, created, customUnit, attendees, isFromGlobalCreate} = transactionParams; if ( isEmptyObject(policyParams.policy) || @@ -6484,7 +6558,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - dismissModalAndOpenReportInInboxTab(activeReportID); + handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate}); if (activeReportID) { notifyNewAction(activeReportID, payeeAccountID); @@ -6539,6 +6613,7 @@ function trackExpense(params: CreateTrackExpenseParams) { attendees, odometerStart, odometerEnd, + isFromGlobalCreate, gpsCoordinates, } = transactionData; const isMoneyRequestReport = isMoneyRequestReportReportUtils(report); @@ -6824,10 +6899,15 @@ function trackExpense(params: CreateTrackExpenseParams) { if (shouldHandleNavigation) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransactions()); + } - if (!params.isRetry) { - dismissModalAndOpenReportInInboxTab(activeReportID); - } + if (!params.isRetry) { + handleNavigateAfterExpenseCreate({ + activeReportID, + transactionID: transaction?.transactionID, + isFromGlobalCreate, + shouldHandleNavigation, + }); } notifyNewAction(activeReportID, payeeAccountID); @@ -7468,6 +7548,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest receipt, odometerStart, odometerEnd, + isFromGlobalCreate, gpsCoordinates, } = transactionParams; @@ -7664,7 +7745,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest const activeReportID = isMoneyRequestReport && report?.reportID ? report.reportID : parameters.chatReportID; if (shouldHandleNavigation) { - dismissModalAndOpenReportInInboxTab(backToReport ?? activeReportID); + handleNavigateAfterExpenseCreate({activeReportID: backToReport ?? activeReportID, isFromGlobalCreate, transactionID: parameters.transactionID}); } if (!isMoneyRequestReport) { @@ -13143,6 +13224,7 @@ export { getSearchOnyxUpdate, setMoneyRequestTimeRate, setMoneyRequestTimeCount, + handleNavigateAfterExpenseCreate, buildMinimalTransactionForFormula, buildOnyxDataForMoneyRequest, createSplitsAndOnyxData, diff --git a/src/libs/actions/QuickActionNavigation.ts b/src/libs/actions/QuickActionNavigation.ts index 3d6b94a8823fc..318e075dc5445 100644 --- a/src/libs/actions/QuickActionNavigation.ts +++ b/src/libs/actions/QuickActionNavigation.ts @@ -15,6 +15,7 @@ type NavigateToQuickActionParams = { lastDistanceExpenseType?: DistanceExpenseType; targetAccountPersonalDetails: PersonalDetails; currentUserAccountID: number; + isFromFloatingActionButton?: boolean; }; function getQuickActionRequestType(action: QuickActionName | undefined, lastDistanceExpenseType?: DistanceExpenseType): IOURequestType | undefined { @@ -37,7 +38,7 @@ function getQuickActionRequestType(action: QuickActionName | undefined, lastDist } function navigateToQuickAction(params: NavigateToQuickActionParams) { - const {isValidReport, quickAction, selectOption, lastDistanceExpenseType, targetAccountPersonalDetails, currentUserAccountID} = params; + const {isValidReport, quickAction, selectOption, lastDistanceExpenseType, targetAccountPersonalDetails, currentUserAccountID, isFromFloatingActionButton} = params; const reportID = isValidReport && quickAction?.chatReportID ? quickAction?.chatReportID : generateReportID(); const requestType = getQuickActionRequestType(quickAction?.action, lastDistanceExpenseType); @@ -45,28 +46,28 @@ function navigateToQuickAction(params: NavigateToQuickActionParams) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: case CONST.QUICK_ACTIONS.REQUEST_SCAN: case CONST.QUICK_ACTIONS.PER_DIEM: - selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SUBMIT, reportID, requestType, true), true); + selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SUBMIT, reportID, requestType, true, undefined, undefined, isFromFloatingActionButton), true); break; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: case CONST.QUICK_ACTIONS.SPLIT_SCAN: case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: - selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SPLIT, reportID, requestType, true), true); + selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SPLIT, reportID, requestType, true, undefined, undefined, isFromFloatingActionButton), true); break; case CONST.QUICK_ACTIONS.SEND_MONEY: - selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, reportID, undefined, true), false); + selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, reportID, undefined, true, undefined, undefined, isFromFloatingActionButton), false); break; case CONST.QUICK_ACTIONS.ASSIGN_TASK: selectOption(() => startOutCreateTaskQuickAction(currentUserAccountID, isValidReport ? reportID : '', targetAccountPersonalDetails), false); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: case CONST.QUICK_ACTIONS.TRACK_SCAN: - selectOption(() => startMoneyRequest(CONST.IOU.TYPE.TRACK, reportID, requestType, true), false); + selectOption(() => startMoneyRequest(CONST.IOU.TYPE.TRACK, reportID, requestType, true, undefined, undefined, isFromFloatingActionButton), false); break; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: - selectOption(() => startDistanceRequest(CONST.IOU.TYPE.SUBMIT, reportID, requestType, true), false); + selectOption(() => startDistanceRequest(CONST.IOU.TYPE.SUBMIT, reportID, requestType, true, undefined, isFromFloatingActionButton), false); break; case CONST.QUICK_ACTIONS.TRACK_DISTANCE: - selectOption(() => startDistanceRequest(CONST.IOU.TYPE.TRACK, reportID, requestType, true), false); + selectOption(() => startDistanceRequest(CONST.IOU.TYPE.TRACK, reportID, requestType, true, undefined, isFromFloatingActionButton), false); break; default: } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 4bf505b1b3c48..21ceece2b5d51 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -52,6 +52,7 @@ import type { } from '@src/types/onyx'; import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage'; import type {OnyxData} from '@src/types/onyx/Request'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type TransactionState from '@src/types/utils/TransactionStateType'; import {getPolicyTagsData} from './Policy/Tag'; @@ -1519,6 +1520,10 @@ function getDraftTransactions(draftTransactions?: OnyxCollection): return Object.values(draftTransactions ?? allTransactionDrafts ?? {}).filter((transaction): transaction is Transaction => !!transaction); } +function mergeTransactionIdsHighlightOnSearchRoute(type: SearchDataTypes, data: Record | null) { + return Onyx.merge(ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, {[type]: data}); +} + function getDuplicateTransactionDetails(transactionID?: string) { if (!transactionID) { return; @@ -1549,5 +1554,6 @@ export { revert, changeTransactionsReport, setTransactionReport, + mergeTransactionIdsHighlightOnSearchRoute, getDuplicateTransactionDetails, }; diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index e0204436e42b3..65b61bd764891 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -151,7 +151,7 @@ type BuildOptimisticTransactionParams = { function buildOptimisticTransactionAndCreateDraft({initialTransaction, currentUserPersonalDetails, reportID}: BuildOptimisticTransactionParams): Transaction { const newTransactionID = generateTransactionID(); - const {currency, iouRequestType, isFromGlobalCreate} = initialTransaction ?? {}; + const {currency, iouRequestType, isFromGlobalCreate, isFromFloatingActionButton} = initialTransaction ?? {}; const newTransaction = { amount: 0, created: format(new Date(), 'yyyy-MM-dd'), @@ -161,6 +161,7 @@ function buildOptimisticTransactionAndCreateDraft({initialTransaction, currentUs reportID, transactionID: newTransactionID, isFromGlobalCreate, + isFromFloatingActionButton, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, } as Transaction; createDraftTransaction(newTransaction); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index b99936696b08c..622269e1be489 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1030,6 +1030,7 @@ function SearchPage({route}: SearchPageProps) { const saveFileAndInitMoneyRequest = (files: FileObject[]) => { const initialTransaction = initMoneyRequest({ isFromGlobalCreate: true, + isFromFloatingActionButton: true, reportID: newReportID, personalPolicy, newIouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 41c12be981657..5b5a9d0f051c5 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -343,7 +343,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } // Start the scan flow directly - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts, true); }); }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID, showRedirectToExpensifyClassicModal]); @@ -356,7 +356,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref const quickActionReportID = policyChatForActivePolicy?.reportID ?? reportID; Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); - startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts, true); }); }, [policyChatForActivePolicy?.policyID, policyChatForActivePolicy?.reportID, reportID, allTransactionDrafts]); @@ -439,7 +439,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref showRedirectToExpensifyClassicModal(); return; } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); }), sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE, }, @@ -482,6 +482,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref lastDistanceExpenseType, targetAccountPersonalDetails, currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, }); }); }; @@ -508,7 +509,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); }); }; @@ -585,7 +586,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref return; } // Start the flow to start tracking a distance request - startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType); + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); }); }, sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE, @@ -648,7 +649,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref return; } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); }), sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, }, diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx index e2bd8a4a32ea1..1889c50fd9a11 100644 --- a/src/pages/iou/request/DistanceRequestStartPage.tsx +++ b/src/pages/iou/request/DistanceRequestStartPage.tsx @@ -112,6 +112,7 @@ function DistanceRequestStartPage({ policy, personalPolicy, isFromGlobalCreate, + isFromFloatingActionButton: transaction?.isFromFloatingActionButton ?? transaction?.isFromGlobalCreate ?? isFromGlobalCreate, currentIouRequestType: transaction?.iouRequestType, newIouRequestType: newIOUType, report, @@ -124,6 +125,8 @@ function DistanceRequestStartPage({ }, [ transaction?.iouRequestType, + transaction?.isFromGlobalCreate, + transaction?.isFromFloatingActionButton, reportID, policy, personalPolicy, diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index b0e1fb87c1096..0d495adc150e6 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -188,6 +188,7 @@ function IOURequestStartPage({ policy, personalPolicy, isFromGlobalCreate: transaction?.isFromGlobalCreate ?? isFromGlobalCreate, + isFromFloatingActionButton: transaction?.isFromFloatingActionButton ?? transaction?.isFromGlobalCreate ?? isFromGlobalCreate, currentIouRequestType: transaction?.iouRequestType, newIouRequestType: newIOUType, report, @@ -201,6 +202,7 @@ function IOURequestStartPage({ [ transaction?.iouRequestType, transaction?.isFromGlobalCreate, + transaction?.isFromGlobalCreate, reportID, policy, personalPolicy, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 98d5460cdb7b7..2fce979351230 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -624,6 +624,7 @@ function IOURequestStepConfirmation({ originalTransactionID: item.comment?.originalTransactionID, source: item.comment?.source, isLinkedTrackedExpenseReportArchived, + isFromGlobalCreate: item?.isFromFloatingActionButton ?? item?.isFromGlobalCreate, ...(isTimeRequest ? {type: CONST.TRANSACTION.TYPE.TIME, count: item.comment?.units?.count, rate: item.comment?.units?.rate, unit: CONST.TIME_TRACKING.UNIT.HOUR} : {}), @@ -713,6 +714,7 @@ function IOURequestStepConfirmation({ billable: transaction.billable, reimbursable: transaction.reimbursable, attendees: transaction.comment?.attendees, + isFromGlobalCreate: transaction.isFromFloatingActionButton ?? transaction.isFromGlobalCreate, }, isASAPSubmitBetaEnabled, currentUserAccountIDParam: currentUserPersonalDetails.accountID, @@ -791,6 +793,7 @@ function IOURequestStepConfirmation({ isLinkedTrackedExpenseReportArchived, odometerStart: isOdometerDistanceRequest ? item.comment?.odometerStart : undefined, odometerEnd: isOdometerDistanceRequest ? item.comment?.odometerEnd : undefined, + isFromGlobalCreate: item?.isFromFloatingActionButton ?? item?.isFromGlobalCreate, gpsCoordinates: isGPSDistanceRequest ? getGPSCoordinates(gpsDraftDetails) : undefined, }, accountantParams: { @@ -872,6 +875,7 @@ function IOURequestStepConfirmation({ receipt: isManualDistanceRequest || isOdometerDistanceRequest ? receiptFiles[transaction.transactionID] : undefined, odometerStart: isOdometerDistanceRequest ? transaction.comment?.odometerStart : undefined, odometerEnd: isOdometerDistanceRequest ? transaction.comment?.odometerEnd : undefined, + isFromGlobalCreate: transaction.isFromFloatingActionButton ?? transaction.isFromGlobalCreate, gpsCoordinates: isGPSDistanceRequest ? getGPSCoordinates(gpsDraftDetails) : undefined, }, backToReport, @@ -1059,6 +1063,7 @@ function IOURequestStepConfirmation({ policyTagList: policyTags, policyCategories, policyRecentlyUsedCategories, + isFromGlobalCreate: transaction?.isFromFloatingActionButton ?? transaction?.isFromGlobalCreate, policyRecentlyUsedTags, }); return; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 4e342f66a3b8b..2d150cf9bc7eb 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -532,6 +532,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the transaction was created globally */ isFromGlobalCreate?: boolean; + /** Whether the transaction was created from the FAB, including Global create button, FloatingCameraButton, QuickAction,... */ + isFromFloatingActionButton?: boolean; + /** The transaction tax rate */ taxRate?: string | undefined; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 37db6c9566c2c..21f24f6634aa5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -27,6 +27,7 @@ import { getPerDiemExpenseInformation, getReportOriginalCreationTimestamp, getReportPreviewAction, + handleNavigateAfterExpenseCreate, initMoneyRequest, initSplitExpense, markRejectViolationAsResolved, @@ -57,6 +58,7 @@ import {clearAllRelatedReportActionErrors} from '@libs/actions/ReportActions'; import {subscribeToUserEvents} from '@libs/actions/User'; import type {ApiCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; +import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import Navigation from '@libs/Navigation/Navigation'; import {rand64} from '@libs/NumberUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; @@ -136,6 +138,7 @@ jest.mock('@src/libs/actions/Report', () => { }; }); jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); +jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => jest.fn()); const unapprovedCashHash = 71801560; const unapprovedCashSimilarSearchHash = 1832274510; @@ -11937,6 +11940,43 @@ describe('actions/IOU', () => { }); }); + it('handleNavigateAfterExpenseCreate', async () => { + const mockedIsReportTopmostSplitNavigator = isReportTopmostSplitNavigator as jest.MockedFunction; + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const activeReportID = '1'; + const transactionID = '1'; + mockedIsReportTopmostSplitNavigator.mockReturnValue(false); + + // When on the Inbox tab, or NOT from the "global create" button, or without a transactionID, + // the function dismissModalAndOpenReportInInboxTab will always be called to handle it, + // so mergeTransactionIdsHighlightOnSearchRoute will never be invoked. + handleNavigateAfterExpenseCreate({activeReportID, isFromGlobalCreate: false}); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(0); + + handleNavigateAfterExpenseCreate({activeReportID, isFromGlobalCreate: true}); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(0); + + mockedIsReportTopmostSplitNavigator.mockReturnValue(true); + handleNavigateAfterExpenseCreate({activeReportID, isFromGlobalCreate: true, transactionID}); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(0); + + // When NOT on the Inbox tab + mockedIsReportTopmostSplitNavigator.mockReturnValue(false); + handleNavigateAfterExpenseCreate({activeReportID, isFromGlobalCreate: true, transactionID}); + + // then mergeTransactionIdsHighlightOnSearchRoute will be called + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(1); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledWith(CONST.SEARCH.DATA_TYPES.EXPENSE, {[transactionID]: true}); + spyOnMergeTransactionIdsHighlightOnSearchRoute.mockClear(); + + // If expense is an invoice + handleNavigateAfterExpenseCreate({activeReportID, isFromGlobalCreate: true, transactionID, isInvoice: true}); + + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(1); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledWith(CONST.SEARCH.DATA_TYPES.INVOICE, {[transactionID]: true}); + spyOnMergeTransactionIdsHighlightOnSearchRoute.mockReset(); + }); + describe('convertBulkTrackedExpensesToIOU', () => { it('should accept personalDetails as a required parameter', async () => { const currentUserAccountID = 1; diff --git a/tests/unit/QuickActionNavigationTest.ts b/tests/unit/QuickActionNavigationTest.ts index af64b11467405..6b379775d3958 100644 --- a/tests/unit/QuickActionNavigationTest.ts +++ b/tests/unit/QuickActionNavigationTest.ts @@ -34,7 +34,7 @@ describe('IOU Utils', () => { }); // Then we should start manual submit request flow - expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.MANUAL, true); + expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.MANUAL, true, undefined, undefined, undefined); }); it('should be navigated to Scan receipt Split Expense', () => { @@ -50,7 +50,7 @@ describe('IOU Utils', () => { }); // Then we should start scan split request flow - expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SPLIT, reportID, CONST.IOU.REQUEST_TYPE.SCAN, true); + expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SPLIT, reportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, undefined, undefined); }); it('should be navigated to Track distance Expense', () => { @@ -66,7 +66,7 @@ describe('IOU Utils', () => { }); // Then we should start distance track request flow - expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.TRACK, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, true); + expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.TRACK, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, true, undefined, undefined); }); it('should be navigated to Map distance Expense by default', () => { @@ -82,7 +82,7 @@ describe('IOU Utils', () => { }); // Then we should start map distance request flow - expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, true); + expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, true, undefined, undefined); }); it('should be navigated to request distance Expense depending on lastDistanceExpenseType', () => { @@ -99,7 +99,7 @@ describe('IOU Utils', () => { }); // Then we should start manual distance request flow - expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, true); + expect(startDistanceRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, true, undefined, undefined); }); it('should be navigated to Per Diem Expense', () => { @@ -115,7 +115,7 @@ describe('IOU Utils', () => { }); // Then we should start per diem request flow - expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.PER_DIEM, true); + expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.PER_DIEM, true, undefined, undefined, undefined); }); }); }); diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 30ac57245929a..d0ffbe9921c2b 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import type {UseSearchHighlightAndScroll} from '@hooks/useSearchHighlightAndScroll'; import {search} from '@libs/actions/Search'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; jest.mock('@libs/actions/Search'); jest.mock('@react-navigation/native', () => ({ @@ -256,6 +258,82 @@ describe('useSearchHighlightAndScroll', () => { expect(result.current.newSearchResultKeys?.size).toBe(2); }); + it('should return new search result keys for manually highlighted expenses', async () => { + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest + .spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute') + .mockImplementationOnce(jest.fn()); + // We need to mock requestAnimationFrame to mimic long Onyx merge overhead + jest.spyOn(global, 'requestAnimationFrame').mockImplementation((callback: FrameRequestCallback) => { + callback(performance.now()); + return 0; + }); + + await Onyx.merge(ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, {[baseProps.queryJSON.type]: {'3': true}}); + + const {rerender, result} = renderHook((props: UseSearchHighlightAndScroll) => useSearchHighlightAndScroll(props), { + initialProps: baseProps, + }); + const updatedProps1 = { + ...baseProps, + searchResults: { + ...baseProps.searchResults, + data: { + transactions_1: { + transactionID: '1', + }, + transactions_2: { + transactionID: '2', + }, + }, + }, + transactions: { + '1': {transactionID: '1'}, + '2': {transactionID: '2'}, + '3': {transactionID: '3'}, + }, + previousTransactions: { + '1': {transactionID: '1'}, + }, + } as unknown as UseSearchHighlightAndScroll; + + // When there is no data yet, even if the transactionID has been added to manual highlight transactionIDs, + // it still will not be included in newSearchResultKeys. + rerender(updatedProps1); + expect(result.current.newSearchResultKeys?.size).toBe(2); + expect([...(result.current.newSearchResultKeys ?? new Set())]).not.toContain('transactions_3'); + + // When the data contains the highlight transactionID, it will be highlighted. + const updatedProps2 = { + ...updatedProps1, + searchResults: { + ...updatedProps1.searchResults, + data: { + transactions_1: { + transactionID: '1', + }, + transactions_2: { + transactionID: '2', + }, + transactions_3: { + transactionID: '3', + }, + }, + }, + } as unknown as UseSearchHighlightAndScroll; + + rerender(updatedProps2); + expect(result.current.newSearchResultKeys?.size).toBe(1); + expect([...(result.current.newSearchResultKeys ?? new Set())]).toContain('transactions_3'); + + // Wait 1s for the timer in useSearchHighlightAndScroll to complete. + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(1); + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledWith(baseProps.queryJSON.type, {'3': false}); + }); + it('should return multiple new search result keys when there are multiple new chats', () => { const chatProps = { ...baseProps,