diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 88818c3eb7c6..804c8dadd553 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -322,6 +322,7 @@ const ONYXKEYS = { POLICY_DRAFTS: 'policyDrafts_', POLICY_JOIN_MEMBER: 'policyJoinMember_', POLICY_CATEGORIES: 'policyCategories_', + POLICY_CATEGORIES_DRAFT: 'policyCategoriesDraft_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_', @@ -335,6 +336,7 @@ const ONYXKEYS = { WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', + REPORT_DRAFT: 'reportDraft_', // REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions). // A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state // directly on the report caused a lot of unnecessary re-renders @@ -538,6 +540,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; + [ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTagList; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED]: boolean; @@ -545,6 +548,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; + [ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index f26d7c25c7e2..9484cff26005 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -14,6 +14,7 @@ import type {ListItem} from './SelectionList/types'; type CategoryPickerOnyxProps = { policyCategories: OnyxEntry; + policyCategoriesDraft: OnyxEntry; policyRecentlyUsedCategories: OnyxEntry; }; @@ -25,7 +26,7 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { onSubmit: (item: ListItem) => void; }; -function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}: CategoryPickerProps) { +function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, policyCategoriesDraft, onSubmit}: CategoryPickerProps) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); @@ -45,7 +46,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [selectedCategory]); const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { - const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p)); + const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter?.((p) => !isEmptyObject(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( [], [], @@ -56,7 +57,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC false, false, true, - policyCategories ?? {}, + policyCategories ?? policyCategoriesDraft ?? {}, validPolicyRecentlyUsedCategories, false, ); @@ -68,7 +69,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC const showInput = !isCategoriesCountBelowThreshold; return [categoryOptions, header, showInput]; - }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories]); + }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories, policyCategoriesDraft]); const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((category) => category.searchText === selectedCategory)[0]?.keyForList, [sections, selectedCategory]); @@ -93,6 +94,9 @@ export default withOnyx({ policyCategories: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, }, + policyCategoriesDraft: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, + }, policyRecentlyUsedCategories: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`, }, diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 4c5bb4d45318..567398663fc7 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -60,12 +60,18 @@ type MoneyRequestConfirmationListOnyxProps = { /** Collection of categories attached to a policy */ policyCategories: OnyxEntry; + /** Collection of draft categories attached to a policy */ + policyCategoriesDraft: OnyxEntry; + /** Collection of tags attached to a policy */ policyTags: OnyxEntry; /** The policy of the report */ policy: OnyxEntry; + /** The draft policy of the report */ + policyDraft: OnyxEntry; + /** The session of the logged in user */ session: OnyxEntry; @@ -193,10 +199,12 @@ function MoneyRequestConfirmationList({ iouType = CONST.IOU.TYPE.SUBMIT, isScanRequest = false, iouAmount, - policyCategories, + policyCategories: policyCategoriesReal, + policyCategoriesDraft, mileageRates, isDistanceRequest = false, - policy, + policy: policyReal, + policyDraft, isPolicyExpenseChat = false, iouCategory = '', shouldShowSmartScanFields = true, @@ -227,6 +235,8 @@ function MoneyRequestConfirmationList({ allPolicies, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { + const policy = policyReal ?? policyDraft; + const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -469,8 +479,8 @@ function MoneyRequestConfirmationList({ ); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + ...participant, })); sections.push({ title: translate('common.to'), @@ -1120,6 +1130,9 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, }, + policyCategoriesDraft: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, + }, policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, @@ -1134,6 +1147,9 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + policyDraft: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, + }, lastSelectedDistanceRates: { key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, }, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b599fdc6d0f1..f1afbb3ec14b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -788,6 +788,7 @@ function getReportOption(participant: Participant): ReportUtils.OptionData { option.text = ReportUtils.getPolicyName(report); option.alternateText = Localize.translateLocal('workspace.common.workspace'); } + option.isDisabled = ReportUtils.isDraftReport(participant.reportID); option.selected = participant.selected; option.isSelected = participant.selected; return option; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7da99d3ca8e6..7e663b1c56ff 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -521,6 +521,13 @@ Onyx.connect({ callback: (value) => (allReports = value), }); +let allReportsDraft: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_DRAFT, + waitForCollectionCallback: true, + callback: (value) => (allReportsDraft = value), +}); + let allPolicies: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -591,11 +598,23 @@ function getChatType(report: OnyxEntry | Participant | EmptyObject): Val * Get the report given a reportID */ function getReport(reportID: string | undefined): OnyxEntry { - if (!allReports) { + if (!allReports && !allReportsDraft) { return null; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; + + return report ?? draftReport ?? null; +} + +/** + * Check if a report is a draft report + */ +function isDraftReport(reportID: string | undefined): boolean { + const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; + + return !!draftReport; } /** @@ -905,8 +924,8 @@ function isGroupPolicy(policyType: string): boolean { /** * Whether the provided report belongs to a Free, Collect or Control policy */ -function isReportInGroupPolicy(report: OnyxEntry): boolean { - const policyType = getPolicyType(report, allPolicies); +function isReportInGroupPolicy(report: OnyxEntry, policy?: OnyxEntry): boolean { + const policyType = policy?.type ?? getPolicyType(report, allPolicies); return isGroupPolicy(policyType); } @@ -4626,7 +4645,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam }; } -function buildOptimisticWorkspaceChats(policyID: string, policyName: string): OptimisticWorkspaceChats { +function buildOptimisticWorkspaceChats(policyID: string, policyName: string, expenseReportId?: string): OptimisticWorkspaceChats { const announceChatData = buildOptimisticChatReport( currentUserAccountID ? [currentUserAccountID] : [], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, @@ -4665,7 +4684,23 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string): Op [adminsCreatedAction.reportActionID]: adminsCreatedAction, }; - const expenseChatData = buildOptimisticChatReport([currentUserAccountID ?? -1], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserAccountID, true, policyName); + const expenseChatData = buildOptimisticChatReport( + [currentUserAccountID ?? -1], + '', + CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID, + currentUserAccountID, + true, + policyName, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + expenseReportId, + ); const expenseChatReportID = expenseChatData.reportID; const expenseReportCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail ?? ''); const expenseReportActionData = { @@ -6390,7 +6425,7 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s } as Transaction); const filteredPolicies = Object.values(allPolicies ?? {}).filter( - (policy) => policy?.type !== CONST.POLICY.TYPE.PERSONAL && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + (policy) => policy && policy.type !== CONST.POLICY.TYPE.PERSONAL && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); if (actionName === CONST.IOU.ACTION.SUBMIT || (allPolicies && filteredPolicies.length > 0)) { @@ -6398,7 +6433,7 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s return; } - const {expenseChatReportID, policyID, policyName} = PolicyActions.createWorkspace(); + const {expenseChatReportID, policyID, policyName} = PolicyActions.createDraftWorkspace(); const isCategorizing = actionName === CONST.IOU.ACTION.CATEGORIZE; IOU.setMoneyRequestParticipants(transactionID, [ @@ -6712,6 +6747,7 @@ export { getInvoiceChatByParticipants, shouldShowMerchantColumn, isCurrentUserInvoiceReceiver, + isDraftReport, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e0b406ad9c45..a74495008fd8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10,6 +10,7 @@ import type { ApproveMoneyRequestParams, CompleteSplitBillParams, CreateDistanceRequestParams, + CreateWorkspaceParams, DeleteMoneyRequestParams, DetachReceiptParams, EditMoneyRequestParams, @@ -82,6 +83,7 @@ type MoneyRequestInformation = { }; type TrackExpenseInformation = { + createdWorkspaceParams?: CreateWorkspaceParams; iouReport?: OnyxTypes.Report; chatReport: OnyxTypes.Report; transaction: OnyxTypes.Transaction; @@ -1113,6 +1115,9 @@ function buildOnyxDataForTrackExpense( const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + let newQuickAction: ValueOf = CONST.QUICK_ACTIONS.TRACK_MANUAL; if (isScanRequest) { newQuickAction = CONST.QUICK_ACTIONS.TRACK_SCAN; @@ -1226,8 +1231,6 @@ function buildOnyxDataForTrackExpense( }); } - const successData: OnyxUpdate[] = []; - if (iouReport) { successData.push( { @@ -1310,8 +1313,6 @@ function buildOnyxDataForTrackExpense( }); } - const failureData: OnyxUpdate[] = []; - failureData.push({ onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, @@ -1968,6 +1969,10 @@ function getTrackExpenseInformation( linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction, existingTransactionID?: string, ): TrackExpenseInformation | EmptyObject { + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + const isPolicyExpenseChat = participant.isPolicyExpenseChat; // STEP 1: Get existing chat report @@ -1984,6 +1989,19 @@ function getTrackExpenseInformation( return {}; } + // Check if the report is a draft + const isDraftReport = ReportUtils.isDraftReport(chatReport?.reportID); + + let createdWorkspaceParams: CreateWorkspaceParams | undefined; + + if (isDraftReport) { + const workspaceData = Policy.buildPolicyData(undefined, policy?.makeMeAdmin, policy?.name, policy?.id, chatReport?.reportID); + createdWorkspaceParams = workspaceData.params; + optimisticData.push(...workspaceData.optimisticData); + successData.push(...workspaceData.successData); + failureData.push(...workspaceData.failureData); + } + // STEP 2: If not in the self-DM flow, we need to use the expense report. // For this, first use the chatReport.iouReportID property. Build a new optimistic expense report if needed. const shouldUseMoneyReport = !!isPolicyExpenseChat; @@ -2091,7 +2109,7 @@ function getTrackExpenseInformation( } // STEP 5: Build Onyx Data - const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( + const trackExpenseOnyxData = buildOnyxDataForTrackExpense( chatReport, iouReport, optimisticTransaction, @@ -2107,6 +2125,7 @@ function getTrackExpenseInformation( ); return { + createdWorkspaceParams, chatReport, iouReport: iouReport ?? undefined, transaction: optimisticTransaction, @@ -2117,9 +2136,9 @@ function getTrackExpenseInformation( transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, onyxData: { - optimisticData, - successData, - failureData, + optimisticData: [...optimisticData, ...trackExpenseOnyxData[0]], + successData: [...successData, ...trackExpenseOnyxData[1]], + failureData: [...failureData, ...trackExpenseOnyxData[2]], }, }; } @@ -3076,6 +3095,7 @@ function categorizeTrackedExpense( taxAmount = 0, billable?: boolean, receipt?: Receipt, + createdWorkspaceParams?: CreateWorkspaceParams, ) { const {optimisticData, successData, failureData} = onyxData; @@ -3118,6 +3138,12 @@ function categorizeTrackedExpense( billable, created, receipt, + policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID, + policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID, + announceChatReportID: createdWorkspaceParams?.announceChatReportID, + announceCreatedReportActionID: createdWorkspaceParams?.announceCreatedReportActionID, + adminsChatReportID: createdWorkspaceParams?.adminsChatReportID, + adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID, }; API.write(WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData}); @@ -3146,6 +3172,7 @@ function shareTrackedExpense( taxAmount = 0, billable?: boolean, receipt?: Receipt, + createdWorkspaceParams?: CreateWorkspaceParams, ) { const {optimisticData, successData, failureData} = onyxData; @@ -3188,6 +3215,12 @@ function shareTrackedExpense( taxAmount, billable, receipt, + policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID, + policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID, + announceChatReportID: createdWorkspaceParams?.announceChatReportID, + announceCreatedReportActionID: createdWorkspaceParams?.announceCreatedReportActionID, + adminsChatReportID: createdWorkspaceParams?.adminsChatReportID, + adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID, }; API.write(WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData}); @@ -3416,6 +3449,7 @@ function trackExpense( const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { + createdWorkspaceParams, iouReport, chatReport, transaction, @@ -3477,6 +3511,7 @@ function trackExpense( taxAmount, billable, receipt, + createdWorkspaceParams, ); break; } @@ -3507,6 +3542,7 @@ function trackExpense( taxAmount, billable, receipt, + createdWorkspaceParams, ); break; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 2c08dd321b11..47d3f54a82b2 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2164,6 +2164,11 @@ function buildOptimisticPolicyCategories(policyID: string, categories: readonly key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: optimisticCategoryMap, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, + value: null, + }, ], successData: [ { @@ -2185,14 +2190,15 @@ function buildOptimisticPolicyCategories(policyID: string, categories: readonly } /** - * Optimistically creates a new workspace and default workspace chats + * Generates onyx data for creating a new workspace * * @param [policyOwnerEmail] the email of the account to make the owner of the policy * @param [makeMeAdmin] leave the calling account as an admin on the policy * @param [policyName] custom policy name we will use for created workspace * @param [policyID] custom policy id we will use for created workspace + * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()): CreateWorkspaceParams { +function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); @@ -2210,7 +2216,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName expenseChatData, expenseReportActionData, expenseCreatedReportActionID, - } = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName); + } = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName, expenseReportId); const optimisticCategoriesData = buildOptimisticPolicyCategories(policyID, CONST.POLICY.DEFAULT_CATEGORIES); @@ -2259,6 +2265,11 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName ...announceChatData, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${announceChatReportID}`, + value: null, + }, { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, @@ -2299,6 +2310,11 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, value: null, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${expenseChatReportID}`, + value: null, + }, ]; const successData: OnyxUpdate[] = [ @@ -2433,11 +2449,118 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName customUnitRateID, }; + return {successData, optimisticData, failureData, params}; +} + +/** + * Optimistically creates a new workspace and default workspace chats + * + * @param [policyOwnerEmail] the email of the account to make the owner of the policy + * @param [makeMeAdmin] leave the calling account as an admin on the policy + * @param [policyName] custom policy name we will use for created workspace + * @param [policyID] custom policy id we will use for created workspace + */ +function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()): CreateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return params; } +/** + * Creates a draft workspace for various money request flows + * + * @param [policyOwnerEmail] the email of the account to make the owner of the policy + * @param [makeMeAdmin] leave the calling account as an admin on the policy + * @param [policyName] custom policy name we will use for created workspace + * @param [policyID] custom policy id we will use for created workspace + */ +function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()): CreateWorkspaceParams { + const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); + + const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); + + const {expenseChatData, announceChatReportID, announceCreatedReportActionID, adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID} = + ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, + value: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + name: workspaceName, + role: CONST.POLICY.ROLE.ADMIN, + owner: sessionEmail, + ownerAccountID: sessionAccountID, + isPolicyExpenseChatEnabled: true, + outputCurrency, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + autoReporting: true, + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, + harvesting: { + enabled: true, + }, + customUnits, + areCategoriesEnabled: true, + areTagsEnabled: false, + areDistanceRatesEnabled: false, + areWorkflowsEnabled: false, + areReportFieldsEnabled: false, + areConnectionsEnabled: false, + employeeList: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + errors: {}, + }, + }, + chatReportIDAdmins: makeMeAdmin ? Number(adminsChatReportID) : undefined, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${expenseChatReportID}`, + value: expenseChatData, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, + value: CONST.POLICY.DEFAULT_CATEGORIES.reduce( + (acc, category) => ({ + ...acc, + [category]: { + name: category, + enabled: true, + errors: null, + }, + }), + {}, + ), + }, + ]; + + const params: CreateWorkspaceParams = { + policyID, + announceChatReportID, + adminsChatReportID, + expenseChatReportID, + ownerEmail: policyOwnerEmail, + makeMeAdmin, + policyName: workspaceName, + type: CONST.POLICY.TYPE.TEAM, + announceCreatedReportActionID, + adminsCreatedReportActionID, + expenseCreatedReportActionID, + customUnitID, + customUnitRateID, + }; + + Onyx.update(optimisticData); + + return params; +} + function openWorkspaceReimburseView(policyID: string) { if (!policyID) { Log.warn('openWorkspaceReimburseView invalid params', {policyID}); @@ -5168,6 +5291,8 @@ export { setPolicyDistanceRatesEnabled, deletePolicyDistanceRates, getPrimaryPolicy, + createDraftWorkspace, + buildPolicyData, }; export type {NewCustomUnit}; diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 4b34a6a19600..0bd546318186 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -32,9 +32,15 @@ type IOURequestStepCategoryOnyxProps = { /** The policy of the report */ policy: OnyxEntry; + /** The draft policy of the report */ + policyDraft: OnyxEntry; + /** Collection of categories attached to a policy */ policyCategories: OnyxEntry; + /** Collection of draft categories attached to a policy */ + policyCategoriesDraft: OnyxEntry; + /** Collection of tags attached to a policy */ policyTags: OnyxEntry; @@ -50,18 +56,24 @@ type IOURequestStepCategoryProps = IOURequestStepCategoryOnyxProps & WithFullTransactionOrNotFoundProps; function IOURequestStepCategory({ - report, + report: reportReal, + reportDraft, route: { params: {transactionID, backTo, action, iouType, reportActionID}, }, transaction, splitDraftTransaction, - policy, + policy: policyReal, + policyDraft, policyTags, - policyCategories, + policyCategories: policyCategoriesReal, + policyCategoriesDraft, reportActions, session, }: IOURequestStepCategoryProps) { + const report = reportReal ?? reportDraft; + const policy = policyReal ?? policyDraft; + const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const styles = useThemeStyles(); const {translate} = useLocalize(); const isEditing = action === CONST.IOU.ACTION.EDIT; @@ -73,7 +85,7 @@ function IOURequestStepCategory({ // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = ReportUtils.isReportInGroupPolicy(report) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = ReportUtils.isReportInGroupPolicy(report, policy) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); @@ -87,6 +99,7 @@ function IOURequestStepCategory({ PolicyActions.openDraftWorkspaceRequest(report?.policyID ?? ''); }; + useNetwork({onReconnect: fetchData}); useEffect(() => { @@ -159,9 +172,15 @@ const IOURequestStepCategoryWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, + policyDraft: { + key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${reportDraft ? reportDraft.policyID : '0'}`, + }, policyCategories: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, }, + policyCategoriesDraft: { + key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${reportDraft ? reportDraft.policyID : '0'}`, + }, policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index e33e4d8fb763..08599496859f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -39,9 +39,15 @@ type IOURequestStepConfirmationOnyxProps = { /** The policy of the report */ policy: OnyxEntry; + /** The draft policy of the report */ + policyDraft: OnyxEntry; + /** The category configuration of the report's policy */ policyCategories: OnyxEntry; + /** The draft category configuration of the report's policy */ + policyCategoriesDraft: OnyxEntry; + /** The tag configuration of the report's policy */ policyTags: OnyxEntry; }; @@ -51,10 +57,13 @@ type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & WithFullTransactionOrNotFoundProps; function IOURequestStepConfirmation({ - policy, + policy: policyReal, + policyDraft, policyTags, - policyCategories, - report, + policyCategories: policyCategoriesReal, + policyCategoriesDraft, + report: reportReal, + reportDraft, route: { params: {iouType, reportID, transactionID, action}, }, @@ -63,6 +72,10 @@ function IOURequestStepConfirmation({ const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const report = reportReal ?? reportDraft; + const policy = policyReal ?? policyDraft; + const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; + const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -129,7 +142,6 @@ function IOURequestStepConfirmation({ if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; } - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }) ?? [], [transaction?.participants, personalDetails, iouType], @@ -139,7 +151,7 @@ function IOURequestStepConfirmation({ useEffect(() => { const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat?.policyID) { + if (policyExpenseChat?.policyID && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { openDraftWorkspaceRequest(policyExpenseChat.policyID); } }, [isOffline, participants, transaction?.billable, policy, transactionID]); @@ -584,9 +596,15 @@ const IOURequestStepConfirmationWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`, }, + policyDraft: { + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, + }, policyCategories: { key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`, }, + policyCategoriesDraft: { + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, + }, policyTags: { key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`, }, diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 36b14f2aaedb..62a7f7d73825 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -15,6 +15,9 @@ import type {Report} from '@src/types/onyx'; type WithWritableReportOrNotFoundOnyxProps = { /** The report corresponding to the reportID in the route params */ report: OnyxEntry; + + /** The draft report corresponding to the reportID in the route params */ + reportDraft: OnyxEntry; }; type MoneyRequestRouteName = @@ -71,6 +74,9 @@ export default function `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? '0'}`, }, + reportDraft: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${route.params.reportID ?? '0'}`, + }, })(forwardRef(WithWritableReportOrNotFound)); }