diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 6e55164421d7c..7c9ba04df00cf 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1173,7 +1173,7 @@ function setWorkspaceReimbursement({ API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData}); } -function leaveWorkspace(policyID?: string) { +function leaveWorkspace(currentUserAccountID: number, policyID?: string) { if (!policyID) { return; } @@ -1197,14 +1197,16 @@ function leaveWorkspace(policyID?: string) { }, ]; - const successData: Array> = [ + const successData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: null, }, ]; - const failureData: Array> = [ + const failureData: Array< + OnyxUpdate + > = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, @@ -1223,62 +1225,95 @@ function leaveWorkspace(policyID?: string) { const pendingChatMembers = ReportUtils.getPendingChatMembers([deprecatedSessionAccountID], [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); for (const report of workspaceChats) { + if (!report?.reportID) { + continue; + } + const parentReport = ReportUtils.getRootParentReport({report}); const reportToCheckOwner = isEmptyObject(parentReport) ? report : parentReport; if (ReportUtils.isPolicyExpenseChat(report) && !ReportUtils.isReportOwner(reportToCheckOwner)) { - continue; - } - - optimisticData.push( - { + // Use merge instead of set to avoid deleting the report too quickly, which could cause a brief "not found" page to appear. + // The remaining parts of the report object will be removed after the API call is successful. + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, value: { - statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + reportID: null, stateNum: CONST.REPORT.STATE_NUM.APPROVED, - oldPolicyName: policy?.name ?? '', - isPinned: false, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [currentUserAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, }, - }, - { + }); + + // Ensure that any remaining data is removed upon successful completion, even if the server sends a report removal response. + // This is done to prevent the removal update from lingering in the applyHTTPSOnyxUpdates function. + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, + value: null, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, + value: report, + }); + } else { + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, + value: { + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + oldPolicyName: policy?.name ?? '', + isPinned: false, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, + value: { + pendingChatMembers, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, + value: { + private_isArchived: currentTime, + }, + }, + ); + + // Restore archived flag on failure + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, + value: { + private_isArchived: null, + }, + }); + successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, value: { - pendingChatMembers, + pendingChatMembers: null, }, - }, - { + }); + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, value: { - private_isArchived: currentTime, + pendingChatMembers: null, }, - }, - ); - - // Restore archived flag on failure - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, - value: { - private_isArchived: null, - }, - }); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, - value: { - pendingChatMembers: null, - }, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, - value: { - pendingChatMembers: null, - }, - }); + }); + } } const params: LeavePolicyParams = { diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 519575157a741..8a2f9289125fe 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -279,10 +279,10 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa return; } - leaveWorkspace(policyID); + leaveWorkspace(currentUserPersonalDetails.accountID, policyID); setIsLeaveModalOpen(false); goBackFromInvalidPolicy(); - }, [policyID]); + }, [currentUserPersonalDetails.accountID, policyID]); const hideDeleteWorkspaceErrorModal = () => { setIsDeleteWorkspaceErrorModalOpen(false); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 74c7b9ad84810..7161260b1ad43 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -251,7 +251,7 @@ function WorkspacesListPage() { return; } - leaveWorkspace(policyIDToLeave); + leaveWorkspace(currentUserPersonalDetails.accountID, policyIDToLeave); setIsLeaveModalOpen(false); }; diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 376ad27d6b1fb..b8e46cec357a5 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -819,6 +819,132 @@ describe('actions/Policy', () => { }); }); + describe('leaveWorkspace', () => { + it("should remove all non-owned workspace chats and keep the user's own workspace chat when leaving a workspace", async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + const policyID = Policy.generatePolicyID(); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + id: policyID, + name: WORKSPACE_NAME, + }); + await waitForBatchedUpdates(); + + const ownWorkspaceChat: Report = { + ...createRandomReport(100, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: '100', + policyID, + ownerAccountID: ESH_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }; + const nonOwnedWorkspaceChat1: Report = { + ...createRandomReport(101, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: '101', + policyID, + ownerAccountID: ESH_ACCOUNT_ID + 1, + type: CONST.REPORT.TYPE.CHAT, + }; + const nonOwnedWorkspaceChat2: Report = { + ...createRandomReport(102, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: '102', + policyID, + ownerAccountID: ESH_ACCOUNT_ID + 2, + type: CONST.REPORT.TYPE.CHAT, + }; + const nonOwnedWorkspaceChats = [nonOwnedWorkspaceChat1, nonOwnedWorkspaceChat2]; + + const getAllWorkspaceReportsSpy = jest.spyOn(ReportUtils, 'getAllWorkspaceReports').mockReturnValue([ownWorkspaceChat, ...nonOwnedWorkspaceChats]); + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + Policy.leaveWorkspace(ESH_ACCOUNT_ID, policyID); + await waitForBatchedUpdates(); + + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.LEAVE_POLICY, + expect.objectContaining({ + policyID, + email: ESH_EMAIL, + }), + expect.anything(), + ); + + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { + optimisticData?: Array<{key?: string; value?: Record | null}>; + successData?: Array<{key?: string; value?: Record | null}>; + failureData?: Array<{key?: string; value?: Record | null}>; + }; + + expect(writeOptions?.optimisticData).toEqual( + expect.arrayContaining( + nonOwnedWorkspaceChats.map((workspaceChat) => + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChat.reportID}`, + value: expect.objectContaining({ + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [ESH_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }), + }), + ), + ), + ); + + const removedWorkspaceChatUpdates = (writeOptions?.optimisticData ?? []).filter((update) => (update.value as {reportID?: string | null} | undefined)?.reportID === null); + expect(removedWorkspaceChatUpdates).toHaveLength(nonOwnedWorkspaceChats.length); + + expect(writeOptions?.optimisticData).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${ownWorkspaceChat.reportID}`, + value: expect.objectContaining({ + reportID: null, + }), + }), + ]), + ); + + expect(writeOptions?.optimisticData).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${ownWorkspaceChat.reportID}`, + value: expect.objectContaining({ + private_isArchived: expect.any(String) as unknown as string, + }), + }), + ]), + ); + + expect(writeOptions?.successData).toEqual( + expect.arrayContaining( + nonOwnedWorkspaceChats.map((workspaceChat) => + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChat.reportID}`, + value: null, + }), + ), + ), + ); + expect(writeOptions?.failureData).toEqual( + expect.arrayContaining( + nonOwnedWorkspaceChats.map((workspaceChat) => + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChat.reportID}`, + value: workspaceChat, + }), + ), + ), + ); + + apiWriteSpy.mockRestore(); + getAllWorkspaceReportsSpy.mockRestore(); + }); + }); + describe('createDraftInitialWorkspace', () => { it('creates a policy draft with disabled workflows when onboarding choice does not enable workflows', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});