diff --git a/package.json b/package.json index 50cc3a255de3..169dc7e7404a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=329 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=327 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/src/components/SelectionList/Search/ReportListItemHeader.tsx b/src/components/SelectionList/Search/ReportListItemHeader.tsx index 82461715a592..c008c56a6b4f 100644 --- a/src/components/SelectionList/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionList/Search/ReportListItemHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {ColorValue} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -7,6 +7,7 @@ import ReportSearchHeader from '@components/ReportSearchHeader'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem, TransactionReportGroupListItemType} from '@components/SelectionList/types'; import TextWithTooltip from '@components/TextWithTooltip'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -14,7 +15,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {handleActionButtonPress} from '@userActions/Search'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; import ActionCell from './ActionCell'; import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow'; @@ -169,12 +172,28 @@ function ReportListItemHeader({ const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to; const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from); - + const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); + const snapshotReport = useMemo(() => { + return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as SearchReport; + }, [snapshot, reportItem.reportID]); + const snapshotPolicy = useMemo(() => { + return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as SearchPolicy; + }, [snapshot, reportItem.policyID]); + const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); const avatarBorderColor = StyleUtils.getItemBackgroundColorStyle(!!reportItem.isSelected, !!isFocused, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ?? theme.highlightBG; const handleOnButtonPress = () => { - handleActionButtonPress(currentSearchHash, reportItem, () => onSelectRow(reportItem as unknown as TItem), shouldUseNarrowLayout && !!canSelectMultiple, currentSearchKey); + handleActionButtonPress( + currentSearchHash, + reportItem, + () => onSelectRow(reportItem as unknown as TItem), + shouldUseNarrowLayout && !!canSelectMultiple, + snapshotReport, + snapshotPolicy, + lastPaymentMethod, + currentSearchKey, + ); }; return !isLargeScreenWidth ? ( diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index fb2380a1f64a..55c9a18896c0 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -8,6 +8,7 @@ import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types'; import TransactionItemRow from '@components/TransactionItemRow'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useSyncFocus from '@hooks/useSyncFocus'; @@ -16,6 +17,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/actions/Search'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow'; function TransactionListItem({ @@ -38,6 +41,15 @@ function TransactionListItem({ const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {currentSearchHash, currentSearchKey} = useSearchContext(); + const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); + const snapshotReport = useMemo(() => { + return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as SearchReport; + }, [snapshot, transactionItem.reportID]); + + const snapshotPolicy = useMemo(() => { + return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as SearchPolicy; + }, [snapshot, transactionItem.policyID]); + const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); const pressableStyle = [ styles.transactionListItemStyle, @@ -62,8 +74,17 @@ function TransactionListItem({ }, [transactionItem]); const handleActionButtonPress = useCallback(() => { - handleActionButtonPressUtil(currentSearchHash, transactionItem, () => onSelectRow(item), shouldUseNarrowLayout && !!canSelectMultiple, currentSearchKey); - }, [canSelectMultiple, currentSearchHash, currentSearchKey, item, onSelectRow, shouldUseNarrowLayout, transactionItem]); + handleActionButtonPressUtil( + currentSearchHash, + transactionItem, + () => onSelectRow(item), + shouldUseNarrowLayout && !!canSelectMultiple, + snapshotReport, + snapshotPolicy, + lastPaymentMethod, + currentSearchKey, + ); + }, [currentSearchHash, transactionItem, shouldUseNarrowLayout, canSelectMultiple, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey, onSelectRow, item]); const handleCheckboxPress = useCallback(() => { onCheckboxPress?.(item); diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b8b89f185ae3..3333755f4955 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {FormOnyxValues} from '@components/Form/types'; import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; @@ -22,33 +22,19 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; -import type {LastPaymentMethod, LastPaymentMethodType, Policy, SearchResults} from '@src/types/onyx'; +import type {LastPaymentMethod, LastPaymentMethodType, Policy} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type Nullable from '@src/types/utils/Nullable'; -let lastPaymentMethod: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, - callback: (val) => { - lastPaymentMethod = val; - }, -}); - -let allSnapshots: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.SNAPSHOT, - callback: (val) => { - allSnapshots = val; - }, - waitForCollectionCallback: true, -}); - function handleActionButtonPress( hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, isInMobileSelectionMode: boolean, + snapshotReport: SearchReport, + snapshotPolicy: SearchPolicy, + lastPaymentMethod: OnyxEntry, currentSearchKey?: SearchKey, ) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. @@ -64,14 +50,13 @@ function handleActionButtonPress( switch (item.action) { case CONST.SEARCH.ACTION_TYPES.PAY: - getPayActionCallback(hash, item, goToItem, currentSearchKey); + getPayActionCallback(hash, item, goToItem, snapshotReport, snapshotPolicy, lastPaymentMethod, currentSearchKey); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: approveMoneyRequestOnSearch(hash, [item.reportID], transactionID, currentSearchKey); return; case CONST.SEARCH.ACTION_TYPES.SUBMIT: { - const policy = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] ?? {}) as SearchPolicy; - submitMoneyRequestOnSearch(hash, [item], [policy], transactionID, currentSearchKey); + submitMoneyRequestOnSearch(hash, [item], [snapshotPolicy], transactionID, currentSearchKey); return; } case CONST.SEARCH.ACTION_TYPES.EXPORT_TO_ACCOUNTING: { @@ -79,7 +64,7 @@ function handleActionButtonPress( return; } - const policy = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] ?? {}) as Policy; + const policy = (snapshotPolicy ?? {}) as Policy; const connectedIntegration = getValidConnectedIntegration(policy); if (!connectedIntegration) { @@ -108,7 +93,15 @@ function getLastPolicyPaymentMethod(policyID: string | undefined, lastPaymentMet return lastPolicyPaymentMethod; } -function getPayActionCallback(hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, currentSearchKey?: SearchKey) { +function getPayActionCallback( + hash: number, + item: TransactionListItemType | TransactionReportGroupListItemType, + goToItem: () => void, + snapshotReport: SearchReport, + snapshotPolicy: SearchPolicy, + lastPaymentMethod: OnyxEntry, + currentSearchKey?: SearchKey, +) { const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethod); if (!lastPolicyPaymentMethod) { @@ -116,8 +109,7 @@ function getPayActionCallback(hash: number, item: TransactionListItemType | Tran return; } - const report = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`] ?? {}) as SearchReport; - const amount = Math.abs((report?.total ?? 0) - (report?.nonReimbursableTotal ?? 0)); + const amount = Math.abs((snapshotReport?.total ?? 0) - (snapshotReport?.nonReimbursableTotal ?? 0)); const transactionID = isTransactionListItemType(item) ? [item.transactionID] : undefined; if (lastPolicyPaymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { @@ -125,7 +117,7 @@ function getPayActionCallback(hash: number, item: TransactionListItemType | Tran return; } - const hasVBBA = !!allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`]?.achAccount?.bankAccountID; + const hasVBBA = !!snapshotPolicy?.achAccount?.bankAccountID; if (hasVBBA) { payMoneyRequestOnSearch(hash, [{reportID: item.reportID, amount, paymentType: lastPolicyPaymentMethod}], transactionID, currentSearchKey); return; diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 55c80bcad1c5..84e248b431fa 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import type {TransactionReportGroupListItemType} from '@components/SelectionList/types'; import {handleActionButtonPress} from '@libs/actions/Search'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {LastPaymentMethod, SearchResults} from '@src/types/onyx'; jest.mock('@src/components/ConfirmedRoute.tsx'); @@ -211,23 +215,62 @@ const updatedMockReportItem = { }), }; +const mockSnapshotForItem: OnyxEntry = { + // @ts-expect-error: Allow partial record in snapshot update for testing + data: { + [`${ONYXKEYS.COLLECTION.POLICY}${mockReportItemWithHold?.policyID}`]: { + ...(mockReportItemWithHold.policyID + ? { + [String(mockReportItemWithHold.policyID)]: { + type: 'policy', + id: String(mockReportItemWithHold.policyID), + role: 'admin', + owner: 'apb@apb.com', + ...mockReportItemWithHold, + }, + } + : {}), + }, + }, +}; + +const mockLastPaymentMethod: OnyxEntry = { + expense: 'Elsewhere', + lastUsed: 'Elsewhere', +}; + describe('handleActionButtonPress', () => { const searchHash = 1; + beforeAll(() => { + Onyx.merge( + `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + // @ts-expect-error: Allow partial record in snapshot update for testing + mockSnapshotForItem, + ); + Onyx.merge(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, mockLastPaymentMethod); + }); + + const snapshotReport = mockSnapshotForItem?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${mockReportItemWithHold.reportID}`] ?? {}; + const snapshotPolicy = mockSnapshotForItem?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${mockReportItemWithHold.policyID}`] ?? {}; + test('Should navigate to item when report has one transaction on hold', () => { const goToItem = jest.fn(() => {}); - handleActionButtonPress(searchHash, mockReportItemWithHold, goToItem, false); + // @ts-expect-error: Allow partial record in snapshot update for testing + handleActionButtonPress(searchHash, mockReportItemWithHold, goToItem, false, snapshotReport, snapshotPolicy, mockLastPaymentMethod); expect(goToItem).toHaveBeenCalledTimes(1); }); test('Should not navigate to item when the hold is removed', () => { const goToItem = jest.fn(() => {}); - handleActionButtonPress(searchHash, updatedMockReportItem, goToItem, false); + // @ts-expect-error: Allow partial record in snapshot update for testing + handleActionButtonPress(searchHash, updatedMockReportItem, goToItem, false, snapshotReport, snapshotPolicy, mockLastPaymentMethod); expect(goToItem).toHaveBeenCalledTimes(0); }); test('Should run goToItem callback when user is in mobile selection mode', () => { const goToItem = jest.fn(() => {}); - handleActionButtonPress(searchHash, updatedMockReportItem, goToItem, true); + // @ts-expect-error: Allow partial record in snapshot update for testing + handleActionButtonPress(searchHash, updatedMockReportItem, goToItem, true, snapshotReport, snapshotPolicy, mockLastPaymentMethod); expect(goToItem).toHaveBeenCalledTimes(1); }); });