diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 0d160972372e..a7adf620070a 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -159,7 +159,9 @@ import { isExpenseReport, isGroupChat as isGroupChatReportUtils, isHiddenForCurrentUser, + isInvoiceReport, isIOUReportUsingReport, + isMoneyRequestReport, isOpenExpenseReport, isProcessingReport, isReportManuallyReimbursed, @@ -5496,7 +5498,20 @@ function resolveActionableMentionWhisper( // When the action belongs to a child report (e.g. a one-transaction thread), also update // the parent report's participants so the members list the user is viewing updates immediately. - const parentInviteData = isInviteResolution && parentReport?.reportID && parentReport.reportID !== reportID ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined; + // When parentReport is the same as the current report (e.g. viewing a transaction thread directly), + // fall back to the report's parentReportID to find the actual ancestor (IOU/expense/invoice report). + const isParentReportDifferent = !!parentReport?.reportID && parentReport.reportID !== reportID; + let parentInviteData = isInviteResolution && isParentReportDifferent ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined; + if (!parentInviteData && isInviteResolution && report?.parentReportID && report.parentReportID !== reportID) { + const ancestorReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + if (ancestorReport && (isMoneyRequestReport(ancestorReport) || isInvoiceReport(ancestorReport))) { + parentInviteData = buildParticipantsInviteData(ancestorReport, inviteeAccountIDs); + } + } + let parentReportIDForUpdate: string | undefined; + if (parentInviteData) { + parentReportIDForUpdate = isParentReportDifferent ? parentReport.reportID : report?.parentReportID; + } const parentParticipantsOptimisticData = parentInviteData?.optimistic; const parentParticipantsFailureData = parentInviteData?.failure; @@ -5523,10 +5538,10 @@ function resolveActionableMentionWhisper( }, ]; - if (parentParticipantsOptimisticData && parentReport?.reportID) { + if (parentParticipantsOptimisticData && parentReportIDForUpdate) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`, value: parentParticipantsOptimisticData, }); } @@ -5554,10 +5569,10 @@ function resolveActionableMentionWhisper( }, ]; - if (parentParticipantsFailureData && parentReport?.reportID) { + if (parentParticipantsFailureData && parentReportIDForUpdate) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`, value: parentParticipantsFailureData, }); } diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index c6cf26b29e6f..ead0698ffbd6 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -7440,6 +7440,85 @@ describe('actions/Report', () => { }); }); + it('should fall back to ancestor report via parentReportID when parentReport matches current report', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TRANSACTION_THREAD_ID = '20'; + const ANCESTOR_IOU_REPORT_ID = '21'; + const WHISPER_ACTION_ID = '20001'; + const EXISTING_PARTICIPANT_ID = 100; + const INVITEE_ACCOUNT_ID = 200; + + // Transaction thread report — parentReportID points to the IOU ancestor + const transactionThreadReport: OnyxTypes.Report = { + ...createRandomReport(20, undefined), + reportID: TRANSACTION_THREAD_ID, + parentReportID: ANCESTOR_IOU_REPORT_ID, + participants: { + [EXISTING_PARTICIPANT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.ADMIN, + }, + }, + lastMessageText: 'Receipt', + lastVisibleActionCreated: '2024-11-19 08:04:13.728', + lastActorAccountID: EXISTING_PARTICIPANT_ID, + }; + + // Ancestor IOU report (money request report) + const ancestorIOUReport: OnyxTypes.Report = { + ...createRandomReport(21, undefined), + reportID: ANCESTOR_IOU_REPORT_ID, + type: CONST.REPORT.TYPE.IOU, + participants: { + [EXISTING_PARTICIPANT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.ADMIN, + }, + }, + }; + + const whisperAction = { + reportActionID: WHISPER_ACTION_ID, + reportID: TRANSACTION_THREAD_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER, + created: '2024-11-19 08:04:13.730', + message: [{html: 'Mentioned @user1', text: 'Mentioned @user1', type: 'COMMENT'}], + originalMessage: { + inviteeAccountIDs: [INVITEE_ACCOUNT_ID], + inviteeEmails: ['user1@example.com'], + whisperedTo: [EXISTING_PARTICIPANT_ID], + }, + } as unknown as OnyxTypes.ReportAction; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}`, transactionThreadReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}`, ancestorIOUReport); + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${TRANSACTION_THREAD_ID}`]: { + [WHISPER_ACTION_ID]: whisperAction, + }, + }); + await waitForBatchedUpdates(); + + // Pass parentReport as the same report (simulating viewing transaction thread directly) + Report.resolveActionableMentionWhisper(transactionThreadReport, whisperAction, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, false, transactionThreadReport); + await waitForBatchedUpdates(); + + // Verify the invitee was added to the transaction thread participants + const updatedThreadReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}` as const); + expect(updatedThreadReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({ + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.MEMBER, + }); + + // Verify the invitee was also added to the ancestor IOU report via the fallback path + const updatedAncestorReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}` as const); + expect(updatedAncestorReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({ + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.MEMBER, + }); + }); + it('should remove optimistically added participants on failure rollback', async () => { const mockFetch = TestHelper.getGlobalFetchMock() as MockFetch; global.fetch = mockFetch;