diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts index e3e600a4e367..27e1032d82a9 100644 --- a/src/libs/API/parameters/RequestMoneyParams.ts +++ b/src/libs/API/parameters/RequestMoneyParams.ts @@ -28,6 +28,7 @@ type RequestMoneyParams = { transactionThreadReportID: string; createdReportActionIDForThread: string; reimbursible?: boolean; + policyID?: string; }; export default RequestMoneyParams; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5ae0fca0a68e..fb1b00538736 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -40,6 +40,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; +import {createDraftReportForPolicyExpenseChat} from './actions/Report'; import Timing from './actions/Timing'; import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; @@ -61,6 +62,7 @@ import * as UserUtils from './UserUtils'; type SearchOption = ReportUtils.OptionData & { item: T; + isOptimisticReportOption?: boolean; }; type OptionList = { @@ -179,6 +181,7 @@ type GetOptionsConfig = { includeDomainEmail?: boolean; action?: IOUAction; shouldBoldTitleByDefault?: boolean; + includePoliciesWithoutExpenseChats?: boolean; }; type GetUserToInviteConfig = { @@ -240,6 +243,13 @@ Onyx.connect({ }, }); +let allReportsDraft: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_DRAFT, + waitForCollectionCallback: true, + callback: (value) => (allReportsDraft = value), +}); + let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, @@ -1499,6 +1509,7 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { const reportMapForAccountIDs: Record = {}; const allReportOptions: Array> = []; + const policyToReportForPolicyExpenseChats: Record = {}; if (reports) { Object.values(reports).forEach((report) => { @@ -1514,6 +1525,10 @@ function createOptionList(personalDetails: OnyxEntry, repor return; } + if (ReportUtils.isPolicyExpenseChat(report) && report.policyID) { + policyToReportForPolicyExpenseChats[report.policyID] = report; + } + // Save the report in the map if this is a single participant so we can associate the reportID with the // personal detail option later. Individuals should not be associated with single participant // policyExpenseChats or chatRooms since those are not people. @@ -1528,6 +1543,46 @@ function createOptionList(personalDetails: OnyxEntry, repor }); } + const policiesWithoutExpenseChats = Object.values(policies ?? {}).filter((policy) => { + if (policy?.type === CONST.POLICY.TYPE.PERSONAL) { + return false; + } + return !policyToReportForPolicyExpenseChats[policy?.id ?? '']; + }); + + // go through each policy and create a optimistic report option for it + if (policiesWithoutExpenseChats && policiesWithoutExpenseChats.length > 0) { + policiesWithoutExpenseChats.forEach((policy) => { + // check for draft report exist in allreportDrafts for the policy + let draftReport = Object.values(allReportsDraft ?? {})?.find((reportDraft) => reportDraft?.policyID === policy?.id); + if (!draftReport) { + draftReport = ReportUtils.buildOptimisticChatReport( + [currentUserAccountID ?? -1], + '', + CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policy?.id, + currentUserAccountID, + true, + policy?.name, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + createDraftReportForPolicyExpenseChat({...draftReport, isOptimisticReport: true}); + } + const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(draftReport); + allReportOptions.push({ + item: draftReport, + isOptimisticReportOption: true, + ...createOption(accountIDs, personalDetails, draftReport, {}), + }); + }); + } const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ item: personalDetail, ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), @@ -1723,6 +1778,7 @@ function getOptions( includeDomainEmail = false, action, shouldBoldTitleByDefault = true, + includePoliciesWithoutExpenseChats = false, }: GetOptionsConfig, ): Options { if (includeCategories) { @@ -1787,6 +1843,9 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { + if (option.isOptimisticReportOption && !includePoliciesWithoutExpenseChats) { + return; + } const report = option.item; const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations); @@ -2136,6 +2195,7 @@ type FilteredOptionsParams = { includeInvoiceRooms?: boolean; action?: IOUAction; sortByReportTypeInSearch?: boolean; + includePoliciesWithoutExpenseChats?: boolean; }; // It is not recommended to pass a search value to getFilteredOptions when passing reports and personalDetails. @@ -2177,6 +2237,7 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue includeInvoiceRooms = false, action, sortByReportTypeInSearch = false, + includePoliciesWithoutExpenseChats = false, } = params; return getOptions( {reports, personalDetails}, @@ -2206,6 +2267,7 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue includeInvoiceRooms, action, sortByReportTypeInSearch, + includePoliciesWithoutExpenseChats, }, ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4bae619d928e..1ef60b626ac7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -300,6 +300,7 @@ type OptimisticChatReport = Pick< | 'chatReportID' | 'iouReportID' | 'isOwnPolicyExpenseChat' + | 'isPolicyExpenseChat' | 'isPinned' | 'lastActorAccountID' | 'lastMessageTranslationKey' @@ -5322,6 +5323,7 @@ function buildOptimisticChatReport( chatType, isOwnPolicyExpenseChat, isPinned: isNewlyCreatedWorkspaceChat, + isPolicyExpenseChat: chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, lastActorAccountID: 0, lastMessageTranslationKey: '', lastMessageHtml: '', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index fb8cd014ec7b..cd0d4d24f000 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3640,6 +3640,7 @@ function requestMoney( transactionThreadReportID, createdReportActionIDForThread, reimbursible, + policyID: policy?.id, }; // eslint-disable-next-line rulesdir/no-multiple-api-calls diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7071c96f8612..f0b0fd4adc5f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -79,7 +79,7 @@ import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; -import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; +import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; @@ -1368,6 +1368,7 @@ function handleReportChanged(report: OnyxEntry) { if (report?.reportID && report.preexistingReportID) { let callback = () => { Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, null); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, null); }; // Only re-route them if they are still looking at the optimistically created report @@ -4125,6 +4126,10 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); } +function createDraftReportForPolicyExpenseChat(draftReport: OptimisticChatReport) { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${draftReport.reportID}`, draftReport); +} + function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_REPORT_TO_CSV, { reportID, @@ -4232,4 +4237,5 @@ export { updateReportName, updateRoomVisibility, updateWriteCapability, + createDraftReportForPolicyExpenseChat, }; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 5dcf4dbd2ea6..b500574288e0 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -134,6 +134,7 @@ function MoneyRequestParticipantsSelector({ sortByReportTypeInSearch: isPaidGroupPolicy, searchValue: '', maxRecentReportsToShow: 0, + includePoliciesWithoutExpenseChats: true, }); return optionList; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index e8fc138f6a52..9d2123d232b0 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -55,13 +55,17 @@ function IOURequestStepConfirmation({ const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`); - const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); - const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); - const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); + const policyIDForReal = IOU.getIOURequestPolicyID(transaction, reportReal ?? reportDraft); + const policyIDForDraft = IOU.getIOURequestPolicyID(transaction, reportDraft); + + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyIDForReal}`); + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyIDForDraft}`); + const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyIDForReal}`); + const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyIDForDraft}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForReal}`); const report = reportReal ?? reportDraft; + // Check if the real policy exists for either reportReal or reportDraft const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 5a0cd6638a07..b74779e5046c 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -411,7 +411,8 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(9); // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length); + // - 1 because when we create the option list we also create a workspace chat optimistic options for the old oldot policies or polices with isPolicyExpenseChatEnabled: false, in this case we have a policy "Hero Policy" with isPolicyExpenseChatEnabled: false, More info: https://github.com/Expensify/App/issues/49344 + expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1); }); it('getFilteredOptions()', () => { @@ -608,8 +609,8 @@ describe('OptionsListUtils', () => { // When we pass an empty search value let results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], ''); - // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1); + // Then we should expect all the recent reports to show but exclude the archived rooms and workspace policy chat report + expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 2); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options const filteredReportsWithWorkspaceRooms = Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).reduce((filtered, option) => { @@ -624,8 +625,8 @@ describe('OptionsListUtils', () => { // When we also have a policy to return rooms in the results results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show - // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 1); + // We should expect all the recent reports to show, excluding the archived rooms and workspace policy chat report + expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 2); }); it('getMemberInviteOptions()', () => {