Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/MoneyRequestHeaderSecondaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import initSplitExpense from '@libs/actions/SplitExpenses';
import {setNameValuePair} from '@libs/actions/User';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getExistingTransactionID} from '@libs/IOUUtils';
import isSearchOriginForMerge from '@libs/Navigation/helpers/isSearchOriginForMerge';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
Expand Down Expand Up @@ -342,7 +343,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
if (!transaction) {
return;
}
const isOnSearch = route.name.toLowerCase().startsWith('search');
const isOnSearch = isSearchOriginForMerge(route.name, route.params?.backTo);
setupMergeTransactionDataAndNavigate(transaction.transactionID, [transaction], localeCompare, getCurrencyDecimals, [], false, isOnSearch);
},
},
Expand Down
2 changes: 2 additions & 0 deletions src/components/MoneyRequestReportView/SelectionToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {dismissRejectUseExplanation} from '@libs/actions/IOU/RejectMoneyRequest';
import {queueExportSearchWithTemplate} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
Expand Down Expand Up @@ -157,6 +158,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
onExportOffline: () => setOfflineModalVisible(true),
policy,
beginExportWithTemplate: (templateName, templateType, transactionIDList) => beginExportWithTemplate(templateName, templateType, transactionIDList),
isOnSearch: isSearchTopmostFullScreenRoute(),
onDeleteSelected,
});

Expand Down
16 changes: 15 additions & 1 deletion src/hooks/useAllTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,24 @@ function useAllTransactions() {
{} as Record<string, OnyxEntry<Transaction>>,
);

return {
const mergedTransactions = {
...filteredSearchTransactions,
...allTransactionsCollection,
};

for (const [key, snapshotTransaction] of Object.entries(filteredSearchTransactions)) {
const onyxTransaction = mergedTransactions[key];
if (!snapshotTransaction?.transactionThreadReportID || !onyxTransaction || onyxTransaction.transactionThreadReportID) {
continue;
}

mergedTransactions[key] = {
...onyxTransaction,
transactionThreadReportID: snapshotTransaction.transactionThreadReportID,
};
}

return mergedTransactions;
}, [currentSearchResults?.data, allTransactionsCollection]);

return allTransactions;
Expand Down
61 changes: 58 additions & 3 deletions src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {useSearchActionsContext, useSearchStateContext} from '@components/Search
import type {BulkPaySelectionData, PaymentData, SearchQueryJSON} from '@components/Search/types';
import {unholdRequest} from '@libs/actions/IOU/Hold';
import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction';
import {deleteAppReport, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
import type {TargetTransactionThreadReportCandidate} from '@libs/actions/MergeTransaction';
import {createTransactionThreadReport, deleteAppReport, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
import {
approveMoneyRequestOnSearch,
bulkDeleteReports,
Expand All @@ -32,7 +33,7 @@ import {
} from '@libs/actions/Search';
import initSplitExpense from '@libs/actions/SplitExpenses';
import {setNameValuePair} from '@libs/actions/User';
import {getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils';
import {getTargetTransactionThreadReportIDForSelection, getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getConnectedIntegration} from '@libs/PolicyUtils';
import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils';
Expand Down Expand Up @@ -224,10 +225,12 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
Comment thread
marufsharifi marked this conversation as resolved.
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
const {isBetaEnabled} = usePermissions();
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
const [betas] = useOnyx(ONYXKEYS.BETAS);

const defaultExpensePolicy = useDefaultExpensePolicy();
const undeleteTransactions = useUndeleteTransactions();
Expand Down Expand Up @@ -1331,7 +1334,57 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
text: translate('common.merge'),
icon: expensifyIcons.ArrowCollapse,
value: CONST.SEARCH.BULK_ACTION_TYPES.MERGE,
onSelected: () => setupMergeTransactionDataAndNavigate(transactionID, searchedTransactions, localeCompare, getCurrencyDecimals, reports, false, true),
onSelected: () => {
const transactionKey: `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}` = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
const searchSnapshotTransaction = currentSearchResults?.data?.[transactionKey];
const selectedTransactionInfo = selectedTransactions[transactionID];
const isSingleSelection = selectedTransactionsKeys.length === 1;
let targetTransactionThreadReportIDOverride = isSingleSelection
? getTargetTransactionThreadReportIDForSelection(searchSnapshotTransaction ?? searchedTransactions.at(0), selectedTransactionInfo)
: undefined;

if (!targetTransactionThreadReportIDOverride && isSingleSelection) {
const selectedReport = selectedTransactionInfo?.report ?? reports.at(0) ?? getReportOrDraftReport(searchSnapshotTransaction?.reportID);
const selectedReportAction = selectedTransactionInfo?.reportAction;
const targetTransaction = searchSnapshotTransaction ?? searchedTransactions.at(0);
const shouldPassTransactionData = !selectedReportAction?.reportActionID || targetTransaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const transactionViolations = targetTransaction
? allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${targetTransaction.transactionID}`]
: undefined;
const createdThreadReport = targetTransaction
? createTransactionThreadReport(
introSelected,
currentUserPersonalDetails.login ?? '',
currentUserPersonalDetails.accountID,
betas,
selectedReport,
selectedReportAction,
shouldPassTransactionData ? targetTransaction : undefined,
shouldPassTransactionData ? transactionViolations : undefined,
)
: undefined;
targetTransactionThreadReportIDOverride = createdThreadReport?.reportID;
}

const targetTransactionThreadReportCandidate: TargetTransactionThreadReportCandidate | undefined = targetTransactionThreadReportIDOverride
? {
transactionID,
threadReportID: targetTransactionThreadReportIDOverride,
}
: undefined;

setupMergeTransactionDataAndNavigate(
transactionID,
searchedTransactions,
localeCompare,
getCurrencyDecimals,
reports,
false,
true,
undefined,
targetTransactionThreadReportCandidate,
);
},
});
}
}
Expand Down Expand Up @@ -1502,6 +1555,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
invokeDuplicateReportHandler,
isExpenseReportType,
handleDeleteSelectedTransactions,
introSelected,
betas,
undeleteTransactions,
currentUserPersonalDetails?.email,
theme.icon,
Expand Down
48 changes: 43 additions & 5 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {useSearchActionsContext, useSearchStateContext} from '@components/Search
import {initBulkEditDraftTransaction} from '@libs/actions/IOU/BulkEdit';
import {unholdRequest} from '@libs/actions/IOU/Hold';
import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction';
import {exportReportToCSV} from '@libs/actions/Report';
import type {TargetTransactionThreadReportCandidate} from '@libs/actions/MergeTransaction';
import {createTransactionThreadReport, exportReportToCSV} from '@libs/actions/Report';
import {getExportTemplates, handlePreventSearchAPI} from '@libs/actions/Search';
import initSplitExpense from '@libs/actions/SplitExpenses';
import {getTargetTransactionThreadReportIDForSelection} from '@libs/MergeTransactionUtils';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {getIOUActionForTransactionID, getReportAction, isDeletedAction} from '@libs/ReportActionsUtils';
import {isMergeActionForSelectedTransactions, isSplitAction} from '@libs/ReportSecondaryActionUtils';
Expand Down Expand Up @@ -89,6 +91,8 @@ function useSelectedTransactionsActions({
const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES);
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [betas] = useOnyx(ONYXKEYS.BETAS);
Comment thread
marufsharifi marked this conversation as resolved.
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const {getCurrencyDecimals} = useCurrencyListActions();

Expand Down Expand Up @@ -419,13 +423,45 @@ function useSelectedTransactionsActions({

const canMergeTransaction = selectedTransactionsList.length < 3 && report && policy && isMergeActionForSelectedTransactions(selectedTransactionsList, [report], [policy]);
if (canMergeTransaction) {
const transactionID = selectedTransactionsList.at(0)?.transactionID;
if (transactionID) {
const selectedTransaction = selectedTransactionsList.at(0);
const transactionID = selectedTransaction?.transactionID;
if (transactionID && selectedTransaction) {
options.push({
text: translate('common.merge'),
icon: expensifyIcons.ArrowCollapse,
value: MERGE,
onSelected: () =>
onSelected: () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-3 (docs)

The fallback chain for resolving targetTransactionThreadReportIDOverride (lines 432-468) duplicates the same pattern found in useSearchBulkActions.ts (lines 1337-1380) and partially in MergeTransactionUtils.getTargetTransactionThreadReportIDForSearchSelection. All three locations implement the same cascading lookup: check reportAction.childReportID, then transaction.transactionThreadReportID, then fall back to computing/creating a thread report.

Consider extracting the common resolution logic into a single shared utility (e.g., extending getTargetTransactionThreadReportIDForSearchSelection or creating a new resolveTargetTransactionThreadReportID function) that both hooks can call, passing in the necessary data sources as parameters. The createTransactionThreadReport fallback could be included as an optional callback or final step in the shared utility.


Reviewed at: b341560 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const isSingleSelection = selectedTransactionsList.length === 1;
let targetTransactionThreadReportIDOverride: string | undefined;
const iouReportAction = isSingleSelection ? getIOUActionForTransactionID(reportActions, transactionID) : undefined;

if (isSingleSelection) {
const selectedTransactionMeta = selectedTransactionsMeta?.[transactionID];
targetTransactionThreadReportIDOverride = getTargetTransactionThreadReportIDForSelection(selectedTransaction, selectedTransactionMeta, iouReportAction);

if (!targetTransactionThreadReportIDOverride) {
const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
const createdThreadReport = createTransactionThreadReport(
introSelected,
login ?? '',
currentUserAccountID,
betas,
report,
iouReportAction,
selectedTransaction,
transactionViolations,
);
targetTransactionThreadReportIDOverride = createdThreadReport?.reportID;
}
}

const targetTransactionThreadReportCandidate: TargetTransactionThreadReportCandidate | undefined = targetTransactionThreadReportIDOverride
? {
transactionID,
threadReportID: targetTransactionThreadReportIDOverride,
}
: undefined;

setupMergeTransactionDataAndNavigate(
transactionID,
selectedTransactionsList,
Expand All @@ -435,7 +471,9 @@ function useSelectedTransactionsActions({
false,
isOnSearch,
selectedTransactionsList.length > 1 ? [policy, policy] : undefined,
),
targetTransactionThreadReportCandidate,
);
},
});
}
}
Expand Down
46 changes: 45 additions & 1 deletion src/libs/MergeTransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {MergeTransaction, Policy, Report, SearchResults, Transaction} from '@src/types/onyx';
import type {MergeTransaction, Policy, Report, ReportAction, SearchResults, Transaction} from '@src/types/onyx';
import type {Attendee} from '@src/types/onyx/IOU';
import SafeString from '@src/utils/SafeString';
import {convertToBackendAmount, convertToDisplayString} from './CurrencyUtils';
Expand Down Expand Up @@ -58,6 +58,11 @@ type MergeFieldData = {

/** Type for merge transaction values that can be null to clear existing values in Onyx */
type MergeTransactionUpdateValues = Partial<Record<keyof MergeTransaction, MergeTransaction[keyof MergeTransaction] | null>>;
type TargetTransactionThreadReportIDSource = {
transaction?: OnyxEntry<Transaction>;
reportAction?: OnyxEntry<ReportAction>;
[key: string]: unknown;
};

const MERGE_FIELD_TRANSLATION_KEYS = {
amount: 'iou.amount',
Expand Down Expand Up @@ -352,6 +357,43 @@ function getTransactionThreadReportID(transaction: OnyxEntry<Transaction>) {
return iouActionOfTargetTransaction?.childReportID;
}

function isValidTargetTransactionThreadReportID(reportID: string | undefined) {
return !!reportID && reportID !== CONST.FAKE_REPORT_ID;
}

function getTargetTransactionThreadReportIDForSelection(
transaction: OnyxEntry<Transaction>,
selectedTransaction?: TargetTransactionThreadReportIDSource,
fallbackReportAction?: OnyxEntry<ReportAction>,
) {
const selectedChildReportID = selectedTransaction?.reportAction?.childReportID;
if (isValidTargetTransactionThreadReportID(selectedChildReportID)) {
return selectedChildReportID;
}

const selectedTransactionThreadReportID = selectedTransaction?.transaction?.transactionThreadReportID;
if (isValidTargetTransactionThreadReportID(selectedTransactionThreadReportID)) {
return selectedTransactionThreadReportID;
}

const transactionThreadReportID = transaction?.transactionThreadReportID;
if (isValidTargetTransactionThreadReportID(transactionThreadReportID)) {
return transactionThreadReportID;
}

const fallbackChildReportID = fallbackReportAction?.childReportID;
if (isValidTargetTransactionThreadReportID(fallbackChildReportID)) {
return fallbackChildReportID;
}

const computedThreadReportID = getTransactionThreadReportID(transaction);
return isValidTargetTransactionThreadReportID(computedThreadReportID) ? computedThreadReportID : undefined;
}

function getTargetTransactionThreadReportIDForSearchSelection(transaction: OnyxEntry<Transaction>, selectedTransaction?: TargetTransactionThreadReportIDSource) {
return getTargetTransactionThreadReportIDForSelection(transaction, selectedTransaction);
}

/**
* Build the merged transaction data for display by combining target transaction with merge transaction updates
* @param targetTransaction - The target transaction to merge into
Expand Down Expand Up @@ -694,6 +736,8 @@ export {
areTransactionsEligibleForMerge,
DERIVED_MERGE_FIELDS,
getRateFromMerchant,
getTargetTransactionThreadReportIDForSelection,
getTargetTransactionThreadReportIDForSearchSelection,
getTransactionsAndReportsFromSearch,
};

Expand Down
34 changes: 34 additions & 0 deletions src/libs/Navigation/helpers/isSearchOriginForMerge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import isSearchTopmostFullScreenRoute from './isSearchTopmostFullScreenRoute';

function normalizeRoute(route?: string): string | undefined {
if (!route) {
return undefined;
}

const decodedRoute = (() => {
try {
return decodeURIComponent(route);
} catch {
return route;
}
})();

return decodedRoute.startsWith('/') ? decodedRoute.substring(1) : decodedRoute;
}

function isSearchOriginForMerge(routeName: string, backTo?: string): boolean {
if (routeName !== SCREENS.RIGHT_MODAL.SEARCH_REPORT) {
return false;
Comment thread
marufsharifi marked this conversation as resolved.
}

const normalizedBackTo = normalizeRoute(backTo);
if (normalizedBackTo) {
return normalizedBackTo.startsWith(ROUTES.SEARCH_ROOT.route);
}

return isSearchTopmostFullScreenRoute();
}

export default isSearchOriginForMerge;
Loading
Loading