From dfbb0e1b57ecb27062693ce803de2c6910ca7e4d Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 23 May 2025 04:51:05 +0300 Subject: [PATCH 0001/1005] pushing violations for tags --- src/libs/ReportUtils.ts | 101 +++++++++++++++++++++++ src/libs/actions/Policy/Tag.ts | 145 ++++++++++++++++++++------------- 2 files changed, 188 insertions(+), 58 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ca8836d0ba659..65fe57de918a9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -24,6 +24,7 @@ import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} fr import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; +import {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting, OnboardingCompanySize, OnboardingPurpose, OnboardingTaskLinks} from '@src/CONST'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -41,7 +42,10 @@ import type { PersonalDetails, PersonalDetailsList, Policy, + PolicyCategories, + PolicyCategory, PolicyReportField, + PolicyTagLists, Report, ReportAction, ReportAttributesDerivedValue, @@ -53,6 +57,7 @@ import type { Task, Transaction, TransactionViolation, + TransactionViolations, UserWallet, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -66,6 +71,7 @@ import type {AllConnectionName, ConnectionName} from '@src/types/onyx/Policy'; import type {InvoiceReceiverType, NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report'; import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction'; import type {PendingChatMember} from '@src/types/onyx/ReportMetadata'; +import type {OnyxData} from '@src/types/onyx/Request'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -122,6 +128,7 @@ import { getPolicyRole, getRuleApprovers, getSubmitToAccountID, + hasDependentTags as hasDependentTagsPolicyUtils, isExpensifyTeam, isInstantSubmitEnabled, isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils, @@ -251,6 +258,7 @@ import { import {addTrailingForwardSlash} from './Url'; import type {AvatarSource} from './UserUtils'; import {generateAccountID, getDefaultAvatarURL} from './UserUtils'; +import ViolationsUtils from './Violations/ViolationsUtils'; // Dynamic Import to avoid circular dependency const UnreadIndicatorUpdaterHelper = () => import('./UnreadIndicatorUpdater'); @@ -908,6 +916,34 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); +let allPolicyTagLists: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTagLists = {}; + return; + } + allPolicyTagLists = value; + }, +}); + +let allPolicyCategories: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_CATEGORIES, + waitForCollectionCallback: true, + callback: (val) => (allPolicyCategories = val), +}); + +let allTransactionViolations: OnyxCollection = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + waitForCollectionCallback: true, + callback: (value) => (allTransactionViolations = value), +}); + let allReports: OnyxCollection; let reportsByPolicyID: ReportByPolicyMap; Onyx.connect({ @@ -1773,6 +1809,70 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return isProcessingReport(report) && submitsToAccountID === report.managerID; } +/** + * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. + * + * @param policyUpdate Changed policy properties, if none pass empty object + * @param policyCategoriesUpdate Changed categories properties, if none pass empty object + */ +function pushTransactionViolationsOnyxData( + onyxData: OnyxData, + policyID: string, + policyUpdate: Partial = {}, + policyTagListsUpdate: Record> = {}, + policyCategoriesUpdate: Record> = {}, +) { + if (policyUpdate === null && policyCategoriesUpdate === null && policyTagListsUpdate === null) { + return; + } + + const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; + const optimisticPolicyCategories = Object.keys(policyCategories).reduce>((acc, categoryName) => { + acc[categoryName] = {...policyCategories[categoryName], ...policyCategoriesUpdate[categoryName]}; + return acc; + }, {}) as PolicyCategories; + + const policyTagLists = allPolicyTagLists?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; + const optimisticPolicyTagLists = Object.keys(policyTagLists).reduce>((acc, tagListName) => { + acc[tagListName] = {...policyTagLists[tagListName], ...policyTagListsUpdate[tagListName]}; + return acc; + }, {}) as PolicyTagLists; + + const policy = {...allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ...policyUpdate} as Policy; + const hasDependentTags = hasDependentTagsPolicyUtils(policy, policyTagLists); + + getAllPolicyReports(policyID).forEach((report) => { + if (!report?.reportID) { + return; + } + + const isReportAnInvoice = isInvoiceReport(report); + + getReportTransactions(report.reportID).forEach((transaction: Transaction) => { + const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? []; + + const optimisticTransactionViolations = ViolationsUtils.getViolationsOnyxData( + transaction, + transactionViolations, + policy, + policyTagLists, + optimisticPolicyCategories, + hasDependentTags, + isReportAnInvoice, + ); + + if (optimisticTransactionViolations) { + onyxData?.optimisticData?.push(optimisticTransactionViolations); + onyxData?.failureData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: transactionViolations, + }); + } + }); + }); +} + /** * Check if the report is a single chat report that isn't a thread * and personal detail of participant is optimistic data @@ -11117,6 +11217,7 @@ export { getReportPersonalDetailsParticipants, isAllowedToSubmitDraftExpenseReport, isWorkspaceEligibleForReportChange, + pushTransactionViolationsOnyxData, getReportAttributes, }; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index e332f3b472c7c..fa74a56a661c5 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -318,6 +318,8 @@ function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record tagsToUpdate[key])), @@ -334,21 +336,23 @@ function deletePolicyTags(policyID: string, tagsToDelete: string[]) { return; } + const policyTagsUpdate: Record>>> = { + [policyTag.name]: { + tags: { + ...tagsToDelete.reduce>>>((acc, tagName) => { + acc[tagName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; + return acc; + }, {}), + }, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - [policyTag.name]: { - tags: { - ...tagsToDelete.reduce>>>((acc, tagName) => { - acc[tagName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; - return acc; - }, {}), - }, - }, - }, + value: policyTagsUpdate, }, ], successData: [ @@ -389,6 +393,8 @@ function deletePolicyTags(policyID: string, tagsToDelete: string[]) { ], }; + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, {}, policyTagsUpdate); + const parameters = { policyID, tags: JSON.stringify(tagsToDelete), @@ -494,38 +500,42 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: updatedApprovalRules[indexToUpdate] = policyTagRule; } + const policyUpdate: Partial = { + rules: { + approvalRules: updatedApprovalRules, + }, + }; + + const policyTagsUpdate: Record>>> = { + [tagList?.name]: { + tags: { + [oldTagName]: null, + [newTagName]: { + ...tag, + name: newTagName, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + ...tag.pendingFields, + name: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + previousTagName: oldTagName, + errors: null, + }, + }, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - [tagList?.name]: { - tags: { - [oldTagName]: null, - [newTagName]: { - ...tag, - name: newTagName, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - ...tag.pendingFields, - name: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - previousTagName: oldTagName, - errors: null, - }, - }, - }, - }, + value: policyTagsUpdate, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - rules: { - approvalRules: updatedApprovalRules, - }, - }, + value: policyUpdate, }, ], successData: [ @@ -571,6 +581,8 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: ], }; + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, policyTagsUpdate); + const parameters: RenamePolicyTagsParams = { policyID, oldName: oldTagName, @@ -582,17 +594,19 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: } function enablePolicyTags(policyID: string, enabled: boolean) { + const policyUpdate = { + areTagsEnabled: enabled, + pendingFields: { + areTagsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - areTagsEnabled: enabled, - pendingFields: { - areTagsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, + value: policyUpdate, }, ], successData: [ @@ -640,6 +654,8 @@ function enablePolicyTags(policyID: string, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); + + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, defaultTagList); } else if (!enabled) { const policyTag = PolicyUtils.getTagLists(policyTagList).at(0); @@ -647,22 +663,24 @@ function enablePolicyTags(policyID: string, enabled: boolean) { return; } + const policyTagsUpdate: Record> = { + [policyTag.name]: { + tags: Object.fromEntries( + Object.keys(policyTag.tags).map((tagName) => [ + tagName, + { + enabled: false, + }, + ]), + ), + } as Partial, + }; + onyxData.optimisticData?.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - [policyTag.name]: { - tags: Object.fromEntries( - Object.keys(policyTag.tags).map((tagName) => [ - tagName, - { - enabled: false, - }, - ]), - ), - }, - }, + value: policyTagsUpdate, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -672,6 +690,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) { }, }, ); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, {...policyUpdate, requiresTag: false}, policyTagsUpdate); } const parameters: EnablePolicyTagsParams = {policyID, enabled}; @@ -738,19 +757,20 @@ function renamePolicyTagList(policyID: string, policyTagListName: {oldName: stri function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); + const policyUpdate: Partial = { + requiresTag, + errors: {requiresTag: null}, + pendingFields: { + requiresTag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - requiresTag, - errors: {requiresTag: null}, - pendingFields: { - requiresTag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, + value: policyUpdate, }, ], successData: [ @@ -782,6 +802,15 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { ], }; + const policyTagListsUpdate = Object.keys(policyTags).reduce>>((acc, key) => { + if (!policyTags[key].required != requiresTag) { + acc[key] = {required: requiresTag}; + } + return acc; + }, {}); + + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, policyTagListsUpdate); + if (isMultiLevelTags) { const getUpdatedTagsData = (required: boolean): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, From 8f995984cae40d7030b47ae458905c3ab3b3f6f5 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 23 May 2025 05:10:56 +0300 Subject: [PATCH 0002/1005] lint --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Policy/Tag.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 65fe57de918a9..2dafdfc6ce20d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1855,7 +1855,7 @@ function pushTransactionViolationsOnyxData( transaction, transactionViolations, policy, - policyTagLists, + optimisticPolicyTagLists, optimisticPolicyCategories, hasDependentTags, isReportAnInvoice, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index fa74a56a661c5..5f383151bb886 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -803,7 +803,7 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { }; const policyTagListsUpdate = Object.keys(policyTags).reduce>>((acc, key) => { - if (!policyTags[key].required != requiresTag) { + if (!policyTags[key].required !== requiresTag) { acc[key] = {required: requiresTag}; } return acc; From 76fbf0c40a412f3f3588d8a4e63a8c90b88eb347 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 24 May 2025 03:40:26 +0300 Subject: [PATCH 0003/1005] lint --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2dafdfc6ce20d..8d376c2a2e8be 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -24,7 +24,7 @@ import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} fr import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; -import {PolicyTagList} from '@pages/workspace/tags/types'; +import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting, OnboardingCompanySize, OnboardingPurpose, OnboardingTaskLinks} from '@src/CONST'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; From 8ad6eaf670037a0cf566418669908aa1d0bc0017 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 27 Jun 2025 00:12:10 +0300 Subject: [PATCH 0004/1005] setting tags violations --- src/libs/ReportUtils.ts | 57 ++--- src/libs/actions/Policy/Category.ts | 39 ++-- src/libs/actions/Policy/Tag.ts | 219 ++++++++++++------ .../workspace/WorkspaceMoreFeaturesPage.tsx | 6 +- src/pages/workspace/tags/EditTagPage.tsx | 4 +- src/pages/workspace/tags/TagSettingsPage.tsx | 4 +- .../workspace/tags/WorkspaceEditTagsPage.tsx | 5 +- .../workspace/tags/WorkspaceTagsPage.tsx | 10 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 14 +- 9 files changed, 220 insertions(+), 138 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a81833507b48e..c686b28a55e3e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -17,8 +17,8 @@ import {FallbackAvatar, IntacctSquare, NetSuiteExport, NetSuiteSquare, QBDSquare import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; -import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; +import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -938,34 +938,6 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -let allPolicyTagLists: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTagLists = {}; - return; - } - allPolicyTagLists = value; - }, -}); - -let allPolicyCategories: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_CATEGORIES, - waitForCollectionCallback: true, - callback: (val) => (allPolicyCategories = val), -}); - -let allTransactionViolations: OnyxCollection = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - waitForCollectionCallback: true, - callback: (value) => (allTransactionViolations = value), -}); - let allReports: OnyxCollection; let reportsByPolicyID: ReportByPolicyMap; Onyx.connect({ @@ -1856,26 +1828,33 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { * * @param policyUpdate Changed policy properties, if none pass empty object * @param policyCategoriesUpdate Changed categories properties, if none pass empty object + * @param policyTagListsUpdate Changed categories properties, if none pass empty object */ function pushTransactionViolationsOnyxData( onyxData: OnyxData, policyID: string, - policyTagLists: PolicyTagLists, policyCategories: PolicyCategories, + policyTagLists: PolicyTagLists, allTransactionViolations: OnyxCollection, - policyUpdate: Partial = {}, + policyUpdate: Partial, policyCategoriesUpdate: Record> = {}, + policyTagListsUpdate: Record> = {}, ): OnyxData { - if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate)) { + if (isEmptyObject(policyUpdate) && (isEmptyObject(policyCategoriesUpdate) || isEmptyObject(policyTagListsUpdate))) { return onyxData; } - const optimisticPolicyCategories = Object.keys(policyCategories).reduce>((acc, categoryName) => { + const optimisticPolicyTagLists = Object.keys(policyTagLists).reduce((acc, tagName) => { + acc[tagName] = {...policyTagLists[tagName], ...(policyTagListsUpdate?.[tagName] ?? {})}; + return acc; + }, {}); + + const optimisticPolicyCategories = Object.keys(policyCategories).reduce((acc, categoryName) => { acc[categoryName] = {...policyCategories[categoryName], ...(policyCategoriesUpdate?.[categoryName] ?? {})}; return acc; - }, {}) as PolicyCategories; + }, {}); const optimisticPolicy = {...allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ...policyUpdate} as Policy; - const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, policyTagLists); + const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); getAllPolicyReports(policyID).forEach((report) => { if (!report?.reportID) { @@ -1885,13 +1864,13 @@ function pushTransactionViolationsOnyxData( const isReportAnInvoice = isInvoiceReport(report); getReportTransactions(report.reportID).forEach((transaction: Transaction) => { - const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? []; + const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]; const optimisticTransactionViolations = ViolationsUtils.getViolationsOnyxData( transaction, - transactionViolations, + transactionViolations ?? [], optimisticPolicy, - policyTagLists, + optimisticPolicyTagLists, optimisticPolicyCategories, hasDependentTags, isReportAnInvoice, @@ -1902,7 +1881,7 @@ function pushTransactionViolationsOnyxData( onyxData?.failureData?.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, - value: transactionViolations, + value: transactionViolations ?? null, }); } }); diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 62f1f1c5ddbbc..4e6c1555cc0f8 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -373,7 +373,7 @@ function setWorkspaceCategoryEnabled( ], }; - pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, allTransactionViolations, {}, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, {}, optimisticPolicyCategoriesData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -796,6 +796,7 @@ function setPolicyCategoryPayrollCode(policyID: string, categoryName: string, pa categoryName, payrollCode, }; + API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE, parameters, onyxData); } @@ -874,20 +875,23 @@ function setWorkspaceRequiresCategory( policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}, ) { + + const policyUpdate = { + requiresCategory, + errors: { + requiresCategory: null, + }, + pendingFields: { + requiresCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - requiresCategory, - errors: { - requiresCategory: null, - }, - pendingFields: { - requiresCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, + value: policyUpdate, }, ], successData: [ @@ -919,9 +923,14 @@ function setWorkspaceRequiresCategory( ], }; - pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, allTransactionViolations, { - requiresCategory, - } as Partial); + pushTransactionViolationsOnyxData( + onyxData, + policyID, + allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, + policyTagLists, + allTransactionViolations, + policyUpdate as Partial + ); const parameters = { policyID, requiresCategory, @@ -1002,7 +1011,7 @@ function deleteWorkspaceCategories( }; const optimisticPolicyData: Partial = shouldDisableRequiresCategory ? {requiresCategory: false} : {}; - pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -1096,7 +1105,7 @@ function enablePolicyCategories( Object.entries(policyCategories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]), ); - pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, allTransactionViolations, policyUpdate, policyCategoriesUpdate); + pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); if (onyxUpdatesToDisableCategories.length > 0) { onyxData.optimisticData?.push(...onyxUpdatesToDisableCategories); diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 7598e6c1a7576..1c59d57e7fb14 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -31,7 +31,17 @@ import type {PolicyTagList} from '@pages/workspace/tags/types'; import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx'; +import type { + ImportedSpreadsheet, + Policy, + PolicyCategories, + PolicyTag, + PolicyTagLists, + PolicyTags, + RecentlyUsedTags, + Report, + TransactionViolations, +} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -232,8 +242,15 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { API.write(WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET, parameters, onyxData); } -function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record, tagListIndex: number) { - const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(tagListIndex); +function setWorkspaceTagEnabled( + policyID: string, + tagsToUpdate: Record, + tagListIndex: number, + policyCategories: PolicyCategories = {}, + transactionViolations: OnyxCollection = {}, +) { + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; + const policyTag = PolicyUtils.getTagLists(policyTags)?.at(tagListIndex); if (!policyTag || tagListIndex === -1) { return; @@ -321,7 +338,7 @@ function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record) { +function setWorkspaceTagRequired( + policyID: string, + tagListIndexes: number[], + isRequired: boolean, + policyTagsLists: PolicyTagLists = {}, + policyCategories: PolicyCategories = {}, + transactionViolations: OnyxCollection = {}, +) { + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; if (!policyTags) { return; } + const optimisticPolicyTagsData = { + ...Object.keys(policyTags).reduce((acc, key) => { + if (tagListIndexes.includes(policyTags[key].orderWeight)) { + acc[key] = { + ...acc[key], + required: isRequired, + errors: undefined, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }; + + return acc; + } + + return acc; + }, {}), + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - ...Object.keys(policyTags).reduce((acc, key) => { - if (tagListIndexes.includes(policyTags[key].orderWeight)) { - acc[key] = { - ...acc[key], - required: isRequired, - errors: undefined, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }; - - return acc; - } - - return acc; - }, {}), - }, + value: optimisticPolicyTagsData, }, ], successData: [ @@ -407,6 +434,8 @@ function setWorkspaceTagRequired(policyID: string, tagListIndexes: number[], isR ], }; + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagsLists, transactionViolations, {}, {}, optimisticPolicyTagsData); + const parameters: SetPolicyTagListsRequired = { policyID, tagListIndexes, @@ -416,8 +445,9 @@ function setWorkspaceTagRequired(policyID: string, tagListIndexes: number[], isR API.write(WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED, parameters, onyxData); } -function deletePolicyTags(policyID: string, tagsToDelete: string[]) { - const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(0); +function deletePolicyTags(policyID: string, tagsToDelete: string[], policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; + const policyTag = PolicyUtils.getTagLists(policyTagLists)?.at(0); if (!policyTag) { return; @@ -480,7 +510,7 @@ function deletePolicyTags(policyID: string, tagsToDelete: string[]) { ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, {}, {}, policyTagsUpdate); const parameters = { policyID, @@ -558,11 +588,18 @@ function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { }); } -function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}, tagListIndex: number) { +function renamePolicyTag( + policyID: string, + policyTag: {oldName: string; newName: string}, + tagListIndex: number, + policyCategories: PolicyCategories = {}, + transactionViolations: OnyxCollection = {}, +) { + const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policy = PolicyUtils.getPolicy(policyID); - const tagList = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(tagListIndex); + const tagList = PolicyUtils.getTagLists(policyTagLists)?.at(tagListIndex); if (!tagList) { return; } @@ -670,7 +707,7 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, policyTagsUpdate); const parameters: RenamePolicyTagsParams = { policyID, @@ -682,7 +719,8 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: API.write(WRITE_COMMANDS.RENAME_POLICY_TAG, parameters, onyxData); } -function enablePolicyTags(policyID: string, enabled: boolean) { +function enablePolicyTags(policyID: string, enabled: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyUpdate = { areTagsEnabled: enabled, pendingFields: { @@ -723,8 +761,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) { ], }; - const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]; - if (!policyTagList) { + if (!policyTagLists) { const defaultTagList: PolicyTagLists = { Tag: { name: 'Tag', @@ -743,10 +780,9 @@ function enablePolicyTags(policyID: string, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); - - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, defaultTagList); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, defaultTagList); } else if (!enabled) { - const policyTag = PolicyUtils.getTagLists(policyTagList).at(0); + const policyTag = PolicyUtils.getTagLists(policyTagLists).at(0); if (!policyTag) { return; @@ -779,7 +815,17 @@ function enablePolicyTags(policyID: string, enabled: boolean) { }, }, ); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, {...policyUpdate, requiresTag: false}, policyTagsUpdate); + + ReportUtils.pushTransactionViolationsOnyxData( + onyxData, + policyID, + policyCategories, + policyTagLists, + transactionViolations, + {...policyUpdate, requiresTag: false}, + {}, + policyTagsUpdate, + ); } const parameters: EnablePolicyTagsParams = {policyID, enabled}; @@ -879,7 +925,14 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet ); } -function renamePolicyTagList(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number) { +function renamePolicyTagList( + policyID: string, + policyTagListName: {oldName: string; newName: string}, + policyTags: OnyxEntry, + tagListIndex: number, + policyCategories: PolicyCategories = {}, + transactionViolations: OnyxCollection = {}, +) { const newName = policyTagListName.newName; const oldName = policyTagListName.oldName; const oldPolicyTags = policyTags?.[oldName] ?? {}; @@ -919,6 +972,28 @@ function renamePolicyTagList(policyID: string, policyTagListName: {oldName: stri }, ], }; + + const policyTagLists = policyTags + ? Object.keys(policyTags ?? {}).reduce((acc, tagName) => { + if (tagName === oldName) { + return acc; + } + acc[tagName] = {...policyTags?.[tagName]}; + return acc; + }, {}) + : {}; + + ReportUtils.pushTransactionViolationsOnyxData( + onyxData, + policyID, + policyCategories, + policyTagLists, + transactionViolations, + {}, + {}, + {newName: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}, + ); + const parameters: RenamePolicyTagListParams = { policyID, oldName, @@ -929,8 +1004,8 @@ function renamePolicyTagList(policyID: string, policyTagListName: {oldName: stri API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData); } -function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { - const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +function setPolicyRequiresTag(policyID: string, requiresTag: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyUpdate: Partial = { requiresTag, errors: {requiresTag: null}, @@ -976,32 +1051,27 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { ], }; - const policyTagListsUpdate = Object.keys(policyTags).reduce>>((acc, key) => { - if (!policyTags[key].required !== requiresTag) { - acc[key] = {required: requiresTag}; - } - return acc; - }, {}); + const getUpdatedTagsData = (required: boolean): PolicyTagLists => ({ + ...Object.keys(policyTagLists).reduce((acc, key) => { + acc[key] = { + ...acc[key], + required, + }; + return acc; + }, {}), + }); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyUpdate, policyTagListsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, getUpdatedTagsData(requiresTag)); - const getUpdatedTagsData = (required: boolean): OnyxUpdate => ({ + const getUpdatedTagsOnyxData = (required: boolean): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, onyxMethod: Onyx.METHOD.MERGE, - value: { - ...Object.keys(policyTags).reduce((acc, key) => { - acc[key] = { - ...acc[key], - required, - }; - return acc; - }, {}), - }, + value: {...getUpdatedTagsData(required)}, }); - onyxData.optimisticData?.push(getUpdatedTagsData(requiresTag)); - onyxData.failureData?.push(getUpdatedTagsData(!requiresTag)); - onyxData.successData?.push(getUpdatedTagsData(requiresTag)); + onyxData.optimisticData?.push(getUpdatedTagsOnyxData(requiresTag)); + onyxData.failureData?.push(getUpdatedTagsOnyxData(!requiresTag)); + onyxData.successData?.push(getUpdatedTagsOnyxData(requiresTag)); const parameters = { policyID, @@ -1011,8 +1081,15 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData); } -function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIndex: number) { - const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(tagListIndex); +function setPolicyTagsRequired( + policyID: string, + requiresTag: boolean, + tagListIndex: number, + policyTagsLists: PolicyTagLists = {}, + policyCategories: PolicyCategories = {}, + transactionViolations: OnyxCollection = {}, +) { + const policyTag = PolicyUtils.getTagLists(policyTagsLists)?.at(tagListIndex); if (!policyTag) { return; @@ -1022,18 +1099,20 @@ function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIn return; } + const policyTagListsUpdate = { + [policyTag.name]: { + required: requiresTag, + pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + errorFields: {required: null}, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - [policyTag.name]: { - required: requiresTag, - pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, - errorFields: {required: null}, - }, - }, + value: policyTagListsUpdate, }, ], successData: [ @@ -1070,6 +1149,8 @@ function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIn requireTagList: requiresTag, }; + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagsLists, transactionViolations, {}, {}, policyTagListsUpdate); + API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData); } diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 00a56ddcb0330..151c4da48c165 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -101,8 +101,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); - + const onDisabledOrganizeSwitchPress = useCallback(() => { if (!hasAccountingConnection) { return; @@ -266,7 +268,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - enablePolicyTags(policyID, isEnabled); + enablePolicyTags(policyID, isEnabled, policyCategories, allTransactionViolations); }, }, { diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index 6a49a42e55f40..48c11b7c105d8 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -30,6 +30,8 @@ type EditTagPageProps = function EditTagPage({route}: EditTagPageProps) { const policyID = route.params.policyID; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); const backTo = route.params.backTo; const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -64,7 +66,7 @@ function EditTagPage({route}: EditTagPageProps) { const tagName = values.tagName.trim(); // Do not call the API if the edited tag name is the same as the current tag name if (currentTagName !== tagName) { - renamePolicyTag(policyID, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight); + renamePolicyTag(policyID, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight, policyCategories, allTransactionViolations); } Keyboard.dismiss(); Navigation.goBack( diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index b3be348539728..a53ef53b25d20 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -50,6 +50,8 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { const policyTag = useMemo(() => getTagList(policyTags, orderWeight), [policyTags, orderWeight]); const policy = usePolicy(policyID); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = React.useState(false); const [isCannotDeleteOrDisableLastTagModalVisible, setIsCannotDeleteOrDisableLastTagModalVisible] = useState(false); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_SETTINGS; @@ -78,7 +80,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - deletePolicyTags(policyID, [currentPolicyTag.name]); + deletePolicyTags(policyID, [currentPolicyTag.name], policyCategories, allTransactionViolations); setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 5aac622743aa1..8d55e1fd7713c 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -27,11 +27,14 @@ type WorkspaceEditTagsPageProps = | PlatformStackScreenProps; function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`, {canBeMissing: true}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); const styles = useThemeStyles(); const {translate} = useLocalize(); const tagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const {inputCallbackRef} = useAutoFocusInput(); + const backTo = route.params.backTo; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_EDIT; @@ -60,7 +63,7 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { const updateTagListName = useCallback( (values: FormOnyxValues) => { if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { - renamePolicyTagList(route.params.policyID, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, policyTags, route.params.orderWeight); + renamePolicyTagList(route.params.policyID, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, policyTags, route.params.orderWeight, policyCategories, allTransactionViolations); } goBackToTagsSettings(); }, diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index aeb6f01ad4561..3e9d5089717ae 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -103,6 +103,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const connectedIntegration = getConnectedIntegration(policy) ?? connectionSyncProgress?.connectionName; const isConnectionVerified = connectedIntegration && !isConnectionUnverified(policy, connectedIntegration); const currentConnectionName = getCurrentConnectionName(policy); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); const [policyTagLists, isMultiLevelTags, hasDependentTags, hasIndependentTags] = useMemo( () => [getTagLists(policyTags), isMultiLevelTagsPolicyUtils(policyTags), hasDependentTagsPolicyUtils(policy, policyTags), hasIndependentTagsPolicyUtils(policy, policyTags)], [policy, policyTags], @@ -166,7 +168,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceRequiresTag = useCallback( (value: boolean, orderWeight: number) => { - setPolicyTagsRequired(policyID, value, orderWeight); + setPolicyTagsRequired(policyID, value, orderWeight, policyTags, policyCategories, allTransactionViolations); }, [policyID], ); @@ -318,7 +320,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - deletePolicyTags(policyID, selectedTags); + deletePolicyTags(policyID, selectedTags, policyCategories, allTransactionViolations); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -517,7 +519,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } setSelectedTags([]); - setWorkspaceTagRequired(policyID, tagListIndexesToMarkOptional, false, policyTags); + setWorkspaceTagRequired(policyID, tagListIndexesToMarkOptional, false, policyTags, policyCategories, allTransactionViolations); }, }); } @@ -529,7 +531,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.NOT_REQUIRED, onSelected: () => { setSelectedTags([]); - setWorkspaceTagRequired(policyID, tagListIndexesToMarkRequired, true, policyTags); + setWorkspaceTagRequired(policyID, tagListIndexesToMarkRequired, true, policyTags, policyCategories, allTransactionViolations); }, }); } diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 4885f9dcd2847..c492e81cb8bb4 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -73,6 +73,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const backTo = route.params.backTo; const policy = usePolicy(policyID); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const {selectionMode} = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); @@ -113,9 +114,10 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { onNavigationCallBack: () => Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID) : undefined), }); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight); + setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); }, [policyID, route.params.orderWeight], ); @@ -213,7 +215,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const deleteTags = () => { - deletePolicyTags(policyID, selectedTags); + deletePolicyTags(policyID, selectedTags, policyCategories, allTransactionViolations); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -283,7 +285,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToDisable, route.params.orderWeight); + setWorkspaceTagEnabled(policyID, tagsToDisable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); } @@ -295,7 +297,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToEnable, route.params.orderWeight); + setWorkspaceTagEnabled(policyID, tagsToEnable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); } @@ -316,7 +318,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; if (!!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) { - setPolicyTagsRequired(policyID, false, route.params.orderWeight); + setPolicyTagsRequired(policyID, false, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); } const navigateToEditTag = () => { @@ -376,7 +378,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } - setPolicyTagsRequired(policyID, on, route.params.orderWeight); + setPolicyTagsRequired(policyID, on, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); }} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} From 741aa02bef8d229a1278b21ff1301f12c4687767 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 27 Jun 2025 01:08:12 +0300 Subject: [PATCH 0005/1005] prettier --- src/libs/actions/Policy/Category.ts | 14 ++++++-------- src/libs/actions/Policy/Tag.ts | 14 ++------------ src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 4 ++-- src/pages/workspace/tags/WorkspaceEditTagsPage.tsx | 9 ++++++++- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 4e6c1555cc0f8..cf890aefcff8f 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -796,7 +796,6 @@ function setPolicyCategoryPayrollCode(policyID: string, categoryName: string, pa categoryName, payrollCode, }; - API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE, parameters, onyxData); } @@ -875,7 +874,6 @@ function setWorkspaceRequiresCategory( policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}, ) { - const policyUpdate = { requiresCategory, errors: { @@ -885,7 +883,7 @@ function setWorkspaceRequiresCategory( requiresCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }; - + const onyxData: OnyxData = { optimisticData: [ { @@ -924,12 +922,12 @@ function setWorkspaceRequiresCategory( }; pushTransactionViolationsOnyxData( - onyxData, - policyID, + onyxData, + policyID, allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, policyTagLists, - allTransactionViolations, - policyUpdate as Partial + allTransactionViolations, + policyUpdate as Partial, ); const parameters = { policyID, @@ -1105,7 +1103,7 @@ function enablePolicyCategories( Object.entries(policyCategories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]), ); - pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); + pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); if (onyxUpdatesToDisableCategories.length > 0) { onyxData.optimisticData?.push(...onyxUpdatesToDisableCategories); diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 1c59d57e7fb14..f8b51273f9345 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -31,17 +31,7 @@ import type {PolicyTagList} from '@pages/workspace/tags/types'; import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { - ImportedSpreadsheet, - Policy, - PolicyCategories, - PolicyTag, - PolicyTagLists, - PolicyTags, - RecentlyUsedTags, - Report, - TransactionViolations, -} from '@src/types/onyx'; +import type {ImportedSpreadsheet, Policy, PolicyCategories, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report, TransactionViolations} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -976,7 +966,7 @@ function renamePolicyTagList( const policyTagLists = policyTags ? Object.keys(policyTags ?? {}).reduce((acc, tagName) => { if (tagName === oldName) { - return acc; + return acc; } acc[tagName] = {...policyTags?.[tagName]}; return acc; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 151c4da48c165..fff4cd954f3a8 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -101,10 +101,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); - + const onDisabledOrganizeSwitchPress = useCallback(() => { if (!hasAccountingConnection) { return; diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index d6d08bdbe2341..8b96991216e35 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -70,7 +70,14 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { const updateTagListName = useCallback( (values: FormOnyxValues) => { if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { - renamePolicyTagList(route.params.policyID, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, policyTags, route.params.orderWeight, policyCategories, allTransactionViolations); + renamePolicyTagList( + route.params.policyID, + {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, + policyTags, + route.params.orderWeight, + policyCategories, + allTransactionViolations, + ); } goBackToTagsSettings(); }, From 6be1636ae832f74cc006eb9c06effe2b003ef4b7 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sun, 6 Jul 2025 23:49:06 +0300 Subject: [PATCH 0006/1005] category tests --- src/libs/ReportUtils.ts | 76 ++++++++++++++++++++++++----------- tests/unit/ReportUtilsTest.ts | 75 +++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 25 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 139e83a81a3fd..f22f18ff686c9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1840,53 +1840,83 @@ function pushTransactionViolationsOnyxData( policyCategoriesUpdate: Record> = {}, policyTagListsUpdate: Record> = {}, ): OnyxData { - if (isEmptyObject(policyUpdate) && (isEmptyObject(policyCategoriesUpdate) || isEmptyObject(policyTagListsUpdate))) { + if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { return onyxData; } - const optimisticPolicyTagLists = Object.keys(policyTagLists).reduce((acc, tagName) => { - acc[tagName] = {...policyTagLists[tagName], ...(policyTagListsUpdate?.[tagName] ?? {})}; - return acc; - }, {}); + const optimisticPolicyTagLists = isEmptyObject(policyTagListsUpdate) + ? policyTagLists + : Object.keys(policyTagLists).reduce((acc, tagName) => { + acc[tagName] = {...policyTagLists[tagName], ...(policyTagListsUpdate?.[tagName] ?? {})}; + return acc; + }, {}); - const optimisticPolicyCategories = Object.keys(policyCategories).reduce((acc, categoryName) => { - acc[categoryName] = {...policyCategories[categoryName], ...(policyCategoriesUpdate?.[categoryName] ?? {})}; - return acc; - }, {}); + const optimisticPolicyCategories = isEmptyObject(policyCategoriesUpdate) + ? policyCategories + : Object.keys(policyCategories).reduce((acc, categoryName) => { + acc[categoryName] = {...policyCategories[categoryName], ...(policyCategoriesUpdate?.[categoryName] ?? {})}; + return acc; + }, {}); const optimisticPolicy = {...allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ...policyUpdate} as Policy; const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); - getAllPolicyReports(policyID).forEach((report) => { + const processedTransactionIDs = new Set(); + const optimisticData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + const reports = getAllPolicyReports(policyID); + + if (!reports || reports.length === 0) { + return onyxData; + } + + reports.forEach((report) => { if (!report?.reportID) { return; } - const isReportAnInvoice = isInvoiceReport(report); + const isInvoice = isInvoiceReport(report); + const transactions = getReportTransactions(report.reportID) ?? []; - getReportTransactions(report.reportID).forEach((transaction: Transaction) => { - const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]; + transactions.forEach((transaction) => { + const transactionID = transaction?.transactionID; + if (!transactionID || processedTransactionIDs.has(transactionID)) { + return; + } + + processedTransactionIDs.add(transactionID); + + const existingViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const optimisticTransactionViolations = ViolationsUtils.getViolationsOnyxData( + const optimisticViolations = ViolationsUtils.getViolationsOnyxData( transaction, - transactionViolations ?? [], + existingViolations ?? [], optimisticPolicy, optimisticPolicyTagLists, optimisticPolicyCategories, hasDependentTags, - isReportAnInvoice, + isInvoice, ); - if (optimisticTransactionViolations) { - onyxData?.optimisticData?.push(optimisticTransactionViolations); - onyxData?.failureData?.push({ + if (optimisticViolations) { + optimisticData.push(optimisticViolations); + failureData.push({ onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, - value: transactionViolations ?? null, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: existingViolations ?? null, }); } }); }); - return onyxData; + + if (optimisticData.length === 0) { + return onyxData; + } + return { + ...onyxData, + optimisticData: optimisticData.length > 0 ? [...(onyxData?.optimisticData ?? []), ...optimisticData] : onyxData.optimisticData, + failureData: failureData.length > 0 ? [...(onyxData?.failureData ?? []), ...failureData] : onyxData.failureData, + }; } /** @@ -11557,4 +11587,4 @@ export type { MissingPaymentMethod, OptimisticNewReport, SelfDMParameters, -}; +}; \ No newline at end of file diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index bd9940a82f880..82413cb12d71d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -54,6 +54,7 @@ import { isReportOutstanding, parseReportRouteParams, prepareOnboardingOnyxData, + pushTransactionViolationsOnyxData, requiresAttentionFromCurrentUser, shouldDisableRename, shouldDisableThread, @@ -70,10 +71,13 @@ import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployee import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {Participant} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; -import {chatReportR14932 as mockedChatReport} from '../../__mocks__/reportData/reports'; +import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; +import {chatReportR14932 as mockedChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; +import {transactionR14932 as mockTransaction} from '../../__mocks__/reportData/transactions'; import * as NumberUtils from '../../src/libs/NumberUtils'; import {convertedInvoiceChat} from '../data/Invoice'; import createRandomPolicy from '../utils/collections/policies'; +import createRandomPolicyCategories from '../utils/collections/policyCategory'; import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; @@ -3605,4 +3609,71 @@ describe('ReportUtils', () => { expect(result).toBe(false); }); }); -}); + + describe('pushTransactionViolationsOnyxData', () => { + it('should push category violation to the Onyx data when category is pending deletion', async () => { + const fakePolicyID = '123'; + + // Given policy categories and first is pending deletion + const fakePolicyCategories = createRandomPolicyCategories(3); + const fakePolicyCategoryNameToDelete = Object.keys(fakePolicyCategories).at(0) ?? ''; + const fakePolicyCategoriesUpdate = { + [fakePolicyCategoryNameToDelete]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + enabled: false, + }, + }; + + const fakePolicy = { + ...createRandomPolicy(0), + id: fakePolicyID, + requiresCategory: true, + areCategoriesEnabled: true, + }; + + const fakePolicyReports: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, Report> = { + [`${ONYXKEYS.COLLECTION.REPORT}${mockIOUReport.reportID}`]: { + ...mockIOUReport, + policyID: fakePolicyID, + }, + [`${ONYXKEYS.COLLECTION.REPORT}${mockedChatReport.reportID}`]: { + ...mockedChatReport, + policyID: fakePolicyID, + }, + }; + + await Onyx.multiSet({ + ...fakePolicyReports, + [`${ONYXKEYS.COLLECTION.POLICY}${fakePolicyID}`]: fakePolicy, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { + [mockIOUAction.reportActionID]: mockIOUAction, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${mockTransaction.transactionID}`]: { + ...mockTransaction, + policyID: fakePolicyID, + category: fakePolicyCategoryNameToDelete, + }, + }); + + const {optimisticData, failureData} = pushTransactionViolationsOnyxData({}, fakePolicyID, fakePolicyCategories, {}, {}, {}, fakePolicyCategoriesUpdate, {}); + + // Checking if one onyx update is in optimisticData for the transaction violations + expect(optimisticData?.at(0)).toMatchObject({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, + value: [ + { + name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, + type: CONST.VIOLATION_TYPES.VIOLATION, + }, + ], + }); + + expect(failureData?.at(0)).toMatchObject({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, + value: null, + }); + }); + }); +}); \ No newline at end of file From 2e1a2329ae22f62b8f797262051fcf90a6eeeab6 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 03:06:17 +0300 Subject: [PATCH 0007/1005] tags test --- src/libs/ReportUtils.ts | 50 ++++++++----- src/libs/actions/Policy/Category.ts | 36 ++++----- src/libs/actions/Policy/Tag.ts | 54 +++++++------- .../request/step/IOURequestStepCategory.tsx | 2 +- .../workspace/WorkspaceMoreFeaturesPage.tsx | 6 +- .../categories/CategorySettingsPage.tsx | 9 ++- .../categories/WorkspaceCategoriesPage.tsx | 20 +++-- .../WorkspaceCategoriesSettingsPage.tsx | 5 +- src/pages/workspace/tags/TagSettingsPage.tsx | 4 +- .../workspace/tags/WorkspaceEditTagsPage.tsx | 8 +- .../workspace/tags/WorkspaceTagsPage.tsx | 10 +-- .../workspace/tags/WorkspaceViewTagsPage.tsx | 8 +- tests/unit/ReportUtilsTest.ts | 73 +++++++++++++------ 13 files changed, 168 insertions(+), 117 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 914b2e2f936ac..31e6ec61b29e3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1823,7 +1823,7 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { */ function pushTransactionViolationsOnyxData( onyxData: OnyxData, - policyID: string, + policy: Policy, policyCategories: PolicyCategories, policyTagLists: PolicyTagLists, allTransactionViolations: OnyxCollection, @@ -1834,33 +1834,47 @@ function pushTransactionViolationsOnyxData( if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { return onyxData; } + + const reports = getAllPolicyReports(policy.id); + + if (!reports || reports.length === 0) { + return onyxData; + } + const optimisticPolicyTagLists = isEmptyObject(policyTagListsUpdate) ? policyTagLists - : Object.keys(policyTagLists).reduce((acc, tagName) => { - acc[tagName] = {...policyTagLists[tagName], ...(policyTagListsUpdate?.[tagName] ?? {})}; - return acc; - }, {}); + : { + ...policyTagLists, + ...Object.keys(policyTagListsUpdate).reduce((acc, tagName) => { + acc[tagName] = { + ...(policyTagLists?.[tagName] ?? {}), + tags: { + ...(policyTagLists?.[tagName].tags ?? {}), + ...policyTagListsUpdate[tagName].tags, + }, + }; + return acc; + }, {}), + }; const optimisticPolicyCategories = isEmptyObject(policyCategoriesUpdate) ? policyCategories - : Object.keys(policyCategories).reduce((acc, categoryName) => { - acc[categoryName] = {...policyCategories[categoryName], ...(policyCategoriesUpdate?.[categoryName] ?? {})}; - return acc; - }, {}); - - const optimisticPolicy = {...allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ...policyUpdate} as Policy; + : { + ...policyCategories, + ...Object.keys(policyCategoriesUpdate).reduce((acc, categoryName) => { + acc[categoryName] = {...(policyCategories?.[categoryName] ?? {}), ...(policyCategoriesUpdate?.[categoryName] ?? {})}; + return acc; + }, {}), + }; + + const optimisticPolicy = {...policy, ...policyUpdate}; const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); const processedTransactionIDs = new Set(); const optimisticData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; - const reports = getAllPolicyReports(policyID); - - if (!reports || reports.length === 0) { - return onyxData; - } - + // Iterate through all reports to find transactions that need optimistic violations reports.forEach((report) => { if (!report?.reportID) { return; @@ -11526,4 +11540,4 @@ export type { MissingPaymentMethod, OptimisticNewReport, SelfDMParameters, -}; \ No newline at end of file +}; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 88d5797fa4269..1b82ff0a6d7a4 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -302,11 +302,12 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category } function setWorkspaceCategoryEnabled( - policyID: string, + policy: Policy, categoriesToUpdate: Record, policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; const optimisticPolicyCategoriesData = { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { @@ -372,7 +373,7 @@ function setWorkspaceCategoryEnabled( ], }; - pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, {}, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, allTransactionViolations, {}, optimisticPolicyCategoriesData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -867,13 +868,9 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData); } -function setWorkspaceRequiresCategory( - policyID: string, - requiresCategory: boolean, - policyTagLists: PolicyTagLists = {}, - allTransactionViolations: OnyxCollection = {}, -) { - const policyUpdate = { +function setWorkspaceRequiresCategory(policy: Policy, requiresCategory: boolean, policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}) { + const policyID = policy.id; + const optimisticPolicyData = { requiresCategory, errors: { requiresCategory: null, @@ -888,7 +885,7 @@ function setWorkspaceRequiresCategory( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: policyUpdate, + value: optimisticPolicyData, }, ], successData: [ @@ -922,11 +919,11 @@ function setWorkspaceRequiresCategory( pushTransactionViolationsOnyxData( onyxData, - policyID, + policy, allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, policyTagLists, allTransactionViolations, - policyUpdate as Partial, + optimisticPolicyData as Partial, ); const parameters = { policyID, @@ -957,12 +954,8 @@ function clearCategoryErrors(policyID: string, categoryName: string) { }); } -function deleteWorkspaceCategories( - policyID: string, - categoryNamesToDelete: string[], - policyTagLists: PolicyTagLists = {}, - transactionViolations: OnyxCollection = {}, -) { +function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string[], policyTagLists: PolicyTagLists = {}, transactionViolations: OnyxCollection = {}) { + const policyID = policy.id; const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; const optimisticPolicyCategoriesData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; @@ -1008,7 +1001,7 @@ function deleteWorkspaceCategories( }; const optimisticPolicyData: Partial = shouldDisableRequiresCategory ? {requiresCategory: false} : {}; - pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -1020,12 +1013,13 @@ function deleteWorkspaceCategories( } function enablePolicyCategories( - policyID: string, + policy: Policy, enabled: boolean, policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}, shouldGoBack = true, ) { + const policyID = policy.id; const onyxUpdatesToDisableCategories: OnyxUpdate[] = []; if (!enabled) { onyxUpdatesToDisableCategories.push( @@ -1102,7 +1096,7 @@ function enablePolicyCategories( Object.entries(policyCategories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]), ); - pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); + pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); if (onyxUpdatesToDisableCategories.length > 0) { onyxData.optimisticData?.push(...onyxUpdatesToDisableCategories); diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 7e3337e1cf0fa..e16eb4c6e1995 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -232,12 +232,13 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { } function setWorkspaceTagEnabled( - policyID: string, + policy: Policy, tagsToUpdate: Record, tagListIndex: number, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTag = PolicyUtils.getTagLists(policyTags)?.at(tagListIndex); @@ -327,7 +328,7 @@ function setWorkspaceTagEnabled( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTags, transactionViolations, {}, {}, optimisticPolicyTagsData); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTags, transactionViolations, {}, {}, optimisticPolicyTagsData); const parameters: SetPolicyTagsEnabled = { policyID, @@ -339,13 +340,14 @@ function setWorkspaceTagEnabled( } function setWorkspaceTagRequired( - policyID: string, + policy: Policy, tagListIndexes: number[], isRequired: boolean, policyTagsLists: PolicyTagLists = {}, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; if (!policyTags) { return; @@ -423,7 +425,7 @@ function setWorkspaceTagRequired( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagsLists, transactionViolations, {}, {}, optimisticPolicyTagsData); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagsLists, transactionViolations, {}, {}, optimisticPolicyTagsData); const parameters: SetPolicyTagListsRequired = { policyID, @@ -434,7 +436,8 @@ function setWorkspaceTagRequired( API.write(WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED, parameters, onyxData); } -function deletePolicyTags(policyID: string, tagsToDelete: string[], policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { +function deletePolicyTags(policy: Policy, tagsToDelete: string[], policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyID = policy.id; const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTag = PolicyUtils.getTagLists(policyTagLists)?.at(0); @@ -499,7 +502,7 @@ function deletePolicyTags(policyID: string, tagsToDelete: string[], policyCatego ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, {}, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, {}, {}, policyTagsUpdate); const parameters = { policyID, @@ -578,16 +581,14 @@ function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { } function renamePolicyTag( - policyID: string, + policy: Policy, policyTag: {oldName: string; newName: string}, tagListIndex: number, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation - const policy = PolicyUtils.getPolicy(policyID); const tagList = PolicyUtils.getTagLists(policyTagLists)?.at(tagListIndex); if (!tagList) { return; @@ -696,7 +697,7 @@ function renamePolicyTag( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, policyTagsUpdate); const parameters: RenamePolicyTagsParams = { policyID, @@ -708,7 +709,8 @@ function renamePolicyTag( API.write(WRITE_COMMANDS.RENAME_POLICY_TAG, parameters, onyxData); } -function enablePolicyTags(policyID: string, enabled: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { +function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyID = policy.id; const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyUpdate = { areTagsEnabled: enabled, @@ -769,7 +771,7 @@ function enablePolicyTags(policyID: string, enabled: boolean, policyCategories: key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, defaultTagList); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, defaultTagList); } else if (!enabled) { const policyTag = PolicyUtils.getTagLists(policyTagLists).at(0); @@ -805,16 +807,7 @@ function enablePolicyTags(policyID: string, enabled: boolean, policyCategories: }, ); - ReportUtils.pushTransactionViolationsOnyxData( - onyxData, - policyID, - policyCategories, - policyTagLists, - transactionViolations, - {...policyUpdate, requiresTag: false}, - {}, - policyTagsUpdate, - ); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, {...policyUpdate, requiresTag: false}, {}, policyTagsUpdate); } const parameters: EnablePolicyTagsParams = {policyID, enabled}; @@ -913,13 +906,14 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet } function renamePolicyTagList( - policyID: string, + policy: Policy, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const newName = policyTagListName.newName; const oldName = policyTagListName.oldName; const oldPolicyTags = policyTags?.[oldName] ?? {}; @@ -972,7 +966,7 @@ function renamePolicyTagList( ReportUtils.pushTransactionViolationsOnyxData( onyxData, - policyID, + policy, policyCategories, policyTagLists, transactionViolations, @@ -991,7 +985,8 @@ function renamePolicyTagList( API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData); } -function setPolicyRequiresTag(policyID: string, requiresTag: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { +function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { + const policyID = policy.id; const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyUpdate: Partial = { requiresTag, @@ -1048,7 +1043,7 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean, policyCate }, {}), }); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, getUpdatedTagsData(requiresTag)); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, getUpdatedTagsData(requiresTag)); const getUpdatedTagsOnyxData = (required: boolean): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, @@ -1069,13 +1064,14 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean, policyCate } function setPolicyTagsRequired( - policyID: string, + policy: Policy, requiresTag: boolean, tagListIndex: number, policyTagsLists: PolicyTagLists = {}, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}, ) { + const policyID = policy.id; const policyTag = PolicyUtils.getTagLists(policyTagsLists)?.at(tagListIndex); if (!policyTag) { @@ -1136,7 +1132,7 @@ function setPolicyTagsRequired( requireTagList: requiresTag, }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyID, policyCategories, policyTagsLists, transactionViolations, {}, {}, policyTagListsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagsLists, transactionViolations, {}, {}, policyTagListsUpdate); API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData); } diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 73fd445538284..d8b3d843aef25 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -198,7 +198,7 @@ function IOURequestStepCategory({ } if (!policy.areCategoriesEnabled) { - enablePolicyCategories(policy.id, true, policyTagLists, allTransactionViolations, false); + enablePolicyCategories(policy, true, policyTagLists, allTransactionViolations, false); } InteractionManager.runAfterInteractions(() => { Navigation.navigate( diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 0c038b129a4d1..f90948ac09dbe 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -250,10 +250,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabledAction: onDisabledOrganizeSwitchPress, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { - if (!policyID) { + if (policy === undefined) { return; } - enablePolicyCategories(policyID, isEnabled, policyTagLists, allTransactionViolations, true); + enablePolicyCategories(policy, isEnabled, policyTagLists, allTransactionViolations, true); }, }, { @@ -268,7 +268,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - enablePolicyTags(policyID, isEnabled, policyCategories, allTransactionViolations); + enablePolicyTags(policy, isEnabled, policyCategories, allTransactionViolations); }, }, { diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 4049d1e3d2162..978cb9c528f23 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -128,7 +128,10 @@ function CategorySettingsPage({ setIsCannotDeleteOrDisableLastCategoryModalVisible(true); return; } - setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}, policyTagLists, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceCategoryEnabled(policy, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}, policyTagLists, allTransactionViolations); }; const navigateToEditCategory = () => { @@ -138,7 +141,9 @@ function CategorySettingsPage({ }; const deleteCategory = () => { - deleteWorkspaceCategories(policyID, [categoryName], policyTagLists, allTransactionViolations); + if (policy !== undefined) { + deleteWorkspaceCategories(policy, [categoryName], policyTagLists, allTransactionViolations); + } setDeleteCategoryConfirmModalVisible(false); navigateBack(); }; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 9cbd79292ff96..623bd852b94b7 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -117,9 +117,11 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const updateWorkspaceCategoryEnabled = useCallback( (value: boolean, categoryName: string) => { - setWorkspaceCategoryEnabled(policyId, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations); + if (policy !== undefined) { + setWorkspaceCategoryEnabled(policy, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations); + } }, - [policyId, policyTagLists, allTransactionViolations], + [policy, policyTagLists, allTransactionViolations], ); const categoryList = useMemo(() => { @@ -227,7 +229,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }; const handleDeleteCategories = () => { - deleteWorkspaceCategories(policyId, selectedCategories, policyTagLists, allTransactionViolations); + if (policy !== undefined && selectedCategories.length >= 0) { + deleteWorkspaceCategories(policy, selectedCategories, policyTagLists, allTransactionViolations); + } setDeleteCategoriesConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -331,7 +335,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return; } setSelectedCategories([]); - setWorkspaceCategoryEnabled(policyId, categoriesToDisable, policyTagLists, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceCategoryEnabled(policy, categoriesToDisable, policyTagLists, allTransactionViolations); }, }); } @@ -353,7 +360,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedCategories([]); - setWorkspaceCategoryEnabled(policyId, categoriesToEnable, policyTagLists, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceCategoryEnabled(policy, categoriesToEnable, policyTagLists, allTransactionViolations); }, }); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 7025a640453d2..d232c607e0111 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -52,7 +52,10 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet isConnectedToAccounting && currentConnectionName ? translate('workspace.categories.needCategoryForExportToIntegration', {connectionName: currentConnectionName}) : undefined; const updateWorkspaceRequiresCategory = (value: boolean) => { - setWorkspaceRequiresCategory(policyID, value, policyTagLists, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceRequiresCategory(policy, value, policyTagLists, allTransactionViolations); }; const {sections} = useMemo(() => { diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index c3706b4bde8ac..03a3d5e0d0987 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -80,7 +80,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - deletePolicyTags(policyID, [currentPolicyTag.name], policyCategories, allTransactionViolations); + deletePolicyTags(policy, [currentPolicyTag.name], policyCategories, allTransactionViolations); setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; @@ -90,7 +90,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { setIsCannotDeleteOrDisableLastTagModalVisible(true); return; } - setWorkspaceTagEnabled(policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); + setWorkspaceTagEnabled(policy, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; const navigateToEditTag = () => { diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 615d49ed517ff..d5f70f04c82a4 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -9,6 +9,7 @@ import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -27,6 +28,7 @@ type WorkspaceEditTagsPageProps = | PlatformStackScreenProps; function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { + const policy = usePolicy(route.params.policyID); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`, {canBeMissing: true}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`, {canBeMissing: true}); const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); @@ -69,9 +71,9 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { const updateTagListName = useCallback( (values: FormOnyxValues) => { - if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { + if (policy !== undefined && values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { renamePolicyTagList( - route.params.policyID, + policy, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, policyTags, route.params.orderWeight, @@ -81,7 +83,7 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { } goBackToTagsSettings(); }, - [tagListName, goBackToTagsSettings, route.params.policyID, route.params.orderWeight, policyTags], + [tagListName, goBackToTagsSettings, route.params.policyID, route.params.orderWeight, policy, policyTags, policyCategories, allTransactionViolations], ); return ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 2061425cc1789..f8fd8d97c26d9 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -161,9 +161,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, 0); + setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, 0); }, - [policyID], + [policy], ); const updateWorkspaceRequiresTag = useCallback( @@ -331,7 +331,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - deletePolicyTags(policyID, selectedTags, policyCategories, allTransactionViolations); + deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -486,7 +486,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToDisable, 0); + setWorkspaceTagEnabled(policy, tagsToDisable, 0); }, }); } @@ -498,7 +498,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToEnable, 0); + setWorkspaceTagEnabled(policy, tagsToEnable, 0); }, }); } diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 8e132ae4982b8..d89728d9da8df 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -117,7 +117,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); }, [policyID, route.params.orderWeight], ); @@ -215,7 +215,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const deleteTags = () => { - deletePolicyTags(policyID, selectedTags, policyCategories, allTransactionViolations); + deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -285,7 +285,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToDisable, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policy, tagsToDisable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); } @@ -297,7 +297,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags([]); - setWorkspaceTagEnabled(policyID, tagsToEnable, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policy, tagsToEnable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); } diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index c08712bf1a840..0f658c058786f 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -69,8 +69,8 @@ import {buildOptimisticTransaction} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployeeList, Report, ReportAction, ReportNameValuePairs, Transaction} from '@src/types/onyx'; -import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; +import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployeeList, PolicyTag, Report, ReportAction, ReportNameValuePairs, Transaction} from '@src/types/onyx'; +import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {Participant} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; @@ -80,8 +80,7 @@ import * as NumberUtils from '../../src/libs/NumberUtils'; import {convertedInvoiceChat} from '../data/Invoice'; import createRandomPolicy from '../utils/collections/policies'; import createRandomPolicyCategories from '../utils/collections/policyCategory'; -import createRandomReportAction from '../utils/collections/reportActions'; -import createRandomReport from '../utils/collections/reports'; +import createRandomPolicyTags from '../utils/collections/policyTags'; import createRandomReportAction, {getRandomDate} from '../utils/collections/reportActions'; import { createAdminRoom, @@ -3929,10 +3928,8 @@ describe('ReportUtils', () => { }); describe('pushTransactionViolationsOnyxData', () => { - it('should push category violation to the Onyx data when category is pending deletion', async () => { - const fakePolicyID = '123'; - - // Given policy categories and first is pending deletion + it('should push category violation to the Onyx data when category and tag is pending deletion', async () => { + // Given policy categories, the first is pending deletion const fakePolicyCategories = createRandomPolicyCategories(3); const fakePolicyCategoryNameToDelete = Object.keys(fakePolicyCategories).at(0) ?? ''; const fakePolicyCategoriesUpdate = { @@ -3942,9 +3939,28 @@ describe('ReportUtils', () => { }, }; + // Given policy tags, the first is pending deletion + const fakePolicyTagListName = 'Tag List'; + const fakePolicyTagsLists = createRandomPolicyTags(fakePolicyTagListName, 3); + const fakePolicyTagsToDelete = Object.entries(fakePolicyTagsLists?.[fakePolicyTagListName]?.tags ?? {}).slice(1, 2); + const fakePolicyTagListsUpdate: Record>>> = { + [fakePolicyTagListName]: { + tags: { + ...fakePolicyTagsToDelete.reduce>>>((acc, [tagName]) => { + acc[tagName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; + return acc; + }, {}), + }, + }, + }; + + const fakePolicyID = '0'; + const fakePolicy = { ...createRandomPolicy(0), id: fakePolicyID, + requiresTag: true, + areTagsEnabled: true, requiresCategory: true, areCategoriesEnabled: true, }; @@ -3960,9 +3976,9 @@ describe('ReportUtils', () => { }, }; + // Popluating Onyx with required data await Onyx.multiSet({ ...fakePolicyReports, - [`${ONYXKEYS.COLLECTION.POLICY}${fakePolicyID}`]: fakePolicy, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { [mockIOUAction.reportActionID]: mockIOUAction, }, @@ -3970,27 +3986,38 @@ describe('ReportUtils', () => { ...mockTransaction, policyID: fakePolicyID, category: fakePolicyCategoryNameToDelete, + tag: fakePolicyTagsToDelete.at(0)?.[0] ?? '', }, }); - const {optimisticData, failureData} = pushTransactionViolationsOnyxData({}, fakePolicyID, fakePolicyCategories, {}, {}, {}, fakePolicyCategoriesUpdate, {}); - - // Checking if one onyx update is in optimisticData for the transaction violations - expect(optimisticData?.at(0)).toMatchObject({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, - value: [ + expect(pushTransactionViolationsOnyxData({}, fakePolicy, fakePolicyCategories, fakePolicyTagsLists, {}, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate)).toMatchObject({ + // Expecting the optimistic data to contain the OUT_OF_POLICY violations for the deleted category and tag + optimisticData: [ { - name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, - type: CONST.VIOLATION_TYPES.VIOLATION, + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, + value: [ + { + name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, + type: CONST.VIOLATION_TYPES.VIOLATION, + }, + + { + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + type: CONST.VIOLATION_TYPES.VIOLATION, + }, + ], }, ], - }); - expect(failureData?.at(0)).toMatchObject({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, - value: null, + // Expecting the failure data to clear the violations. + failureData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mockTransaction.transactionID}`, + value: null, + }, + ], }); }); }); From f3d2be841c67ed3bf9cd31b1718d1cb632ab26dd Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 03:27:26 +0300 Subject: [PATCH 0008/1005] policy from usePolicy --- .../categories/WorkspaceCategoriesPage.tsx | 5 ++-- src/pages/workspace/tags/EditTagPage.tsx | 8 ++++--- src/pages/workspace/tags/TagSettingsPage.tsx | 7 +++++- .../workspace/tags/WorkspaceTagsPage.tsx | 22 ++++++++++++++--- .../tags/WorkspaceTagsSettingsPage.tsx | 9 +++++-- .../workspace/tags/WorkspaceViewTagsPage.tsx | 24 +++++++++++++++---- tests/actions/PolicyCategoryTest.ts | 6 ++--- tests/actions/PolicyTagTest.ts | 24 +++++++++---------- 8 files changed, 75 insertions(+), 30 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 623bd852b94b7..4028795ded788 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -117,9 +117,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const updateWorkspaceCategoryEnabled = useCallback( (value: boolean, categoryName: string) => { - if (policy !== undefined) { - setWorkspaceCategoryEnabled(policy, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations); + if (policy === undefined) { + return; } + setWorkspaceCategoryEnabled(policy, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations); }, [policy, policyTagLists, allTransactionViolations], ); diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index c6b8adf172e52..b0bef36f994c5 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -29,6 +30,7 @@ type EditTagPageProps = function EditTagPage({route}: EditTagPageProps) { const policyID = route.params.policyID; + const policy = usePolicy(policyID); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); @@ -65,8 +67,8 @@ function EditTagPage({route}: EditTagPageProps) { (values: FormOnyxValues) => { const tagName = values.tagName.trim(); // Do not call the API if the edited tag name is the same as the current tag name - if (currentTagName !== tagName) { - renamePolicyTag(policyID, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight, policyCategories, allTransactionViolations); + if (policy !== undefined && currentTagName !== tagName) { + renamePolicyTag(policy, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight, policyCategories, allTransactionViolations); } Keyboard.dismiss(); Navigation.goBack( @@ -75,7 +77,7 @@ function EditTagPage({route}: EditTagPageProps) { : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, route.params.tagName), ); }, - [currentTagName, policyID, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], + [currentTagName, policy, policyID, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 03a3d5e0d0987..c3f88326b4b2b 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -80,7 +80,9 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - deletePolicyTags(policy, [currentPolicyTag.name], policyCategories, allTransactionViolations); + if (policy !== undefined){ + deletePolicyTags(policy, [currentPolicyTag.name], policyCategories, allTransactionViolations); + } setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; @@ -90,6 +92,9 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { setIsCannotDeleteOrDisableLastTagModalVisible(true); return; } + if (policy === undefined){ + return; + } setWorkspaceTagEnabled(policy, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index f8fd8d97c26d9..ce67eeae0a8cf 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -331,7 +331,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); + if (policy !== undefined && selectedTags.length > 0) { + deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); + } setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -486,6 +488,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } setSelectedTags([]); + if (policy === undefined) { + return; + } + // Disable the selected tags setWorkspaceTagEnabled(policy, tagsToDisable, 0); }, }); @@ -498,6 +504,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags([]); + if (policy === undefined) { + return; + } + // Enable the selected tags setWorkspaceTagEnabled(policy, tagsToEnable, 0); }, }); @@ -530,7 +540,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } setSelectedTags([]); - setWorkspaceTagRequired(policyID, tagListIndexesToMarkOptional, false, policyTags, policyCategories, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceTagRequired(policy, tagListIndexesToMarkOptional, false, policyTags, policyCategories, allTransactionViolations); }, }); } @@ -542,7 +555,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.NOT_REQUIRED, onSelected: () => { setSelectedTags([]); - setWorkspaceTagRequired(policyID, tagListIndexesToMarkRequired, true, policyTags, policyCategories, allTransactionViolations); + if (policy === undefined) { + return; + } + setWorkspaceTagRequired(policy, tagListIndexesToMarkRequired, true, policyTags, policyCategories, allTransactionViolations); }, }); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 6f7aa12ac183e..47ab821e17d62 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -25,6 +25,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; +import usePolicy from '@hooks/usePolicy'; type WorkspaceTagsSettingsPageProps = | PlatformStackScreenProps @@ -55,6 +56,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const backTo = route.params.backTo; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const styles = useThemeStyles(); + const policy = usePolicy(policyID); const {translate} = useLocalize(); const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; @@ -62,9 +64,12 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); const updateWorkspaceRequiresTag = useCallback( (value: boolean) => { - setPolicyRequiresTag(policyID, value); + if (policy === undefined) { + return; + } + setPolicyRequiresTag(policy, value); }, - [policyID], + [policy], ); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_SETTINGS; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index d89728d9da8df..68da63598cb3c 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -117,6 +117,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { + if (policy === undefined){ + return; + } setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); }, [policyID, route.params.orderWeight], @@ -215,7 +218,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const deleteTags = () => { - deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); + if (policy !== undefined && selectedTags.length > 0) { + deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); + } setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -285,6 +290,10 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } setSelectedTags([]); + if (policy === undefined) { + return; + } + // Disable the selected tags setWorkspaceTagEnabled(policy, tagsToDisable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); @@ -297,6 +306,10 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags([]); + if (policy === undefined) { + return; + } + // Enable the selected tags setWorkspaceTagEnabled(policy, tagsToEnable, route.params.orderWeight, policyCategories, allTransactionViolations); }, }); @@ -317,8 +330,8 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { ); }; - if (!!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) { - setPolicyTagsRequired(policyID, false, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); + if (policy !== undefined && !!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) { + setPolicyTagsRequired(policy, false, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); } const navigateToEditTag = () => { @@ -378,7 +391,10 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } - setPolicyTagsRequired(policyID, on, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); + if (policy === undefined) { + return; + } + setPolicyTagsRequired(policy, on, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); }} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 351adaf8e982c..1de13a9254302 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -31,7 +31,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - Category.setWorkspaceRequiresCategory(fakePolicy.id, true); + Category.setWorkspaceRequiresCategory(fakePolicy, true); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ @@ -169,7 +169,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.setWorkspaceCategoryEnabled(fakePolicy.id, categoriesToUpdate); + Category.setWorkspaceCategoryEnabled(fakePolicy, categoriesToUpdate); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ @@ -214,7 +214,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.deleteWorkspaceCategories(fakePolicy.id, categoriesToDelete); + Category.deleteWorkspaceCategories(fakePolicy, categoriesToDelete); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index f590f3be27b85..4a629529646f7 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -34,7 +34,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - setPolicyRequiresTag(fakePolicy.id, true); + setPolicyRequiresTag(fakePolicy, true); return waitForBatchedUpdates(); }) .then( @@ -81,7 +81,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - setPolicyRequiresTag(fakePolicy.id, false); + setPolicyRequiresTag(fakePolicy, false); return waitForBatchedUpdates(); }) .then( @@ -129,7 +129,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { mockFetch?.fail?.(); - setPolicyRequiresTag(fakePolicy.id, false); + setPolicyRequiresTag(fakePolicy, false); return waitForBatchedUpdates(); }) @@ -162,7 +162,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - setPolicyRequiresTag(fakePolicy.id, true); + setPolicyRequiresTag(fakePolicy, true); await waitForBatchedUpdates(); let updatePolicyTags: PolicyTagLists | undefined; @@ -193,7 +193,7 @@ describe('actions/Policy', () => { }) .then(() => { renamePolicyTagList( - fakePolicy.id, + fakePolicy, { oldName: oldTagListName, newName: newTagListName, @@ -261,7 +261,7 @@ describe('actions/Policy', () => { mockFetch?.fail?.(); renamePolicyTagList( - fakePolicy.id, + fakePolicy, { oldName: oldTagListName, newName: newTagListName, @@ -419,7 +419,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate, 0); + setWorkspaceTagEnabled(fakePolicy, tagsToUpdate, 0); return waitForBatchedUpdates(); }) .then( @@ -492,7 +492,7 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate, 0); + setWorkspaceTagEnabled(fakePolicy, tagsToUpdate, 0); return waitForBatchedUpdates(); }) .then(mockFetch?.resume) @@ -539,7 +539,7 @@ describe('actions/Policy', () => { }) .then(() => { renamePolicyTag( - fakePolicy.id, + fakePolicy, { oldName: oldTagName ?? '', newName: newTagName, @@ -609,7 +609,7 @@ describe('actions/Policy', () => { mockFetch?.fail?.(); renamePolicyTag( - fakePolicy.id, + fakePolicy, { oldName: oldTagName, newName: newTagName, @@ -657,7 +657,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - deletePolicyTags(fakePolicy.id, tagsToDelete); + deletePolicyTags(fakePolicy, tagsToDelete); return waitForBatchedUpdates(); }) .then( @@ -717,7 +717,7 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - deletePolicyTags(fakePolicy.id, tagsToDelete); + deletePolicyTags(fakePolicy, tagsToDelete); return waitForBatchedUpdates(); }) .then(mockFetch?.resume) From 01f481e9f887133c214cf0add46983835b605c67 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 03:29:11 +0300 Subject: [PATCH 0009/1005] comments --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 0f658c058786f..b7617161fc9da 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -3976,7 +3976,7 @@ describe('ReportUtils', () => { }, }; - // Popluating Onyx with required data + // Populating Onyx with required data await Onyx.multiSet({ ...fakePolicyReports, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { From c7c6d828d0c82d4c2541dbb835c8329e009292e0 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 03:30:21 +0300 Subject: [PATCH 0010/1005] lint --- src/pages/workspace/tags/EditTagPage.tsx | 4 ++-- src/pages/workspace/tags/TagSettingsPage.tsx | 4 ++-- src/pages/workspace/tags/WorkspaceEditTagsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 10 ++++++++-- src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx | 10 +++++----- src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 4 ++-- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index b0bef36f994c5..39ab68e770e5b 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -8,8 +8,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; -import usePolicy from '@hooks/usePolicy'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -77,7 +77,7 @@ function EditTagPage({route}: EditTagPageProps) { : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, route.params.tagName), ); }, - [currentTagName, policy, policyID, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], + [allTransactionViolations, currentTagName, policyID, policy, policyCategories, policyTags, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index c3f88326b4b2b..8b86cc23f55e0 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -80,7 +80,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - if (policy !== undefined){ + if (policy !== undefined) { deletePolicyTags(policy, [currentPolicyTag.name], policyCategories, allTransactionViolations); } setIsDeleteTagModalOpen(false); @@ -92,7 +92,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { setIsCannotDeleteOrDisableLastTagModalVisible(true); return; } - if (policy === undefined){ + if (policy === undefined) { return; } setWorkspaceTagEnabled(policy, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index d5f70f04c82a4..9fb7c9c65c461 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -83,7 +83,7 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { } goBackToTagsSettings(); }, - [tagListName, goBackToTagsSettings, route.params.policyID, route.params.orderWeight, policy, policyTags, policyCategories, allTransactionViolations], + [tagListName, goBackToTagsSettings, route.params.orderWeight, policy, policyTags, policyCategories, allTransactionViolations], ); return ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index ce67eeae0a8cf..8c8d58f5e7e2a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -161,6 +161,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { + if (policy === undefined) { + return; + } setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, 0); }, [policy], @@ -168,9 +171,12 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceRequiresTag = useCallback( (value: boolean, orderWeight: number) => { - setPolicyTagsRequired(policyID, value, orderWeight, policyTags, policyCategories, allTransactionViolations); + if (policy === undefined) { + return; + } + setPolicyTagsRequired(policy, value, orderWeight, policyTags, policyCategories, allTransactionViolations); }, - [policyID], + [policyID, policyTags, policyCategories, allTransactionViolations], ); const tagList = useMemo(() => { diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 47ab821e17d62..ae10491eb510a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -11,6 +11,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {disableWorkspaceBillableExpenses, setPolicyBillableMode} from '@libs/actions/Policy/Policy'; import {clearPolicyTagListErrors, setPolicyRequiresTag} from '@libs/actions/Policy/Tag'; @@ -25,7 +26,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; -import usePolicy from '@hooks/usePolicy'; type WorkspaceTagsSettingsPageProps = | PlatformStackScreenProps @@ -56,7 +56,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const backTo = route.params.backTo; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const styles = useThemeStyles(); - const policy = usePolicy(policyID); + const currentPolicy = usePolicy(policyID); const {translate} = useLocalize(); const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; @@ -64,12 +64,12 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); const updateWorkspaceRequiresTag = useCallback( (value: boolean) => { - if (policy === undefined) { + if (currentPolicy === undefined) { return; } - setPolicyRequiresTag(policy, value); + setPolicyRequiresTag(currentPolicy, value); }, - [policy], + [currentPolicy], ); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_SETTINGS; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 68da63598cb3c..4ace323c156dc 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -117,12 +117,12 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policy === undefined){ + if (policy === undefined) { return; } setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); }, - [policyID, route.params.orderWeight], + [policy, route.params.orderWeight], ); const tagList = useMemo( From 7e1913326bdb31232a6477d2730542bacc798547 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 04:27:37 +0300 Subject: [PATCH 0011/1005] fixing deps warning --- src/pages/workspace/tags/EditTagPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index 39ab68e770e5b..451e11eaf2efd 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -77,7 +77,7 @@ function EditTagPage({route}: EditTagPageProps) { : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, route.params.tagName), ); }, - [allTransactionViolations, currentTagName, policyID, policy, policyCategories, policyTags, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], + [allTransactionViolations, currentTagName, policyID, policy, policyCategories, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 8c8d58f5e7e2a..0f30ec95fefd5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -176,7 +176,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { } setPolicyTagsRequired(policy, value, orderWeight, policyTags, policyCategories, allTransactionViolations); }, - [policyID, policyTags, policyCategories, allTransactionViolations], + [allTransactionViolations, policy, policyCategories, policyTags], ); const tagList = useMemo(() => { diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 4ace323c156dc..600fedf1c43ab 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -60,6 +60,9 @@ type WorkspaceViewTagsProps = | PlatformStackScreenProps; function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { + const backTo = route.params.backTo; + const policyID = route.params.policyID; + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for the small screen selection mode // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); @@ -69,14 +72,14 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const dropdownButtonRef = useRef(null); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); - const policyID = route.params.policyID; - const backTo = route.params.backTo; const policy = usePolicy(policyID); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const {selectionMode} = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); + const currentPolicyTag = policyTags?.[currentTagListName]; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW; const [isCannotMakeAllTagsOptionalModalVisible, setIsCannotMakeAllTagsOptionalModalVisible] = useState(false); @@ -114,7 +117,6 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { onNavigationCallBack: () => Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID) : undefined), }); - const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}) ?? []; const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { if (policy === undefined) { @@ -122,7 +124,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { } setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); }, - [policy, route.params.orderWeight], + [allTransactionViolations, policy, policyCategories, route.params.orderWeight], ); const tagList = useMemo( From 6700d1ac59b5d258132a349bc1a4ab6389a462d7 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 7 Jul 2025 05:07:01 +0300 Subject: [PATCH 0012/1005] prettier --- src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 600fedf1c43ab..37da77bff72c8 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -79,7 +79,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const {selectionMode} = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); - + const currentPolicyTag = policyTags?.[currentTagListName]; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW; const [isCannotMakeAllTagsOptionalModalVisible, setIsCannotMakeAllTagsOptionalModalVisible] = useState(false); From a9cca16d2b9252b7505520ebef870d867bfb3c92 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 11 Jul 2025 02:22:06 +0300 Subject: [PATCH 0013/1005] ignoring invoice reports --- src/libs/ReportUtils.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d05666c215684..3eea1d7ebd5d6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1879,14 +1879,12 @@ function pushTransactionViolationsOnyxData( // Iterate through all reports to find transactions that need optimistic violations reports.forEach((report) => { - if (!report?.reportID) { + // Skipping invoice report because they do not have category or tag violations + if (!report?.reportID || isInvoiceReport(report)) { return; } - const isInvoice = isInvoiceReport(report); - const transactions = getReportTransactions(report.reportID) ?? []; - - transactions.forEach((transaction) => { + getReportTransactions(report.reportID).forEach((transaction) => { const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { return; @@ -1903,7 +1901,7 @@ function pushTransactionViolationsOnyxData( optimisticPolicyTagLists, optimisticPolicyCategories, hasDependentTags, - isInvoice, + false, ); if (optimisticViolations) { From 0ba68300c49238c31a4049cd92497f60ba4247eb Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 11 Jul 2025 03:20:03 +0300 Subject: [PATCH 0014/1005] performance tests --- tests/perf-test/ReportUtils.perf-test.ts | 68 +++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 06a020678fafb..e0b8303b301b1 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -14,12 +14,13 @@ import { getTransactionDetails, getWorkspaceChats, getWorkspaceIcon, + pushTransactionViolationsOnyxData, shouldReportBeInOptionList, temporary_getMoneyRequestOptions, } from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Policy, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; import {chatReportR14932 as chatReport} from '../../__mocks__/reportData/reports'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; @@ -28,6 +29,8 @@ import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import createRandomPolicyCategories from 'tests/utils/collections/policyCategory'; +import createRandomPolicyTags from 'tests/utils/collections/policyTags'; const getMockedReports = (length = 500) => createCollection( @@ -198,6 +201,69 @@ describe('ReportUtils', () => { await measureFunction(() => getTransactionDetails(transaction, 'yyyy-MM-dd')); }); + + test('[ReportUtils] pushTransactionViolationsOnyxData on 1k reports with 3 transactions on each report', async () => { + // Current policy with categories and tags enabled but does not require them + const policy = { + ...createRandomPolicy(1), + areCategoriesEnabled: true, + requiresCategory: false, + areTagsEnabled: true, + requiresTag: false, + }; + + // Simulate a policy optimistic data when requires categories and tags is updated eg (setRequiresCategory) + const policyOptimisticData = { + requiresCategory: true, + requiresTag: true, + }; + + // Create a report collection with 1000 reports linked to the policy + const reportCollection = Object.values(getMockedReports(1000)).reduce>((acc, report) => { + acc[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = { + ...report, policyID: + policy.id, + }; + return acc; + }, {}); + + // Create a transaction collection with 3 transactions for each report + const transactionCollection = Object.values(reportCollection).reduce>((acc, report, index) => { + for (let transactionIndex = 0; transactionIndex < 3; transactionIndex++) { + // Unique transactionID + const transactionID = index + (transactionIndex * 1000); + // Create a transaction with no category and no tag + acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = { + ...createRandomTransaction(transactionID), + reportID: report.reportID, + category: undefined, + }; + } + return acc; + }, {}); + + await Onyx.multiSet({ + ...reportCollection, + ...transactionCollection, + [ONYXKEYS.COLLECTION.POLICY]: {[policy.id]: policy}, + }); + + const policyTags = createRandomPolicyTags('Tags', 8); + const policyCategories = createRandomPolicyCategories(8); + + await waitForBatchedUpdates(); + await measureFunction(() => pushTransactionViolationsOnyxData( + {}, + policy, + policyCategories, + policyTags, + {}, + policyOptimisticData, + {}, + {}, + )); + }); + test('[ReportUtils] getIOUReportActionDisplayMessage on 1k policies', async () => { const reportAction = { ...createRandomReportAction(1), From b9779cd3321519aa6926c438fe5a6e71a14ecd33 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 11 Jul 2025 03:59:01 +0300 Subject: [PATCH 0015/1005] mig --- tests/perf-test/ReportUtils.perf-test.ts | 52 +++++++++++++----------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index e0b8303b301b1..8159ae0a5d39a 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; +import createRandomPolicyCategories from 'tests/utils/collections/policyCategory'; +import createRandomPolicyTags from 'tests/utils/collections/policyTags'; import { canDeleteReportAction, canShowReportRecipientLocalTime, @@ -29,8 +31,6 @@ import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import createRandomPolicyCategories from 'tests/utils/collections/policyCategory'; -import createRandomPolicyTags from 'tests/utils/collections/policyTags'; const getMockedReports = (length = 500) => createCollection( @@ -201,28 +201,27 @@ describe('ReportUtils', () => { await measureFunction(() => getTransactionDetails(transaction, 'yyyy-MM-dd')); }); - test('[ReportUtils] pushTransactionViolationsOnyxData on 1k reports with 3 transactions on each report', async () => { // Current policy with categories and tags enabled but does not require them const policy = { - ...createRandomPolicy(1), - areCategoriesEnabled: true, + ...createRandomPolicy(1), + areCategoriesEnabled: true, requiresCategory: false, - areTagsEnabled: true, - requiresTag: false, + areTagsEnabled: true, + requiresTag: false, }; // Simulate a policy optimistic data when requires categories and tags is updated eg (setRequiresCategory) const policyOptimisticData = { requiresCategory: true, - requiresTag: true, + requiresTag: true, }; // Create a report collection with 1000 reports linked to the policy const reportCollection = Object.values(getMockedReports(1000)).reduce>((acc, report) => { acc[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = { - ...report, policyID: - policy.id, + ...report, + policyID: policy.id, }; return acc; }, {}); @@ -230,11 +229,10 @@ describe('ReportUtils', () => { // Create a transaction collection with 3 transactions for each report const transactionCollection = Object.values(reportCollection).reduce>((acc, report, index) => { for (let transactionIndex = 0; transactionIndex < 3; transactionIndex++) { - // Unique transactionID - const transactionID = index + (transactionIndex * 1000); + const transactionID = index + transactionIndex * 1000; // Create a transaction with no category and no tag acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = { - ...createRandomTransaction(transactionID), + ...createRandomTransaction(transactionID), reportID: report.reportID, category: undefined, }; @@ -242,9 +240,26 @@ describe('ReportUtils', () => { return acc; }, {}); + const reportActionsCollection = Object.values(transactionCollection).reduce>>((acc, transaction, index) => { + acc[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] = { + [index.toString()]: { + ...createRandomReportAction(index + 1), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUReportID: transaction.reportID, + IOUTransactionID: transaction.transactionID, + amount: transaction.amount, + currency: transaction.currency, + }, + }, + }; + return acc; + }, {}); + await Onyx.multiSet({ ...reportCollection, ...transactionCollection, + ...reportActionsCollection, [ONYXKEYS.COLLECTION.POLICY]: {[policy.id]: policy}, }); @@ -252,16 +267,7 @@ describe('ReportUtils', () => { const policyCategories = createRandomPolicyCategories(8); await waitForBatchedUpdates(); - await measureFunction(() => pushTransactionViolationsOnyxData( - {}, - policy, - policyCategories, - policyTags, - {}, - policyOptimisticData, - {}, - {}, - )); + await measureFunction(() => pushTransactionViolationsOnyxData({}, policy, policyCategories, policyTags, {}, policyOptimisticData, {}, {})); }); test('[ReportUtils] getIOUReportActionDisplayMessage on 1k policies', async () => { From d3a1a6403a4edf5783b16283cfc57668868d78b0 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 22 Jul 2025 22:51:10 +0300 Subject: [PATCH 0016/1005] refactoring --- src/libs/ReportUtils.ts | 60 ++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1689e8e7ab560..f2263cea8d48c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1826,9 +1826,14 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { /** * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. * + * @param onyxData The OnyxData object to push updates to + * @param policy The current policy + * @param policyCategories The current policy categories + * @param policyTagLists The current policy tags + * @param allTransactionViolations The policy transactions violations * @param policyUpdate Changed policy properties, if none pass empty object * @param policyCategoriesUpdate Changed categories properties, if none pass empty object - * @param policyTagListsUpdate Changed categories properties, if none pass empty object + * @param policyTagListsUpdate Changed tag properties, if none pass empty object */ function pushTransactionViolationsOnyxData( onyxData: OnyxData, @@ -1839,7 +1844,7 @@ function pushTransactionViolationsOnyxData( policyUpdate: Partial, policyCategoriesUpdate: Record> = {}, policyTagListsUpdate: Record> = {}, -): OnyxData { +) { if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { return onyxData; } @@ -1884,16 +1889,19 @@ function pushTransactionViolationsOnyxData( const failureData: OnyxUpdate[] = []; // Iterate through all reports to find transactions that need optimistic violations - reports.forEach((report) => { - // Skipping invoice report because they do not have category or tag violations + for (const report of reports) { + // Skipping invoice report because should not have any category or tag violations if (!report?.reportID || isInvoiceReport(report)) { - return; + continue; } - getReportTransactions(report.reportID).forEach((transaction) => { + const transcations = getReportTransactions(report.reportID); + + for (const transaction of transcations) { + // If transaction's optimistic violations already is pushed, skip it to ensure one duplicates update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { - return; + continue; } processedTransactionIDs.add(transactionID); @@ -1910,25 +1918,35 @@ function pushTransactionViolationsOnyxData( false, ); - if (optimisticViolations) { - optimisticData.push(optimisticViolations); - failureData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: existingViolations ?? null, - }); + if (isEmptyObject(optimisticViolations)) { + continue; } - }); - }); + optimisticData.push(optimisticViolations); + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: existingViolations ?? null, + }); + } + } + + // Excluding failure data since both optimistic data and failure data contain the same number of updates if (optimisticData.length === 0) { return onyxData; } - return { - ...onyxData, - optimisticData: optimisticData.length > 0 ? [...(onyxData?.optimisticData ?? []), ...optimisticData] : onyxData.optimisticData, - failureData: failureData.length > 0 ? [...(onyxData?.failureData ?? []), ...failureData] : onyxData.failureData, - }; + + if (!onyxData.optimisticData) { + onyxData.optimisticData = []; + } + + if (!onyxData.failureData) { + onyxData.failureData = []; + } + + // Push optimistic data and failure data to OnyxData + onyxData.optimisticData.push(...optimisticData); + onyxData.failureData.push(...failureData); } /** From 5474a34f2d08396cb123240a37179e7f7a9ab04e Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 23 Jul 2025 00:21:47 +0300 Subject: [PATCH 0017/1005] lint --- src/libs/ReportUtils.ts | 45 +++++++++++------------------------ tests/unit/ReportUtilsTest.ts | 7 +++++- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f2263cea8d48c..46cb00954c546 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1826,8 +1826,8 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { /** * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. * - * @param onyxData The OnyxData object to push updates to - * @param policy The current policy + * @param onyxData The OnyxData object to push updates to + * @param policy The current policy * @param policyCategories The current policy categories * @param policyTagLists The current policy tags * @param allTransactionViolations The policy transactions violations @@ -1842,17 +1842,17 @@ function pushTransactionViolationsOnyxData( policyTagLists: PolicyTagLists, allTransactionViolations: OnyxCollection, policyUpdate: Partial, - policyCategoriesUpdate: Record> = {}, - policyTagListsUpdate: Record> = {}, + policyCategoriesUpdate: Record>, + policyTagListsUpdate: Record>, ) { if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { - return onyxData; + return; } const reports = getAllPolicyReports(policy.id); if (!reports || reports.length === 0) { - return onyxData; + return; } const optimisticPolicyTagLists = isEmptyObject(policyTagListsUpdate) @@ -1885,20 +1885,20 @@ function pushTransactionViolationsOnyxData( const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); const processedTransactionIDs = new Set(); - const optimisticData: OnyxUpdate[] = []; - const failureData: OnyxUpdate[] = []; // Iterate through all reports to find transactions that need optimistic violations for (const report of reports) { + // Skipping invoice report because should not have any category or tag violations if (!report?.reportID || isInvoiceReport(report)) { continue; } - + const transcations = getReportTransactions(report.reportID); - for (const transaction of transcations) { - // If transaction's optimistic violations already is pushed, skip it to ensure one duplicates update per transaction + for (const transaction of transcations){ + + // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { continue; @@ -1922,31 +1922,14 @@ function pushTransactionViolationsOnyxData( continue; } - optimisticData.push(optimisticViolations); - failureData.push({ + onyxData.optimisticData?.push(optimisticViolations); + onyxData.failureData?.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, value: existingViolations ?? null, }); - } - } - - // Excluding failure data since both optimistic data and failure data contain the same number of updates - if (optimisticData.length === 0) { - return onyxData; - } - - if (!onyxData.optimisticData) { - onyxData.optimisticData = []; - } - - if (!onyxData.failureData) { - onyxData.failureData = []; + }; } - - // Push optimistic data and failure data to OnyxData - onyxData.optimisticData.push(...optimisticData); - onyxData.failureData.push(...failureData); } /** diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 4475f3ae66847..d3b3119725b85 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,6 +4,7 @@ import {renderHook} from '@testing-library/react-native'; import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {OnyxData} from '@src/types/onyx/Request'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {putOnHold} from '@libs/actions/IOU'; import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow'; @@ -4258,7 +4259,11 @@ describe('ReportUtils', () => { }, }); - expect(pushTransactionViolationsOnyxData({}, fakePolicy, fakePolicyCategories, fakePolicyTagsLists, {}, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate)).toMatchObject({ + const onyxData: OnyxData = {}; + + pushTransactionViolationsOnyxData(onyxData, fakePolicy, fakePolicyCategories, fakePolicyTagsLists, {}, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); + + expect(onyxData).toMatchObject({ // Expecting the optimistic data to contain the OUT_OF_POLICY violations for the deleted category and tag optimisticData: [ { From 19107d26bc4dfb64d9dd44f4a9a41337fe940ea6 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 23 Jul 2025 00:28:23 +0300 Subject: [PATCH 0018/1005] tests --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 46cb00954c546..7927be9eae6a9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1842,8 +1842,8 @@ function pushTransactionViolationsOnyxData( policyTagLists: PolicyTagLists, allTransactionViolations: OnyxCollection, policyUpdate: Partial, - policyCategoriesUpdate: Record>, - policyTagListsUpdate: Record>, + policyCategoriesUpdate: Record> = {}, + policyTagListsUpdate: Record> = {}, ) { if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { return; From 05ba6e3207b7bc97cf287b204b5f7b2700abcebb Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 23 Jul 2025 00:32:58 +0300 Subject: [PATCH 0019/1005] prettier.. --- src/libs/ReportUtils.ts | 12 +++++------- tests/unit/ReportUtilsTest.ts | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7927be9eae6a9..b54da77181226 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1826,8 +1826,8 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { /** * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. * - * @param onyxData The OnyxData object to push updates to - * @param policy The current policy + * @param onyxData The OnyxData object to push updates to + * @param policy The current policy * @param policyCategories The current policy categories * @param policyTagLists The current policy tags * @param allTransactionViolations The policy transactions violations @@ -1888,16 +1888,14 @@ function pushTransactionViolationsOnyxData( // Iterate through all reports to find transactions that need optimistic violations for (const report of reports) { - // Skipping invoice report because should not have any category or tag violations if (!report?.reportID || isInvoiceReport(report)) { continue; } - - const transcations = getReportTransactions(report.reportID); - for (const transaction of transcations){ + const transcations = getReportTransactions(report.reportID); + for (const transaction of transcations) { // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { @@ -1928,7 +1926,7 @@ function pushTransactionViolationsOnyxData( key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, value: existingViolations ?? null, }); - }; + } } } diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index d3b3119725b85..5924596e44eda 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,7 +4,6 @@ import {renderHook} from '@testing-library/react-native'; import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxData} from '@src/types/onyx/Request'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {putOnHold} from '@libs/actions/IOU'; import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow'; @@ -79,6 +78,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployeeList, PolicyTag, Report, ReportAction, ReportNameValuePairs, Transaction} from '@src/types/onyx'; import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {Participant} from '@src/types/onyx/Report'; +import type {OnyxData} from '@src/types/onyx/Request'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; import {chatReportR14932 as mockedChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; @@ -4259,7 +4259,7 @@ describe('ReportUtils', () => { }, }); - const onyxData: OnyxData = {}; + const onyxData: OnyxData = {optimisticData: [], failureData: []}; pushTransactionViolationsOnyxData(onyxData, fakePolicy, fakePolicyCategories, fakePolicyTagsLists, {}, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); From d6e0248c162081f9f1df94fcc6a61b3fe7b9dbd9 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 23 Jul 2025 00:34:42 +0300 Subject: [PATCH 0020/1005] spell check --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b54da77181226..942a2c6457867 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1893,9 +1893,9 @@ function pushTransactionViolationsOnyxData( continue; } - const transcations = getReportTransactions(report.reportID); + const transactions = getReportTransactions(report.reportID); - for (const transaction of transcations) { + for (const transaction of transactions) { // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { From 757b0e80cf2f75658f6141bfa3b2f2989525578a Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 27 Jul 2025 21:51:20 +0000 Subject: [PATCH 0021/1005] resolving conflicts --- src/libs/ReportUtils.ts | 93 ++++++++++++++++++---------------- src/libs/actions/Policy/Tag.ts | 15 +++++- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 98cfd86adcfb4..19d8b5eb09288 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1826,26 +1826,29 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { /** * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. * - * @param onyxData The OnyxData object to push updates to - * @param policy The current policy - * @param policyCategories The current policy categories - * @param policyTagLists The current policy tags - * @param allTransactionViolations The policy transactions violations - * @param policyUpdate Changed policy properties, if none pass empty object - * @param policyCategoriesUpdate Changed categories properties, if none pass empty object - * @param policyTagListsUpdate Changed tag properties, if none pass empty object + * @param onyxData - The OnyxData object to push updates to + * @param policy - The current policy + * @param policyCategories - The categories of the current policy + * @param policyTagLists - The tags of the current policy + * @param transactionViolations - The policy transactions violations + * @param policyUpdate - Changed policy properties, if none pass empty object + * @param policyCategoriesUpdate - Changed categories properties, if none pass empty object + * @param policyTagListsUpdate - Changed tag properties, if none pass empty object */ function pushTransactionViolationsOnyxData( onyxData: OnyxData, policy: Policy, policyCategories: PolicyCategories, policyTagLists: PolicyTagLists, - allTransactionViolations: OnyxCollection, + transactionViolations: OnyxCollection, policyUpdate: Partial, policyCategoriesUpdate: Record> = {}, policyTagListsUpdate: Record> = {}, ) { - if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate) && isEmptyObject(policyTagListsUpdate)) { + const isPolicyCategoriesUpdateEmpty = Object.keys(policyCategoriesUpdate).length === 0; + const isPolicyTagListsUpdateEmpty = Object.keys(policyTagListsUpdate).length === 0; + + if (isPolicyCategoriesUpdateEmpty && isPolicyTagListsUpdateEmpty && Object.keys(policyTagListsUpdate).length === 0) { return; } @@ -1855,39 +1858,44 @@ function pushTransactionViolationsOnyxData( return; } - const optimisticPolicyTagLists = isEmptyObject(policyTagListsUpdate) - ? policyTagLists - : { - ...policyTagLists, - ...Object.keys(policyTagListsUpdate).reduce((acc, tagName) => { - acc[tagName] = { - ...(policyTagLists?.[tagName] ?? {}), - tags: { - ...(policyTagLists?.[tagName].tags ?? {}), - ...policyTagListsUpdate[tagName].tags, - }, - }; - return acc; - }, {}), - }; - - const optimisticPolicyCategories = isEmptyObject(policyCategoriesUpdate) + // Merge the existing PolicyCategories with the optimistic updates + const optimisticPolicyCategories: PolicyCategories = isPolicyCategoriesUpdateEmpty ? policyCategories : { - ...policyCategories, - ...Object.keys(policyCategoriesUpdate).reduce((acc, categoryName) => { - acc[categoryName] = {...(policyCategories?.[categoryName] ?? {}), ...(policyCategoriesUpdate?.[categoryName] ?? {})}; - return acc; - }, {}), - }; + ...policyCategories, + ...Object.entries(policyCategoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { + acc[categoryName] = { + ...(policyCategories?.[categoryName] ?? {}), + ...categoryUpdate, + }; + return acc; + }, {}), + }; + + // Merge the existing PolicyTagLists with the optimistic updates + const optimisticPolicyTagLists: PolicyTagLists = isPolicyTagListsUpdateEmpty + ? policyTagLists + : { + ...policyTagLists, + ...Object.entries(policyTagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { + acc[tagListName] = { + ...(policyTagLists?.[tagListName] ?? {}), + ...tagListUpdate, + }; + return acc; + }, {}), + }; + // Merge the existing Policy with the optimistic updates const optimisticPolicy = {...policy, ...policyUpdate}; + const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); const processedTransactionIDs = new Set(); // Iterate through all reports to find transactions that need optimistic violations for (const report of reports) { + // Skipping invoice report because should not have any category or tag violations if (!report?.reportID || isInvoiceReport(report)) { continue; @@ -1896,6 +1904,7 @@ function pushTransactionViolationsOnyxData( const transactions = getReportTransactions(report.reportID); for (const transaction of transactions) { + // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { @@ -1904,7 +1913,7 @@ function pushTransactionViolationsOnyxData( processedTransactionIDs.add(transactionID); - const existingViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const existingViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const optimisticViolations = ViolationsUtils.getViolationsOnyxData( transaction, @@ -1916,16 +1925,14 @@ function pushTransactionViolationsOnyxData( false, ); - if (isEmptyObject(optimisticViolations)) { - continue; + if (!isEmptyObject(optimisticViolations)) { + onyxData.optimisticData?.push(optimisticViolations); + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: existingViolations ?? null, + }); } - - onyxData.optimisticData?.push(optimisticViolations); - onyxData.failureData?.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: existingViolations ?? null, - }); } } } diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index e16eb4c6e1995..782b939ffb28e 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -328,7 +328,20 @@ function setWorkspaceTagEnabled( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTags, transactionViolations, {}, {}, optimisticPolicyTagsData); + ReportUtils.pushTransactionViolationsOnyxData( + onyxData, + policy, + policyCategories, + policyTags, + transactionViolations, + {}, + {}, + { + [policyTag.name]: { + tags: optimisticPolicyTagsData, + }, + } + ); const parameters: SetPolicyTagsEnabled = { policyID, From 3ffa2ed9bb18a2d074803062ad6bcc6387a1dc56 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 27 Jul 2025 21:57:42 +0000 Subject: [PATCH 0022/1005] prettier.. --- src/libs/ReportUtils.ts | 40 ++++++++++++++++------------------ src/libs/actions/Policy/Tag.ts | 2 +- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 19d8b5eb09288..07e46d1941b9f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1862,40 +1862,39 @@ function pushTransactionViolationsOnyxData( const optimisticPolicyCategories: PolicyCategories = isPolicyCategoriesUpdateEmpty ? policyCategories : { - ...policyCategories, - ...Object.entries(policyCategoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { - acc[categoryName] = { - ...(policyCategories?.[categoryName] ?? {}), - ...categoryUpdate, - }; - return acc; - }, {}), - }; + ...policyCategories, + ...Object.entries(policyCategoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { + acc[categoryName] = { + ...(policyCategories?.[categoryName] ?? {}), + ...categoryUpdate, + }; + return acc; + }, {}), + }; // Merge the existing PolicyTagLists with the optimistic updates const optimisticPolicyTagLists: PolicyTagLists = isPolicyTagListsUpdateEmpty ? policyTagLists : { - ...policyTagLists, - ...Object.entries(policyTagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { - acc[tagListName] = { - ...(policyTagLists?.[tagListName] ?? {}), - ...tagListUpdate, - }; - return acc; - }, {}), - }; + ...policyTagLists, + ...Object.entries(policyTagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { + acc[tagListName] = { + ...(policyTagLists?.[tagListName] ?? {}), + ...tagListUpdate, + }; + return acc; + }, {}), + }; // Merge the existing Policy with the optimistic updates const optimisticPolicy = {...policy, ...policyUpdate}; - + const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); const processedTransactionIDs = new Set(); // Iterate through all reports to find transactions that need optimistic violations for (const report of reports) { - // Skipping invoice report because should not have any category or tag violations if (!report?.reportID || isInvoiceReport(report)) { continue; @@ -1904,7 +1903,6 @@ function pushTransactionViolationsOnyxData( const transactions = getReportTransactions(report.reportID); for (const transaction of transactions) { - // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction const transactionID = transaction?.transactionID; if (!transactionID || processedTransactionIDs.has(transactionID)) { diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 782b939ffb28e..c54dcd845379a 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -340,7 +340,7 @@ function setWorkspaceTagEnabled( [policyTag.name]: { tags: optimisticPolicyTagsData, }, - } + }, ); const parameters: SetPolicyTagsEnabled = { From 11f6bba9ef36d5c09f0cc26a43e0010d7a554410 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 1 Aug 2025 04:38:27 +0300 Subject: [PATCH 0023/1005] tags.. --- src/libs/ReportUtils.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 07e46d1941b9f..809fc7f577c68 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1877,10 +1877,13 @@ function pushTransactionViolationsOnyxData( ? policyTagLists : { ...policyTagLists, - ...Object.entries(policyTagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { - acc[tagListName] = { - ...(policyTagLists?.[tagListName] ?? {}), - ...tagListUpdate, + ...Object.entries(policyTagListsUpdate).reduce((acc, [tagName, tagUpdate]) => { + acc[tagName] = { + ...(policyTagLists?.[tagName] ?? {}), + tags: { + ...(policyTagLists?.[tagName]?.tags ?? {}), + ...(tagUpdate?.tags ?? {}), + }, }; return acc; }, {}), From a3cf66c31c98811bb6a193d47c3ceda5b97c7179 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:50:02 +0000 Subject: [PATCH 0024/1005] introduce invitation for cards flow --- src/CONST/index.ts | 2 + src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/actions/Card.ts | 10 + .../workspace/WorkspaceInviteMessagePage.tsx | 239 +--------------- .../assignCard/AssignCardFeedPage.tsx | 8 + .../companyCards/assignCard/AssigneeStep.tsx | 122 ++++++-- .../assignCard/AssigneeStep.tsx.rej | 14 + .../assignCard/InviteNewMemberStep.tsx | 94 +++++++ .../expensifyCard/issueNew/AssigneeStep.tsx | 131 +++++++-- .../issueNew/InviteNewMemberStep.tsx | 70 +++++ .../issueNew/IssueNewCardPage.tsx | 4 + .../WorkspaceInviteMessageComponent.tsx | 263 ++++++++++++++++++ src/types/onyx/AssignCard.ts | 3 + src/types/onyx/Card.ts | 3 + 22 files changed, 703 insertions(+), 269 deletions(-) create mode 100644 src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej create mode 100644 src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx create mode 100644 src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx create mode 100644 src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ea6ae81763033..e86264a203889 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3201,6 +3201,7 @@ const CONST = { CARD_NAME: 'CardName', TRANSACTION_START_DATE: 'TransactionStartDate', CONFIRMATION: 'Confirmation', + INVITE_NEW_MEMBER: 'InviteNewMember', }, TRANSACTION_START_DATE_OPTIONS: { FROM_BEGINNING: 'fromBeginning', @@ -3245,6 +3246,7 @@ const CONST = { LIMIT: 'Limit', CARD_NAME: 'CardName', CONFIRMATION: 'Confirmation', + INVITE_NEW_MEMBER: 'InviteNewMember', }, CARD_TYPE: { PHYSICAL: 'physical', diff --git a/src/languages/de.ts b/src/languages/de.ts index 13825e67e931f..a80348e482d4d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4916,6 +4916,7 @@ const translations = { issueCard: 'Karte ausstellen', issueNewCard: { whoNeedsCard: 'Wer braucht eine Karte?', + inviteNewMember: 'Neues Mitglied einladen', findMember: 'Mitglied finden', chooseCardType: 'Wählen Sie einen Kartentyp aus', physicalCard: 'Physische Karte', diff --git a/src/languages/en.ts b/src/languages/en.ts index b19319cb1c94f..af0e44c037c8e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4897,6 +4897,7 @@ const translations = { issueCard: 'Issue card', issueNewCard: { whoNeedsCard: 'Who needs a card?', + inviteNewMember: 'Invite new member', findMember: 'Find member', chooseCardType: 'Choose a card type', physicalCard: 'Physical card', diff --git a/src/languages/es.ts b/src/languages/es.ts index 725d200359f06..861659305d678 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5156,6 +5156,7 @@ const translations = { getStartedIssuing: 'Empieza emitiendo tu primera tarjeta virtual o física.', issueNewCard: { whoNeedsCard: '¿Quién necesita una tarjeta?', + inviteNewMember: 'Invitar nuevo miembro', findMember: 'Buscar miembro', chooseCardType: 'Elegir un tipo de tarjeta', physicalCard: 'Tarjeta física', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index cf19c6444c162..62aedabd62288 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4930,6 +4930,7 @@ const translations = { issueCard: 'Émettre une carte', issueNewCard: { whoNeedsCard: "Qui a besoin d'une carte ?", + inviteNewMember: 'Inviter un nouveau membre', findMember: 'Trouver un membre', chooseCardType: 'Choisissez un type de carte', physicalCard: 'Carte physique', diff --git a/src/languages/it.ts b/src/languages/it.ts index d0328b32ed17e..451102434bb3e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4929,6 +4929,7 @@ const translations = { issueCard: 'Emetti carta', issueNewCard: { whoNeedsCard: 'Chi ha bisogno di una carta?', + inviteNewMember: 'Invita nuovo membro', findMember: 'Trova membro', chooseCardType: 'Scegli un tipo di carta', physicalCard: 'Carta fisica', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2ba80810ca82a..a380115c712a0 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4908,6 +4908,7 @@ const translations = { issueCard: 'カードを発行', issueNewCard: { whoNeedsCard: '誰がカードを必要としていますか?', + inviteNewMember: '新しいメンバーを招待', findMember: 'メンバーを探す', chooseCardType: 'カードタイプを選択', physicalCard: '物理カード', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6cc8727e4481f..1eb09d995f065 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4931,6 +4931,7 @@ const translations = { issueCard: 'Kaart uitgeven', issueNewCard: { whoNeedsCard: 'Wie heeft een kaart nodig?', + inviteNewMember: 'Nieuw lid uitnodigen', findMember: 'Lid zoeken', chooseCardType: 'Kies een kaarttype', physicalCard: 'Fysieke kaart', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c690d69d9c462..ce6a9307d36e8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4920,6 +4920,7 @@ const translations = { issueCard: 'Wydaj kartę', issueNewCard: { whoNeedsCard: 'Kto potrzebuje karty?', + inviteNewMember: 'Zaproś nowego członka', findMember: 'Znajdź członka', chooseCardType: 'Wybierz typ karty', physicalCard: 'Fizyczna karta', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8193bb5dd0e4f..0d45c81c7b83d 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4844,6 +4844,7 @@ const translations = { issueCard: '发卡', issueNewCard: { whoNeedsCard: '谁需要一张卡?', + inviteNewMember: '邀请新成员', findMember: '查找成员', chooseCardType: '选择卡类型', physicalCard: '实体卡', diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 758b05e08ebfd..771807e416268 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -376,6 +376,15 @@ function setIssueNewCardStepAndData({data, isEditing, step, policyID, isChangeAs }); } +function setDraftInviteAccountID(assigneeEmail: string | undefined, assigneeAccountID: number | undefined, policyID: string | undefined) { + if (!policyID) { + return; + } + Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, { + [assigneeEmail ?? '']: assigneeAccountID, + }); +} + function clearIssueNewCardFlow(policyID: string | undefined) { Onyx.set(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, { currentStep: null, @@ -976,5 +985,6 @@ export { getCardDefaultName, queueExpensifyCardForBilling, clearIssueNewCardFormData, + setDraftInviteAccountID, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 83a144684149a..03494bf308ffc 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,43 +1,10 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {InteractionManager, Keyboard, View} from 'react-native'; -import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import ReportActionAvatars from '@components/ReportActionAvatars'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Text from '@components/Text'; -import TextInput from '@components/TextInput'; +import React from 'react'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; -import {clearDraftValues} from '@libs/actions/FormActions'; -import {openExternalLink} from '@libs/actions/Link'; -import {addMembersToWorkspace, clearWorkspaceInviteRoleDraft} from '@libs/actions/Policy/Member'; -import {setWorkspaceInviteMessageDraft} from '@libs/actions/Policy/Policy'; -import getIsNarrowLayout from '@libs/getIsNarrowLayout'; -import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; -import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import INPUT_IDS from '@src/types/form/WorkspaceInviteMessageForm'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; +import WorkspaceInviteMessageComponent from './members/WorkspaceInviteMessageComponent'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -46,205 +13,13 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: WorkspaceInviteMessagePageProps) { - const styles = useThemeStyles(); - const {translate, formatPhoneNumber} = useLocalize(); - const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true}); - - const viewportOffsetTop = useViewportOffsetTop(); - const [welcomeNote, setWelcomeNote] = useState(); - - const {inputCallbackRef, inputRef} = useAutoFocusInput(); - - const [invitedEmailsToAccountIDsDraft, invitedEmailsToAccountIDsDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, { - canBeMissing: true, - }); - const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`, { - canBeMissing: true, - }); - const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); - const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, invitedEmailsToAccountIDsDraftResult, formDataResult); - - const welcomeNoteSubject = useMemo( - () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, - [policy?.name, currentUserPersonalDetails?.displayName], - ); - - const getDefaultWelcomeNote = useCallback(() => { - return ( - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? - // workspaceInviteMessageDraft can be an empty string - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - workspaceInviteMessageDraft ?? - translate('workspace.common.welcomeNote') - ); - }, [workspaceInviteMessageDraft, translate, formData]); - - useEffect(() => { - if (isOnyxLoading) { - return; - } - if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { - setWelcomeNote(getDefaultWelcomeNote()); - return; - } - if (isEmptyObject(policy)) { - return; - } - Navigation.goBack(route.params.backTo); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isOnyxLoading]); - - const sendInvitation = () => { - Keyboard.dismiss(); - const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); - // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - addMembersToWorkspace( - invitedEmailsToAccountIDsDraft ?? {}, - `${welcomeNoteSubject}\n\n${welcomeNote}`, - route.params.policyID, - policyMemberAccountIDs, - workspaceInviteRoleDraft, - formatPhoneNumber, - ); - setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); - clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); - if ((route.params?.backTo as string)?.endsWith('members')) { - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal()); - return; - } - - if (getIsNarrowLayout()) { - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID), {forceReplace: true}); - return; - } - - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.dismissModal(); - InteractionManager.runAfterInteractions(() => { - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); - }); - }); - }; - - /** Opens privacy url as an external link */ - const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { - event?.preventDefault(); - openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); - }; - - const validate = (): FormInputErrors => { - const errorFields: FormInputErrors = {}; - if (isEmptyObject(invitedEmailsToAccountIDsDraft) && !isOnyxLoading) { - errorFields.welcomeMessage = translate('workspace.inviteMessage.inviteNoMembersError'); - } - return errorFields; - }; - - const policyName = policy?.name; - - useEffect(() => { - return () => { - clearWorkspaceInviteRoleDraft(route.params.policyID); - }; - }, [route.params.policyID]); - return ( - - - Navigation.dismissModal()} - onBackButtonPress={() => Navigation.goBack(route.params.backTo)} - /> - - - {translate('common.privacy')} - - - } - > - - - - - {translate('workspace.inviteMessage.inviteMessagePrompt')} - - - - { - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(route.params.policyID, Navigation.getActiveRoute())); - }} - /> - - { - setWelcomeNote(text); - }} - ref={(element: AnimatedTextInputRef) => { - if (!element) { - return; - } - if (!inputRef.current) { - updateMultilineInputRange(element); - } - inputCallbackRef(element); - }} - shouldSaveDraft - /> - - - - + backTo={route.params.backTo} + currentUserPersonalDetails={currentUserPersonalDetails} + /> ); } diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 35e59f32522a8..c6c4d71ac6f09 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -18,6 +18,7 @@ import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardSelectionStep from './CardSelectionStep'; import ConfirmationStep from './ConfirmationStep'; +import InviteNewMemberStep from './InviteNewMemberStep'; import TransactionStartDateStep from './TransactionStartDateStep'; type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps; @@ -98,6 +99,13 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { backTo={shouldUseBackToParam ? backTo : undefined} /> ); + case CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER: + return ( + + ); default: return ( ; @@ -37,6 +38,75 @@ type AssigneeStepProps = { feed: OnyxTypes.CompanyCardFeed; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AssigneeStep.getValidOptions'}); + +function useOptions() { + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const existingDelegates = useMemo( + () => + account?.delegatedAccess?.delegates?.reduce( + (prev, {email}) => { + // eslint-disable-next-line no-param-reassign + prev[email] = true; + return prev; + }, + {} as Record, + ), + [account?.delegatedAccess?.delegates], + ); + + const defaultOptions = useMemo(() => { + const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions, existingDelegates]); + + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; +} + function AssigneeStep({policy, feed}: AssigneeStepProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); @@ -74,6 +144,17 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { return; } + if (userToInvite?.login === selectedMember) { + setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER, + data: { + email: selectedMember, + assigneeAccountID: userToInvite?.accountID ?? CONST.DEFAULT_NUMBER_ID, + }, + }); + return; + } + const personalDetail = getPersonalDetailByEmail(selectedMember); const memberName = personalDetail?.firstName ? personalDetail.firstName : personalDetail?.login; const data: Partial = { @@ -105,7 +186,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { Navigation.goBack(); }; - const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const shouldShowSearchInput = policy?.employeeList; const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; const membersDetails = useMemo(() => { @@ -144,7 +225,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, [isOffline, policy?.employeeList, selectedMember, formatPhoneNumber, localeCompare]); const sections = useMemo(() => { - if (!debouncedSearchTerm) { + if (!debouncedSearchValue) { return [ { data: membersDetails, @@ -153,8 +234,8 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ]; } - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); - const filteredOptions = tokenizedSearch(membersDetails, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); + const searchValueForPhoneOrEmail = getSearchValueForPhoneOrEmail(debouncedSearchValue).toLowerCase(); + const filteredOptions = tokenizedSearch(membersDetails, searchValueForPhoneOrEmail, (option) => [option.text ?? '', option.alternateText ?? '']); return [ { @@ -162,14 +243,22 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { data: filteredOptions, shouldShow: true, }, + { + title: undefined, + data: userToInvite ? [userToInvite] : [], + shouldShow: !!userToInvite, + }, + ...(personalDetails + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: !!personalDetails, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); - - return getHeaderMessage(sections[0].data.length !== 0, false, searchValue); - }, [debouncedSearchTerm, sections]); + }, [debouncedSearchValue, membersDetails, userToInvite, personalDetails]); return ( {translate('workspace.companyCards.whoNeedsCardAssigned')} } + showLoadingPlaceholder={!areOptionsInitialized} /> ); diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej new file mode 100644 index 0000000000000..0cd9222bed92a --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej @@ -0,0 +1,14 @@ +diff a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx (rejected hunks) +@@ -45,11 +115,11 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { + const [list] = useCardsList(policy?.id, feed); + const [cardFeeds] = useCardFeeds(policy?.id); + const filteredCardList = getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); ++ const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); + + const isEditing = assignCard?.isEditing; + + const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); +- const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [shouldShowError, setShouldShowError] = useState(false); + + const selectMember = (assignee: ListItem) => { diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx new file mode 100644 index 0000000000000..b11af64a1986c --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -0,0 +1,94 @@ +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useCardFeeds from '@hooks/useCardFeeds'; +import useCardsList from '@hooks/useCardsList'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; +import WorkspaceInviteMessageComponent from '@pages/workspace/members/WorkspaceInviteMessageComponent'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import {setAssignCardStepAndData} from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; +import type {CompanyCardFeed} from '@src/types/onyx/CardFeeds'; + +type InviteeNewMemberStepProps = WithPolicyAndFullscreenLoadingProps & + WithCurrentUserPersonalDetailsProps & { + /** Selected feed */ + feed: CompanyCardFeed; + }; + +function InviteNewMemberStep({policy, route, currentUserPersonalDetails, feed}: InviteeNewMemberStepProps) { + const {translate} = useLocalize(); + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); + const isEditing = assignCard?.isEditing; + const [list] = useCardsList(policy?.id, feed); + const [cardFeeds] = useCardFeeds(policy?.id); + const filteredCardList = getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); + + const handleBackButtonPress = () => { + if (isEditing) { + setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, isEditing: false}); + } else { + setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, + data: { + ...assignCard?.data, + assigneeAccountID: undefined, + email: undefined, + }, + isEditing: false, + }); + } + }; + + const goToNextStep = () => { + let nextStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; + const data: Partial = { + email: assignCard?.data?.email, + cardName: getDefaultCardName(assignCard?.data?.email), + }; + + if (hasOnlyOneCardToAssign(filteredCardList)) { + nextStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; + data.cardNumber = Object.keys(filteredCardList).at(0); + data.encryptedCardNumber = Object.values(filteredCardList).at(0); + } + + setAssignCardStepAndData({ + currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep, + data, + isEditing: false, + }); + }; + + return ( + + + + ); +} + +InviteNewMemberStep.displayName = 'InviteNewMemberStep'; + +export default withPolicyAndFullscreenLoading(withCurrentUserPersonalDetails(InviteNewMemberStep)); diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 8e8a9874b14da..bc4b0dc4d3ac5 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -1,7 +1,9 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import {useBetas} from '@components/OnyxListItemProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -11,19 +13,18 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getHeaderMessage, getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; +import memoize from '@libs/memoize'; +import {filterAndOrderOptions, getHeaderMessage, getSearchValueForPhoneOrEmail, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail, getUserNameByEmail} from '@libs/PersonalDetailsUtils'; import {isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; -import {clearIssueNewCardFlow, getCardDefaultName, setIssueNewCardStepAndData} from '@userActions/Card'; +import {clearIssueNewCardFlow, getCardDefaultName, setDraftInviteAccountID, setIssueNewCardStepAndData} from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {IssueNewCardData} from '@src/types/onyx/Card'; -const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; - type AssigneeStepProps = { // The policy that the card will be issued under policy: OnyxEntry; @@ -35,17 +36,85 @@ type AssigneeStepProps = { startStepIndex: number; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AssigneeStep.getValidOptions'}); + +function useOptions() { + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const existingDelegates = useMemo( + () => + account?.delegatedAccess?.delegates?.reduce( + (prev, {email}) => { + // eslint-disable-next-line no-param-reassign + prev[email] = true; + return prev; + }, + {} as Record, + ), + [account?.delegatedAccess?.delegates], + ); + + const defaultOptions = useMemo(() => { + const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions, existingDelegates]); + + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; +} + function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); + const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const isEditing = issueNewCard?.isEditing; - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const submit = (assignee: ListItem) => { const data: Partial = { assigneeEmail: assignee?.login ?? '', @@ -56,6 +125,17 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { data.cardTitle = getCardDefaultName(getUserNameByEmail(assignee?.login ?? '', 'firstName')); } + if (userToInvite?.accountID === assignee?.accountID) { + data.assigneeAccountID = assignee?.accountID; + setIssueNewCardStepAndData({ + step: CONST.EXPENSIFY_CARD.STEP.INVITE_NEW_MEMBER, + data, + policyID, + }); + setDraftInviteAccountID(data.assigneeEmail, assignee?.accountID, policyID); + return; + } + setIssueNewCardStepAndData({ step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, data, @@ -73,7 +153,7 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { clearIssueNewCardFlow(policyID); }; - const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const shouldShowSearchInput = policy?.employeeList; const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; const membersDetails = useMemo(() => { @@ -111,7 +191,7 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { }, [isOffline, policy?.employeeList, formatPhoneNumber, localeCompare]); const sections = useMemo(() => { - if (!debouncedSearchTerm) { + if (!debouncedSearchValue) { return [ { data: membersDetails, @@ -120,8 +200,8 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { ]; } - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); - const filteredOptions = tokenizedSearch(membersDetails, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); + const searchValueForOptions = getSearchValueForPhoneOrEmail(debouncedSearchValue).toLowerCase(); + const filteredOptions = tokenizedSearch(membersDetails, searchValueForOptions, (option) => [option.text ?? '', option.alternateText ?? '']); return [ { @@ -129,14 +209,22 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { data: filteredOptions, shouldShow: true, }, + { + title: undefined, + data: userToInvite ? [userToInvite] : [], + shouldShow: !!userToInvite, + }, + ...(personalDetails + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: !!personalDetails, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); - - return getHeaderMessage(sections[0].data.length !== 0, false, searchValue); - }, [debouncedSearchTerm, sections]); + }, [debouncedSearchValue, membersDetails, userToInvite, personalDetails]); return ( {translate('workspace.card.issueNewCard.whoNeedsCard')} ); diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx new file mode 100644 index 0000000000000..679adf3ce71db --- /dev/null +++ b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {setIssueNewCardStepAndData} from '@libs/actions/Card'; +import WorkspaceInviteMessageComponent from '@pages/workspace/members/WorkspaceInviteMessageComponent'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type InviteeNewMemberStepProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps; + +function InviteNewMemberStep({policy, route, currentUserPersonalDetails}: InviteeNewMemberStepProps) { + const {translate} = useLocalize(); + const policyID = route.params.policyID; + const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); + + const isEditing = issueNewCard?.isEditing; + const handleBackButtonPress = () => { + if (isEditing) { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false, policyID}); + return; + } + + setIssueNewCardStepAndData({ + step: CONST.EXPENSIFY_CARD.STEP.ASSIGNEE, + data: {...issueNewCard?.data, assigneeAccountID: undefined, assigneeEmail: undefined}, + isEditing: false, + policyID, + }); + }; + + const goToNextStep = () => { + if (isEditing) { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false, policyID}); + } else { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, isEditing: false, policyID}); + } + }; + + return ( + + + + ); +} + +InviteNewMemberStep.displayName = 'InviteNewMemberStep'; + +export default withPolicyAndFullscreenLoading(withCurrentUserPersonalDetails(InviteNewMemberStep)); diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx index 9aa83113150c1..a3c7e808bb486 100644 --- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx @@ -19,6 +19,7 @@ import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardTypeStep from './CardTypeStep'; import ConfirmationStep from './ConfirmationStep'; +import InviteNewMemberStep from './InviteNewMemberStep'; import LimitStep from './LimitStep'; import LimitTypeStep from './LimitTypeStep'; @@ -31,6 +32,7 @@ function getStartStepIndex(issueNewCard: OnyxEntry): number { const STEP_INDEXES: Record = { [CONST.EXPENSIFY_CARD.STEP.ASSIGNEE]: 0, + [CONST.EXPENSIFY_CARD.STEP.INVITE_NEW_MEMBER]: 0, [CONST.EXPENSIFY_CARD.STEP.CARD_TYPE]: 1, [CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE]: 2, [CONST.EXPENSIFY_CARD.STEP.LIMIT]: 3, @@ -108,6 +110,8 @@ function IssueNewCardPage({policy, route}: IssueNewCardPageProps) { startStepIndex={startStepIndex} /> ); + case CONST.EXPENSIFY_CARD.STEP.INVITE_NEW_MEMBER: + return ; default: return ( ; + policyID: string; + backTo: Routes | undefined; + currentUserPersonalDetails: OnyxEntry; + shouldShowBackButton?: boolean; + isInviteNewMemberStep?: boolean; + goToNextStep?: () => void; +}; + +function WorkspaceInviteMessageComponent({ + policy, + policyID, + backTo, + currentUserPersonalDetails, + shouldShowBackButton = true, + isInviteNewMemberStep = false, + goToNextStep, +}: WorkspaceInviteMessageComponentProps) { + const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true}); + + const viewportOffsetTop = useViewportOffsetTop(); + const [welcomeNote, setWelcomeNote] = useState(); + + const {inputCallbackRef, inputRef} = useAutoFocusInput(); + + const [invitedEmailsToAccountIDsDraft, invitedEmailsToAccountIDsDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID.toString()}`, { + canBeMissing: true, + }); + const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID.toString()}`, { + canBeMissing: true, + }); + const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID.toString()}`, {canBeMissing: true}); + const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, invitedEmailsToAccountIDsDraftResult, formDataResult); + + const welcomeNoteSubject = useMemo( + () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, + [policy?.name, currentUserPersonalDetails?.displayName], + ); + + const getDefaultWelcomeNote = useCallback(() => { + return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft ?? + translate('workspace.common.welcomeNote') + ); + }, [workspaceInviteMessageDraft, translate, formData]); + + useEffect(() => { + if (isOnyxLoading) { + return; + } + if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { + setWelcomeNote(getDefaultWelcomeNote()); + return; + } + if (isEmptyObject(policy)) { + return; + } + Navigation.goBack(backTo); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isOnyxLoading]); + + const sendInvitation = () => { + Keyboard.dismiss(); + const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details + addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID, policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); + setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); + clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); + if (goToNextStep) { + goToNextStep(); + return; + } + if ((backTo as string)?.endsWith('members')) { + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal()); + return; + } + + if (getIsNarrowLayout()) { + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID), {forceReplace: true}); + return; + } + + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.dismissModal(); + InteractionManager.runAfterInteractions(() => { + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + }); + }); + }; + + /** Opens privacy url as an external link */ + const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { + event?.preventDefault(); + openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); + }; + + const validate = (): FormInputErrors => { + const errorFields: FormInputErrors = {}; + if (isEmptyObject(invitedEmailsToAccountIDsDraft) && !isOnyxLoading) { + errorFields.welcomeMessage = translate('workspace.inviteMessage.inviteNoMembersError'); + } + return errorFields; + }; + + const policyName = policy?.name; + + useEffect(() => { + return () => { + clearWorkspaceInviteRoleDraft(policyID); + }; + }, [policyID]); + + return ( + + + {shouldShowBackButton && ( + Navigation.dismissModal()} + onBackButtonPress={() => Navigation.goBack(backTo)} + /> + )} + + + {translate('common.privacy')} + + + } + > + {isInviteNewMemberStep && {translate('workspace.card.issueNewCard.inviteNewMember')}} + + + + + {translate('workspace.inviteMessage.inviteMessagePrompt')} + + + + { + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID, Navigation.getActiveRoute())); + }} + /> + + { + setWelcomeNote(text); + }} + ref={(element: AnimatedTextInputRef) => { + if (!element) { + return; + } + if (!inputRef.current) { + updateMultilineInputRange(element); + } + inputCallbackRef(element); + }} + shouldSaveDraft + /> + + + + + ); +} + +WorkspaceInviteMessageComponent.displayName = 'WorkspaceInviteMessageComponent'; + +export default WorkspaceInviteMessageComponent; diff --git a/src/types/onyx/AssignCard.ts b/src/types/onyx/AssignCard.ts index dd0ba64f86831..9e3df19aa898c 100644 --- a/src/types/onyx/AssignCard.ts +++ b/src/types/onyx/AssignCard.ts @@ -40,6 +40,9 @@ type AssignCardData = { /** Plaid accounts */ plaidAccounts?: LinkAccount[] | PlaidAccount[]; + + /** The account ID of the cardholder */ + assigneeAccountID?: number; }; /** Model of assign card flow */ diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 55308ad81fb96..6e3f6e003e998 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -212,6 +212,9 @@ type IssueNewCardData = { /** The email address of the cardholder */ assigneeEmail: string; + /** The account ID of the cardholder */ + assigneeAccountID?: number; + /** Card type */ cardType: ValueOf; From 07ca138fa1dada58ed728e83acaf1ac58440446e Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:01:00 +0000 Subject: [PATCH 0025/1005] fix patch failure --- .../companyCards/assignCard/AssigneeStep.tsx | 2 +- .../companyCards/assignCard/AssigneeStep.tsx.rej | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index ffde9071aa4f1..e43f88d84e3d5 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -117,10 +117,10 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { const [cardFeeds] = useCardFeeds(policy?.id); const filteredCardList = getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed], workspaceCardFeeds); + const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const isEditing = assignCard?.isEditing; const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [shouldShowError, setShouldShowError] = useState(false); const selectMember = (assignee: ListItem) => { diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej deleted file mode 100644 index 0cd9222bed92a..0000000000000 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx.rej +++ /dev/null @@ -1,14 +0,0 @@ -diff a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx (rejected hunks) -@@ -45,11 +115,11 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { - const [list] = useCardsList(policy?.id, feed); - const [cardFeeds] = useCardFeeds(policy?.id); - const filteredCardList = getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); -+ const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); - - const isEditing = assignCard?.isEditing; - - const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); -- const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [shouldShowError, setShouldShowError] = useState(false); - - const selectMember = (assignee: ListItem) => { From 5009d0a9962d2cce73cafb508256f73125d23872 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:07:44 +0000 Subject: [PATCH 0026/1005] fix lint --- src/ROUTES.ts | 2 +- src/libs/API/parameters/AddMembersToWorkspaceParams.ts | 2 +- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/Policy/Member.ts | 8 ++++---- src/libs/actions/Policy/Policy.ts | 4 ++-- .../companyCards/assignCard/InviteNewMemberStep.tsx | 5 +++-- .../expensifyCard/issueNew/InviteNewMemberStep.tsx | 2 +- .../members/WorkspaceInviteMessageComponent.tsx | 10 +++++----- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 43797bd8322f6..5ca6004567ef5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1007,7 +1007,7 @@ const ROUTES = { }, WORKSPACE_INVITE_MESSAGE_ROLE: { route: 'workspaces/:policyID/invite-message/role', - getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`workspaces/${policyID}/invite-message/role`, backTo)}` as const, + getRoute: (policyID: string | undefined, backTo?: string) => `${getUrlWithBackToParam(`workspaces/${policyID}/invite-message/role`, backTo)}` as const, }, WORKSPACE_OVERVIEW: { route: 'workspaces/:policyID/overview', diff --git a/src/libs/API/parameters/AddMembersToWorkspaceParams.ts b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts index abfed55e2df3a..d8927ad12a03b 100644 --- a/src/libs/API/parameters/AddMembersToWorkspaceParams.ts +++ b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts @@ -1,7 +1,7 @@ type AddMembersToWorkspaceParams = { employees: string; welcomeNote: string; - policyID: string; + policyID: string | undefined; reportCreationData?: string; announceChatReportID?: string; announceCreatedReportActionID?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 73be6c9783866..c443edc19d915 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7575,7 +7575,7 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio }; } -function buildOptimisticAnnounceChat(policyID: string, accountIDs: number[]): OptimisticAnnounceChat { +function buildOptimisticAnnounceChat(policyID: string | undefined, accountIDs: number[]): OptimisticAnnounceChat { const announceReport = getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation @@ -9441,7 +9441,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { ); } -function getRoom(type: ValueOf, policyID: string): OnyxEntry { +function getRoom(type: ValueOf, policyID: string | undefined): OnyxEntry { const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index ea1f2108696ca..d0c1f66aed38a 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -140,7 +140,7 @@ function getPolicy(policyID: string | undefined): OnyxEntry { */ function buildRoomMembersOnyxData( roomType: typeof CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE | typeof CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, - policyID: string, + policyID: string | undefined, accountIDs: number[], ): OnyxDataReturnType { const report = ReportUtils.getRoom(roomType, policyID); @@ -876,7 +876,7 @@ function clearWorkspaceOwnerChangeFlow(policyID: string) { function buildAddMembersToWorkspaceOnyxData( invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, - policyID: string, + policyID: string | undefined, policyMemberAccountIDs: number[], role: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], @@ -978,7 +978,7 @@ function buildAddMembersToWorkspaceOnyxData( function addMembersToWorkspace( invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, - policyID: string, + policyID: string | undefined, policyMemberAccountIDs: number[], role: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], @@ -1181,7 +1181,7 @@ function setWorkspaceInviteRoleDraft(policyID: string, role: ValueOf { if (isEditing) { @@ -78,7 +79,7 @@ function InviteNewMemberStep({policy, route, currentUserPersonalDetails, feed}: > ; - policyID: string; + policyID: string | undefined; backTo: Routes | undefined; currentUserPersonalDetails: OnyxEntry; shouldShowBackButton?: boolean; @@ -66,13 +66,13 @@ function WorkspaceInviteMessageComponent({ const {inputCallbackRef, inputRef} = useAutoFocusInput(); - const [invitedEmailsToAccountIDsDraft, invitedEmailsToAccountIDsDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID.toString()}`, { + const [invitedEmailsToAccountIDsDraft, invitedEmailsToAccountIDsDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, { canBeMissing: true, }); - const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID.toString()}`, { + const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, { canBeMissing: true, }); - const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID.toString()}`, {canBeMissing: true}); + const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID}`, {canBeMissing: true}); const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, invitedEmailsToAccountIDsDraftResult, formDataResult); const welcomeNoteSubject = useMemo( @@ -110,7 +110,7 @@ function WorkspaceInviteMessageComponent({ Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID, policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); + addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID , policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); if (goToNextStep) { From c714574f867a486cbe34afdb50533486d3726ba3 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:43:03 +0530 Subject: [PATCH 0027/1005] fix prettier --- src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index 1b9a3adeb603e..abc31bbf7f90a 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -110,7 +110,7 @@ function WorkspaceInviteMessageComponent({ Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID , policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); + addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID, policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); if (goToNextStep) { From c85e35e895279fa981d5e2f28d48ca31c6c84fc1 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:18:44 +0000 Subject: [PATCH 0028/1005] fix translations --- src/languages/pt-BR.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 50ae38dc8b302..40928b05e15c3 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4926,6 +4926,7 @@ const translations = { issueCard: 'Emitir cartão', issueNewCard: { whoNeedsCard: 'Quem precisa de um cartão?', + inviteNewMember: 'Convide um novo membro', findMember: 'Encontrar membro', chooseCardType: 'Escolha um tipo de cartão', physicalCard: 'Cartão físico', From fc2260162d41fddb20b0597d896096dacc5d283f Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 14 Aug 2025 06:07:24 +0000 Subject: [PATCH 0029/1005] apply fixes --- .../companyCards/assignCard/AssigneeStep.tsx | 27 ++++++++++++++++--- .../assignCard/InviteNewMemberStep.tsx | 2 ++ .../issueNew/InviteNewMemberStep.tsx | 1 + .../WorkspaceInviteMessageComponent.tsx | 3 +++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index e43f88d84e3d5..7796baedc35fd 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; +import {setDraftInviteAccountID} from '@libs/actions/Card'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; import memoize from '@libs/memoize'; import {filterAndOrderOptions, getHeaderMessage, getSearchValueForPhoneOrEmail, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; @@ -139,7 +140,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { return; } - if (!selectedMember) { + if (!selectedMember || (!searchValue && selectedMember !== policy?.employeeList?.[selectedMember]?.email)) { setShouldShowError(true); return; } @@ -149,9 +150,10 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { currentStep: CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER, data: { email: selectedMember, - assigneeAccountID: userToInvite?.accountID ?? CONST.DEFAULT_NUMBER_ID, + assigneeAccountID: userToInvite?.accountID, }, }); + setDraftInviteAccountID(selectedMember, userToInvite?.accountID, policy?.id); return; } @@ -224,6 +226,23 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { return membersList; }, [isOffline, policy?.employeeList, selectedMember, formatPhoneNumber, localeCompare]); + const membersDetailsWithInviteNewMember = useMemo(() => { + if (!userToInvite) { + return {}; + } + + const newMember: ListItem = { + keyForList: userToInvite?.login, + text: userToInvite?.login, + alternateText: userToInvite?.login, + login: userToInvite?.login, + isSelected: selectedMember === userToInvite?.login, + accountID: userToInvite?.accountID, + }; + + return newMember; + }, [selectedMember, userToInvite]); + const sections = useMemo(() => { if (!debouncedSearchValue) { return [ @@ -245,7 +264,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, { title: undefined, - data: userToInvite ? [userToInvite] : [], + data: userToInvite ? [membersDetailsWithInviteNewMember] : [], shouldShow: !!userToInvite, }, ...(personalDetails @@ -258,7 +277,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ] : []), ]; - }, [debouncedSearchValue, membersDetails, userToInvite, personalDetails]); + }, [debouncedSearchValue, membersDetails, userToInvite, membersDetailsWithInviteNewMember, personalDetails]); return ( ); diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx index b4ead3eec244b..710b6fcafb556 100644 --- a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx @@ -60,6 +60,7 @@ function InviteNewMemberStep({policy, route, currentUserPersonalDetails}: Invite shouldShowBackButton={false} isInviteNewMemberStep goToNextStep={goToNextStep} + shouldShowTooltip={false} /> ); diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index abc31bbf7f90a..225aa311d53c6 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -43,6 +43,7 @@ type WorkspaceInviteMessageComponentProps = { policyID: string | undefined; backTo: Routes | undefined; currentUserPersonalDetails: OnyxEntry; + shouldShowTooltip?: boolean; shouldShowBackButton?: boolean; isInviteNewMemberStep?: boolean; goToNextStep?: () => void; @@ -53,6 +54,7 @@ function WorkspaceInviteMessageComponent({ policyID, backTo, currentUserPersonalDetails, + shouldShowTooltip = true, shouldShowBackButton = true, isInviteNewMemberStep = false, goToNextStep, @@ -209,6 +211,7 @@ function WorkspaceInviteMessageComponent({ displayInRows: true, }} secondaryAvatarContainerStyle={styles.secondAvatarInline} + shouldShowTooltip={shouldShowTooltip} /> From 5cbdef2c0e41aa24a9e4a981a849aca17f285cdf Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 14 Aug 2025 19:26:54 +0300 Subject: [PATCH 0030/1005] update tags --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0741b858f933b..6eae67d4dafc7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1896,6 +1896,7 @@ function pushTransactionViolationsOnyxData( ...Object.entries(policyTagListsUpdate).reduce((acc, [tagName, tagUpdate]) => { acc[tagName] = { ...(policyTagLists?.[tagName] ?? {}), + ...tagUpdate, tags: { ...(policyTagLists?.[tagName]?.tags ?? {}), ...(tagUpdate?.tags ?? {}), From aafacf0006c114c9401fa759dae18d6614664914 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 21 Aug 2025 21:08:30 +0700 Subject: [PATCH 0031/1005] fix number format polyfill isn't applied --- src/libs/NumberFormatUtils/index.ts | 5 +++-- src/libs/NumberFormatUtils/intlPolyfill.ios.ts | 10 ---------- src/libs/NumberFormatUtils/intlPolyfill.ts | 2 -- src/setup/index.ts | 4 ---- 4 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 src/libs/NumberFormatUtils/intlPolyfill.ios.ts delete mode 100644 src/libs/NumberFormatUtils/intlPolyfill.ts diff --git a/src/libs/NumberFormatUtils/index.ts b/src/libs/NumberFormatUtils/index.ts index 02bb14b29eed7..cf04fa6eca566 100644 --- a/src/libs/NumberFormatUtils/index.ts +++ b/src/libs/NumberFormatUtils/index.ts @@ -1,9 +1,10 @@ +import intlPolyfill from '@libs/IntlPolyfill'; import memoize from '@libs/memoize'; import CONST from '@src/CONST'; import type Locale from '@src/types/onyx/Locale'; -import initPolyfill from './intlPolyfill'; -initPolyfill(); +// Polyfill the Intl API if locale data is not as expected +intlPolyfill(); const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10, monitoringName: 'NumberFormatUtils'}); diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ios.ts b/src/libs/NumberFormatUtils/intlPolyfill.ios.ts deleted file mode 100644 index 4745284c0b61f..0000000000000 --- a/src/libs/NumberFormatUtils/intlPolyfill.ios.ts +++ /dev/null @@ -1,10 +0,0 @@ -import intlPolyfill from '@libs/IntlPolyfill'; - -// On iOS, polyfills from `additionalSetup` are applied after memoization, which results in incorrect cache entry of `Intl.NumberFormat` (e.g. lacking `formatToParts` method). -// To fix this, we need to apply the polyfill manually before memoization. -// For further information, see: https://github.com/Expensify/App/pull/43868#issuecomment-2217637217 -const initPolyfill = () => { - intlPolyfill(); -}; - -export default initPolyfill; diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ts b/src/libs/NumberFormatUtils/intlPolyfill.ts deleted file mode 100644 index 31fedd6a01b6a..0000000000000 --- a/src/libs/NumberFormatUtils/intlPolyfill.ts +++ /dev/null @@ -1,2 +0,0 @@ -const initPolyfill = () => {}; -export default initPolyfill; diff --git a/src/setup/index.ts b/src/setup/index.ts index eccb0c58f57a3..d847122a6aa92 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -1,6 +1,5 @@ import {I18nManager} from 'react-native'; import Onyx from 'react-native-onyx'; -import intlPolyfill from '@libs/IntlPolyfill'; import {setDeviceID} from '@userActions/Device'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; @@ -60,9 +59,6 @@ export default function () { I18nManager.allowRTL(false); I18nManager.forceRTL(false); - // Polyfill the Intl API if locale data is not as expected - intlPolyfill(); - // Perform any other platform-specific setup platformSetup(); From 897fe8646008cd65d6e7be4634df2ac7d87fdcfd Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Fri, 22 Aug 2025 10:43:49 -0700 Subject: [PATCH 0032/1005] Create Uber-for-Business.md New help page for the new Uber for Business integration. --- .../connections/Uber-for-Business.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 docs/articles/new-expensify/connections/Uber-for-Business.md diff --git a/docs/articles/new-expensify/connections/Uber-for-Business.md b/docs/articles/new-expensify/connections/Uber-for-Business.md new file mode 100644 index 0000000000000..cad2c8321d5a9 --- /dev/null +++ b/docs/articles/new-expensify/connections/Uber-for-Business.md @@ -0,0 +1,73 @@ +--- +title: Uber for Business +description: Learn how to connect Uber for Business to your Expensify workspace, invite employees, and automate receipt collection for Uber Rides and Uber Eats. +keywords: [New Expensify, Uber for Business, Uber integration, automate Uber receipts, connect Uber to Expensify, U4B, Uber Eats, Uber Rides, receipt automation] +--- + +
+ +Connect your Expensify workspace to Uber for Business to automatically import Uber Rides and Uber Eats receipts for your team. This guide walks you through setup, employee management, and receipt import. + +# Connect the Uber for Business Integration + +## Step 1: Enable Receipt Partners in your Workspace +1. Go to **Workspaces > [Workspace Name] > More features**. +2. Toggle on **Receipt partners**. +3. Once enabled, a new **Receipt partners** section will appear in the left-hand menu. + +## Step 2: Connect Uber for Business +1. Go to **Workspaces > [Workspace Name] > Receipt partners** +3. Click **Connect** next to Uber for Business. +4. Authorize or create your Uber for Business (U4B) account in the new browser tab that opens. +5. When complete, the tab will close, and a list of workspace members not yet invited to U4B will appear. +6. Confirm which workspace members should be invited to use the connection (they’ll all be selected by default). + +If your connection fails or expires, a warning will appear under **Workspaces > Receipt partners > Uber for Business**. To resolve the error, click the three-dot menu and choose **Enter credentials** to reconnect. + +# Manage the Uber for Business connection + +## Manage invites + +When you first connect to Uber for Business, all workspace members will be invited by default. To manually invite members later: + +1. Go to **Workspaces > [Workspace Name] > Receipt partners** +2. Click **Manage invites**. +3. From here, you can filter the members list, invite new employees, or resend any outstanding invites that haven't been accepted yet. + +As an admin, you can also automatically keep your Uber for Business roster in sync with your Expensify workspace members list. We offer two configurable settings: + +- Invite new workspace members to Uber for Business +- Deactivate removed workspace members from Uber for Business + +## Configure a Centralized Billing Account (Optional) + +We will automatically detect if your Uber for Business organization uses centralized billing, but you’ll still need to choose which Expensify account should receive the receipts: + +1. Go to **Workspaces > Receipt partners > Uber for Business**. +2. Click **Edit** next to **Central billing account**. +3. Select a workspace member (The workspace owner is selected by default). +4. Save your changes. + +# Disconnect Uber for Business + +1. Go to **Workspaces > Receipt partners > Uber for Business**. +2. Click the three-dot menu. +3. Select **Disconnect** and confirm in the modal window. + + +# FAQs + +## Can I use this integration in Expensify Classic? +The Uber for Business connection can only be connected and managed in New Expensify, but once connected, imported Uber receipts will show up in both New Expensify and Expensify Classic. + +## Can I invite employees again if they didn’t accept the first time? +Yes! Go to **Manage invites**, find their name or email, and click **Resend**. + +## What do the different buttons and badges on the Manage Invites page mean? +- **Invite button** – The employee has never been invited. +- **Resend button** - The employee has been invited, but has not yet accepted. +- **Pending badge** – The invite has been accepted, but additional approval is required within Uber. +- **Linked badge** – The employee has accepted the invite. +- **Suspended badge** – The employee’s Uber account is linked, but has been temporarily suspended by Uber (i.e rider disputes, etc). + +
From c67f60da328b83e1e0922149415359a00d86c870 Mon Sep 17 00:00:00 2001 From: allgandalf Date: Sat, 23 Aug 2025 01:47:55 +0530 Subject: [PATCH 0033/1005] remove usage of Onyx.connect --- src/libs/Navigation/NavigationRoot.tsx | 3 ++- src/libs/actions/Welcome/OnboardingFlow.ts | 13 ++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 2208fe41ecf46..355ffc44136c7 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -107,7 +107,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const [currentOnboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true}); const [currentOnboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); const [onboardingInitialPath] = useOnyx(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, {canBeMissing: true}); - + const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true}); const previousAuthenticated = usePrevious(authenticated); const initialState = useMemo(() => { @@ -141,6 +141,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, + onboardingValues, }), linkingConfig.config, ); diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index 0859b94ce8940..79a4d96f22f1e 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -16,17 +16,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Locale, Onboarding} from '@src/types/onyx'; -let onboardingValues: Onboarding; -Onyx.connect({ - key: ONYXKEYS.NVP_ONBOARDING, - callback: (value) => { - if (value === undefined) { - return; - } - onboardingValues = value; - }, -}); - type OnboardingCompanySize = ValueOf; type OnboardingPurpose = ValueOf; @@ -37,6 +26,7 @@ type GetOnboardingInitialPathParamsType = { currentOnboardingPurposeSelected: OnyxEntry; currentOnboardingCompanySize: OnyxEntry; onboardingInitialPath: OnyxEntry; + onboardingValues: OnyxEntry; }; type OnboardingTaskLinks = Partial<{ @@ -103,6 +93,7 @@ function getOnboardingInitialPath(getOnboardingInitialPathParams: GetOnboardingI currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath = '', + onboardingValues, } = getOnboardingInitialPathParams; const state = getStateFromPath(onboardingInitialPath, linkingConfig.config); const currentOnboardingValues = onboardingValuesParam ?? onboardingValues; From e23b97481de7f820149a1416ea7a8bcd023659c1 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Sat, 23 Aug 2025 01:51:05 +0530 Subject: [PATCH 0034/1005] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b45e0b69edc9..fb3b8a93cf8d5 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=243 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=242 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", From baab19c55b5e33b1d70ba8f4d5a8dc68f7d4eedb Mon Sep 17 00:00:00 2001 From: allgandalf Date: Sat, 23 Aug 2025 02:27:19 +0530 Subject: [PATCH 0035/1005] fix typecheck --- src/hooks/useOnboardingFlow.ts | 2 ++ src/libs/actions/Report.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index dd5bea40b29dc..a928c4544d2f8 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -104,6 +104,7 @@ function useOnboardingFlowRouter() { currentOnboardingCompanySize, currentOnboardingPurposeSelected, onboardingInitialPath, + onboardingValues, }); } } @@ -118,6 +119,7 @@ function useOnboardingFlowRouter() { currentOnboardingCompanySize, currentOnboardingPurposeSelected, onboardingInitialPath, + onboardingValues, }); } }); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 5e47526932c50..cb40e80cb41b4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3505,6 +3505,7 @@ function openReportFromDeepLink( currentOnboardingPurposeSelected, currentOnboardingCompanySize, onboardingInitialPath, + onboardingValues: val, }), onCompleted: handleDeeplinkNavigation, onCanceled: handleDeeplinkNavigation, From c60558d3dca57eacb42aaac864f0f3a948784366 Mon Sep 17 00:00:00 2001 From: allgandalf Date: Sat, 23 Aug 2025 15:02:39 +0530 Subject: [PATCH 0036/1005] add unit tests --- src/libs/actions/Welcome/OnboardingFlow.ts | 2 +- tests/unit/OnboardingFlowTest.ts | 91 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/unit/OnboardingFlowTest.ts diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index 79a4d96f22f1e..9a3bc0ba09a5e 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -384,5 +384,5 @@ const getOnboardingMessages = (locale?: Locale) => { }; }; -export type {OnboardingMessage, OnboardingTask, OnboardingTaskLinks, OnboardingPurpose, OnboardingCompanySize}; +export type {OnboardingMessage, OnboardingTask, OnboardingTaskLinks, OnboardingPurpose, OnboardingCompanySize, GetOnboardingInitialPathParamsType}; export {getOnboardingInitialPath, startOnboardingFlow, getOnboardingMessages}; diff --git a/tests/unit/OnboardingFlowTest.ts b/tests/unit/OnboardingFlowTest.ts new file mode 100644 index 0000000000000..e22d8e4823c89 --- /dev/null +++ b/tests/unit/OnboardingFlowTest.ts @@ -0,0 +1,91 @@ +import {getOnboardingInitialPath} from '@libs/actions/Welcome/OnboardingFlow'; +import type {GetOnboardingInitialPathParamsType} from '@libs/actions/Welcome/OnboardingFlow'; +import CONST from '@src/CONST'; + +describe('OnboardingFlow', () => { + describe('getOnboardingInitialPath', () => { + it('should return the correct path for personal spend', () => { + const params: GetOnboardingInitialPathParamsType = { + isUserFromPublicDomain: false, + hasAccessiblePolicies: true, + onboardingValuesParam: { + hasCompletedGuidedSetupFlow: false, + shouldRedirectToClassicAfterMerge: false, + shouldValidate: false, + isMergingAccountBlocked: false, + isMergeAccountStepCompleted: false, + signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.INDIVIDUAL, + }, + currentOnboardingPurposeSelected: CONST.ONBOARDING_CHOICES.PERSONAL_SPEND, + currentOnboardingCompanySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + onboardingInitialPath: '/', + onboardingValues: undefined, + }; + const path = getOnboardingInitialPath(params); + expect(path).toBe('/onboarding/personal-details'); + }); + + it('should return the correct path for SMB', () => { + const params: GetOnboardingInitialPathParamsType = { + isUserFromPublicDomain: true, + hasAccessiblePolicies: true, + onboardingValuesParam: { + hasCompletedGuidedSetupFlow: false, + shouldRedirectToClassicAfterMerge: false, + shouldValidate: false, + isMergingAccountBlocked: false, + isMergeAccountStepCompleted: false, + signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB, + }, + currentOnboardingPurposeSelected: CONST.ONBOARDING_CHOICES.EMPLOYER, + currentOnboardingCompanySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + onboardingInitialPath: '/', + onboardingValues: undefined, + }; + const path = getOnboardingInitialPath(params); + expect(path).toBe('/onboarding/work-email'); + }); + + it('should return the correct path for VSB', () => { + const params: GetOnboardingInitialPathParamsType = { + isUserFromPublicDomain: false, + hasAccessiblePolicies: false, + onboardingValuesParam: { + hasCompletedGuidedSetupFlow: false, + shouldRedirectToClassicAfterMerge: false, + shouldValidate: false, + isMergingAccountBlocked: false, + isMergeAccountStepCompleted: false, + signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB, + }, + currentOnboardingPurposeSelected: CONST.ONBOARDING_CHOICES.EMPLOYER, + currentOnboardingCompanySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + onboardingInitialPath: '/', + onboardingValues: undefined, + }; + const path = getOnboardingInitialPath(params); + expect(path).toBe('/onboarding/accounting'); + }); + + it('should return the correct path for SMB and is not from public domain', () => { + const params: GetOnboardingInitialPathParamsType = { + isUserFromPublicDomain: false, + hasAccessiblePolicies: false, + onboardingValuesParam: { + hasCompletedGuidedSetupFlow: false, + shouldRedirectToClassicAfterMerge: false, + shouldValidate: false, + isMergingAccountBlocked: false, + isMergeAccountStepCompleted: false, + signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB, + }, + currentOnboardingPurposeSelected: CONST.ONBOARDING_CHOICES.SUBMIT, + currentOnboardingCompanySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + onboardingInitialPath: '/', + onboardingValues: undefined, + }; + const path = getOnboardingInitialPath(params); + expect(path).toBe('/onboarding/employees'); + }); + }); +}); From 2378d23a5d9ccc9faf15f964d4d450e2c5588891 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 28 Aug 2025 03:45:58 +0300 Subject: [PATCH 0037/1005] created usePolicyData hook --- src/hooks/usePolicyData.ts | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/hooks/usePolicyData.ts diff --git a/src/hooks/usePolicyData.ts b/src/hooks/usePolicyData.ts new file mode 100644 index 0000000000000..92c7d33eb0fb7 --- /dev/null +++ b/src/hooks/usePolicyData.ts @@ -0,0 +1,56 @@ +import {useOnyx } from 'react-native-onyx'; +import type {Report, Policy, PolicyCategories, PolicyTagLists } from '@src/types/onyx'; +import type { OnyxValueWithOfflineFeedback } from '@src/types/onyx/OnyxCommon'; +import type {ReportTransactionsAndViolationsDerivedValue } from '@src/types/onyx/DerivedValues'; +import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; +import usePolicy from './usePolicy'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type PolicyData = { + policy: OnyxValueWithOfflineFeedback; + reports: OnyxValueWithOfflineFeedback[]; + tags: PolicyTagLists; + categories: PolicyCategories; + transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; +}; + + +function usePolicyData(policyID?: string):PolicyData { + const policy = usePolicy(policyID); + const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); + + const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + + const [reports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`, { + canBeMissing: false, + selector: (reportCollection) => { + if (!policyID) { + return []; + } + return Object.values(reportCollection ?? {}).filter((report) => report !== undefined && report.reportID && report.policyID === policyID); + } + }); + + const transactionsAndViolations = (reports ?? []).reduce((acc, report) => { + if (report === undefined || !report.reportID) { + return acc; + } + const reportTransactionsAndViolations = allReportsTransactionsAndViolations?.[report.reportID]; + if (reportTransactionsAndViolations) { + acc[report.reportID] = reportTransactionsAndViolations; + } + return acc; + }, {} as ReportTransactionsAndViolationsDerivedValue); + + return { + policy: policy as OnyxValueWithOfflineFeedback, + reports: reports as OnyxValueWithOfflineFeedback[], + tags: tags ?? {}, + categories: categories ?? {}, + transactionsAndViolations: transactionsAndViolations, + }; +} + +export type {PolicyData} +export default usePolicyData; \ No newline at end of file From 418e1a9c5b4975bb3ecc7151f639d10d0dac6b39 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 28 Aug 2025 03:46:46 +0300 Subject: [PATCH 0038/1005] convert params to policyData --- src/libs/ReportUtils.ts | 109 +++++---- src/libs/actions/Policy/Category.ts | 129 ++++++----- src/libs/actions/Policy/Tag.ts | 213 ++++++++---------- .../request/step/IOURequestStepCategory.tsx | 6 +- .../categories/CategorySettingsPage.tsx | 22 +- .../categories/WorkspaceCategoriesPage.tsx | 28 +-- .../WorkspaceCategoriesSettingsPage.tsx | 29 +-- src/pages/workspace/tags/TagSettingsPage.tsx | 17 +- .../workspace/tags/WorkspaceEditTagsPage.tsx | 18 +- .../workspace/tags/WorkspaceTagsPage.tsx | 46 ++-- .../tags/WorkspaceTagsSettingsPage.tsx | 23 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 25 +- 12 files changed, 302 insertions(+), 363 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 938b3156cf69c..afa1f3671efa2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8,6 +8,7 @@ import isEmpty from 'lodash/isEmpty'; import isNumber from 'lodash/isNumber'; import mapValues from 'lodash/mapValues'; import lodashMaxBy from 'lodash/maxBy'; +import type { OnyxValueWithOfflineFeedback } from '@src/types/onyx/OnyxCommon'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; @@ -18,6 +19,7 @@ import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; +import type {PolicyData} from '@hooks/usePolicyData'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; @@ -53,7 +55,6 @@ import type { Task, Transaction, TransactionViolation, - TransactionViolations, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; @@ -1851,65 +1852,48 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { } /** - * Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update. + * Updates optimistic transaction violations to OnyxData for the given policy and categories onyx update. * * @param onyxData - The OnyxData object to push updates to - * @param policy - The current policy - * @param policyCategories - The categories of the current policy - * @param policyTagLists - The tags of the current policy - * @param transactionViolations - The policy transactions violations + * @param policyData - The current policy * @param policyUpdate - Changed policy properties, if none pass empty object * @param policyCategoriesUpdate - Changed categories properties, if none pass empty object - * @param policyTagListsUpdate - Changed tag properties, if none pass empty object + * @param policyTagsUpdate - Changed tag properties, if none pass empty object */ function pushTransactionViolationsOnyxData( onyxData: OnyxData, - policy: Policy, - policyCategories: PolicyCategories, - policyTagLists: PolicyTagLists, - transactionViolations: OnyxCollection, - policyUpdate: Partial, + policyData: PolicyData, + policyUpdate: Partial>, policyCategoriesUpdate: Record> = {}, - policyTagListsUpdate: Record> = {}, + policyTagsUpdate: Record> = {}, ) { - const isPolicyCategoriesUpdateEmpty = Object.keys(policyCategoriesUpdate).length === 0; - const isPolicyTagListsUpdateEmpty = Object.keys(policyTagListsUpdate).length === 0; - - if (isPolicyCategoriesUpdateEmpty && isPolicyTagListsUpdateEmpty && Object.keys(policyTagListsUpdate).length === 0) { + if (!policyData.policy || policyData.reports.length === 0 || isEmptyObject(policyData.transactionsAndViolations)) { return; } - const reports = getAllPolicyReports(policy.id); - - if (!reports || reports.length === 0) { + const isPolicyUpdateEmpty = isEmptyObject(policyUpdate); + const isPolicyTagsUpdateEmpty = isEmptyObject(policyTagsUpdate); + const isPolicyCategoriesUpdateEmpty = isEmptyObject(policyCategoriesUpdate); + + // If there are no updates to policy, categories or tags, return early + if (isPolicyUpdateEmpty && isPolicyTagsUpdateEmpty && isPolicyCategoriesUpdateEmpty ) { return; } - // Merge the existing PolicyCategories with the optimistic updates - const optimisticPolicyCategories: PolicyCategories = isPolicyCategoriesUpdateEmpty - ? policyCategories - : { - ...policyCategories, - ...Object.entries(policyCategoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { - acc[categoryName] = { - ...(policyCategories?.[categoryName] ?? {}), - ...categoryUpdate, - }; - return acc; - }, {}), - }; + // Merge the existing policy with the optimistic updates + const optimisticPolicy = {...policyData.policy, ...policyUpdate}; - // Merge the existing PolicyTagLists with the optimistic updates - const optimisticPolicyTagLists: PolicyTagLists = isPolicyTagListsUpdateEmpty - ? policyTagLists + // Merge the existing tagLists with the optimistic updates + const optimisticPolicyTagLists = isPolicyTagsUpdateEmpty + ? policyData.tags : { - ...policyTagLists, - ...Object.entries(policyTagListsUpdate).reduce((acc, [tagName, tagUpdate]) => { + ...policyData.tags, + ...Object.entries(policyTagsUpdate).reduce((acc, [tagName, tagUpdate]) => { acc[tagName] = { - ...(policyTagLists?.[tagName] ?? {}), + ...(policyData.tags?.[tagName] ?? {}), ...tagUpdate, tags: { - ...(policyTagLists?.[tagName]?.tags ?? {}), + ...(policyData.tags?.[tagName]?.tags ?? {}), ...(tagUpdate?.tags ?? {}), }, }; @@ -1917,32 +1901,41 @@ function pushTransactionViolationsOnyxData( }, {}), }; - // Merge the existing Policy with the optimistic updates - const optimisticPolicy = {...policy, ...policyUpdate}; + // Merge the existing categories with the optimistic updates + const optimisticPolicyCategories = isPolicyCategoriesUpdateEmpty + ? policyData.categories + : { + ...policyData.categories, + ...Object.entries(policyCategoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { + acc[categoryName] = { + ...(policyData.categories?.[categoryName] ?? {}), + ...categoryUpdate, + }; + return acc; + }, {}), + }; const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticPolicyTagLists); - const processedTransactionIDs = new Set(); - - // Iterate through all reports to find transactions that need optimistic violations - for (const report of reports) { - // Skipping invoice report because should not have any category or tag violations - if (!report?.reportID || isInvoiceReport(report)) { + // Iterate through all policy reports to find transactions that need optimistic violations + for (const report of policyData.reports) { + + // Skipping invoice reports since they should not have any category or tag violations + if (isInvoiceReport(report)) { continue; } - const transactions = getReportTransactions(report.reportID); + const reportTransactionsAndViolations = policyData.transactionsAndViolations[report.reportID]; - for (const transaction of transactions) { - // Skip it if transaction's optimistic violations already is pushed to ensure one update per transaction - const transactionID = transaction?.transactionID; - if (!transactionID || processedTransactionIDs.has(transactionID)) { - continue; - } + if (isEmptyObject(reportTransactionsAndViolations)) { + continue; + } + + const {transactions, violations} = reportTransactionsAndViolations - processedTransactionIDs.add(transactionID); + for (const transaction of Object.values(transactions)) { - const existingViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const existingViolations = violations[transaction.transactionID]; const optimisticViolations = ViolationsUtils.getViolationsOnyxData( transaction, @@ -1958,7 +1951,7 @@ function pushTransactionViolationsOnyxData( onyxData.optimisticData?.push(optimisticViolations); onyxData.failureData?.push({ onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, value: existingViolations ?? null, }); } diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index d593ca6a5073d..ec8dec73eeb61 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -3,6 +3,7 @@ import lodashUnion from 'lodash/union'; import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PartialDeep} from 'type-fest'; +import type {PolicyData} from '@hooks/usePolicyData'; import * as API from '@libs/API'; import type { EnablePolicyCategoriesParams, @@ -33,7 +34,7 @@ import {pushTransactionViolationsOnyxData} from '@libs/ReportUtils'; import {getFinishOnboardingTaskOnyxData} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyCategory, PolicyTagLists, RecentlyUsedCategories, TransactionViolations} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories} from '@src/types/onyx'; import type {ApprovalRule, ExpenseRule, MccGroup} from '@src/types/onyx/Policy'; import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -268,15 +269,18 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category return lodashUnion([category], policyRecentlyUsedCategories); } -function setWorkspaceCategoryEnabled( - policy: Policy, - categoriesToUpdate: Record, - policyTagLists: PolicyTagLists = {}, - allTransactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; - const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; - const optimisticPolicyCategoriesData = { +function setWorkspaceCategoryEnabled(policyData: PolicyData, categoriesToUpdate: Record) { + if (Object.keys(categoriesToUpdate).length === 0) { + Log.warn('[setWorkspaceCategoryEnabled] The "categoriesToUpdate" param is empty with no categories to update.'); + return + } + + if (!policyData.policy){ + return; + } + + const policyID = policyData.policy.id; + const policyCategoriesOptimisticData = { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { acc[key] = { ...categoriesToUpdate[key], @@ -296,7 +300,7 @@ function setWorkspaceCategoryEnabled( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: optimisticPolicyCategoriesData, + value: policyCategoriesOptimisticData, }, ], successData: [ @@ -325,7 +329,7 @@ function setWorkspaceCategoryEnabled( value: { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { acc[key] = { - ...policyCategories[key], + ...policyData.categories[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), pendingFields: { enabled: null, @@ -340,7 +344,7 @@ function setWorkspaceCategoryEnabled( ], }; - pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, allTransactionViolations, {}, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -420,23 +424,33 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED, parameters, onyxData); } -function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: string, maxAmountNoReceipt: number) { - const originalMaxAmountNoReceipt = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.maxAmountNoReceipt; +function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string, maxAmountNoReceipt: number) { + if (!categoryName){ + Log.warn('[setPolicyCategoryReceiptsRequired] The "categoryName" param is empty with no category to update.'); + } + + if (policyData.policy === undefined) { + return; + }; + + const policyID = policyData.policy.id; + const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; + const policyCategoriesOptimisticData = { + [categoryName]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + maxAmountNoReceipt, + }, + }; const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - maxAmountNoReceipt, - }, - }, + value: policyCategoriesOptimisticData, }, ], successData: [ @@ -472,6 +486,7 @@ function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: strin ], }; + pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); const parameters: SetPolicyCategoryReceiptsRequiredParams = { policyID, categoryName, @@ -835,9 +850,13 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData); } -function setWorkspaceRequiresCategory(policy: Policy, requiresCategory: boolean, policyTagLists: PolicyTagLists = {}, allTransactionViolations: OnyxCollection = {}) { - const policyID = policy.id; - const optimisticPolicyData = { +function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean) { + if (policyData.policy === undefined) { + return; + }; + + const policyID = policyData.policy.id; + const policyCategoriesOptimisticData: Partial = { requiresCategory, errors: { requiresCategory: null, @@ -852,7 +871,7 @@ function setWorkspaceRequiresCategory(policy: Policy, requiresCategory: boolean, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: optimisticPolicyData, + value: policyCategoriesOptimisticData, }, ], successData: [ @@ -884,14 +903,7 @@ function setWorkspaceRequiresCategory(policy: Policy, requiresCategory: boolean, ], }; - pushTransactionViolationsOnyxData( - onyxData, - policy, - allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, - policyTagLists, - allTransactionViolations, - optimisticPolicyData as Partial, - ); + pushTransactionViolationsOnyxData(onyxData, policyData, policyCategoriesOptimisticData); const parameters = { policyID, requiresCategory, @@ -921,16 +933,23 @@ function clearCategoryErrors(policyID: string, categoryName: string) { }); } -function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string[], policyTagLists: PolicyTagLists = {}, transactionViolations: OnyxCollection = {}) { - const policyID = policy.id; - const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; - const optimisticPolicyCategoriesData = categoryNamesToDelete.reduce>>((acc, categoryName) => { +function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete: string[]) { + if (categoryNamesToDelete.length === 0) { + Log.warn('[deleteWorkspaceCategories] The "categoryNamesToDelete" param is empty with no categories to delete.'); + return; + } + + if (policyData.policy === undefined) { + return; + }; + const policyID = policyData.policy.id; + const policyCategoriesOptimisticData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; return acc; }, {}); const shouldDisableRequiresCategory = !hasEnabledOptions( - Object.values(policyCategories).filter((category) => !categoryNamesToDelete.includes(category.name) && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), + Object.values(policyData.categories).filter((category) => !categoryNamesToDelete.includes(category.name) && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), ); const onyxData: OnyxData = { @@ -938,7 +957,7 @@ function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: optimisticPolicyCategoriesData, + value: policyCategoriesOptimisticData, }, ], successData: [ @@ -959,7 +978,7 @@ function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string acc[categoryName] = { pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.deleteFailureMessage'), - enabled: !!policyCategories?.[categoryName]?.enabled, + enabled: !!policyData.categories?.[categoryName]?.enabled, }; return acc; }, {}), @@ -968,7 +987,7 @@ function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string }; const optimisticPolicyData: Partial = shouldDisableRequiresCategory ? {requiresCategory: false} : {}; - pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData); + pushTransactionViolationsOnyxData(onyxData, policyData, optimisticPolicyData, policyCategoriesOptimisticData); appendSetupCategoriesOnboardingData(onyxData); const parameters = { @@ -979,14 +998,12 @@ function deleteWorkspaceCategories(policy: Policy, categoryNamesToDelete: string API.write(WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES, parameters, onyxData); } -function enablePolicyCategories( - policy: Policy, - enabled: boolean, - policyTagLists: PolicyTagLists = {}, - allTransactionViolations: OnyxCollection = {}, - shouldGoBack = true, -) { - const policyID = policy.id; +function enablePolicyCategories(policyData: PolicyData, enabled: boolean, shouldGoBack = true) { + if (policyData.policy === undefined) { + return; + }; + + const policyID = policyData.policy.id; const onyxUpdatesToDisableCategories: OnyxUpdate[] = []; if (!enabled) { onyxUpdatesToDisableCategories.push( @@ -994,7 +1011,7 @@ function enablePolicyCategories( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: Object.fromEntries( - Object.entries(allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}).map(([categoryName]) => [ + Object.entries(policyData.categories).map(([categoryName]) => [ categoryName, { enabled: false, @@ -1057,13 +1074,11 @@ function enablePolicyCategories( }, }; - const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}; - const policyCategoriesUpdate: Record> = Object.fromEntries( - Object.entries(policyCategories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]), + Object.entries(policyData.categories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]), ); - pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, allTransactionViolations, policyUpdate, policyCategoriesUpdate); + pushTransactionViolationsOnyxData(onyxData, policyData, policyUpdate, policyCategoriesUpdate); if (onyxUpdatesToDisableCategories.length > 0) { onyxData.optimisticData?.push(...onyxUpdatesToDisableCategories); diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index c54dcd845379a..0245bb74bb47a 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,6 +1,7 @@ import lodashCloneDeep from 'lodash/cloneDeep'; -import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {PolicyData} from '@hooks/usePolicyData'; import * as API from '@libs/API'; import type { EnablePolicyTagsParams, @@ -30,7 +31,7 @@ import {getTagArrayFromName} from '@libs/TransactionUtils'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ImportedSpreadsheet, Policy, PolicyCategories, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report, TransactionViolations} from '@src/types/onyx'; +import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -231,22 +232,15 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { API.write(WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET, parameters, onyxData); } -function setWorkspaceTagEnabled( - policy: Policy, - tagsToUpdate: Record, - tagListIndex: number, - policyCategories: PolicyCategories = {}, - transactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; - const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const policyTag = PolicyUtils.getTagLists(policyTags)?.at(tagListIndex); +function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record, tagListIndex: number) { + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); - if (!policyTag || tagListIndex === -1) { + if (!policyTag || tagListIndex === -1 || !policyData.policy) { return; } - const optimisticPolicyTagsData = { + const policyID = policyData.policy.id; + const policyTagsOptimisticData = { ...Object.keys(tagsToUpdate).reduce((acc, key) => { acc[key] = { ...policyTag.tags[key], @@ -269,7 +263,7 @@ function setWorkspaceTagEnabled( key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { [policyTag.name]: { - tags: optimisticPolicyTagsData, + tags: policyTagsOptimisticData, }, }, }, @@ -330,15 +324,12 @@ function setWorkspaceTagEnabled( ReportUtils.pushTransactionViolationsOnyxData( onyxData, - policy, - policyCategories, - policyTags, - transactionViolations, + policyData, {}, {}, { [policyTag.name]: { - tags: optimisticPolicyTagsData, + tags: policyTagsOptimisticData, }, }, ); @@ -352,23 +343,15 @@ function setWorkspaceTagEnabled( API.write(WRITE_COMMANDS.SET_POLICY_TAGS_ENABLED, parameters, onyxData); } -function setWorkspaceTagRequired( - policy: Policy, - tagListIndexes: number[], - isRequired: boolean, - policyTagsLists: PolicyTagLists = {}, - policyCategories: PolicyCategories = {}, - transactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; - const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - if (!policyTags) { +function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[], isRequired: boolean) { + if (!policyData.tags || !policyData.policy) { return; } - const optimisticPolicyTagsData = { - ...Object.keys(policyTags).reduce((acc, key) => { - if (tagListIndexes.includes(policyTags[key].orderWeight)) { + const policyID = policyData.policy.id; + const policyTagsOptimisticData = { + ...Object.keys(policyData.tags).reduce((acc, key) => { + if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { acc[key] = { ...acc[key], required: isRequired, @@ -391,7 +374,7 @@ function setWorkspaceTagRequired( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: optimisticPolicyTagsData, + value: policyTagsOptimisticData, }, ], successData: [ @@ -399,8 +382,8 @@ function setWorkspaceTagRequired( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.keys(policyTags).reduce((acc, key) => { - if (tagListIndexes.includes(policyTags[key].orderWeight)) { + ...Object.keys(policyData.tags).reduce((acc, key) => { + if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { acc[key] = { ...acc[key], errors: undefined, @@ -422,7 +405,7 @@ function setWorkspaceTagRequired( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.keys(policyTags).reduce((acc, key) => { + ...Object.keys(policyData.tags).reduce((acc, key) => { acc[key] = { ...acc[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -438,7 +421,7 @@ function setWorkspaceTagRequired( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagsLists, transactionViolations, {}, {}, optimisticPolicyTagsData); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); const parameters: SetPolicyTagListsRequired = { policyID, @@ -449,16 +432,19 @@ function setWorkspaceTagRequired( API.write(WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED, parameters, onyxData); } -function deletePolicyTags(policy: Policy, tagsToDelete: string[], policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { - const policyID = policy.id; - const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const policyTag = PolicyUtils.getTagLists(policyTagLists)?.at(0); +function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { + if (policyData.policy === undefined || tagsToDelete.length === 0) { + return; + } + + const policyID = policyData.policy.id; + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(0); if (!policyTag) { return; } - const policyTagsUpdate: Record>>> = { + const policyTagsOptimisticData: Record>>> = { [policyTag.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { @@ -474,7 +460,7 @@ function deletePolicyTags(policy: Policy, tagsToDelete: string[], policyCategori { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsUpdate, + value: policyTagsOptimisticData, }, ], successData: [ @@ -515,7 +501,7 @@ function deletePolicyTags(policy: Policy, tagsToDelete: string[], policyCategori ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, {}, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); const parameters = { policyID, @@ -593,16 +579,14 @@ function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { }); } -function renamePolicyTag( - policy: Policy, - policyTag: {oldName: string; newName: string}, - tagListIndex: number, - policyCategories: PolicyCategories = {}, - transactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; - const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const tagList = PolicyUtils.getTagLists(policyTagLists)?.at(tagListIndex); +function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; newName: string}, tagListIndex: number) { + + if (policyData.policy === undefined) { + return; + } + + const policyID = policyData.policy.id; + const tagList = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); if (!tagList) { return; } @@ -611,7 +595,7 @@ function renamePolicyTag( const newTagName = PolicyUtils.escapeTagName(policyTag.newName); const policyTagRule = PolicyUtils.getTagApproverRule(policyID, oldTagName); - const approvalRules = policy?.rules?.approvalRules ?? []; + const approvalRules = policyData.policy?.rules?.approvalRules ?? []; const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); // Its related by name, so the corresponding rule has to be updated to handle offline scenario @@ -629,13 +613,13 @@ function renamePolicyTag( updatedApprovalRules[indexToUpdate] = policyTagRule; } - const policyUpdate: Partial = { + const policyOptimisticData: Partial = { rules: { approvalRules: updatedApprovalRules, }, }; - const policyTagsUpdate: Record>>> = { + const policyTagsOptimisticData: Record>>> = { [tagList?.name]: { tags: { [oldTagName]: null, @@ -659,12 +643,12 @@ function renamePolicyTag( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsUpdate, + value: policyTagsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: policyUpdate, + value: policyOptimisticData, }, ], successData: [ @@ -710,7 +694,7 @@ function renamePolicyTag( ], }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, policyTagsOptimisticData); const parameters: RenamePolicyTagsParams = { policyID, @@ -722,10 +706,12 @@ function renamePolicyTag( API.write(WRITE_COMMANDS.RENAME_POLICY_TAG, parameters, onyxData); } -function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { - const policyID = policy.id; - const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const policyUpdate = { +function enablePolicyTags(policyData: PolicyData, enabled: boolean) { + if (policyData.policy === undefined) { + return; + } + const policyID = policyData.policy.id; + const policyOptimisticData = { areTagsEnabled: enabled, pendingFields: { areTagsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -737,7 +723,7 @@ function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: Po { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: policyUpdate, + value: policyOptimisticData, }, ], successData: [ @@ -765,7 +751,7 @@ function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: Po ], }; - if (!policyTagLists) { + if (!policyData.tags) { const defaultTagList: PolicyTagLists = { Tag: { name: 'Tag', @@ -784,15 +770,15 @@ function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: Po key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, defaultTagList); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, defaultTagList); } else if (!enabled) { - const policyTag = PolicyUtils.getTagLists(policyTagLists).at(0); + const policyTag = PolicyUtils.getTagLists(policyData.tags).at(0); if (!policyTag) { return; } - const policyTagsUpdate: Record> = { + const policyTagsOptimisticData: Record> = { [policyTag.name]: { tags: Object.fromEntries( Object.keys(policyTag.tags).map((tagName) => [ @@ -809,7 +795,7 @@ function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: Po { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsUpdate, + value: policyTagsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -820,7 +806,9 @@ function enablePolicyTags(policy: Policy, enabled: boolean, policyCategories: Po }, ); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, {...policyUpdate, requiresTag: false}, {}, policyTagsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {...policyOptimisticData, requiresTag: false}, {}, policyTagsOptimisticData); + }else{ + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, {}); } const parameters: EnablePolicyTagsParams = {policyID, enabled}; @@ -918,18 +906,15 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet ); } -function renamePolicyTagList( - policy: Policy, - policyTagListName: {oldName: string; newName: string}, - policyTags: OnyxEntry, - tagListIndex: number, - policyCategories: PolicyCategories = {}, - transactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; +function renamePolicyTagList(policyData: PolicyData, policyTagListName: {oldName: string; newName: string}, tagListIndex: number) { + if (policyData.policy === undefined) { + return; + } + + const policyID = policyData.policy.id; const newName = policyTagListName.newName; const oldName = policyTagListName.oldName; - const oldPolicyTags = policyTags?.[oldName] ?? {}; + const oldPolicyTags = policyData.tags?.[oldName] ?? {}; const onyxData: OnyxData = { optimisticData: [ { @@ -967,26 +952,17 @@ function renamePolicyTagList( ], }; - const policyTagLists = policyTags - ? Object.keys(policyTags ?? {}).reduce((acc, tagName) => { + const policyTagLists = policyData.tags + ? Object.keys(policyData.tags ?? {}).reduce((acc, tagName) => { if (tagName === oldName) { return acc; } - acc[tagName] = {...policyTags?.[tagName]}; + acc[tagName] = {...policyData.tags?.[tagName]}; return acc; }, {}) : {}; - ReportUtils.pushTransactionViolationsOnyxData( - onyxData, - policy, - policyCategories, - policyTagLists, - transactionViolations, - {}, - {}, - {newName: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}, - ); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, {newName: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}); const parameters: RenamePolicyTagListParams = { policyID, @@ -998,10 +974,13 @@ function renamePolicyTagList( API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData); } -function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCategories: PolicyCategories = {}, transactionViolations: OnyxCollection = {}) { - const policyID = policy.id; - const policyTagLists = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const policyUpdate: Partial = { +function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { + if (policyData.policy === undefined) { + return; + } + + const policyID = policyData.policy.id; + const policyOptimisticData: Partial = { requiresTag, errors: {requiresTag: null}, pendingFields: { @@ -1014,7 +993,7 @@ function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCatego { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: policyUpdate, + value: policyOptimisticData, }, ], successData: [ @@ -1047,7 +1026,7 @@ function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCatego }; const getUpdatedTagsData = (required: boolean): PolicyTagLists => ({ - ...Object.keys(policyTagLists).reduce((acc, key) => { + ...Object.keys(policyData.tags).reduce((acc, key) => { acc[key] = { ...acc[key], required, @@ -1056,8 +1035,6 @@ function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCatego }, {}), }); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagLists, transactionViolations, policyUpdate, {}, getUpdatedTagsData(requiresTag)); - const getUpdatedTagsOnyxData = (required: boolean): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, onyxMethod: Onyx.METHOD.MERGE, @@ -1068,6 +1045,8 @@ function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCatego onyxData.failureData?.push(getUpdatedTagsOnyxData(!requiresTag)); onyxData.successData?.push(getUpdatedTagsOnyxData(requiresTag)); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, getUpdatedTagsData(requiresTag)); + const parameters = { policyID, requiresTag, @@ -1076,26 +1055,14 @@ function setPolicyRequiresTag(policy: Policy, requiresTag: boolean, policyCatego API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData); } -function setPolicyTagsRequired( - policy: Policy, - requiresTag: boolean, - tagListIndex: number, - policyTagsLists: PolicyTagLists = {}, - policyCategories: PolicyCategories = {}, - transactionViolations: OnyxCollection = {}, -) { - const policyID = policy.id; - const policyTag = PolicyUtils.getTagLists(policyTagsLists)?.at(tagListIndex); - - if (!policyTag) { - return; - } - - if (!policyTag.name) { +function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tagListIndex: number) { + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); + if (!policyTag || !policyTag.name || !policyData.policy) { return; } - const policyTagListsUpdate = { + const policyID = policyData.policy.id; + const policyTagsOptimisticData = { [policyTag.name]: { required: requiresTag, pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, @@ -1108,7 +1075,7 @@ function setPolicyTagsRequired( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagListsUpdate, + value: policyTagsOptimisticData, }, ], successData: [ @@ -1145,7 +1112,7 @@ function setPolicyTagsRequired( requireTagList: requiresTag, }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policy, policyCategories, policyTagsLists, transactionViolations, {}, {}, policyTagListsUpdate); + ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData); } diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 676f18ec5364e..9015a85019dc5 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -32,6 +32,7 @@ import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNo import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import usePolicyData from '@hooks/usePolicyData'; type IOURequestStepCategoryProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; @@ -54,8 +55,7 @@ function IOURequestStepCategory({ const report = reportReal ?? reportDraft; const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); + const policyData = usePolicyData(policy?.id); const {currentSearchHash} = useSearchContext(); const styles = useThemeStyles(); const theme = useTheme(); @@ -168,7 +168,7 @@ function IOURequestStepCategory({ } if (!policy.areCategoriesEnabled) { - enablePolicyCategories(policy, true, policyTagLists, allTransactionViolations, false); + enablePolicyCategories({...policyData, categories: policyCategories}, true, false); } InteractionManager.runAfterInteractions(() => { Navigation.navigate( diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 4c78bb3f3d6bf..c41c7ebe8cb73 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -13,7 +13,7 @@ import Switch from '@components/Switch'; import Text from '@components/Text'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; +import usePolicyData from '@hooks/usePolicyData'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; @@ -35,7 +35,6 @@ import { setWorkspaceCategoryEnabled, } from '@userActions/Policy/Category'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -50,21 +49,19 @@ function CategorySettingsPage({ }, navigation, }: CategorySettingsPageProps) { - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: false}); const styles = useThemeStyles(); const {translate} = useLocalize(); const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false); const policy = usePolicy(policyID); + const policyData = usePolicyData(policyID); const {environmentURL} = useEnvironment(); - const policyCategory = policyCategories?.[categoryName] ?? Object.values(policyCategories ?? {}).find((category) => category.previousCategoryName === categoryName); - const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; + const policyCategory = policyData.categories?.[categoryName] ?? Object.values(policyData.categories ?? {}).find((category) => category.previousCategoryName === categoryName); + const policyCurrency = policyData.policy?.outputCurrency ?? CONST.CURRENCY.USD; const policyCategoryExpenseLimitType = policyCategory?.expenseLimitType ?? CONST.POLICY.EXPENSE_LIMIT_TYPES.EXPENSE; const [isCannotDeleteOrDisableLastCategoryModalVisible, setIsCannotDeleteOrDisableLastCategoryModalVisible] = useState(false); - const shouldPreventDisableOrDelete = isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [policyCategory]); + const shouldPreventDisableOrDelete = isDisablingOrDeletingLastEnabledCategory(policy, policyData.categories, [policyCategory]); const areCommentsRequired = policyCategory?.areCommentsRequired ?? false; const isQuickSettingsFlow = name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_SETTINGS; @@ -129,10 +126,7 @@ function CategorySettingsPage({ setIsCannotDeleteOrDisableLastCategoryModalVisible(true); return; } - if (policy === undefined) { - return; - } - setWorkspaceCategoryEnabled(policy, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}, policyTagLists, allTransactionViolations); + setWorkspaceCategoryEnabled(policyData, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}); }; const navigateToEditCategory = () => { @@ -142,8 +136,8 @@ function CategorySettingsPage({ }; const deleteCategory = () => { - if (policy !== undefined) { - deleteWorkspaceCategories(policy, [categoryName], policyTagLists, allTransactionViolations); + if (policyData.policy !== undefined) { + deleteWorkspaceCategories(policyData, [categoryName]); } setDeleteCategoryConfirmModalVisible(false); navigateBack(); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 0f4f84077667d..ffd56be238e6d 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -29,7 +29,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; @@ -76,11 +76,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const {environmentURL} = useEnvironment(); const policyId = route.params.policyID; const backTo = route.params?.backTo; - const policy = usePolicy(policyId); const isMobileSelectionModeEnabled = useMobileSelectionMode(); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyId}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyId}`, {canBeMissing: true}); + const policyData = usePolicyData(policyId); + const policy = policyData.policy; + const policyCategories = policyData.categories; const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); @@ -142,12 +141,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const updateWorkspaceCategoryEnabled = useCallback( (value: boolean, categoryName: string) => { - if (policy === undefined) { - return; - } - setWorkspaceCategoryEnabled(policy, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations); + setWorkspaceCategoryEnabled(policyData, {[categoryName]: {name: categoryName, enabled: value}}); }, - [policy, policyTagLists, allTransactionViolations], + [policyData], ); const categoryList = useMemo(() => { @@ -259,7 +255,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const handleDeleteCategories = () => { if (policy !== undefined && selectedCategories.length >= 0) { - deleteWorkspaceCategories(policy, selectedCategories, policyTagLists, allTransactionViolations); + deleteWorkspaceCategories(policyData, selectedCategories); } setDeleteCategoriesConfirmModalVisible(false); @@ -364,10 +360,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return; } setSelectedCategories([]); - if (policy === undefined) { - return; - } - setWorkspaceCategoryEnabled(policy, categoriesToDisable, policyTagLists, allTransactionViolations); + setWorkspaceCategoryEnabled(policyData, categoriesToDisable); }, }); } @@ -389,10 +382,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedCategories([]); - if (policy === undefined) { - return; - } - setWorkspaceCategoryEnabled(policy, categoriesToEnable, policyTagLists, allTransactionViolations); + setWorkspaceCategoryEnabled(policyData, categoriesToEnable); }, }); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index d232c607e0111..f35ce04cc89dc 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {InteractionManager, Keyboard, View} from 'react-native'; import CategorySelectorModal from '@components/CategorySelector/CategorySelectorModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -8,7 +8,7 @@ import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -34,39 +34,30 @@ type WorkspaceCategoriesSettingsPageProps = WithPolicyConnectionsProps & ); function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSettingsPageProps) { + const {policyID, backTo} = route.params; const styles = useThemeStyles(); const {translate} = useLocalize(); + const policyData = usePolicyData(policyID); const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; - const policyID = route.params.policyID; - const backTo = route.params.backTo; - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [currentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); const currentConnectionName = getCurrentConnectionName(policy); const [isSelectorModalVisible, setIsSelectorModalVisible] = useState(false); const [categoryID, setCategoryID] = useState(); const [groupID, setGroupID] = useState(); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_SETTINGS; const toggleSubtitle = isConnectedToAccounting && currentConnectionName ? translate('workspace.categories.needCategoryForExportToIntegration', {connectionName: currentConnectionName}) : undefined; - const updateWorkspaceRequiresCategory = (value: boolean) => { - if (policy === undefined) { - return; - } - setWorkspaceRequiresCategory(policy, value, policyTagLists, allTransactionViolations); - }; + const updateWorkspaceRequiresCategory = useCallback((value: boolean) => setWorkspaceRequiresCategory(policyData, value), [policyData]); const {sections} = useMemo(() => { - if (!(currentPolicy && currentPolicy.mccGroup)) { + if (!(policyData.policy && policyData.policy.mccGroup)) { return {sections: [{data: []}]}; } return { sections: [ { - data: Object.entries(currentPolicy.mccGroup).map( + data: Object.entries(policyData.policy.mccGroup).map( ([mccKey, mccGroup]) => ({ categoryID: mccGroup.category, @@ -79,9 +70,9 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet }, ], }; - }, [currentPolicy]); + }, [policyData.policy]); - const hasEnabledCategories = hasEnabledOptions(policyCategories ?? {}); + const hasEnabledCategories = hasEnabledOptions(policyData.categories); const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledCategories || isConnectedToAccounting; const setNewCategory = (selectedCategory: ListItem) => { @@ -129,7 +120,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet /> - {!!currentPolicy && (sections.at(0)?.data?.length ?? 0) > 0 && ( + {!!policyData.policy && (sections.at(0)?.data?.length ?? 0) > 0 && ( ; function TagSettingsPage({route, navigation}: TagSettingsPageProps) { - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`, {canBeMissing: true}); const {orderWeight, policyID, tagName, backTo, parentTagsFilter} = route.params; + + const policyData = usePolicyData(policyID); + const {policy, tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const policyTag = useMemo(() => getTagListByOrderWeight(policyTags, orderWeight), [policyTags, orderWeight]); - const policy = usePolicy(policyID); const {environmentURL} = useEnvironment(); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = React.useState(false); const [isCannotDeleteOrDisableLastTagModalVisible, setIsCannotDeleteOrDisableLastTagModalVisible] = useState(false); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_SETTINGS; @@ -81,9 +78,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - if (policy !== undefined) { - deletePolicyTags(policy, [currentPolicyTag.name], policyCategories, allTransactionViolations); - } + deletePolicyTags(policyData, [currentPolicyTag.name]); setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; @@ -96,7 +91,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { if (policy === undefined) { return; } - setWorkspaceTagEnabled(policy, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); + setWorkspaceTagEnabled(policyData, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; const navigateToEditTag = () => { diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 9fb7c9c65c461..e896b84f0adc1 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -8,8 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -28,10 +27,8 @@ type WorkspaceEditTagsPageProps = | PlatformStackScreenProps; function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { - const policy = usePolicy(route.params.policyID); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`, {canBeMissing: true}); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`, {canBeMissing: true}); - const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); + const policyData = usePolicyData(route.params.policyID); + const {tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const tagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); @@ -71,19 +68,16 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { const updateTagListName = useCallback( (values: FormOnyxValues) => { - if (policy !== undefined && values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { + if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { renamePolicyTagList( - policy, + policyData, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, - policyTags, route.params.orderWeight, - policyCategories, - allTransactionViolations, ); } goBackToTagsSettings(); }, - [tagListName, goBackToTagsSettings, route.params.orderWeight, policy, policyTags, policyCategories, allTransactionViolations], + [tagListName, goBackToTagsSettings, route.params.orderWeight, policyData], ); return ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 8a5b0d5ebcf75..7f5697084152b 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -30,6 +30,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; @@ -93,7 +94,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const policyID = route.params.policyID; const backTo = route.params.backTo; const policy = usePolicy(policyID); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const {environmentURL} = useEnvironment(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); @@ -102,11 +102,15 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const connectedIntegration = getConnectedIntegration(policy) ?? connectionSyncProgress?.connectionName; const isConnectionVerified = connectedIntegration && !isConnectionUnverified(policy, connectedIntegration); const currentConnectionName = getCurrentConnectionName(policy); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); + const policyData = usePolicyData(policyID); const [policyTagLists, isMultiLevelTags, hasDependentTags, hasIndependentTags] = useMemo( - () => [getTagLists(policyTags), isMultiLevelTagsPolicyUtils(policyTags), hasDependentTagsPolicyUtils(policy, policyTags), hasIndependentTagsPolicyUtils(policy, policyTags)], - [policy, policyTags], + () => [ + getTagLists(policyData.tags), + isMultiLevelTagsPolicyUtils(policyData.tags), + hasDependentTagsPolicyUtils(policy, policyData.tags), + hasIndependentTagsPolicyUtils(policy, policyData.tags), + ], + [policy, policyData.tags], ); const canSelectMultiple = !hasDependentTags && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); const fetchTags = useCallback(() => { @@ -205,12 +209,12 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policy === undefined) { + if (policyData?.policy === undefined) { return; } - setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, 0); + setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, 0); }, - [policy], + [policyData], ); const updateWorkspaceRequiresTag = useCallback( @@ -218,9 +222,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (policy === undefined) { return; } - setPolicyTagsRequired(policy, value, orderWeight, policyTags, policyCategories, allTransactionViolations); + setPolicyTagsRequired(policyData, value, orderWeight); }, - [allTransactionViolations, policy, policyCategories, policyTags], + [policyData], ); const tagList = useMemo(() => { @@ -253,7 +257,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { isOn={isSwitchEnabled} accessibilityLabel={translate('workspace.tags.requiresTag')} onToggle={(newValue: boolean) => { - if (isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList])) { + if (isMakingLastRequiredTagListOptional(policy, policyData.tags, [policyTagList])) { setIsCannotMakeLastTagOptionalModalVisible(true); return; } @@ -261,7 +265,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { updateWorkspaceRequiresTag(newValue, policyTagList.orderWeight); }} disabled={isSwitchDisabled} - showLockIcon={isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList])} + showLockIcon={isMakingLastRequiredTagListOptional(policy, policyData.tags, [policyTagList])} /> ), }; @@ -292,7 +296,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { /> ), })); - }, [isMultiLevelTags, policyTagLists, hasDependentTags, translate, policy, policyTags, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); + }, [isMultiLevelTags, policyTagLists, hasDependentTags, translate, policy, policyData.tags, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); const filterTag = useCallback((tag: TagListItem, searchInput: string) => { const tagText = StringUtils.normalize(tag.text?.toLowerCase() ?? ''); @@ -380,9 +384,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - if (policy !== undefined && selectedTags.length > 0) { - deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); - } + deletePolicyTags(policyData, selectedTags); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -393,7 +395,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }); }; - const isLoading = !isOffline && policyTags === undefined; + const isLoading = !isOffline && policyData.tags === undefined; const hasVisibleTags = tagList.some((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); const navigateToImportSpreadsheet = useCallback(() => { @@ -538,7 +540,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } // Disable the selected tags - setWorkspaceTagEnabled(policy, tagsToDisable, 0); + setWorkspaceTagEnabled(policyData, tagsToDisable, 0); }, }); } @@ -554,7 +556,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { return; } // Enable the selected tags - setWorkspaceTagEnabled(policy, tagsToEnable, 0); + setWorkspaceTagEnabled(policyData, tagsToEnable, 0); }, }); } @@ -581,7 +583,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { text: translate('workspace.tags.notRequireTags'), value: CONST.POLICY.BULK_ACTION_TYPES.REQUIRE, onSelected: () => { - if (isMakingLastRequiredTagListOptional(policy, policyTags, selectedTagLists)) { + if (isMakingLastRequiredTagListOptional(policy, policyData.tags, selectedTagLists)) { setIsCannotMakeLastTagOptionalModalVisible(true); return; } @@ -589,7 +591,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (policy === undefined) { return; } - setWorkspaceTagRequired(policy, tagListIndexesToMarkOptional, false, policyTags, policyCategories, allTransactionViolations); + setWorkspaceTagRequired(policyData, tagListIndexesToMarkOptional, false); }, }); } @@ -604,7 +606,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (policy === undefined) { return; } - setWorkspaceTagRequired(policy, tagListIndexesToMarkRequired, true, policyTags, policyCategories, allTransactionViolations); + setWorkspaceTagRequired(policyData, tagListIndexesToMarkRequired, true); }, }); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index ae10491eb510a..a51f7becab0a5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -10,8 +10,7 @@ import Switch from '@components/Switch'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import {disableWorkspaceBillableExpenses, setPolicyBillableMode} from '@libs/actions/Policy/Policy'; import {clearPolicyTagListErrors, setPolicyRequiresTag} from '@libs/actions/Policy/Tag'; @@ -22,7 +21,6 @@ import {getTagLists as getTagListsUtil, isMultiLevelTags as isMultiLevelTagsUtil import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; @@ -54,22 +52,21 @@ function toggleBillableExpenses(policy: OnyxEntry) { function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const policyID = route.params.policyID; const backTo = route.params.backTo; - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const styles = useThemeStyles(); - const currentPolicy = usePolicy(policyID); + const policyData = usePolicyData(policyID); const {translate} = useLocalize(); - const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); - const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; + const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyData.tags), isMultiLevelTagsUtil(policyData.tags)], [policyData.tags]); + const isLoading = !getTagListsUtil(policyData.tags)?.at(0) || Object.keys(policyData.tags ?? {}).at(0) === 'undefined'; const {isOffline} = useNetwork(); - const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); + const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyData.tags ?? {}).flatMap(({tags}) => Object.values(tags))); const updateWorkspaceRequiresTag = useCallback( (value: boolean) => { - if (currentPolicy === undefined) { + if (policyData.policy === undefined) { return; } - setPolicyRequiresTag(currentPolicy, value); + setPolicyRequiresTag(policyData, value); }, - [currentPolicy], + [policyData], ); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_SETTINGS; @@ -77,9 +74,9 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { {!isMultiLevelTags && ( clearPolicyTagListErrors(policyID, policyTagLists.at(0)?.orderWeight ?? 0)} - pendingAction={policyTags?.[policyTagLists.at(0)?.name ?? '']?.pendingAction} + pendingAction={policyData.tags?.[policyTagLists.at(0)?.name ?? '']?.pendingAction} errorRowStyles={styles.mh5} > getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); - const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); + const currentTagListName = useMemo(() => getTagListName(policyData.tags, route.params.orderWeight), [policyData.tags, route.params.orderWeight]); + const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyData.tags), [policy, policyData.tags]); const currentPolicyTag = policyTags?.[currentTagListName]; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW; @@ -121,9 +122,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { if (policy === undefined) { return; } - setWorkspaceTagEnabled(policy, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight); }, - [allTransactionViolations, policy, policyCategories, route.params.orderWeight], + [policyData, route.params.orderWeight], ); const tagList = useMemo( @@ -219,8 +220,8 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const deleteTags = () => { - if (policy !== undefined && selectedTags.length > 0) { - deletePolicyTags(policy, selectedTags, policyCategories, allTransactionViolations); + if (!!policyData.policy.id !== undefined && selectedTags.length > 0) { + deletePolicyTags(policyData, selectedTags); } setIsDeleteTagsConfirmModalVisible(false); @@ -295,7 +296,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } // Disable the selected tags - setWorkspaceTagEnabled(policy, tagsToDisable, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policyData, tagsToDisable, route.params.orderWeight); }, }); } @@ -311,7 +312,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } // Enable the selected tags - setWorkspaceTagEnabled(policy, tagsToEnable, route.params.orderWeight, policyCategories, allTransactionViolations); + setWorkspaceTagEnabled(policyData, tagsToEnable, route.params.orderWeight); }, }); } @@ -332,7 +333,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; if (policy !== undefined && !!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) { - setPolicyTagsRequired(policy, false, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); + setPolicyTagsRequired(policyData, false, route.params.orderWeight); } const navigateToEditTag = () => { @@ -395,7 +396,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { if (policy === undefined) { return; } - setPolicyTagsRequired(policy, on, route.params.orderWeight, policyTags, policyCategories, allTransactionViolations); + setPolicyTagsRequired(policyData, on, route.params.orderWeight); }} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} From 63e0d8c70c415c276dab5065a2e7a116df780459 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 28 Aug 2025 04:32:30 +0300 Subject: [PATCH 0039/1005] converted renamePolicyCategory to accept policyData --- src/libs/actions/Policy/Category.ts | 97 +++++++++++-------- .../workspace/categories/EditCategoryPage.tsx | 14 +-- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index ec8dec73eeb61..46bd1a486e515 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -272,10 +272,10 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category function setWorkspaceCategoryEnabled(policyData: PolicyData, categoriesToUpdate: Record) { if (Object.keys(categoriesToUpdate).length === 0) { Log.warn('[setWorkspaceCategoryEnabled] The "categoriesToUpdate" param is empty with no categories to update.'); - return + return; } - if (!policyData.policy){ + if (!policyData.policy) { return; } @@ -425,25 +425,25 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st } function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string, maxAmountNoReceipt: number) { - if (!categoryName){ + if (!categoryName) { Log.warn('[setPolicyCategoryReceiptsRequired] The "categoryName" param is empty with no category to update.'); } if (policyData.policy === undefined) { return; - }; + } const policyID = policyData.policy.id; const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; const policyCategoriesOptimisticData = { - [categoryName]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - maxAmountNoReceipt, + [categoryName]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, - }; + maxAmountNoReceipt, + }, + }; const onyxData: OnyxData = { optimisticData: [ @@ -587,11 +587,14 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[]) API.write(WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET, parameters, onyxData); } -function renamePolicyCategory(policyID: string, policyCategory: {oldName: string; newName: string}) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation - const policy = getPolicy(policyID); - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[policyCategory.oldName]; +function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: string; newName: string}) { + if (policyData.policy === undefined) { + return; + } + + const policy = policyData.policy; + const policyID = policy.id; + const policyCategoryToUpdate = policyData.categories?.[policyCategory.oldName]; const policyCategoryApproverRule = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], policyCategory.oldName); const policyCategoryExpenseRule = CategoryUtils.getCategoryExpenseRule(policy?.rules?.expenseRules ?? [], policyCategory.oldName); @@ -628,6 +631,28 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string updatedApprovalRules[indexToUpdate] = policyCategoryApproverRule; } + const policyOptimisticData = { + rules: { + approvalRules: updatedApprovalRules, + expenseRules: updatedExpenseRules, + }, + mccGroup: updatedMccGroup, + }; + + const policyCategoriesOptimisticData = { + [policyCategory.newName]: { + ...policyCategoryToUpdate, + errors: null, + name: policyCategory.newName, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + ...(policyCategoryToUpdate?.pendingFields ?? {}), + name: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + previousCategoryName: policyCategory.oldName, + }, + }; + const onyxData: OnyxData = { optimisticData: [ { @@ -635,29 +660,13 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [policyCategory.oldName]: null, - [policyCategory.newName]: { - ...policyCategoryToUpdate, - errors: null, - name: policyCategory.newName, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - ...(policyCategoryToUpdate?.pendingFields ?? {}), - name: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - previousCategoryName: policyCategory.oldName, - }, + ...policyCategoriesOptimisticData, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - rules: { - approvalRules: updatedApprovalRules, - expenseRules: updatedExpenseRules, - }, - mccGroup: updatedMccGroup, - }, + value: policyOptimisticData, }, ], successData: [ @@ -706,6 +715,16 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string ], }; + const policyCategories = Object.values(policyData.categories ?? {}).reduce((acc, category) => { + if (category.name === policyCategory.oldName) { + return acc; + } + acc[category.name] = category; + return acc; + }, {}); + + pushTransactionViolationsOnyxData(onyxData, {...policyData, categories: policyCategories}, policyOptimisticData, policyCategoriesOptimisticData); + const parameters = { policyID, categories: JSON.stringify({[policyCategory.oldName]: policyCategory.newName}), @@ -853,7 +872,7 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean) { if (policyData.policy === undefined) { return; - }; + } const policyID = policyData.policy.id; const policyCategoriesOptimisticData: Partial = { @@ -938,10 +957,10 @@ function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete Log.warn('[deleteWorkspaceCategories] The "categoryNamesToDelete" param is empty with no categories to delete.'); return; } - + if (policyData.policy === undefined) { return; - }; + } const policyID = policyData.policy.id; const policyCategoriesOptimisticData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; @@ -1001,8 +1020,8 @@ function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete function enablePolicyCategories(policyData: PolicyData, enabled: boolean, shouldGoBack = true) { if (policyData.policy === undefined) { return; - }; - + } + const policyID = policyData.policy.id; const onyxUpdatesToDisableCategories: OnyxUpdate[] = []; if (!enabled) { diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 6d8541ea6014f..88ae9d6fc8979 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -15,6 +15,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import CategoryForm from './CategoryForm'; +import usePolicyData from '@hooks/usePolicyData'; type EditCategoryPageProps = | PlatformStackScreenProps @@ -22,13 +23,14 @@ type EditCategoryPageProps = function EditCategoryPage({route}: EditCategoryPageProps) { const policyID = route.params.policyID; - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const policyData = usePolicyData(policyID); const styles = useThemeStyles(); const {translate} = useLocalize(); const currentCategoryName = route.params.categoryName; const backTo = route.params?.backTo; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_EDIT; - + + const validate = useCallback( (values: FormOnyxValues) => { const errors: FormInputErrors = {}; @@ -36,7 +38,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { if (!newCategoryName) { errors.categoryName = translate('workspace.categories.categoryRequiredError'); - } else if (policyCategories?.[newCategoryName] && currentCategoryName !== newCategoryName) { + } else if (policyData.categories?.[newCategoryName] && currentCategoryName !== newCategoryName) { errors.categoryName = translate('workspace.categories.existingCategoryError'); } else if ([...newCategoryName].length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units. @@ -45,7 +47,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { return errors; }, - [policyCategories, currentCategoryName, translate], + [policyData.categories, currentCategoryName, translate], ); const editCategory = useCallback( @@ -53,7 +55,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { const newCategoryName = values.categoryName.trim(); // Do not call the API if the edited category name is the same as the current category name if (currentCategoryName !== newCategoryName) { - renamePolicyCategory(policyID, {oldName: currentCategoryName, newName: values.categoryName}); + renamePolicyCategory(policyData, {oldName: currentCategoryName, newName: values.categoryName}); } // Ensure Onyx.update is executed before navigation to prevent UI blinking issues, affecting the category name and rate. @@ -95,7 +97,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { onSubmit={editCategory} validateEdit={validate} categoryName={currentCategoryName} - policyCategories={policyCategories} + policyCategories={policyData.categories} /> From c5470a2df71c8d4b0cad9d326f3149d1ec250038 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 28 Aug 2025 07:57:03 +0300 Subject: [PATCH 0040/1005] refactored tests and perfomance tests --- tests/actions/PolicyCategoryTest.ts | 18 ++++++++--- tests/actions/PolicyTagTest.ts | 41 ++++++++++++++++-------- tests/perf-test/ReportUtils.perf-test.ts | 32 ++++++++++++------ tests/ui/UnreadIndicatorsTest.tsx | 2 +- tests/unit/ReportUtilsTest.ts | 25 ++++++++++++--- 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 1de13a9254302..80fb2f9262c37 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -1,4 +1,6 @@ +import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +import usePolicyData from '@hooks/usePolicyData'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Category from '@src/libs/actions/Policy/Category'; @@ -31,7 +33,9 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - Category.setWorkspaceRequiresCategory(fakePolicy, true); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + Category.setWorkspaceRequiresCategory(policyData.current, true); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ @@ -116,7 +120,9 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.renamePolicyCategory(fakePolicy.id, { + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + Category.renamePolicyCategory(policyData.current, { oldName: oldCategoryName ?? '', newName: newCategoryName, }); @@ -169,7 +175,9 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.setWorkspaceCategoryEnabled(fakePolicy, categoriesToUpdate); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + Category.setWorkspaceCategoryEnabled(policyData.current, categoriesToUpdate); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ @@ -214,7 +222,9 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.deleteWorkspaceCategories(fakePolicy, categoriesToDelete); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 4a629529646f7..29fd537e36673 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -1,4 +1,6 @@ +import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +import usePolicyData from '@hooks/usePolicyData'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import {createPolicyTag, deletePolicyTags, renamePolicyTag, renamePolicyTagList, setPolicyRequiresTag, setWorkspaceTagEnabled} from '@libs/actions/Policy/Tag'; import CONST from '@src/CONST'; @@ -34,7 +36,8 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - setPolicyRequiresTag(fakePolicy, true); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setPolicyRequiresTag(policyData.current, true); return waitForBatchedUpdates(); }) .then( @@ -81,7 +84,8 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - setPolicyRequiresTag(fakePolicy, false); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setPolicyRequiresTag(policyData.current, false); return waitForBatchedUpdates(); }) .then( @@ -129,7 +133,8 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { mockFetch?.fail?.(); - setPolicyRequiresTag(fakePolicy, false); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setPolicyRequiresTag(policyData.current, false); return waitForBatchedUpdates(); }) @@ -162,7 +167,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - setPolicyRequiresTag(fakePolicy, true); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setPolicyRequiresTag(policyData.current, true); await waitForBatchedUpdates(); let updatePolicyTags: PolicyTagLists | undefined; @@ -192,13 +198,14 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + renamePolicyTagList( - fakePolicy, + policyData.current, { oldName: oldTagListName, newName: newTagListName, }, - fakePolicyTags, Object.values(fakePolicyTags).at(0)?.orderWeight ?? 0, ); return waitForBatchedUpdates(); @@ -260,13 +267,13 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); renamePolicyTagList( - fakePolicy, + policyData.current, { oldName: oldTagListName, newName: newTagListName, }, - fakePolicyTags, Object.values(fakePolicyTags).at(0)?.orderWeight ?? 0, ); return waitForBatchedUpdates(); @@ -419,7 +426,8 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - setWorkspaceTagEnabled(fakePolicy, tagsToUpdate, 0); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); return waitForBatchedUpdates(); }) .then( @@ -492,7 +500,8 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - setWorkspaceTagEnabled(fakePolicy, tagsToUpdate, 0); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); return waitForBatchedUpdates(); }) .then(mockFetch?.resume) @@ -538,8 +547,9 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); renamePolicyTag( - fakePolicy, + policyData.current, { oldName: oldTagName ?? '', newName: newTagName, @@ -608,8 +618,9 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); renamePolicyTag( - fakePolicy, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -657,7 +668,8 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - deletePolicyTags(fakePolicy, tagsToDelete); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + deletePolicyTags(policyData.current, tagsToDelete); return waitForBatchedUpdates(); }) .then( @@ -717,7 +729,8 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - deletePolicyTags(fakePolicy, tagsToDelete); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + deletePolicyTags(policyData.current, tagsToDelete); return waitForBatchedUpdates(); }) .then(mockFetch?.resume) diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 84d2f4edc1d6a..8520560e2cce5 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -1,7 +1,9 @@ +import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; import createRandomPolicyCategories from 'tests/utils/collections/policyCategory'; import createRandomPolicyTags from 'tests/utils/collections/policyTags'; +import usePolicyData from '@hooks/usePolicyData'; import { canDeleteReportAction, canShowReportRecipientLocalTime, @@ -203,13 +205,14 @@ describe('ReportUtils', () => { await measureFunction(() => getTransactionDetails(transaction, 'yyyy-MM-dd')); }); - test('[ReportUtils] pushTransactionViolationsOnyxData on 1k reports with 3 transactions on each report', async () => { + test('[ReportUtils] pushTransactionViolationsOnyxData on 1k reports with 100 expenses on each report', async () => { // Current policy with categories and tags enabled but does not require them const policy = { ...createRandomPolicy(1), areCategoriesEnabled: true, - requiresCategory: false, areTagsEnabled: true, + + requiresCategory: false, requiresTag: false, }; @@ -220,7 +223,7 @@ describe('ReportUtils', () => { }; // Create a report collection with 1000 reports linked to the policy - const reportCollection = Object.values(getMockedReports(1000)).reduce>((acc, report) => { + const reportCollection = Object.values(getMockedReports(10000)).reduce>((acc, report) => { acc[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = { ...report, policyID: policy.id, @@ -228,15 +231,17 @@ describe('ReportUtils', () => { return acc; }, {}); - // Create a transaction collection with 3 transactions for each report + // Create a transaction collection with 8 transactions for each report const transactionCollection = Object.values(reportCollection).reduce>((acc, report, index) => { - for (let transactionIndex = 0; transactionIndex < 3; transactionIndex++) { - const transactionID = index + transactionIndex * 1000; + for (let transactionIndex = 0; transactionIndex < 100; transactionIndex++) { + const transactionID = index * 10 + transactionIndex; + // Create a transaction with no category and no tag acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = { ...createRandomTransaction(transactionID), reportID: report.reportID, category: undefined, + tag: undefined, }; } return acc; @@ -258,18 +263,25 @@ describe('ReportUtils', () => { return acc; }, {}); + const policyTags = createRandomPolicyTags('Tags', 8); + const policyCategories = createRandomPolicyCategories(8); + await Onyx.multiSet({ ...reportCollection, ...transactionCollection, ...reportActionsCollection, [ONYXKEYS.COLLECTION.POLICY]: {[policy.id]: policy}, + [ONYXKEYS.COLLECTION.POLICY_TAGS]: {[policy.id]: policyTags}, + [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: {[policy.id]: policyCategories}, }); - const policyTags = createRandomPolicyTags('Tags', 8); - const policyCategories = createRandomPolicyCategories(8); - await waitForBatchedUpdates(); - await measureFunction(() => pushTransactionViolationsOnyxData({}, policy, policyCategories, policyTags, {}, policyOptimisticData, {}, {})); + + const {result: policyData} = renderHook(() => usePolicyData(policy.id)); + + const onyxData = {optimisticData: [], failureData: []}; + + await measureFunction(() => pushTransactionViolationsOnyxData(onyxData, policyData.current, policyOptimisticData)); }); test('[ReportUtils] getIOUReportActionDisplayMessage on 1k policies', async () => { diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 44a0575a71fc4..623db7e3e70d3 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -342,7 +342,7 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify notification was created - expect(LocalNotification.showCommentNotification).toBeCalled(); + expect(LocalNotification.showCommentNotification).toHaveBeenCalled(); }) .then(() => { // // Verify the new report option appears in the LHN diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index fa31c575c60b7..6d6cc6720ea43 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,6 +4,7 @@ import {renderHook} from '@testing-library/react-native'; import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import usePolicyData from '@hooks/usePolicyData'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {putOnHold} from '@libs/actions/IOU'; import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow'; @@ -82,10 +83,21 @@ import {buildOptimisticTransaction} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployeeList, PolicyTag, Report, ReportAction, ReportActions, ReportNameValuePairs, Transaction} from '@src/types/onyx'; +import type { + Beta, + OnyxInputOrEntry, + PersonalDetailsList, + Policy, + PolicyEmployeeList, + PolicyTag, + Report, + ReportAction, + ReportActions, + ReportNameValuePairs, + Transaction, +} from '@src/types/onyx'; import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {Participant} from '@src/types/onyx/Report'; -import type {OnyxData} from '@src/types/onyx/Request'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; import {chatReportR14932 as mockedChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; @@ -4600,6 +4612,9 @@ describe('ReportUtils', () => { // Populating Onyx with required data await Onyx.multiSet({ ...fakePolicyReports, + [`${ONYXKEYS.COLLECTION.POLICY_TAGS}`]: fakePolicyTagsLists, + [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`]: fakePolicyCategories, + [`${ONYXKEYS.COLLECTION.POLICY}${fakePolicyID}`]: fakePolicy, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { [mockIOUAction.reportActionID]: mockIOUAction, }, @@ -4611,9 +4626,11 @@ describe('ReportUtils', () => { }, }); - const onyxData: OnyxData = {optimisticData: [], failureData: []}; + const {result: policyData} = renderHook(() => usePolicyData(fakePolicyID)); - pushTransactionViolationsOnyxData(onyxData, fakePolicy, fakePolicyCategories, fakePolicyTagsLists, {}, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); + const onyxData = {optimisticData: [], failureData: []}; + + pushTransactionViolationsOnyxData(onyxData, policyData.current, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); expect(onyxData).toMatchObject({ // Expecting the optimistic data to contain the OUT_OF_POLICY violations for the deleted category and tag From 42e2557252b230a64b905c589f7962243b46b704 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 28 Aug 2025 08:24:35 +0300 Subject: [PATCH 0041/1005] refactored other functions and pages to depend on usePolicyData --- src/hooks/usePolicyData.ts | 29 ++++---- src/libs/ReportUtils.ts | 11 ++- src/libs/actions/Policy/Category.ts | 64 +++++------------ src/libs/actions/Policy/Tag.ts | 70 +++++++------------ .../FloatingActionButtonAndPopover.tsx | 1 - .../request/step/IOURequestStepCategory.tsx | 2 +- .../workspace/WorkspaceMoreFeaturesPage.tsx | 15 ++-- .../CategoryRequireReceiptsOverPage.tsx | 15 ++-- .../categories/CategorySettingsPage.tsx | 2 +- .../workspace/categories/EditCategoryPage.tsx | 16 ++--- .../categories/WorkspaceCategoriesPage.tsx | 15 ++-- .../WorkspaceCategoriesSettingsPage.tsx | 17 +++-- src/pages/workspace/tags/EditTagPage.tsx | 18 ++--- src/pages/workspace/tags/TagSettingsPage.tsx | 9 +-- .../workspace/tags/WorkspaceEditTagsPage.tsx | 30 +++----- .../workspace/tags/WorkspaceTagsPage.tsx | 36 +++++----- .../workspace/tags/WorkspaceViewTagsPage.tsx | 37 +++++----- 17 files changed, 159 insertions(+), 228 deletions(-) diff --git a/src/hooks/usePolicyData.ts b/src/hooks/usePolicyData.ts index 92c7d33eb0fb7..829343a0bd146 100644 --- a/src/hooks/usePolicyData.ts +++ b/src/hooks/usePolicyData.ts @@ -1,21 +1,20 @@ -import {useOnyx } from 'react-native-onyx'; -import type {Report, Policy, PolicyCategories, PolicyTagLists } from '@src/types/onyx'; -import type { OnyxValueWithOfflineFeedback } from '@src/types/onyx/OnyxCommon'; -import type {ReportTransactionsAndViolationsDerivedValue } from '@src/types/onyx/DerivedValues'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; -import usePolicy from './usePolicy'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; +import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; +import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; +import useOnyx from './useOnyx'; +import usePolicy from './usePolicy'; type PolicyData = { policy: OnyxValueWithOfflineFeedback; - reports: OnyxValueWithOfflineFeedback[]; + reports: Array>; tags: PolicyTagLists; categories: PolicyCategories; transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; }; - -function usePolicyData(policyID?: string):PolicyData { +function usePolicyData(policyID?: string): PolicyData { const policy = usePolicy(policyID); const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); @@ -28,12 +27,12 @@ function usePolicyData(policyID?: string):PolicyData { if (!policyID) { return []; } - return Object.values(reportCollection ?? {}).filter((report) => report !== undefined && report.reportID && report.policyID === policyID); - } + return Object.values(reportCollection ?? {}).filter((report) => !!report?.reportID && report.policyID === policyID); + }, }); const transactionsAndViolations = (reports ?? []).reduce((acc, report) => { - if (report === undefined || !report.reportID) { + if (!report?.reportID) { return acc; } const reportTransactionsAndViolations = allReportsTransactionsAndViolations?.[report.reportID]; @@ -45,12 +44,12 @@ function usePolicyData(policyID?: string):PolicyData { return { policy: policy as OnyxValueWithOfflineFeedback, - reports: reports as OnyxValueWithOfflineFeedback[], + reports: reports as Array>, tags: tags ?? {}, categories: categories ?? {}, - transactionsAndViolations: transactionsAndViolations, + transactionsAndViolations, }; } -export type {PolicyData} -export default usePolicyData; \ No newline at end of file +export type {PolicyData}; +export default usePolicyData; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index afa1f3671efa2..c9252934cb18b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8,7 +8,6 @@ import isEmpty from 'lodash/isEmpty'; import isNumber from 'lodash/isNumber'; import mapValues from 'lodash/mapValues'; import lodashMaxBy from 'lodash/maxBy'; -import type { OnyxValueWithOfflineFeedback } from '@src/types/onyx/OnyxCommon'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; @@ -60,7 +59,7 @@ import type {Attendee, Participant} from '@src/types/onyx/IOU'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction'; import type Onboarding from '@src/types/onyx/Onboarding'; -import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields, Errors, Icon, OnyxValueWithOfflineFeedback, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {AllConnectionName, ConnectionName} from '@src/types/onyx/Policy'; @@ -1874,9 +1873,9 @@ function pushTransactionViolationsOnyxData( const isPolicyUpdateEmpty = isEmptyObject(policyUpdate); const isPolicyTagsUpdateEmpty = isEmptyObject(policyTagsUpdate); const isPolicyCategoriesUpdateEmpty = isEmptyObject(policyCategoriesUpdate); - + // If there are no updates to policy, categories or tags, return early - if (isPolicyUpdateEmpty && isPolicyTagsUpdateEmpty && isPolicyCategoriesUpdateEmpty ) { + if (isPolicyUpdateEmpty && isPolicyTagsUpdateEmpty && isPolicyCategoriesUpdateEmpty) { return; } @@ -1919,7 +1918,6 @@ function pushTransactionViolationsOnyxData( // Iterate through all policy reports to find transactions that need optimistic violations for (const report of policyData.reports) { - // Skipping invoice reports since they should not have any category or tag violations if (isInvoiceReport(report)) { continue; @@ -1931,10 +1929,9 @@ function pushTransactionViolationsOnyxData( continue; } - const {transactions, violations} = reportTransactionsAndViolations + const {transactions, violations} = reportTransactionsAndViolations; for (const transaction of Object.values(transactions)) { - const existingViolations = violations[transaction.transactionID]; const optimisticViolations = ViolationsUtils.getViolationsOnyxData( diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 46bd1a486e515..38e8f20477515 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -270,15 +270,6 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category } function setWorkspaceCategoryEnabled(policyData: PolicyData, categoriesToUpdate: Record) { - if (Object.keys(categoriesToUpdate).length === 0) { - Log.warn('[setWorkspaceCategoryEnabled] The "categoriesToUpdate" param is empty with no categories to update.'); - return; - } - - if (!policyData.policy) { - return; - } - const policyID = policyData.policy.id; const policyCategoriesOptimisticData = { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { @@ -425,14 +416,6 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st } function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string, maxAmountNoReceipt: number) { - if (!categoryName) { - Log.warn('[setPolicyCategoryReceiptsRequired] The "categoryName" param is empty with no category to update.'); - } - - if (policyData.policy === undefined) { - return; - } - const policyID = policyData.policy.id; const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; const policyCategoriesOptimisticData = { @@ -487,6 +470,7 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: }; pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); + const parameters: SetPolicyCategoryReceiptsRequiredParams = { policyID, categoryName, @@ -496,23 +480,25 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED, parameters, onyxData); } -function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: string) { - const originalMaxAmountNoReceipt = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.maxAmountNoReceipt; +function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string) { + const policyID = policyData.policy.id; + const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; + const policyCategoriesOptimisticData = { + [categoryName]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + maxAmountNoReceipt: null, + }, + }; const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - maxAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - maxAmountNoReceipt: null, - }, - }, + value: policyCategoriesOptimisticData, }, ], successData: [ @@ -548,6 +534,8 @@ function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: st ], }; + pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); + const parameters: RemovePolicyCategoryReceiptsRequiredParams = { policyID, categoryName, @@ -588,10 +576,6 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[]) } function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: string; newName: string}) { - if (policyData.policy === undefined) { - return; - } - const policy = policyData.policy; const policyID = policy.id; const policyCategoryToUpdate = policyData.categories?.[policyCategory.oldName]; @@ -870,10 +854,6 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: } function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean) { - if (policyData.policy === undefined) { - return; - } - const policyID = policyData.policy.id; const policyCategoriesOptimisticData: Partial = { requiresCategory, @@ -953,14 +933,6 @@ function clearCategoryErrors(policyID: string, categoryName: string) { } function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete: string[]) { - if (categoryNamesToDelete.length === 0) { - Log.warn('[deleteWorkspaceCategories] The "categoryNamesToDelete" param is empty with no categories to delete.'); - return; - } - - if (policyData.policy === undefined) { - return; - } const policyID = policyData.policy.id; const policyCategoriesOptimisticData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; @@ -1018,10 +990,6 @@ function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete } function enablePolicyCategories(policyData: PolicyData, enabled: boolean, shouldGoBack = true) { - if (policyData.policy === undefined) { - return; - } - const policyID = policyData.policy.id; const onyxUpdatesToDisableCategories: OnyxUpdate[] = []; if (!enabled) { diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 0245bb74bb47a..7b0644e70f42d 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -26,7 +26,7 @@ import Log from '@libs/Log'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as PolicyUtils from '@libs/PolicyUtils'; import {goBackWhenEnableFeature} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getAllPolicyReports, pushTransactionViolationsOnyxData} from '@libs/ReportUtils'; import {getTagArrayFromName} from '@libs/TransactionUtils'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import CONST from '@src/CONST'; @@ -48,7 +48,7 @@ Onyx.connect({ // and unset the draft indicator (pencil icon) alongside removing any draft comments. Clearing these values will keep the newly archived chats from being displayed in the LHN. // More info: https://github.com/Expensify/App/issues/14260 const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); - const policyReports = ReportUtils.getAllPolicyReports(policyID); + const policyReports = getAllPolicyReports(policyID); const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; policyReports.forEach((policyReport) => { @@ -322,7 +322,7 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, tagName) => { - if (tagName === oldName) { - return acc; - } - acc[tagName] = {...policyData.tags?.[tagName]}; - return acc; - }, {}) - : {}; + const tags = Object.entries(policyData.tags ?? {}).reduce((acc, [tagName, tag]) => { + if (tagName !== oldName) { + acc[tagName] = {...tag}; + } + return acc; + }, {}); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, {newName: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}); + pushTransactionViolationsOnyxData(onyxData, {...policyData, tags}, {}, {}, policyTagsOptimisticData); const parameters: RenamePolicyTagListParams = { policyID, @@ -975,10 +959,6 @@ function renamePolicyTagList(policyData: PolicyData, policyTagListName: {oldName } function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { - if (policyData.policy === undefined) { - return; - } - const policyID = policyData.policy.id; const policyOptimisticData: Partial = { requiresTag, @@ -1045,7 +1025,7 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { onyxData.failureData?.push(getUpdatedTagsOnyxData(!requiresTag)); onyxData.successData?.push(getUpdatedTagsOnyxData(requiresTag)); - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, getUpdatedTagsData(requiresTag)); + pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, getUpdatedTagsData(requiresTag)); const parameters = { policyID, @@ -1057,7 +1037,7 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tagListIndex: number) { const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); - if (!policyTag || !policyTag.name || !policyData.policy) { + if (!policyTag || !policyTag.name) { return; } @@ -1106,14 +1086,14 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag ], }; + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); + const parameters: SetPolicyTagsRequired = { policyID, tagListIndex, requireTagList: requiresTag, }; - ReportUtils.pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); - API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData); } diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 90a10e956d3ce..f3fbd6b8cf8d2 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -399,7 +399,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT isDelegateAccessRestricted, showDelegateNoAccessModal, isReportArchived, - isManualDistanceTrackingEnabled, ]); const isTravelEnabled = useMemo(() => { diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 9015a85019dc5..970020d24fdd0 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -13,6 +13,7 @@ import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePolicyData from '@hooks/usePolicyData'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -32,7 +33,6 @@ import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNo import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import usePolicyData from '@hooks/usePolicyData'; type IOURequestStepCategoryProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 6a94b533b1cfe..f11a24e38f8eb 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePolicyData from '@hooks/usePolicyData'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -105,11 +106,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: true}); const workspaceCards = getAllCardsForWorkspace(workspaceAccountID, cardList, cardFeeds); const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards); - - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); - const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); + const policyData = usePolicyData(policyID); const defaultFundID = useDefaultFundID(policyID); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`, {canBeMissing: true}); const paymentBankAccountID = cardSettings?.paymentBankAccountID; @@ -259,10 +256,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabledAction: onDisabledOrganizeSwitchPress, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { - if (policy === undefined) { + if (policyData.policy === undefined) { return; } - enablePolicyCategories(policy, isEnabled, policyTagLists, allTransactionViolations, true); + enablePolicyCategories(policyData, isEnabled, true); }, }, { @@ -274,10 +271,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro pendingAction: policy?.pendingFields?.areTagsEnabled, disabledAction: onDisabledOrganizeSwitchPress, action: (isEnabled: boolean) => { - if (!policyID) { + if (policyData.policy === undefined) { return; } - enablePolicyTags(policy, isEnabled, policyCategories, allTransactionViolations); + enablePolicyTags(policyData, isEnabled); }, }, { diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx index e1a68e11b1268..d92a734aa5ef6 100644 --- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -5,8 +5,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,7 +14,6 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -40,8 +38,8 @@ function CategoryRequireReceiptsOverPage({ }: EditCategoryPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const policy = usePolicy(policyID); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const policyData = usePolicyData(policyID); + const {policy, categories: policyCategories} = policyData; const isAlwaysSelected = policyCategories?.[categoryName]?.maxAmountNoReceipt === 0; const isNeverSelected = policyCategories?.[categoryName]?.maxAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE; @@ -92,10 +90,13 @@ function CategoryRequireReceiptsOverPage({ sections={[{data: requireReceiptsOverListData}]} ListItem={RadioListItem} onSelectRow={(item) => { + if (policyData.policy === undefined) { + return; + } if (typeof item.value === 'number') { - Category.setPolicyCategoryReceiptsRequired(policyID, categoryName, item.value); + Category.setPolicyCategoryReceiptsRequired(policyData, categoryName, item.value); } else { - Category.removePolicyCategoryReceiptsRequired(policyID, categoryName); + Category.removePolicyCategoryReceiptsRequired(policyData, categoryName); } Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index c41c7ebe8cb73..22bdda71ca242 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -13,8 +13,8 @@ import Switch from '@components/Switch'; import Text from '@components/Text'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import usePolicyData from '@hooks/usePolicyData'; import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 88ae9d6fc8979..7d87bbdf2377f 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -3,7 +3,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -11,26 +11,22 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {renamePolicyCategory} from '@userActions/Policy/Category'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import CategoryForm from './CategoryForm'; -import usePolicyData from '@hooks/usePolicyData'; type EditCategoryPageProps = | PlatformStackScreenProps | PlatformStackScreenProps; function EditCategoryPage({route}: EditCategoryPageProps) { - const policyID = route.params.policyID; + const {backTo, policyID, categoryName: currentCategoryName} = route.params; const policyData = usePolicyData(policyID); const styles = useThemeStyles(); const {translate} = useLocalize(); - const currentCategoryName = route.params.categoryName; - const backTo = route.params?.backTo; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_EDIT; - - + const validate = useCallback( (values: FormOnyxValues) => { const errors: FormInputErrors = {}; @@ -54,7 +50,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { (values: FormOnyxValues) => { const newCategoryName = values.categoryName.trim(); // Do not call the API if the edited category name is the same as the current category name - if (currentCategoryName !== newCategoryName) { + if (policyData.policy !== undefined && currentCategoryName !== newCategoryName) { renamePolicyCategory(policyData, {oldName: currentCategoryName, newName: values.categoryName}); } @@ -68,7 +64,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { ); }); }, - [isQuickSettingsFlow, currentCategoryName, policyID, backTo], + [isQuickSettingsFlow, currentCategoryName, policyData, policyID, backTo], ); return ( diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ffd56be238e6d..68d2c718f8f1d 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -74,12 +74,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false); const [isCannotDeleteOrDisableLastCategoryModalVisible, setIsCannotDeleteOrDisableLastCategoryModalVisible] = useState(false); const {environmentURL} = useEnvironment(); - const policyId = route.params.policyID; - const backTo = route.params?.backTo; + const {backTo, policyID: policyId} = route.params; const isMobileSelectionModeEnabled = useMobileSelectionMode(); const policyData = usePolicyData(policyId); - const policy = policyData.policy; - const policyCategories = policyData.categories; + const {policy, categories: policyCategories} = policyData; const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); @@ -141,6 +139,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const updateWorkspaceCategoryEnabled = useCallback( (value: boolean, categoryName: string) => { + if (policyData.policy === undefined) { + return; + } setWorkspaceCategoryEnabled(policyData, {[categoryName]: {name: categoryName, enabled: value}}); }, [policyData], @@ -360,6 +361,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return; } setSelectedCategories([]); + if (policyData.policy === undefined || Object.keys(categoriesToDisable).length === 0) { + return; + } setWorkspaceCategoryEnabled(policyData, categoriesToDisable); }, }); @@ -382,6 +386,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedCategories([]); + if (policyData.policy === undefined || Object.keys(categoriesToEnable).length === 0) { + return; + } setWorkspaceCategoryEnabled(policyData, categoriesToEnable); }, }); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index f35ce04cc89dc..3fb1405444ca1 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -22,7 +22,6 @@ import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOpt import {setWorkspaceRequiresCategory} from '@userActions/Policy/Category'; import {clearPolicyErrorField, setWorkspaceDefaultSpendCategory} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import SpendCategorySelectorListItem from './SpendCategorySelectorListItem'; @@ -47,17 +46,25 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet const toggleSubtitle = isConnectedToAccounting && currentConnectionName ? translate('workspace.categories.needCategoryForExportToIntegration', {connectionName: currentConnectionName}) : undefined; - const updateWorkspaceRequiresCategory = useCallback((value: boolean) => setWorkspaceRequiresCategory(policyData, value), [policyData]); + const updateWorkspaceRequiresCategory = useCallback( + (value: boolean) => { + if (policyData.policy === undefined) { + return; + } + setWorkspaceRequiresCategory(policyData, value); + }, + [policyData], + ); const {sections} = useMemo(() => { - if (!(policyData.policy && policyData.policy.mccGroup)) { + if (!(policy && policy.mccGroup)) { return {sections: [{data: []}]}; } return { sections: [ { - data: Object.entries(policyData.policy.mccGroup).map( + data: Object.entries(policy.mccGroup).map( ([mccKey, mccGroup]) => ({ categoryID: mccGroup.category, @@ -70,7 +77,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet }, ], }; - }, [policyData.policy]); + }, [policy]); const hasEnabledCategories = hasEnabledOptions(policyData.categories); const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledCategories || isConnectedToAccounting; diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index dc541d68766b0..e45680db9369b 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -8,8 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; +import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -29,12 +28,9 @@ type EditTagPageProps = | PlatformStackScreenProps; function EditTagPage({route}: EditTagPageProps) { - const policyID = route.params.policyID; - const policy = usePolicy(policyID); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [allTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}`, {canBeMissing: true}); - const backTo = route.params.backTo; + const {backTo, policyID} = route.params; + const policyData = usePolicyData(policyID); + const {tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -67,8 +63,8 @@ function EditTagPage({route}: EditTagPageProps) { (values: FormOnyxValues) => { const tagName = values.tagName.trim(); // Do not call the API if the edited tag name is the same as the current tag name - if (policy !== undefined && currentTagName !== tagName) { - renamePolicyTag(policy, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight, policyCategories, allTransactionViolations); + if (policyData.policy !== undefined && currentTagName !== tagName) { + renamePolicyTag(policyData, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight); } Keyboard.dismiss(); Navigation.goBack( @@ -77,7 +73,7 @@ function EditTagPage({route}: EditTagPageProps) { : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, route.params.tagName), ); }, - [allTransactionViolations, currentTagName, policyID, policy, policyCategories, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], + [policyData, currentTagName, policyID, route.params.tagName, route.params.orderWeight, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 80ac1d6bd5de9..16eb635d36c4a 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -42,11 +42,10 @@ type TagSettingsPageProps = function TagSettingsPage({route, navigation}: TagSettingsPageProps) { const {orderWeight, policyID, tagName, backTo, parentTagsFilter} = route.params; - - const policyData = usePolicyData(policyID); - const {policy, tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); + const policyData = usePolicyData(policyID); + const {policy, tags: policyTags} = policyData; const policyTag = useMemo(() => getTagListByOrderWeight(policyTags, orderWeight), [policyTags, orderWeight]); const {environmentURL} = useEnvironment(); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); @@ -78,7 +77,9 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - deletePolicyTags(policyData, [currentPolicyTag.name]); + if (policyData.policy !== undefined) { + deletePolicyTags(policyData, [currentPolicyTag.name]); + } setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index e896b84f0adc1..d9a877d472a0c 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -27,14 +27,14 @@ type WorkspaceEditTagsPageProps = | PlatformStackScreenProps; function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { - const policyData = usePolicyData(route.params.policyID); + const {policyID, backTo, orderWeight} = route.params; + const policyData = usePolicyData(policyID); const {tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); - const tagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); + const tagListName = useMemo(() => getTagListName(policyTags, orderWeight), [policyTags, orderWeight]); const {inputCallbackRef} = useAutoFocusInput(); - const backTo = route.params.backTo; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_EDIT; const validateTagName = useCallback( @@ -46,12 +46,12 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { if (values[INPUT_IDS.POLICY_TAGS_NAME]?.trim() === '0') { errors[INPUT_IDS.POLICY_TAGS_NAME] = translate('workspace.tags.invalidTagNameError'); } - if (policyTags && Object.values(policyTags).find((tag) => tag.orderWeight !== route.params.orderWeight && tag.name === values[INPUT_IDS.POLICY_TAGS_NAME])) { + if (policyTags && Object.values(policyTags).find((tag) => tag.orderWeight !== orderWeight && tag.name === values[INPUT_IDS.POLICY_TAGS_NAME])) { errors[INPUT_IDS.POLICY_TAGS_NAME] = translate('workspace.tags.existingTagError'); } return errors; }, - [translate, policyTags, route.params.orderWeight], + [translate, policyTags, orderWeight], ); const goBackToTagsSettings = useCallback(() => { @@ -59,31 +59,23 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { Navigation.goBack(backTo); return; } - Navigation.goBack( - route.params.orderWeight - ? ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(route?.params?.policyID, route.params.orderWeight) - : ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(route?.params?.policyID), - ); - }, [isQuickSettingsFlow, route.params.orderWeight, route.params?.policyID, backTo]); + Navigation.goBack(orderWeight ? ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(policyID, orderWeight) : ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(policyID)); + }, [isQuickSettingsFlow, orderWeight, policyID, backTo]); const updateTagListName = useCallback( (values: FormOnyxValues) => { - if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { - renamePolicyTagList( - policyData, - {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, - route.params.orderWeight, - ); + if (policyData.policy !== undefined && values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { + renamePolicyTagList(policyData, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, orderWeight); } goBackToTagsSettings(); }, - [tagListName, goBackToTagsSettings, route.params.orderWeight, policyData], + [tagListName, goBackToTagsSettings, orderWeight, policyData], ); return ( [ - getTagLists(policyData.tags), - isMultiLevelTagsPolicyUtils(policyData.tags), - hasDependentTagsPolicyUtils(policy, policyData.tags), - hasIndependentTagsPolicyUtils(policy, policyData.tags), - ], - [policy, policyData.tags], + () => [getTagLists(policyTags), isMultiLevelTagsPolicyUtils(policyTags), hasDependentTagsPolicyUtils(policy, policyTags), hasIndependentTagsPolicyUtils(policy, policyTags)], + [policy, policyTags], ); const canSelectMultiple = !hasDependentTags && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); const fetchTags = useCallback(() => { @@ -209,7 +203,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policyData?.policy === undefined) { + if (policyData.policy === undefined) { return; } setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, 0); @@ -219,7 +213,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceRequiresTag = useCallback( (value: boolean, orderWeight: number) => { - if (policy === undefined) { + if (policyData.policy === undefined) { return; } setPolicyTagsRequired(policyData, value, orderWeight); @@ -257,7 +251,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { isOn={isSwitchEnabled} accessibilityLabel={translate('workspace.tags.requiresTag')} onToggle={(newValue: boolean) => { - if (isMakingLastRequiredTagListOptional(policy, policyData.tags, [policyTagList])) { + if (isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList])) { setIsCannotMakeLastTagOptionalModalVisible(true); return; } @@ -265,7 +259,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { updateWorkspaceRequiresTag(newValue, policyTagList.orderWeight); }} disabled={isSwitchDisabled} - showLockIcon={isMakingLastRequiredTagListOptional(policy, policyData.tags, [policyTagList])} + showLockIcon={isMakingLastRequiredTagListOptional(policy, policyTags, [policyTagList])} /> ), }; @@ -296,7 +290,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { /> ), })); - }, [isMultiLevelTags, policyTagLists, hasDependentTags, translate, policy, policyData.tags, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); + }, [isMultiLevelTags, policyTagLists, hasDependentTags, translate, policy, policyTags, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); const filterTag = useCallback((tag: TagListItem, searchInput: string) => { const tagText = StringUtils.normalize(tag.text?.toLowerCase() ?? ''); @@ -384,7 +378,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - deletePolicyTags(policyData, selectedTags); + if (policyData.policy !== undefined) { + deletePolicyTags(policyData, selectedTags); + } setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -395,7 +391,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }); }; - const isLoading = !isOffline && policyData.tags === undefined; + const isLoading = !isOffline && policyTags === undefined; const hasVisibleTags = tagList.some((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); const navigateToImportSpreadsheet = useCallback(() => { @@ -583,7 +579,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { text: translate('workspace.tags.notRequireTags'), value: CONST.POLICY.BULK_ACTION_TYPES.REQUIRE, onSelected: () => { - if (isMakingLastRequiredTagListOptional(policy, policyData.tags, selectedTagLists)) { + if (isMakingLastRequiredTagListOptional(policy, policyTags, selectedTagLists)) { setIsCannotMakeLastTagOptionalModalVisible(true); return; } diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index d1e623d1b6725..d7ead8a518552 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -19,8 +19,6 @@ import useFilteredSelection from '@hooks/useFilteredSelection'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; import usePolicyData from '@hooks/usePolicyData'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; @@ -48,7 +46,6 @@ import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {PolicyTag} from '@src/types/onyx'; @@ -60,8 +57,7 @@ type WorkspaceViewTagsProps = | PlatformStackScreenProps; function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { - const backTo = route.params.backTo; - const policyID = route.params.policyID; + const {policyID, backTo, orderWeight} = route.params; // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for the small screen selection mode // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -72,13 +68,12 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const dropdownButtonRef = useRef(null); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); - const policy = usePolicy(policyID); const policyData = usePolicyData(policyID); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); + const {policy, tags: policyTags} = policyData; const isMobileSelectionModeEnabled = useMobileSelectionMode(); - const currentTagListName = useMemo(() => getTagListName(policyData.tags, route.params.orderWeight), [policyData.tags, route.params.orderWeight]); - const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyData.tags), [policy, policyData.tags]); + const currentTagListName = useMemo(() => getTagListName(policyTags, orderWeight), [policyTags, orderWeight]); + const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); const currentPolicyTag = policyTags?.[currentTagListName]; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_LIST_VIEW; @@ -119,12 +114,12 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policy === undefined) { + if (policyData.policy === undefined) { return; } - setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight); + setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, orderWeight); }, - [policyData, route.params.orderWeight], + [policyData, orderWeight], ); const tagList = useMemo( @@ -214,13 +209,13 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const navigateToTagSettings = (tag: TagListItem) => { Navigation.navigate( isQuickSettingsFlow - ? ROUTES.SETTINGS_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, tag.value, backTo) - : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, tag.value, tag?.rules?.parentTagsFilter ?? undefined), + ? ROUTES.SETTINGS_TAG_SETTINGS.getRoute(policyID, orderWeight, tag.value, backTo) + : ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, orderWeight, tag.value, tag?.rules?.parentTagsFilter ?? undefined), ); }; const deleteTags = () => { - if (!!policyData.policy.id !== undefined && selectedTags.length > 0) { + if (policyData.policy !== undefined && selectedTags.length > 0) { deletePolicyTags(policyData, selectedTags); } setIsDeleteTagsConfirmModalVisible(false); @@ -296,7 +291,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } // Disable the selected tags - setWorkspaceTagEnabled(policyData, tagsToDisable, route.params.orderWeight); + setWorkspaceTagEnabled(policyData, tagsToDisable, orderWeight); }, }); } @@ -312,7 +307,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } // Enable the selected tags - setWorkspaceTagEnabled(policyData, tagsToEnable, route.params.orderWeight); + setWorkspaceTagEnabled(policyData, tagsToEnable, orderWeight); }, }); } @@ -333,7 +328,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; if (policy !== undefined && !!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) { - setPolicyTagsRequired(policyData, false, route.params.orderWeight); + setPolicyTagsRequired(policyData, false, orderWeight); } const navigateToEditTag = () => { @@ -396,11 +391,11 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { if (policy === undefined) { return; } - setPolicyTagsRequired(policyData, on, route.params.orderWeight); + setPolicyTagsRequired(policyData, on, orderWeight); }} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} - onCloseError={() => clearPolicyTagListErrorField(policyID, route.params.orderWeight, 'required')} + onCloseError={() => clearPolicyTagListErrorField(policyID, orderWeight, 'required')} disabled={!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)} showLockIcon={isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])} /> @@ -446,7 +441,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} addBottomSafeAreaPadding onDismissError={(item) => { - clearPolicyTagErrors(policyID, item.value, route.params.orderWeight); + clearPolicyTagErrors(policyID, item.value, orderWeight); }} /> )} From 628089d7a96f4edfb75ae373704646eee6d42037 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:25:10 +0530 Subject: [PATCH 0042/1005] fix typefailure --- .../workspace/members/WorkspaceInviteMessageComponent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index 225aa311d53c6..f6fedf696bb9b 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -212,6 +212,7 @@ function WorkspaceInviteMessageComponent({ }} secondaryAvatarContainerStyle={styles.secondAvatarInline} shouldShowTooltip={shouldShowTooltip} + invitedEmailsToAccountIDs={invitedEmailsToAccountIDsDraft} /> @@ -243,7 +244,7 @@ function WorkspaceInviteMessageComponent({ onChangeText={(text: string) => { setWelcomeNote(text); }} - ref={(element: AnimatedTextInputRef) => { + ref={(element: AnimatedTextInputRef | null) => { if (!element) { return; } From 2c5e44719da33d876cef0ff63df5fe0145d198a9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Aug 2025 17:51:07 +0200 Subject: [PATCH 0043/1005] Testing reanimated v4 --- babel.config.js | 8 +-- package-lock.json | 28 ++++++++- package.json | 3 +- patches/react-native-reanimated/details.md | 15 ----- ...ted+3.19.1+002+dontWhitelistTextProp.patch | 12 ---- ...1+003+correctly-handle-Easing.bezier.patch | 62 ------------------- 6 files changed, 33 insertions(+), 95 deletions(-) delete mode 100644 patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch delete mode 100644 patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch diff --git a/babel.config.js b/babel.config.js index e303de7eddd5c..998f99b5c2670 100644 --- a/babel.config.js +++ b/babel.config.js @@ -43,9 +43,9 @@ const defaultPluginsForWebpack = [ // We use `@babel/plugin-transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. '@babel/plugin-transform-class-properties', - + '@babel/plugin-proposal-export-namespace-from', // Keep it last - 'react-native-reanimated/plugin', + 'react-native-worklets/plugin', ]; // The Fullstory annotate plugin generated a few errors when executed in Electron. Let's @@ -81,8 +81,6 @@ const metro = { ['@babel/plugin-proposal-class-properties', {loose: true}], ['@babel/plugin-proposal-private-methods', {loose: true}], ['@babel/plugin-proposal-private-property-in-object', {loose: true}], - // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation - 'react-native-reanimated/plugin', /* Fullstory */ '@fullstory/react-native', @@ -132,6 +130,8 @@ const metro = { }, }, ], + // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/ + 'react-native-worklets/plugin', ], env: { production: { diff --git a/package-lock.json b/package-lock.json index 997679f795a31..e11d41ac71313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "3.19.1", + "react-native-reanimated": "^3.19.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -131,6 +131,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", + "react-native-worklets": "^0.4.2", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" @@ -31886,6 +31887,8 @@ }, "node_modules/react-native-reanimated": { "version": "3.19.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.19.1.tgz", + "integrity": "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", @@ -32132,6 +32135,29 @@ "react-native": "*" } }, + "node_modules/react-native-worklets": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.4.2.tgz", + "integrity": "sha512-02IMmU2rOL6vrF7uA6cLAeN4haXOMTBh7opmVYQbjYG8mNAb0cnhmkvkdQupmpFjBpWZRJnBGYJJa471a/9IPg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { "version": "0.79.2", "license": "MIT" diff --git a/package.json b/package.json index dcf32cb8613f7..4ea4ca1ed507a 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "3.19.1", + "react-native-reanimated": "^3.19.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -201,6 +201,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", + "react-native-worklets": "^0.4.2", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" diff --git a/patches/react-native-reanimated/details.md b/patches/react-native-reanimated/details.md index ec97da45b4a62..130e3d94c3efa 100644 --- a/patches/react-native-reanimated/details.md +++ b/patches/react-native-reanimated/details.md @@ -7,18 +7,3 @@ - Upstream PR/issue: 🛑 - E/App issue: 🛑 - PR Introducing Patch: [Upgrade to React Native 0.76](https://github.com/Expensify/App/pull/51475) - -### [react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch](react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch) - -- Reason: In Expensify `text` prop in a JS prop and not in native code. Recheck if this is still needed when migrating to v4. -- Upstream PR/issue: 🛑 -- E/App issue: 🛑 -- PR Introducing Patch: [NR 0.75 upgrade](https://github.com/Expensify/App/pull/45289) - -### [react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch](react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch) - -- Reason: The Easing.bezier animation doesn't work on web -- Upstream PR/issue: https://github.com/software-mansion/react-native-reanimated/pull/8049 -- E/App issue: https://github.com/Expensify/App/pull/63623 -- PR Introducing Patch: 🛑 - diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch deleted file mode 100644 index 583cc7015ee44..0000000000000 --- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+002+dontWhitelistTextProp.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx -index d4b31f2..ced6561 100644 ---- a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx -+++ b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx -@@ -46,7 +46,6 @@ function createCircularDoublesBuffer(size: number) { - } - - const DEFAULT_BUFFER_SIZE = 20; --addWhitelistedNativeProps({ text: true }); - const AnimatedTextInput = createAnimatedComponent(TextInput); - - function loopAnimationFrame(fn: (lastTime: number, time: number) => void) { diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch deleted file mode 100644 index b623fc7f758e9..0000000000000 --- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+003+correctly-handle-Easing.bezier.patch +++ /dev/null @@ -1,62 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js -index ed0f9d3..6baf136 100644 ---- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js -+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/Easing.web.js -@@ -14,4 +14,18 @@ export const WebEasings = { - export function getEasingByName(easingName) { - return `cubic-bezier(${WebEasings[easingName].toString()})`; - } --//# sourceMappingURL=Easing.web.js.map -\ No newline at end of file -+export function maybeGetBezierEasing(easing) { -+ if (!('factory' in easing)) { -+ return null; -+ } -+ const easingFactory = easing.factory; -+ if (!('__closure' in easingFactory)) { -+ return null; -+ } -+ const closure = easingFactory.__closure; -+ if (!('Bezier' in closure)) { -+ return null; -+ } -+ return `cubic-bezier(${closure.x1}, ${closure.y1}, ${closure.x2}, ${closure.y2})`; -+} -+ -diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js -index 7f724c4..ad53a74 100644 ---- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js -+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/componentUtils.js -@@ -10,18 +10,28 @@ import { setElementPosition, snapshots } from "./componentStyle.js"; - import { Animations, TransitionType } from "./config.js"; - import { TransitionGenerator } from "./createAnimation.js"; - import { scheduleAnimationCleanup } from "./domUtils.js"; --import { getEasingByName, WebEasings } from "./Easing.web.js"; -+import { -+ getEasingByName, -+ maybeGetBezierEasing, -+ WebEasings -+} from "./Easing.web.js"; - import { prepareCurvedTransition } from "./transition/Curved.web.js"; - function getEasingFromConfig(config) { - if (!config.easingV) { - return getEasingByName('linear'); - } - const easingName = config.easingV[EasingNameSymbol]; -- if (!(easingName in WebEasings)) { -- logger.warn(`Selected easing is not currently supported on web.`); -+ if (easingName in WebEasings) { -+ return getEasingByName(easingName); -+ } -+ const bezierEasing = maybeGetBezierEasing(config.easingV); -+ if (!bezierEasing) { -+ logger.warn( -+ `Selected easing is not currently supported on web. Using linear easing instead.` -+ ); - return getEasingByName('linear'); - } -- return getEasingByName(easingName); -+ return bezierEasing; - } - function getRandomDelay(maxDelay = 1000) { - return Math.floor(Math.random() * (maxDelay + 1)) / 1000; From 8878252dc3a7223f1bc8cf507b407ec22b1d0bb6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Aug 2025 08:54:32 +0200 Subject: [PATCH 0044/1005] Bump reanimated to v4 --- package-lock.json | 49 ++++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index e11d41ac71313..6330ba3b5a346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^3.19.1", + "react-native-reanimated": "^4.0.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -31448,7 +31448,9 @@ "license": "MIT" }, "node_modules/react-native-is-edge-to-edge": { - "version": "1.1.7", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", "license": "MIT", "peerDependencies": { "react": "*", @@ -31485,14 +31487,6 @@ "react-native-reanimated": ">=3.0.0" } }, - "node_modules/react-native-keyboard-controller/node_modules/react-native-is-edge-to-edge": { - "version": "1.2.1", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-launch-arguments": { "version": "4.0.2", "license": "MIT", @@ -31886,28 +31880,31 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.19.1.tgz", - "integrity": "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.0.3.tgz", + "integrity": "sha512-apXILxR2gRi3n0Xi0UILr+72vXj1etooOId/4nCgzKfNnvcp+dRzt7UQdFU0/nc+4bPWlSsiIskDxdYXr2KNmw==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.0.0-0", - "@babel/plugin-transform-class-properties": "^7.0.0-0", - "@babel/plugin-transform-classes": "^7.0.0-0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", - "@babel/plugin-transform-optional-chaining": "^7.0.0-0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", - "@babel/plugin-transform-template-literals": "^7.0.0-0", - "@babel/plugin-transform-unicode-regex": "^7.0.0-0", - "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^2.0.0", - "invariant": "^2.2.4", - "react-native-is-edge-to-edge": "1.1.7" + "react-native-is-edge-to-edge": "^1.2.1", + "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-worklets": ">=0.4.0" + } + }, + "node_modules/react-native-reanimated/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/react-native-release-profiler": { diff --git a/package.json b/package.json index 4ea4ca1ed507a..cebbe9d43718c 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^3.19.1", + "react-native-reanimated": "^4.0.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", From f35e02be4e2ae7b0b79222d91ac199b10b1c4a40 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Aug 2025 09:29:14 +0200 Subject: [PATCH 0045/1005] Add live-markdown temporary patch --- ...wn+0.1.299+001+reanimated-v4-temp-backport.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch diff --git a/patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch b/patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch new file mode 100644 index 0000000000000..58a1ce5354685 --- /dev/null +++ b/patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec +index 21de59d..7165118 100644 +--- a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec ++++ b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec +@@ -24,7 +24,7 @@ Pod::Spec.new do |s| + + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" + +- s.dependency "RNReanimated/worklets" ++ s.dependency "RNWorklets" + + s.xcconfig = { + "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}", From fdfec07fdf755ea26b3544f5f71925f3bac26e47 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 30 Aug 2025 04:42:22 +0300 Subject: [PATCH 0046/1005] fixed policy and catergory tests --- tests/actions/PolicyCategoryTest.ts | 9 +++++---- tests/actions/PolicyTagTest.ts | 29 +++++++++++++++++------------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 80fb2f9262c37..a7e6a928cfea0 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -10,6 +10,7 @@ import createRandomPolicyCategories from '../utils/collections/policyCategory'; import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; OnyxUpdateManager(); describe('actions/PolicyCategory', () => { @@ -34,7 +35,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.setWorkspaceRequiresCategory(policyData.current, true); await waitForBatchedUpdates(); await new Promise((resolve) => { @@ -121,7 +122,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.renamePolicyCategory(policyData.current, { oldName: oldCategoryName ?? '', newName: newCategoryName, @@ -176,7 +177,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.setWorkspaceCategoryEnabled(policyData.current, categoriesToUpdate); await waitForBatchedUpdates(); await new Promise((resolve) => { @@ -223,7 +224,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete); await waitForBatchedUpdates(); await new Promise((resolve) => { diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 29fd537e36673..8e25179165fe3 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -2,6 +2,7 @@ import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import usePolicyData from '@hooks/usePolicyData'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {createPolicyTag, deletePolicyTags, renamePolicyTag, renamePolicyTagList, setPolicyRequiresTag, setWorkspaceTagEnabled} from '@libs/actions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,7 +13,10 @@ import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + OnyxUpdateManager(); + + describe('actions/Policy', () => { beforeAll(() => { Onyx.init({ @@ -36,7 +40,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setPolicyRequiresTag(policyData.current, true); return waitForBatchedUpdates(); }) @@ -84,7 +88,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setPolicyRequiresTag(policyData.current, false); return waitForBatchedUpdates(); }) @@ -133,7 +137,7 @@ describe('actions/Policy', () => { return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) .then(() => { mockFetch?.fail?.(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setPolicyRequiresTag(policyData.current, false); return waitForBatchedUpdates(); }) @@ -167,7 +171,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setPolicyRequiresTag(policyData.current, true); await waitForBatchedUpdates(); @@ -198,7 +202,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTagList( policyData.current, @@ -267,7 +271,8 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + renamePolicyTagList( policyData.current, { @@ -426,7 +431,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); return waitForBatchedUpdates(); }) @@ -500,7 +505,7 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); return waitForBatchedUpdates(); }) @@ -547,7 +552,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTag( policyData.current, { @@ -618,7 +623,7 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTag( policyData.current, { @@ -668,7 +673,7 @@ describe('actions/Policy', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); }) .then(() => { - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); deletePolicyTags(policyData.current, tagsToDelete); return waitForBatchedUpdates(); }) @@ -729,7 +734,7 @@ describe('actions/Policy', () => { .then(() => { mockFetch?.fail?.(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); deletePolicyTags(policyData.current, tagsToDelete); return waitForBatchedUpdates(); }) From d177fd0cfd14c979465f33c9b04fcd8a0ddf6d78 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sun, 31 Aug 2025 08:28:05 +0300 Subject: [PATCH 0047/1005] Created usePolicyData.test.ts --- tests/unit/usePolicyData.test.ts | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/unit/usePolicyData.test.ts diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts new file mode 100644 index 0000000000000..840e27bbfe4ed --- /dev/null +++ b/tests/unit/usePolicyData.test.ts @@ -0,0 +1,126 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import usePolicyData from '@hooks/usePolicyData'; +import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyCategories, PolicyTagLists, Report, ReportActions, Transaction} from '@src/types/onyx'; +import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; +import {iouReportR14932 as mockedIOUReport} from '../../__mocks__/reportData/reports'; +import {transactionR14932 as mockedTransaction} from '../../__mocks__/reportData/transactions'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomPolicyCategories from '../utils/collections/policyCategory'; +import createRandomPolicyTags from '../utils/collections/policyTags'; +import {createAdminRoom, createAnnounceRoom} from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +// Mock data ids + +const mockPolicy: Policy = createRandomPolicy(0); +const mockPolicyTagLists: PolicyTagLists = createRandomPolicyTags('Tags', 8); +const mockPolicyCategories: PolicyCategories = createRandomPolicyCategories(8); + +const mockIOUReport = {...mockedIOUReport, policyID: mockPolicy.id}; +const mockAdminsRoom: Report = {...createAdminRoom(1234), policyID: mockPolicy.id}; +const mockAnnounceRoom: Report = {...createAnnounceRoom(5678), policyID: mockPolicy.id}; + +const mockTransaction = { + ...mockedTransaction, + reportID: mockIOUReport.reportID, + category: Object.values(mockPolicyCategories).at(0)?.name, + tag: Object.values(mockPolicyTagLists).at(0)?.name, +}; + +const expectedTransactionsAndViolations = { + [mockIOUReport.reportID]: { + transactions: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${mockTransaction.transactionID}`]: mockTransaction, + }, + violations: {}, + }, +}; + +const reportsCollection: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, Report> = { + [`${ONYXKEYS.COLLECTION.REPORT}${mockIOUReport.reportID}`]: mockIOUReport, + [`${ONYXKEYS.COLLECTION.REPORT}${mockAdminsRoom.reportID}`]: mockAdminsRoom, + [`${ONYXKEYS.COLLECTION.REPORT}${mockAnnounceRoom.reportID}`]: mockAnnounceRoom, +}; + +const reportActionsCollection: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`, ReportActions> = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { + [mockIOUAction.reportActionID]: mockIOUAction, + }, +}; +const policiesCollection: Record<`${typeof ONYXKEYS.COLLECTION.POLICY}${string}`, Policy> = { + [`${ONYXKEYS.COLLECTION.POLICY}${mockPolicy.id}`]: mockPolicy, +}; + +const policiesTagListsCollection: Record<`${typeof ONYXKEYS.COLLECTION.POLICY_TAGS}${string}`, PolicyTagLists> = { + [`${ONYXKEYS.COLLECTION.POLICY_TAGS}${mockPolicy.id}`]: mockPolicyTagLists, +}; + +const policiesCategoriesCollection: Record<`${typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${string}`, PolicyCategories> = { + [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${mockPolicy.id}`]: mockPolicyCategories, +}; + +const transactionsCollection: Record<`${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`, Transaction> = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${mockTransaction.transactionID}`]: mockTransaction, +}; + +describe('usePolicyData', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + initOnyxDerivedValues(); + return waitForBatchedUpdates(); + }); + + beforeEach(() => { + return Onyx.clear().then(waitForBatchedUpdates); + }); + + test('returns reports filtered by a policy ID that exists in the onyx', async () => { + await Onyx.multiSet({ + ...reportsCollection, + ...reportActionsCollection, + ...policiesCollection, + ...policiesTagListsCollection, + ...policiesCategoriesCollection, + ...transactionsCollection, + }); + + await waitForBatchedUpdates(); + + const {result} = renderHook(() => usePolicyData(mockPolicy.id), {wrapper: OnyxListItemProvider}); + + await waitForBatchedUpdates(); + + expect(result.current.policy).toEqual(mockPolicy); + expect(result.current.tags).toEqual(mockPolicyTagLists); + expect(result.current.categories).toEqual(mockPolicyCategories); + + expect(result.current.reports).toHaveLength(1); + expect(result.current.reports.at(0)).toEqual(mockIOUReport); + + expect(result.current.transactionsAndViolations).toEqual(expectedTransactionsAndViolations); + }); + + test('returns default empty values when policy ID does not exist in the onyx', () => { + const {result} = renderHook(() => usePolicyData('non_existent_policy_id'), {wrapper: OnyxListItemProvider}); + + expect(result.current.reports).toEqual([]); + expect(result.current.tags).toEqual({}); + expect(result.current.categories).toEqual({}); + expect(result.current.policy).toBeUndefined(); + expect(result.current.transactionsAndViolations).toEqual({}); + }); + + test('returns default empty values when policyID is undefined', () => { + const {result} = renderHook(() => usePolicyData(undefined), {wrapper: OnyxListItemProvider}); + + expect(result.current.reports).toEqual([]); + expect(result.current.tags).toEqual({}); + expect(result.current.categories).toEqual({}); + expect(result.current.policy).toBeUndefined(); + expect(result.current.transactionsAndViolations).toEqual({}); + }); +}); From 0bb275f86959588216ac651250be9fd1776ae68e Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sun, 31 Aug 2025 08:29:18 +0300 Subject: [PATCH 0048/1005] fixed tests --- src/hooks/usePolicyData.ts | 54 ++++++++++++------- src/libs/actions/Policy/Tag.ts | 5 +- .../CategoryRequireReceiptsOverPage.tsx | 10 ++-- tests/actions/PolicyCategoryTest.ts | 10 ++-- tests/actions/PolicyTagTest.ts | 4 +- tests/unit/ReportUtilsTest.ts | 25 ++++++--- 6 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/hooks/usePolicyData.ts b/src/hooks/usePolicyData.ts index 829343a0bd146..fff95360d18a1 100644 --- a/src/hooks/usePolicyData.ts +++ b/src/hooks/usePolicyData.ts @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; @@ -8,45 +9,58 @@ import usePolicy from './usePolicy'; type PolicyData = { policy: OnyxValueWithOfflineFeedback; - reports: Array>; tags: PolicyTagLists; categories: PolicyCategories; + reports: Array>; transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; }; +/** + * Custom hook to retrieve policy-related data. + * @param policyID The ID of the policy to retrieve data for. + * @returns An object containing policy data, including tags, categories, reports, and transactions/violations. + */ + function usePolicyData(policyID?: string): PolicyData { - const policy = usePolicy(policyID); + const policy = usePolicy(policyID) as OnyxValueWithOfflineFeedback; const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); - const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [tagsLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [reports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`, { - canBeMissing: false, - selector: (reportCollection) => { - if (!policyID) { + const [reportCollection] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { + canBeMissing: true, + selector: (allReports) => { + if (!allReports) { return []; } - return Object.values(reportCollection ?? {}).filter((report) => !!report?.reportID && report.policyID === policyID); + const reportIDsWithTransactions = Object.keys(allReportsTransactionsAndViolations ?? {}); + return Object.values(allReports).filter((report) => !!report && report?.policyID === policyID && reportIDsWithTransactions.includes(report.reportID)); }, }); - const transactionsAndViolations = (reports ?? []).reduce((acc, report) => { - if (!report?.reportID) { - return acc; - } - const reportTransactionsAndViolations = allReportsTransactionsAndViolations?.[report.reportID]; - if (reportTransactionsAndViolations) { - acc[report.reportID] = reportTransactionsAndViolations; + const reports: Array> = useMemo(() => { + if (!reportCollection) { + return []; } - return acc; - }, {} as ReportTransactionsAndViolationsDerivedValue); + return reportCollection as Array>; + }, [reportCollection]); + + const transactionsAndViolations = useMemo(() => { + return reports.reduce((acc, report) => { + if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { + acc[report.reportID] = allReportsTransactionsAndViolations?.[report.reportID]; + } + return acc; + }, {}); + }, [reports, allReportsTransactionsAndViolations]); return { - policy: policy as OnyxValueWithOfflineFeedback, - reports: reports as Array>, - tags: tags ?? {}, + tags: tagsLists ?? {}, categories: categories ?? {}, + + policy, + reports, transactionsAndViolations, }; } diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index b621f62a0c3b8..c5c7d497e3804 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,5 +1,5 @@ import lodashCloneDeep from 'lodash/cloneDeep'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PolicyData} from '@hooks/usePolicyData'; import * as API from '@libs/API'; @@ -31,12 +31,11 @@ import {getTagArrayFromName} from '@libs/TransactionUtils'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ImportedSpreadsheet, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; +import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; - let allPolicyTags: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx index d92a734aa5ef6..9857bc21ac066 100644 --- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -7,12 +7,12 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToShortDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import * as Category from '@userActions/Policy/Category'; +import {removePolicyCategoryReceiptsRequired, setPolicyCategoryReceiptsRequired} from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -49,7 +49,7 @@ function CategoryRequireReceiptsOverPage({ { value: null, text: translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`, { - defaultAmount: CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), + defaultAmount: convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), }), keyForList: CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.DEFAULT, isSelected: !isAlwaysSelected && !isNeverSelected, @@ -94,9 +94,9 @@ function CategoryRequireReceiptsOverPage({ return; } if (typeof item.value === 'number') { - Category.setPolicyCategoryReceiptsRequired(policyData, categoryName, item.value); + setPolicyCategoryReceiptsRequired(policyData, categoryName, item.value); } else { - Category.removePolicyCategoryReceiptsRequired(policyData, categoryName); + removePolicyCategoryReceiptsRequired(policyData, categoryName); } Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index a7e6a928cfea0..5142aa45374c0 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -1,5 +1,6 @@ import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; import usePolicyData from '@hooks/usePolicyData'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; @@ -10,7 +11,6 @@ import createRandomPolicyCategories from '../utils/collections/policyCategory'; import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import OnyxListItemProvider from '@components/OnyxListItemProvider'; OnyxUpdateManager(); describe('actions/PolicyCategory', () => { @@ -35,7 +35,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.setWorkspaceRequiresCategory(policyData.current, true); await waitForBatchedUpdates(); await new Promise((resolve) => { @@ -122,7 +122,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.renamePolicyCategory(policyData.current, { oldName: oldCategoryName ?? '', newName: newCategoryName, @@ -177,7 +177,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.setWorkspaceCategoryEnabled(policyData.current, categoriesToUpdate); await waitForBatchedUpdates(); await new Promise((resolve) => { @@ -224,7 +224,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete); await waitForBatchedUpdates(); await new Promise((resolve) => { diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 8e25179165fe3..5b72746a79064 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -1,8 +1,8 @@ import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; import usePolicyData from '@hooks/usePolicyData'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; -import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {createPolicyTag, deletePolicyTags, renamePolicyTag, renamePolicyTagList, setPolicyRequiresTag, setWorkspaceTagEnabled} from '@libs/actions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -13,10 +13,8 @@ import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - OnyxUpdateManager(); - describe('actions/Policy', () => { beforeAll(() => { Onyx.init({ diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 93ef27bd34806..9a83154a787ed 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,9 +4,11 @@ import {renderHook} from '@testing-library/react-native'; import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; import usePolicyData from '@hooks/usePolicyData'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {putOnHold} from '@libs/actions/IOU'; +import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow'; import DateUtils from '@libs/DateUtils'; import {translateLocal} from '@libs/Localize'; @@ -325,7 +327,6 @@ const policy: Policy = { describe('ReportUtils', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); - const policyCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [policy], (current) => current.id); Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: participantsPersonalDetails, @@ -4690,6 +4691,9 @@ describe('ReportUtils', () => { }); describe('pushTransactionViolationsOnyxData', () => { + beforeAll(() => { + initOnyxDerivedValues(); + }); it('should push category violation to the Onyx data when category and tag is pending deletion', async () => { // Given policy categories, the first is pending deletion const fakePolicyCategories = createRandomPolicyCategories(3); @@ -4741,27 +4745,32 @@ describe('ReportUtils', () => { // Populating Onyx with required data await Onyx.multiSet({ ...fakePolicyReports, - [`${ONYXKEYS.COLLECTION.POLICY_TAGS}`]: fakePolicyTagsLists, - [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`]: fakePolicyCategories, + [`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicyID}`]: fakePolicyTagsLists, + [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicyID}`]: fakePolicyCategories, [`${ONYXKEYS.COLLECTION.POLICY}${fakePolicyID}`]: fakePolicy, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${mockIOUReport.reportID}`]: { [mockIOUAction.reportActionID]: mockIOUAction, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}${mockTransaction.transactionID}`]: { ...mockTransaction, + reportID: mockIOUReport.reportID, policyID: fakePolicyID, category: fakePolicyCategoryNameToDelete, tag: fakePolicyTagsToDelete.at(0)?.[0] ?? '', }, }); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicyID)); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => usePolicyData(fakePolicyID), {wrapper: OnyxListItemProvider}); + + await waitForBatchedUpdates(); const onyxData = {optimisticData: [], failureData: []}; - pushTransactionViolationsOnyxData(onyxData, policyData.current, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); + pushTransactionViolationsOnyxData(onyxData, result.current, {}, fakePolicyCategoriesUpdate, fakePolicyTagListsUpdate); - expect(onyxData).toMatchObject({ + const expectedOnyxData = { // Expecting the optimistic data to contain the OUT_OF_POLICY violations for the deleted category and tag optimisticData: [ { @@ -4789,7 +4798,9 @@ describe('ReportUtils', () => { value: null, }, ], - }); + }; + + expect(onyxData).toMatchObject(expectedOnyxData); }); }); From bd77f1ab98c40bbec55cff0299024d85eb9401f7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 1 Sep 2025 14:44:16 +0200 Subject: [PATCH 0049/1005] Update patches and reanimated version --- package-lock.json | 33 +++++++++++++------ package.json | 4 +-- patches/react-native-reanimated/details.md | 2 +- ....0+001+catch-all-exceptions-on-stoi.patch} | 6 ++-- 4 files changed, 29 insertions(+), 16 deletions(-) rename patches/react-native-reanimated/{react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch => react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch} (87%) diff --git a/package-lock.json b/package-lock.json index 90f23f7e76957..0d1496074d506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^4.0.3", + "react-native-reanimated": "4.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -132,7 +132,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.4.2", + "react-native-worklets": "^0.5.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" @@ -31893,9 +31893,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.0.3.tgz", - "integrity": "sha512-apXILxR2gRi3n0Xi0UILr+72vXj1etooOId/4nCgzKfNnvcp+dRzt7UQdFU0/nc+4bPWlSsiIskDxdYXr2KNmw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.0.tgz", + "integrity": "sha512-L8FqZn8VjZyBaCUMYFyx1Y+T+ZTbblaudpxReOXJ66RnOf52g6UM4Pa/IjwLD1XAw1FUxLRQrtpdjbkEc74FiQ==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", @@ -31905,7 +31905,7 @@ "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", - "react-native-worklets": ">=0.4.0" + "react-native-worklets": ">=0.5.0" } }, "node_modules/react-native-reanimated/node_modules/semver": { @@ -32146,9 +32146,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.4.2.tgz", - "integrity": "sha512-02IMmU2rOL6vrF7uA6cLAeN4haXOMTBh7opmVYQbjYG8mNAb0cnhmkvkdQupmpFjBpWZRJnBGYJJa471a/9IPg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.0.tgz", + "integrity": "sha512-+/tbUCBEVchxe5xxFvvdC18PRZFCoxq1emcjIezXodElFguk1MaBYpbqABvZHBGrrEOo9k99h4Jbt0i9ArCbxg==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", @@ -32160,7 +32160,8 @@ "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^2.0.0" + "convert-source-map": "^2.0.0", + "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", @@ -32168,6 +32169,18 @@ "react-native": "*" } }, + "node_modules/react-native-worklets/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { "version": "0.79.2", "license": "MIT" diff --git a/package.json b/package.json index cac4bb14fef52..a58be76f530a0 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^4.0.3", + "react-native-reanimated": "4.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -202,7 +202,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.4.2", + "react-native-worklets": "^0.5.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" diff --git a/patches/react-native-reanimated/details.md b/patches/react-native-reanimated/details.md index 130e3d94c3efa..33055066aaa6f 100644 --- a/patches/react-native-reanimated/details.md +++ b/patches/react-native-reanimated/details.md @@ -1,7 +1,7 @@ # `react-native-reanimated` patches -### [react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch) +### [react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch) - Reason: Reanimated wasn't able to catch an exception here, so the catch clause was broadened. - Upstream PR/issue: 🛑 diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch b/patches/react-native-reanimated/react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch similarity index 87% rename from patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch rename to patches/react-native-reanimated/react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch index 2fdf25db7f640..4e168c22a926b 100644 --- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+001+catch-all-exceptions-on-stoi.patch +++ b/patches/react-native-reanimated/react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp -index 8102462..f2738d2 100644 +index 6574d2d..8cb9b2f 100644 --- a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp +++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp -@@ -853,7 +853,7 @@ void LayoutAnimationsProxy::transferConfigFromNativeID( +@@ -805,7 +805,7 @@ void LayoutAnimationsProxy::transferConfigFromNativeID( auto nativeId = stoi(nativeIdString); layoutAnimationsManager_->transferConfigFromNativeID(nativeId, tag); } catch (std::invalid_argument) { @@ -10,4 +10,4 @@ index 8102462..f2738d2 100644 + } catch (...) { } } - + From 95b331157935aff9f2527bc9791357aa1072fb08 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 1 Sep 2025 14:46:52 +0200 Subject: [PATCH 0050/1005] Update live-markdown patch --- ...e-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch => @expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch} (100%) diff --git a/patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch b/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.299+001+reanimated-v4-temp-backport.patch rename to patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch From 7fe04520d17f839f31098d53144b379c88d18abf Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 3 Sep 2025 05:28:59 +0300 Subject: [PATCH 0051/1005] lint --- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 1 + src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 1 + .../receiptPartners/InviteReceiptPartnerPolicyPage.tsx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 164dd59ef0e00..ed43fb716b02d 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -401,6 +401,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT isDelegateAccessRestricted, showDelegateNoAccessModal, isReportArchived, + isManualDistanceTrackingEnabled, ]); const isTravelEnabled = useMemo(() => { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1ada08ce8e3b5..85259f0915bc7 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -932,6 +932,7 @@ function IOURequestStepConfirmation({ currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, + transactions, transactionTaxCode, transactionTaxAmount, policy, diff --git a/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx b/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx index dcb9ef7f75a6b..f2d716107ef23 100644 --- a/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx +++ b/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx @@ -126,7 +126,7 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP shouldShow: true, }, ]; - }, [workspaceMembers, debouncedSearchTerm, selectedOptions]); + }, [countryCode, workspaceMembers, debouncedSearchTerm, selectedOptions]); // Pre-select all members only once on first load. useEffect(() => { From 6a8ba26c9d7eee3a7c46f27611223004eb92c9ba Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 3 Sep 2025 09:49:07 +0200 Subject: [PATCH 0052/1005] Fix versions --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d1496074d506..950ae1a2f3d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "4.1", + "react-native-reanimated": "4.1.0", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -132,7 +132,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.5.0", + "react-native-worklets": "0.5.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" diff --git a/package.json b/package.json index a58be76f530a0..6f940b9ed32f1 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "4.1", + "react-native-reanimated": "4.1.0", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -202,7 +202,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.5.0", + "react-native-worklets": "0.5.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1" From e4e74af0a1a2d5d4862d0a4eccd9cc1b1d18eff5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 3 Sep 2025 09:50:04 +0200 Subject: [PATCH 0053/1005] Remove patch for reduce motion animation --- ...04+reduce-motion-animation-callbacks.patch | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch diff --git a/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch b/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch deleted file mode 100644 index 03310811d5df8..0000000000000 --- a/patches/react-native-reanimated/react-native-reanimated+3.19.1+004+reduce-motion-animation-callbacks.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -index a2f6cf1..93a37f5 100644 ---- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -@@ -503,13 +503,6 @@ export function createAnimatedComponent( - return; - } - -- if (this._isReducedMotion(currentConfig)) { -- if (!previousConfig) { -- return; -- } -- currentConfig = undefined; -- } -- - updateLayoutAnimations( - isFabric() && type === LayoutAnimationType.ENTERING - ? this.reanimatedID -@@ -608,14 +601,6 @@ export function createAnimatedComponent( - }, - }); - -- _isReducedMotion(config?: LayoutAnimationOrBuilder): boolean { -- return config && -- 'getReduceMotion' in config && -- typeof config.getReduceMotion === 'function' -- ? getReduceMotionFromConfig(config.getReduceMotion()) -- : getReduceMotionFromConfig(); -- } -- - // This is a component lifecycle method from React, therefore we are not calling it directly. - // It is called before the component gets rerendered. This way we can access components' position before it changed - // and later on, in componentDidUpdate, calculate translation for layout transition. From 9031c0aa4a4f227e7bc109594ab1a9c77399305c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 3 Sep 2025 14:43:44 +0200 Subject: [PATCH 0054/1005] Fix ts check with live markdown patch --- ....302+001+reanimated-v4-temp-backport.patch | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch b/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch index 58a1ce5354685..137e3ba31e0e3 100644 --- a/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch +++ b/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch @@ -11,3 +11,29 @@ index 21de59d..7165118 100644 s.xcconfig = { "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}", +diff --git a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts b/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts +index e6ad088..90f2487 100644 +--- a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts ++++ b/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts +@@ -2,7 +2,7 @@ import { TextInput } from 'react-native'; + import React from 'react'; + import type { TextInputProps } from 'react-native'; + import type { WorkletRuntime } from 'react-native-reanimated'; +-import type { ShareableRef, WorkletFunction } from 'react-native-reanimated/lib/typescript/commonTypes'; ++import type { ShareableRef, WorkletFunction } from 'react-native-worklets'; + import type { PartialMarkdownStyle } from './styleUtils'; + import type { InlineImagesInputProps, MarkdownRange } from './commonTypes'; + declare global { +diff --git a/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx b/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx +index 82ee895..55c0e90 100644 +--- a/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx ++++ b/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx +@@ -3,7 +3,7 @@ import React from 'react'; + import type {TextInputProps} from 'react-native'; + import {createWorkletRuntime, makeShareableCloneRecursive} from 'react-native-reanimated'; + import type {WorkletRuntime} from 'react-native-reanimated'; +-import type {ShareableRef, WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; ++import type {ShareableRef, WorkletFunction} from 'react-native-worklets'; + + import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; + import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; From d44c18125096ac8fa969b50e9568405e46527160 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 3 Sep 2025 14:58:05 +0200 Subject: [PATCH 0055/1005] Fix patch for live markdown --- ...wn+0.1.302+001+reanimated-v4-temp-backport.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch b/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch index 137e3ba31e0e3..bad0ea4481574 100644 --- a/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch +++ b/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch @@ -37,3 +37,16 @@ index 82ee895..55c0e90 100644 import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; +diff --git a/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts b/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts +index 6e14e25..38d92bb 100644 +--- a/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts ++++ b/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts +@@ -4,7 +4,7 @@ import {Platform} from 'react-native'; + import {ExpensiMark} from 'expensify-common'; + import {unescapeText} from 'expensify-common/dist/utils'; + import {decode} from 'html-entities'; +-import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; ++import type {WorkletFunction} from 'react-native-worklets'; + import type {MarkdownType, MarkdownRange} from './commonTypes'; + import {groupRanges, sortRanges, splitRangesOnEmojis} from './rangeUtils'; + From 582f5f917a14684346b1a1b9d7816f4198c1999d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 3 Sep 2025 17:50:10 +0200 Subject: [PATCH 0056/1005] Remove patch name --- ....patch => @expensify+react-native-live-markdown+0.1.302.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch => @expensify+react-native-live-markdown+0.1.302.patch} (100%) diff --git a/patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch b/patches/@expensify+react-native-live-markdown+0.1.302.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.302+001+reanimated-v4-temp-backport.patch rename to patches/@expensify+react-native-live-markdown+0.1.302.patch From 7e93dd5e50927f3c1e082dae1fc4b12d49de7339 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 16:55:04 +0200 Subject: [PATCH 0057/1005] Prepare live markdown temporary patch so the app builds on both android and ios --- Gemfile.lock | 8 +- ios/Podfile.lock | 81 ++++++--- ...y+react-native-live-markdown+0.1.302.patch | 168 +++++++++++++++++- 3 files changed, 227 insertions(+), 30 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3c7253c08db7f..fbaf94f378e96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -172,10 +172,10 @@ GEM google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) - ffi (1.17.0) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 01acb1c37bb96..709de39e249cf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2898,7 +2898,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets + - RNWorklets - Yoga - RNLocalize (2.2.6): - React-Core @@ -3003,7 +3003,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.19.1): + - RNReanimated (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3026,10 +3026,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.19.1) - - RNReanimated/worklets (= 3.19.1) + - RNReanimated/reanimated (= 4.1.0) + - RNWorklets - Yoga - - RNReanimated/reanimated (3.19.1): + - RNReanimated/reanimated (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3052,9 +3052,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.19.1) + - RNReanimated/reanimated/apple (= 4.1.0) + - RNWorklets - Yoga - - RNReanimated/reanimated/apple (3.19.1): + - RNReanimated/reanimated/apple (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3077,8 +3078,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNWorklets - Yoga - - RNReanimated/worklets (3.19.1): + - RNScreens (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3095,15 +3097,16 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets/apple (= 3.19.1) + - RNScreens/common (= 4.12.0) - Yoga - - RNReanimated/worklets/apple (3.19.1): + - RNScreens/common (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3120,6 +3123,7 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils @@ -3127,7 +3131,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (4.12.0): + - RNShare (11.0.2): - DoubleConversion - glog - hermes-engine @@ -3144,16 +3148,19 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.12.0) - Yoga - - RNScreens/common (4.12.0): + - RNSound (0.11.2): + - React-Core + - RNSound/Core (= 0.11.2) + - RNSound/Core (0.11.2): + - React-Core + - RNSVG (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3170,15 +3177,15 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNSVG/common (= 15.9.0) - Yoga - - RNShare (11.0.2): + - RNSVG/common (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3202,12 +3209,32 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSound (0.11.2): - - React-Core - - RNSound/Core (= 0.11.2) - - RNSound/Core (0.11.2): + - RNWorklets (0.5.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety - React-Core - - RNSVG (15.9.0): + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets/worklets (= 0.5.0) + - Yoga + - RNWorklets/worklets (0.5.0): - DoubleConversion - glog - hermes-engine @@ -3230,9 +3257,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.9.0) + - RNWorklets/worklets/apple (= 0.5.0) - Yoga - - RNSVG/common (15.9.0): + - RNWorklets/worklets/apple (0.5.0): - DoubleConversion - glog - hermes-engine @@ -3416,6 +3443,7 @@ DEPENDENCIES: - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) + - RNWorklets (from `../node_modules/react-native-worklets`) - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -3738,6 +3766,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-sound" RNSVG: :path: "../node_modules/react-native-svg" + RNWorklets: + :path: "../node_modules/react-native-worklets" VisionCamera: :path: "../node_modules/react-native-vision-camera" Yoga: @@ -3911,17 +3941,18 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: b249b5be5a3659025aed8898aaaafd567dc2d660 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 33974079f2be0b5c5e6032582c265494f00a8ce4 + RNLiveMarkdown: 335a4d04da4d4b940a19b750cccae2ee1ea21a0e RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 315b95f92be3bf9c86cac2925e39c9ab2ad2c793 RNNitroSQLite: a1b0f1a665c54fc0f2510a292c1bf8ac4d44231b RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 0692f0b36f15b83a7936c3611cab923e1396eeb9 + RNReanimated: 223b1b3895100ae576756fa8e0d09e49354f9bdf RNScreens: 9cb0d4b69067d62d51c278730bb0322f5cbd8d8e RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 2aeb75970331da3a64cb89309f7fd7bd3769260a + RNWorklets: 18e24f98345960e357e8e0f7908c6b67704c34a2 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/patches/@expensify+react-native-live-markdown+0.1.302.patch b/patches/@expensify+react-native-live-markdown+0.1.302.patch index bad0ea4481574..8d39864cce21e 100644 --- a/patches/@expensify+react-native-live-markdown+0.1.302.patch +++ b/patches/@expensify+react-native-live-markdown+0.1.302.patch @@ -1,7 +1,16 @@ diff --git a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec -index 21de59d..7165118 100644 +index 21de59d..da055d6 100644 --- a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec +++ b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec +@@ -5,7 +5,7 @@ react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir + react_native_minor_version = react_native_json['version'].split('.')[1].to_i + + pods_root = Pod::Config.instance.project_pods_root +-react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-reanimated/package.json')"`) ++react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`) + react_native_reanimated_node_modules_dir_from_pods_root = Pathname.new(react_native_reanimated_node_modules_dir).relative_path_from(pods_root).to_s + + package = JSON.parse(File.read(File.join(__dir__, "package.json"))) @@ -24,7 +24,7 @@ Pod::Spec.new do |s| s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" @@ -11,6 +20,163 @@ index 21de59d..7165118 100644 s.xcconfig = { "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}", +diff --git a/node_modules/@expensify/react-native-live-markdown/android/build.gradle b/node_modules/@expensify/react-native-live-markdown/android/build.gradle +index 001a23c..232263c 100644 +--- a/node_modules/@expensify/react-native-live-markdown/android/build.gradle ++++ b/node_modules/@expensify/react-native-live-markdown/android/build.gradle +@@ -1,3 +1,5 @@ ++import org.apache.tools.ant.taskdefs.condition.Os ++ + buildscript { + repositories { + google() +@@ -68,6 +70,15 @@ def getReactNativeMinorVersion() { + + def REACT_NATIVE_MINOR_VERSION = getReactNativeMinorVersion() + ++def toPlatformFileString(String path) { ++ if (Os.isFamily(Os.FAMILY_WINDOWS)) { ++ path = path.replace(File.separatorChar, '/' as char) ++ } ++ return path ++} ++ ++def reactNativeRootDir = resolveReactNativeDirectory() ++ + android { + if (supportsNamespace()) { + namespace "com.expensify.livemarkdown" +@@ -94,6 +105,7 @@ android { + arguments "-DANDROID_STL=c++_shared", + "-DANDROID_TOOLCHAIN=clang", + "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", ++ "-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}", + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + } +@@ -175,7 +187,7 @@ repositories { + dependencies { + implementation "com.facebook.react:react-android" // version substituted by RNGP + implementation "com.facebook.react:hermes-android" // version substituted by RNGP +- implementation project(":react-native-reanimated") ++ implementation project(":react-native-worklets") + } + + // This fixes linking errors due to undefined symbols from libworklets.so. +@@ -183,6 +195,6 @@ dependencies { + // like a header-only library. During build, config files are not regenerated + // because the cache key does not change and AGP thinks that they are up-to-date. + afterEvaluate { +- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabDebugPackage") +- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabReleasePackage") ++ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabDebugPackage") ++ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabReleasePackage") + } +diff --git a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt +index e5ba0ea..c0456eb 100644 +--- a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt ++++ b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt +@@ -4,6 +4,9 @@ cmake_minimum_required(VERSION 3.13) + + set(CMAKE_VERBOSE_MAKEFILE on) + ++include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake") ++add_compile_options(${folly_FLAGS}) ++ + add_compile_options(-fvisibility=hidden -fexceptions -frtti) + + string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}") +@@ -15,16 +18,16 @@ file(GLOB CPP_SRC CONFIGURE_DEPENDS "${CPP_DIR}/*.cpp") + + add_library(${CMAKE_PROJECT_NAME} SHARED ${ANDROID_SRC} ${CPP_SRC}) + +-target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR}) ++target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR} "${REACT_NATIVE_DIR}/ReactCommon/jsiexecutor") + + find_package(fbjni REQUIRED CONFIG) + find_package(ReactAndroid REQUIRED CONFIG) +-find_package(react-native-reanimated REQUIRED CONFIG) ++find_package(react-native-worklets REQUIRED CONFIG) + + target_link_libraries( + ${CMAKE_PROJECT_NAME} + fbjni::fbjni + ReactAndroid::jsi + ReactAndroid::reactnative +- react-native-reanimated::worklets ++ react-native-worklets::worklets + ) +diff --git a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm +index 9d585e3..fe46961 100644 +--- a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm ++++ b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm +@@ -19,7 +19,7 @@ @implementation MarkdownParser { + const auto &markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + +- std::shared_ptr markdownWorklet; ++ std::shared_ptr markdownWorklet; + try { + markdownWorklet = expensify::livemarkdown::getMarkdownWorklet([parserId intValue]); + } catch (const std::out_of_range &error) { +diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp +index 56fd6de..67f93eb 100644 +--- a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp ++++ b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp +@@ -17,11 +17,11 @@ std::shared_ptr getMarkdownRuntime() { + return globalMarkdownWorkletRuntime; + } + +-std::unordered_map> globalMarkdownShareableWorklets; ++std::unordered_map> globalMarkdownShareableWorklets; + std::mutex globalMarkdownShareableWorkletsMutex; + int nextParserId = 1; + +-const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet) { ++const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet) { + assert(markdownWorklet != nullptr); + auto parserId = nextParserId++; + std::unique_lock lock(globalMarkdownShareableWorkletsMutex); +@@ -34,7 +34,7 @@ void unregisterMarkdownWorklet(const int parserId) { + globalMarkdownShareableWorklets.erase(parserId); + } + +-std::shared_ptr getMarkdownWorklet(const int parserId) { ++std::shared_ptr getMarkdownWorklet(const int parserId) { + std::unique_lock lock(globalMarkdownShareableWorkletsMutex); + return globalMarkdownShareableWorklets.at(parserId); + } +diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h +index 1edfb45..e181726 100644 +--- a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h ++++ b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h +@@ -14,11 +14,11 @@ void setMarkdownRuntime(const std::shared_ptr &markdownWorkletRu + + std::shared_ptr getMarkdownRuntime(); + +-const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet); ++const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet); + + void unregisterMarkdownWorklet(const int parserId); + +-std::shared_ptr getMarkdownWorklet(const int parserId); ++std::shared_ptr getMarkdownWorklet(const int parserId); + + } // namespace livemarkdown + } // namespace expensify +diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp b/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp +index 5332e30..f1f25a5 100644 +--- a/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp ++++ b/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp +@@ -23,7 +23,7 @@ void injectJSIBindings(jsi::Runtime &rt) { + jsi::PropNameID::forAscii(rt, "jsi_registerMarkdownWorklet"), + 1, + [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { +- auto parserId = registerMarkdownWorklet(extractShareableOrThrow(rt, args[0])); ++ auto parserId = registerMarkdownWorklet(extractSerializableOrThrow(rt, args[0])); + return jsi::Value(parserId); + })); + diff --git a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts b/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts index e6ad088..90f2487 100644 --- a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts From 1c37a3e06c82c8fa7b8afd464acc5262f11ed370 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 17:17:11 +0200 Subject: [PATCH 0058/1005] Move patch to a catalog --- .../@expensify+react-native-live-markdown+0.1.302.patch | 0 patches/@expensify/react-native-live-markdown/details.md | 9 +++++++++ 2 files changed, 9 insertions(+) rename patches/{ => @expensify/react-native-live-markdown}/@expensify+react-native-live-markdown+0.1.302.patch (100%) create mode 100644 patches/@expensify/react-native-live-markdown/details.md diff --git a/patches/@expensify+react-native-live-markdown+0.1.302.patch b/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.302.patch rename to patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch diff --git a/patches/@expensify/react-native-live-markdown/details.md b/patches/@expensify/react-native-live-markdown/details.md new file mode 100644 index 0000000000000..a209fe239a53c --- /dev/null +++ b/patches/@expensify/react-native-live-markdown/details.md @@ -0,0 +1,9 @@ + +# `@expensify/react-native-live-markdown` patches + +### [@expensify+react-native-live-markdown+0.1.302.patch](@expensify+react-native-live-markdown+0.1.302.patch) + +- Reason: This is a temporary patch to make the app buildable and testable with live-markdown. The plan is to bump live-markdown first, and then update reanimated. This patch is not meant for production — it exists only for testing purposes. +- Upstream PR/issue: 🛑 +- E/App issue: 🛑 +- PR Introducing Patch: [Upgrade Reanimated to v4](https://github.com/Expensify/App/pull/69469) From 5999f801587c34a62bbb207addd19463b2a400a9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 17:30:19 +0200 Subject: [PATCH 0059/1005] Fix comment --- babel.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel.config.js b/babel.config.js index 998f99b5c2670..691a74e1479c4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -130,7 +130,7 @@ const metro = { }, }, ], - // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/ + // The worklets babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/ 'react-native-worklets/plugin', ], env: { From ed9b358b79136cc7b115d4ae3800ae654309f1fc Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 17:35:04 +0200 Subject: [PATCH 0060/1005] Remove unrelated podfile changes --- ios/Podfile.lock | 49 +++++++++++++----------------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 709de39e249cf..ee76bd93f7eeb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3131,7 +3131,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNShare (11.0.2): + - RNScreens (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3148,19 +3148,16 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNScreens/common (= 4.12.0) - Yoga - - RNSound (0.11.2): - - React-Core - - RNSound/Core (= 0.11.2) - - RNSound/Core (0.11.2): - - React-Core - - RNSVG (15.9.0): + - RNScreens/common (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3177,15 +3174,15 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.9.0) - Yoga - - RNSVG/common (15.9.0): + - RNShare (11.0.2): - DoubleConversion - glog - hermes-engine @@ -3209,32 +3206,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNWorklets (0.5.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.11.18.00) - - RCTRequired - - RCTTypeSafety + - RNSound (0.11.2): - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-hermes - - React-ImageManager - - React-jsi - - React-NativeModulesApple - - React-RCTFabric - - React-renderercss - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - RNWorklets/worklets (= 0.5.0) - - Yoga - - RNWorklets/worklets (0.5.0): + - RNSound/Core (= 0.11.2) + - RNSound/Core (0.11.2): + - React-Core + - RNSVG (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3257,9 +3234,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets/apple (= 0.5.0) + - RNSVG/common (= 15.9.0) - Yoga - - RNWorklets/worklets/apple (0.5.0): + - RNSVG/common (15.9.0): - DoubleConversion - glog - hermes-engine From 87aeb55974d48f6e3548b138638c22419361fd0f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 17:39:27 +0200 Subject: [PATCH 0061/1005] Reset podfile --- ios/Podfile.lock | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ee76bd93f7eeb..01acb1c37bb96 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2898,7 +2898,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets + - RNReanimated/worklets - Yoga - RNLocalize (2.2.6): - React-Core @@ -3003,7 +3003,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (4.1.0): + - RNReanimated (3.19.1): - DoubleConversion - glog - hermes-engine @@ -3026,10 +3026,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.1.0) - - RNWorklets + - RNReanimated/reanimated (= 3.19.1) + - RNReanimated/worklets (= 3.19.1) - Yoga - - RNReanimated/reanimated (4.1.0): + - RNReanimated/reanimated (3.19.1): - DoubleConversion - glog - hermes-engine @@ -3052,10 +3052,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.1.0) - - RNWorklets + - RNReanimated/reanimated/apple (= 3.19.1) - Yoga - - RNReanimated/reanimated/apple (4.1.0): + - RNReanimated/reanimated/apple (3.19.1): - DoubleConversion - glog - hermes-engine @@ -3078,9 +3077,8 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets - Yoga - - RNScreens (4.12.0): + - RNReanimated/worklets (3.19.1): - DoubleConversion - glog - hermes-engine @@ -3097,16 +3095,15 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.12.0) + - RNReanimated/worklets/apple (= 3.19.1) - Yoga - - RNScreens/common (4.12.0): + - RNReanimated/worklets/apple (3.19.1): - DoubleConversion - glog - hermes-engine @@ -3123,7 +3120,6 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils @@ -3420,7 +3416,6 @@ DEPENDENCIES: - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) - - RNWorklets (from `../node_modules/react-native-worklets`) - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -3743,8 +3738,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-sound" RNSVG: :path: "../node_modules/react-native-svg" - RNWorklets: - :path: "../node_modules/react-native-worklets" VisionCamera: :path: "../node_modules/react-native-vision-camera" Yoga: @@ -3918,18 +3911,17 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: b249b5be5a3659025aed8898aaaafd567dc2d660 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 335a4d04da4d4b940a19b750cccae2ee1ea21a0e + RNLiveMarkdown: 33974079f2be0b5c5e6032582c265494f00a8ce4 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 315b95f92be3bf9c86cac2925e39c9ab2ad2c793 RNNitroSQLite: a1b0f1a665c54fc0f2510a292c1bf8ac4d44231b RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 223b1b3895100ae576756fa8e0d09e49354f9bdf + RNReanimated: 0692f0b36f15b83a7936c3611cab923e1396eeb9 RNScreens: 9cb0d4b69067d62d51c278730bb0322f5cbd8d8e RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 2aeb75970331da3a64cb89309f7fd7bd3769260a - RNWorklets: 18e24f98345960e357e8e0f7908c6b67704c34a2 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c From 7ce74909746b5cb6b659fcf4661ff5ef2eb9bbf5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Sep 2025 17:49:57 +0200 Subject: [PATCH 0062/1005] Commit verified podfile.lock --- ios/Podfile.lock | 81 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 01acb1c37bb96..e2de0e52c91bb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2898,7 +2898,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets + - RNWorklets - Yoga - RNLocalize (2.2.6): - React-Core @@ -3003,7 +3003,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.19.1): + - RNReanimated (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3026,10 +3026,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.19.1) - - RNReanimated/worklets (= 3.19.1) + - RNReanimated/reanimated (= 4.1.0) + - RNWorklets - Yoga - - RNReanimated/reanimated (3.19.1): + - RNReanimated/reanimated (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3052,9 +3052,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.19.1) + - RNReanimated/reanimated/apple (= 4.1.0) + - RNWorklets - Yoga - - RNReanimated/reanimated/apple (3.19.1): + - RNReanimated/reanimated/apple (4.1.0): - DoubleConversion - glog - hermes-engine @@ -3077,8 +3078,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNWorklets - Yoga - - RNReanimated/worklets (3.19.1): + - RNScreens (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3095,15 +3097,16 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets/apple (= 3.19.1) + - RNScreens/common (= 4.12.0) - Yoga - - RNReanimated/worklets/apple (3.19.1): + - RNScreens/common (4.12.0): - DoubleConversion - glog - hermes-engine @@ -3120,6 +3123,7 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-renderercss - React-rendererdebug - React-utils @@ -3127,7 +3131,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (4.12.0): + - RNShare (11.0.2): - DoubleConversion - glog - hermes-engine @@ -3144,16 +3148,19 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.12.0) - Yoga - - RNScreens/common (4.12.0): + - RNSound (0.11.2): + - React-Core + - RNSound/Core (= 0.11.2) + - RNSound/Core (0.11.2): + - React-Core + - RNSVG (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3170,15 +3177,15 @@ PODS: - React-jsi - React-NativeModulesApple - React-RCTFabric - - React-RCTImage - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNSVG/common (= 15.9.0) - Yoga - - RNShare (11.0.2): + - RNSVG/common (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3202,12 +3209,32 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSound (0.11.2): - - React-Core - - RNSound/Core (= 0.11.2) - - RNSound/Core (0.11.2): + - RNWorklets (0.5.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety - React-Core - - RNSVG (15.9.0): + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets/worklets (= 0.5.0) + - Yoga + - RNWorklets/worklets (0.5.0): - DoubleConversion - glog - hermes-engine @@ -3230,9 +3257,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.9.0) + - RNWorklets/worklets/apple (= 0.5.0) - Yoga - - RNSVG/common (15.9.0): + - RNWorklets/worklets/apple (0.5.0): - DoubleConversion - glog - hermes-engine @@ -3416,6 +3443,7 @@ DEPENDENCIES: - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) + - RNWorklets (from `../node_modules/react-native-worklets`) - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -3738,6 +3766,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-sound" RNSVG: :path: "../node_modules/react-native-svg" + RNWorklets: + :path: "../node_modules/react-native-worklets" VisionCamera: :path: "../node_modules/react-native-vision-camera" Yoga: @@ -3911,17 +3941,18 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: b249b5be5a3659025aed8898aaaafd567dc2d660 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 33974079f2be0b5c5e6032582c265494f00a8ce4 + RNLiveMarkdown: 7a023e8ca721643cb5a9297e279e4b085093137c RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 315b95f92be3bf9c86cac2925e39c9ab2ad2c793 RNNitroSQLite: a1b0f1a665c54fc0f2510a292c1bf8ac4d44231b RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 0692f0b36f15b83a7936c3611cab923e1396eeb9 + RNReanimated: 223b1b3895100ae576756fa8e0d09e49354f9bdf RNScreens: 9cb0d4b69067d62d51c278730bb0322f5cbd8d8e RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 2aeb75970331da3a64cb89309f7fd7bd3769260a + RNWorklets: 18e24f98345960e357e8e0f7908c6b67704c34a2 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c From f1c51c8273f6aa65727698d10b2a4f9bcc3b0a3b Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Fri, 5 Sep 2025 20:13:08 -0700 Subject: [PATCH 0063/1005] Update docs/articles/new-expensify/connections/Uber-for-Business.md Co-authored-by: Tom Rhys Jones --- docs/articles/new-expensify/connections/Uber-for-Business.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/connections/Uber-for-Business.md b/docs/articles/new-expensify/connections/Uber-for-Business.md index cad2c8321d5a9..0adc6744feba9 100644 --- a/docs/articles/new-expensify/connections/Uber-for-Business.md +++ b/docs/articles/new-expensify/connections/Uber-for-Business.md @@ -28,7 +28,7 @@ If your connection fails or expires, a warning will appear under **Workspaces > ## Manage invites -When you first connect to Uber for Business, all workspace members will be invited by default. To manually invite members later: +To manually invite members to Uber for Business later: 1. Go to **Workspaces > [Workspace Name] > Receipt partners** 2. Click **Manage invites**. From 8175bf3ab8a03d850a6ce48511e7b64a4b5453ba Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Fri, 5 Sep 2025 20:13:34 -0700 Subject: [PATCH 0064/1005] Update docs/articles/new-expensify/connections/Uber-for-Business.md Co-authored-by: Tom Rhys Jones --- docs/articles/new-expensify/connections/Uber-for-Business.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/connections/Uber-for-Business.md b/docs/articles/new-expensify/connections/Uber-for-Business.md index 0adc6744feba9..b12880f817faa 100644 --- a/docs/articles/new-expensify/connections/Uber-for-Business.md +++ b/docs/articles/new-expensify/connections/Uber-for-Business.md @@ -20,7 +20,7 @@ Connect your Expensify workspace to Uber for Business to automatically import Ub 3. Click **Connect** next to Uber for Business. 4. Authorize or create your Uber for Business (U4B) account in the new browser tab that opens. 5. When complete, the tab will close, and a list of workspace members not yet invited to U4B will appear. -6. Confirm which workspace members should be invited to use the connection (they’ll all be selected by default). +6. Confirm which workspace members should be invited to U4B (they’ll all be selected by default). If your connection fails or expires, a warning will appear under **Workspaces > Receipt partners > Uber for Business**. To resolve the error, click the three-dot menu and choose **Enter credentials** to reconnect. From 46babe988355663a4958d259a05c8a23baedf7f4 Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Fri, 5 Sep 2025 20:23:51 -0700 Subject: [PATCH 0065/1005] Update Uber-for-Business.md --- docs/articles/new-expensify/connections/Uber-for-Business.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/connections/Uber-for-Business.md b/docs/articles/new-expensify/connections/Uber-for-Business.md index b12880f817faa..eab2cdcfc4a22 100644 --- a/docs/articles/new-expensify/connections/Uber-for-Business.md +++ b/docs/articles/new-expensify/connections/Uber-for-Business.md @@ -20,7 +20,7 @@ Connect your Expensify workspace to Uber for Business to automatically import Ub 3. Click **Connect** next to Uber for Business. 4. Authorize or create your Uber for Business (U4B) account in the new browser tab that opens. 5. When complete, the tab will close, and a list of workspace members not yet invited to U4B will appear. -6. Confirm which workspace members should be invited to U4B (they’ll all be selected by default). +6. Confirm which workspace members should be invited to U4B (they’ll all be selected by default), which will trigger an invite from Uber that each member must accept. If your connection fails or expires, a warning will appear under **Workspaces > Receipt partners > Uber for Business**. To resolve the error, click the three-dot menu and choose **Enter credentials** to reconnect. From 0b5fa5574cfd15a390d7f40b22ff1ae99ef11266 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 7 Sep 2025 23:16:03 +0000 Subject: [PATCH 0066/1005] reverted dependency line in InviteReceiptPartnerPolicyPage.tsx by removing countryCode from the dependencies --- .../receiptPartners/InviteReceiptPartnerPolicyPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx b/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx index f2d716107ef23..dcb9ef7f75a6b 100644 --- a/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx +++ b/src/pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage.tsx @@ -126,7 +126,7 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP shouldShow: true, }, ]; - }, [countryCode, workspaceMembers, debouncedSearchTerm, selectedOptions]); + }, [workspaceMembers, debouncedSearchTerm, selectedOptions]); // Pre-select all members only once on first load. useEffect(() => { From 641931ebf1b0fd5a1e29e2d329fd7aa0d1731a4d Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 7 Sep 2025 23:20:07 +0000 Subject: [PATCH 0067/1005] Moved 'usePolicyData' to it's own folder and create types file --- src/hooks/usePolicyData.ts | 69 ----------------------------- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Policy/Category.ts | 2 +- src/libs/actions/Policy/Tag.ts | 2 +- 4 files changed, 3 insertions(+), 72 deletions(-) delete mode 100644 src/hooks/usePolicyData.ts diff --git a/src/hooks/usePolicyData.ts b/src/hooks/usePolicyData.ts deleted file mode 100644 index fff95360d18a1..0000000000000 --- a/src/hooks/usePolicyData.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {useMemo} from 'react'; -import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; -import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; -import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import useOnyx from './useOnyx'; -import usePolicy from './usePolicy'; - -type PolicyData = { - policy: OnyxValueWithOfflineFeedback; - tags: PolicyTagLists; - categories: PolicyCategories; - reports: Array>; - transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; -}; - -/** - * Custom hook to retrieve policy-related data. - * @param policyID The ID of the policy to retrieve data for. - * @returns An object containing policy data, including tags, categories, reports, and transactions/violations. - */ - -function usePolicyData(policyID?: string): PolicyData { - const policy = usePolicy(policyID) as OnyxValueWithOfflineFeedback; - const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); - - const [tagsLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); - const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - - const [reportCollection] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { - canBeMissing: true, - selector: (allReports) => { - if (!allReports) { - return []; - } - const reportIDsWithTransactions = Object.keys(allReportsTransactionsAndViolations ?? {}); - return Object.values(allReports).filter((report) => !!report && report?.policyID === policyID && reportIDsWithTransactions.includes(report.reportID)); - }, - }); - - const reports: Array> = useMemo(() => { - if (!reportCollection) { - return []; - } - return reportCollection as Array>; - }, [reportCollection]); - - const transactionsAndViolations = useMemo(() => { - return reports.reduce((acc, report) => { - if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { - acc[report.reportID] = allReportsTransactionsAndViolations?.[report.reportID]; - } - return acc; - }, {}); - }, [reports, allReportsTransactionsAndViolations]); - - return { - tags: tagsLists ?? {}, - categories: categories ?? {}, - - policy, - reports, - transactionsAndViolations, - }; -} - -export type {PolicyData}; -export default usePolicyData; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 27eb1effe3c02..cdfa05c7630d8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -18,7 +18,7 @@ import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; -import type {PolicyData} from '@hooks/usePolicyData'; +import type {PolicyData} from '@hooks/usePolicyData/types'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 7faf0afc7ea83..114870c35e00e 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -3,7 +3,7 @@ import lodashUnion from 'lodash/union'; import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PartialDeep} from 'type-fest'; -import type {PolicyData} from '@hooks/usePolicyData'; +import type {PolicyData} from '@hooks/usePolicyData/types'; import * as API from '@libs/API'; import type { EnablePolicyCategoriesParams, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index c5c7d497e3804..622ddc2497307 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,7 +1,7 @@ import lodashCloneDeep from 'lodash/cloneDeep'; import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {PolicyData} from '@hooks/usePolicyData'; +import type {PolicyData} from '@hooks/usePolicyData/types'; import * as API from '@libs/API'; import type { EnablePolicyTagsParams, From 70496e68c9bc7d43a65b364f379a9ab8417dcc70 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 7 Sep 2025 23:22:16 +0000 Subject: [PATCH 0068/1005] Created 'usePolicyData' to own folder and split 'usePolicyData' into index.ts and types.ts files --- src/hooks/usePolicyData/index.ts | 52 ++++++++++++++++++++++++++++++++ src/hooks/usePolicyData/types.ts | 13 ++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/hooks/usePolicyData/index.ts create mode 100644 src/hooks/usePolicyData/types.ts diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts new file mode 100644 index 0000000000000..2339d7c99fb28 --- /dev/null +++ b/src/hooks/usePolicyData/index.ts @@ -0,0 +1,52 @@ +import {useMemo} from 'react'; +import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; +import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; +import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; +import useOnyx from '../useOnyx'; +import usePolicy from '../usePolicy'; +import { PolicyData } from './types'; + +/** + * Retrieves policy-related data such as tags, categories, reports, and transactions/violations. + * @param policyID The ID of the policy to retrieve data for. + * @returns An object containing policy data + */ + +function usePolicyData(policyID?: string): PolicyData { + const policy = usePolicy(policyID); + const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); + + const [tagsLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + + const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { + canBeMissing: true, + selector: (allReports) => { + if (!allReports) { + return []; + } + // Filter reports to only include those that belong to the specified policy and have associated transactions + const reportIDsWithTransactionsAndViolations = Object.keys(allReportsTransactionsAndViolations ?? {}); + return Object.values(allReports).filter((report) => !!report && report?.policyID === policyID && reportIDsWithTransactionsAndViolations.includes(report.reportID)); + }, + }); + + const transactionsAndViolations = useMemo(() => (reports ?? []).reduce((acc, report) => { + if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { + acc[report.reportID] = allReportsTransactionsAndViolations[report.reportID]; + } + return acc; + }, {}), [reports, allReportsTransactionsAndViolations]); + + return { + reports: reports as Array>, + policy: policy as OnyxValueWithOfflineFeedback, + categories: categories as PolicyCategories, + tags: tagsLists as PolicyTagLists, + transactionsAndViolations, + }; +} + +export default usePolicyData; diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts new file mode 100644 index 0000000000000..82abc3f34ec51 --- /dev/null +++ b/src/hooks/usePolicyData/types.ts @@ -0,0 +1,13 @@ +import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; +import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; +import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; + +type PolicyData = { + policy: OnyxValueWithOfflineFeedback; + tags: PolicyTagLists; + categories: PolicyCategories; + reports: Array>; + transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; +}; + +export type {PolicyData}; From 72d9f86b546b4bf109b374ea4fadf63e60d82c96 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 7 Sep 2025 23:29:11 +0000 Subject: [PATCH 0069/1005] fixing 'usePolicyData' catergories and tags empty value --- src/hooks/usePolicyData/index.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index 2339d7c99fb28..83cd742588649 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -6,7 +6,7 @@ import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/ import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import useOnyx from '../useOnyx'; import usePolicy from '../usePolicy'; -import { PolicyData } from './types'; +import {PolicyData} from './types'; /** * Retrieves policy-related data such as tags, categories, reports, and transactions/violations. @@ -33,18 +33,22 @@ function usePolicyData(policyID?: string): PolicyData { }, }); - const transactionsAndViolations = useMemo(() => (reports ?? []).reduce((acc, report) => { - if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { - acc[report.reportID] = allReportsTransactionsAndViolations[report.reportID]; - } - return acc; - }, {}), [reports, allReportsTransactionsAndViolations]); + const transactionsAndViolations = useMemo( + () => + (reports ?? []).reduce((acc, report) => { + if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { + acc[report.reportID] = allReportsTransactionsAndViolations[report.reportID]; + } + return acc; + }, {}), + [reports, allReportsTransactionsAndViolations], + ); return { reports: reports as Array>, policy: policy as OnyxValueWithOfflineFeedback, - categories: categories as PolicyCategories, - tags: tagsLists as PolicyTagLists, + categories: categories ?? {}, + tags: tagsLists ?? {}, transactionsAndViolations, }; } From cd59515f9b44f85662eec71fafbe573789621109 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 8 Sep 2025 00:23:15 +0000 Subject: [PATCH 0070/1005] lint... --- src/hooks/usePolicyData/index.ts | 56 ++++++++++++++++------------- src/hooks/usePolicyData/types.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Policy/Category.ts | 2 +- src/libs/actions/Policy/Tag.ts | 2 +- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index 83cd742588649..b1297372ec470 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -1,15 +1,15 @@ import {useMemo} from 'react'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import useOnyx from '../useOnyx'; -import usePolicy from '../usePolicy'; -import {PolicyData} from './types'; +import type PolicyData from './types'; /** - * Retrieves policy-related data such as tags, categories, reports, and transactions/violations. + * Retrieves policy tags, categories, reports and their associated transactions and violations. * @param policyID The ID of the policy to retrieve data for. * @returns An object containing policy data */ @@ -18,38 +18,46 @@ function usePolicyData(policyID?: string): PolicyData { const policy = usePolicy(policyID); const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); - const [tagsLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { canBeMissing: true, selector: (allReports) => { - if (!allReports) { - return []; + if (!policyID || !allReports || !allReportsTransactionsAndViolations) { + return {}; } // Filter reports to only include those that belong to the specified policy and have associated transactions - const reportIDsWithTransactionsAndViolations = Object.keys(allReportsTransactionsAndViolations ?? {}); - return Object.values(allReports).filter((report) => !!report && report?.policyID === policyID && reportIDsWithTransactionsAndViolations.includes(report.reportID)); + return Object.keys(allReportsTransactionsAndViolations).reduce>((acc, reportID) => { + const policyReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + if (policyReport?.policyID === policyID) { + acc[reportID] = policyReport; + } + return acc; + }, {}); }, }); - const transactionsAndViolations = useMemo( - () => - (reports ?? []).reduce((acc, report) => { - if (report?.reportID && allReportsTransactionsAndViolations?.[report.reportID]) { - acc[report.reportID] = allReportsTransactionsAndViolations[report.reportID]; - } - return acc; - }, {}), - [reports, allReportsTransactionsAndViolations], - ); + const transactionsAndViolations = useMemo(() => { + if (!reports || !allReportsTransactionsAndViolations) { + return {}; + } + + return Object.keys(reports).reduce((acc, reportID) => { + if (allReportsTransactionsAndViolations[reportID]) { + acc[reportID] = allReportsTransactionsAndViolations[reportID]; + } + return acc; + }, {}); + }, [reports, allReportsTransactionsAndViolations]); return { - reports: reports as Array>, - policy: policy as OnyxValueWithOfflineFeedback, - categories: categories ?? {}, - tags: tagsLists ?? {}, transactionsAndViolations, + + tags: tags ?? {}, + categories: categories ?? {}, + policy: policy as OnyxValueWithOfflineFeedback, + reports: Object.values(reports ?? {}) as Array>, }; } diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts index 82abc3f34ec51..df3367cd1fccc 100644 --- a/src/hooks/usePolicyData/types.ts +++ b/src/hooks/usePolicyData/types.ts @@ -10,4 +10,4 @@ type PolicyData = { transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; }; -export type {PolicyData}; +export default PolicyData; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cdfa05c7630d8..206d3cf16bf9d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -18,7 +18,7 @@ import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; -import type {PolicyData} from '@hooks/usePolicyData/types'; +import type PolicyData from '@hooks/usePolicyData/types'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 114870c35e00e..68989d13faaa4 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -3,7 +3,7 @@ import lodashUnion from 'lodash/union'; import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PartialDeep} from 'type-fest'; -import type {PolicyData} from '@hooks/usePolicyData/types'; +import type PolicyData from '@hooks/usePolicyData/types'; import * as API from '@libs/API'; import type { EnablePolicyCategoriesParams, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 622ddc2497307..8134bb3bf2b76 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,7 +1,7 @@ import lodashCloneDeep from 'lodash/cloneDeep'; import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {PolicyData} from '@hooks/usePolicyData/types'; +import type PolicyData from '@hooks/usePolicyData/types'; import * as API from '@libs/API'; import type { EnablePolicyTagsParams, From 251280f6bef50224f36ae0b31382341a1e5474da Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:24:51 +0000 Subject: [PATCH 0071/1005] part 1: apply reviewers suggestions --- .../members/WorkspaceInviteMessageComponent.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index f6fedf696bb9b..626c22d4b1cbd 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -83,14 +83,7 @@ function WorkspaceInviteMessageComponent({ ); const getDefaultWelcomeNote = useCallback(() => { - return ( - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? - // workspaceInviteMessageDraft can be an empty string - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - workspaceInviteMessageDraft ?? - translate('workspace.common.welcomeNote') - ); + return formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? workspaceInviteMessageDraft ?? translate('workspace.common.welcomeNote'); }, [workspaceInviteMessageDraft, translate, formData]); useEffect(() => { @@ -105,6 +98,10 @@ function WorkspaceInviteMessageComponent({ return; } Navigation.goBack(backTo); + + // We only want to run this useEffect when the onyx values have loaded + // We navigate back to the main members screen when the invitation has been sent + // This is decided when onyx values have loaded and if `invitedEmailsToAccountIDsDraft` is empty // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOnyxLoading]); @@ -112,6 +109,7 @@ function WorkspaceInviteMessageComponent({ Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details + // See https://github.com/Expensify/App/blob/main/README.md#workspace, we set conditions about who can leave the workspace addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID, policyMemberAccountIDs, workspaceInviteRoleDraft, formatPhoneNumber); setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); @@ -225,7 +223,7 @@ function WorkspaceInviteMessageComponent({ description={translate('common.role')} shouldShowRightIcon onPress={() => { - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID ?? '', Navigation.getActiveRoute())); }} /> From 90d612ca4f62e8e881599827f8ff1ea3adca6137 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:40:32 +0000 Subject: [PATCH 0072/1005] apply reviewers comments --- src/ROUTES.ts | 10 ++++++-- .../companyCards/assignCard/AssigneeStep.tsx | 22 +++++------------ .../expensifyCard/issueNew/AssigneeStep.tsx | 24 ++++++------------- .../WorkspaceInviteMessageComponent.tsx | 2 +- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b8ebefadc40ef..ebf5544cc42a9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1191,8 +1191,14 @@ const ROUTES = { WORKSPACE_INVITE_MESSAGE_ROLE: { route: 'workspaces/:policyID/invite-message/role', - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`workspaces/${policyID}/invite-message/role`, backTo)}` as const, + getRoute: (policyID: string | undefined, backTo?: string) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_INVITE_MESSAGE_ROLE route'); + } + + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + return getUrlWithBackToParam(`workspaces/${policyID}/invite-message/role` as const, backTo); + }, }, WORKSPACE_OVERVIEW: { route: 'workspaces/:policyID/overview', diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index ad5824ffd9ed3..211c152b59b8b 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -47,18 +47,8 @@ function useOptions() { const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {options: optionsList, areOptionsInitialized} = useOptionsList(); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); - const existingDelegates = useMemo( - () => - account?.delegatedAccess?.delegates?.reduce( - (prev, {email}) => { - // eslint-disable-next-line no-param-reassign - prev[email] = true; - return prev; - }, - {} as Record, - ), - [account?.delegatedAccess?.delegates], - ); + const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( @@ -89,7 +79,7 @@ function useOptions() { }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); @@ -103,7 +93,7 @@ function useOptions() { ...filteredOptions, headerMessage, }; - }, [debouncedSearchValue, defaultOptions, existingDelegates]); + }, [debouncedSearchValue, defaultOptions, existingDelegates, countryCode]); return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } @@ -254,7 +244,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ]; } - const searchValueForPhoneOrEmail = getSearchValueForPhoneOrEmail(debouncedSearchValue).toLowerCase(); + const searchValueForPhoneOrEmail = getSearchValueForPhoneOrEmail(debouncedSearchValue, countryCode).toLowerCase(); const filteredOptions = tokenizedSearch(membersDetails, searchValueForPhoneOrEmail, (option) => [option.text ?? '', option.alternateText ?? '']); return [ @@ -278,7 +268,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ] : []), ]; - }, [debouncedSearchValue, membersDetails, userToInvite, membersDetailsWithInviteNewMember, personalDetails]); + }, [debouncedSearchValue, membersDetails, userToInvite, membersDetailsWithInviteNewMember, personalDetails, countryCode]); return ( - account?.delegatedAccess?.delegates?.reduce( - (prev, {email}) => { - // eslint-disable-next-line no-param-reassign - prev[email] = true; - return prev; - }, - {} as Record, - ), - [account?.delegatedAccess?.delegates], - ); + const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( @@ -87,7 +77,7 @@ function useOptions() { }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); @@ -101,7 +91,7 @@ function useOptions() { ...filteredOptions, headerMessage, }; - }, [debouncedSearchValue, defaultOptions, existingDelegates]); + }, [debouncedSearchValue, defaultOptions, existingDelegates, countryCode]); return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } @@ -112,8 +102,8 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { const {isOffline} = useNetwork(); const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); - const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const {userToInvite, searchValue, personalDetails, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const currency = useCurrencyForExpensifyCard({policyID}); const isEditing = issueNewCard?.isEditing; @@ -204,7 +194,7 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { ]; } - const searchValueForOptions = getSearchValueForPhoneOrEmail(debouncedSearchValue).toLowerCase(); + const searchValueForOptions = getSearchValueForPhoneOrEmail(debouncedSearchValue, countryCode).toLowerCase(); const filteredOptions = tokenizedSearch(membersDetails, searchValueForOptions, (option) => [option.text ?? '', option.alternateText ?? '']); return [ @@ -228,7 +218,7 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { ] : []), ]; - }, [debouncedSearchValue, membersDetails, userToInvite, personalDetails]); + }, [debouncedSearchValue, membersDetails, userToInvite, personalDetails, countryCode]); return ( { - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID ?? '', Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID, Navigation.getActiveRoute())); }} /> From efca7cd8a18b581f35362938a5cec6aacbdf3aa9 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:06:45 +0000 Subject: [PATCH 0073/1005] extract useOptions --- src/libs/UseOptionsUtils.ts | 70 +++++++++++++++++++ .../companyCards/assignCard/AssigneeStep.tsx | 66 +---------------- .../expensifyCard/issueNew/AssigneeStep.tsx | 68 +----------------- 3 files changed, 75 insertions(+), 129 deletions(-) create mode 100644 src/libs/UseOptionsUtils.ts diff --git a/src/libs/UseOptionsUtils.ts b/src/libs/UseOptionsUtils.ts new file mode 100644 index 0000000000000..04610d67d99f4 --- /dev/null +++ b/src/libs/UseOptionsUtils.ts @@ -0,0 +1,70 @@ +import {useMemo, useState} from 'react'; +import {useBetas} from '@components/OnyxListItemProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useOnyx from '@hooks/useOnyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import memoize from './memoize'; +import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from './OptionsListUtils'; + +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AssigneeStep.getValidOptions'}); + +function useOptions() { + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); + + const defaultOptions = useMemo(() => { + const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions, existingDelegates, countryCode]); + + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; +} + +export default useOptions; diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 211c152b59b8b..400de659641e9 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -4,26 +4,23 @@ import type {OnyxEntry} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; -import {useBetas} from '@components/OnyxListItemProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftInviteAccountID} from '@libs/actions/Card'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; -import memoize from '@libs/memoize'; -import {filterAndOrderOptions, getHeaderMessage, getSearchValueForPhoneOrEmail, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; +import useOptions from '@libs/UseOptionsUtils'; import Navigation from '@navigation/Navigation'; import {setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; @@ -39,65 +36,6 @@ type AssigneeStepProps = { feed: OnyxTypes.CompanyCardFeed; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AssigneeStep.getValidOptions'}); - -function useOptions() { - const betas = useBetas(); - const [isLoading, setIsLoading] = useState(true); - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {options: optionsList, areOptionsInitialized} = useOptionsList(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); - - const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( - { - reports: optionsList.reports, - personalDetails: optionsList.personalDetails, - }, - { - betas, - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, - }, - ); - - const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); - - if (isLoading) { - // eslint-disable-next-line react-compiler/react-compiler - setIsLoading(false); - } - - return { - userToInvite, - recentReports, - personalDetails, - currentUserOption, - headerMessage, - }; - }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); - - const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, - maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - }); - const headerMessage = getHeaderMessage( - (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, - !!filteredOptions.userToInvite, - debouncedSearchValue, - ); - - return { - ...filteredOptions, - headerMessage, - }; - }, [debouncedSearchValue, defaultOptions, existingDelegates, countryCode]); - - return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; -} - function AssigneeStep({policy, feed}: AssigneeStepProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 5cb42a8016f47..945058aa1102d 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -1,24 +1,21 @@ -import React, {useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; -import {useBetas} from '@components/OnyxListItemProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import useCurrencyForExpensifyCard from '@hooks/useCurrencyForExpensifyCard'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import memoize from '@libs/memoize'; -import {filterAndOrderOptions, getHeaderMessage, getSearchValueForPhoneOrEmail, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail, getUserNameByEmail} from '@libs/PersonalDetailsUtils'; import {isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; +import useOptions from '@libs/UseOptionsUtils'; import Navigation from '@navigation/Navigation'; import {clearIssueNewCardFlow, getCardDefaultName, setDraftInviteAccountID, setIssueNewCardStepAndData} from '@userActions/Card'; import CONST from '@src/CONST'; @@ -37,65 +34,6 @@ type AssigneeStepProps = { startStepIndex: number; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AssigneeStep.getValidOptions'}); - -function useOptions() { - const betas = useBetas(); - const [isLoading, setIsLoading] = useState(true); - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {options: optionsList, areOptionsInitialized} = useOptionsList(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); - - const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( - { - reports: optionsList.reports, - personalDetails: optionsList.personalDetails, - }, - { - betas, - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, - }, - ); - - const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); - - if (isLoading) { - // eslint-disable-next-line react-compiler/react-compiler - setIsLoading(false); - } - - return { - userToInvite, - recentReports, - personalDetails, - currentUserOption, - headerMessage, - }; - }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); - - const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, - maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - }); - const headerMessage = getHeaderMessage( - (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, - !!filteredOptions.userToInvite, - debouncedSearchValue, - ); - - return { - ...filteredOptions, - headerMessage, - }; - }, [debouncedSearchValue, defaultOptions, existingDelegates, countryCode]); - - return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; -} - function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); From 7ccd78186a360e33ff5b7b4f91d14607cf3d1e9e Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:16:40 +0000 Subject: [PATCH 0074/1005] move isLoading in useEffect --- src/libs/UseOptionsUtils.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/UseOptionsUtils.ts b/src/libs/UseOptionsUtils.ts index 04610d67d99f4..234d0c40fbb4a 100644 --- a/src/libs/UseOptionsUtils.ts +++ b/src/libs/UseOptionsUtils.ts @@ -1,4 +1,4 @@ -import {useMemo, useState} from 'react'; +import {useEffect, useMemo, useState} from 'react'; import {useBetas} from '@components/OnyxListItemProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -19,6 +19,14 @@ function useOptions() { const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const existingDelegates = useMemo(() => Object.fromEntries((account?.delegatedAccess?.delegates ?? []).map(({email}) => [email, true])), [account?.delegatedAccess?.delegates]); + useEffect(() => { + if (!isLoading || !optionsList.reports || !optionsList.personalDetails) { + return; + } + + setIsLoading(false); + }, [isLoading, optionsList.reports, optionsList.personalDetails]); + const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( { @@ -33,11 +41,6 @@ function useOptions() { const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); - if (isLoading) { - // eslint-disable-next-line react-compiler/react-compiler - setIsLoading(false); - } - return { userToInvite, recentReports, @@ -45,7 +48,7 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { From c6709ae824190f103de96e906850de038ef57ce9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 11 Sep 2025 14:56:12 +0200 Subject: [PATCH 0075/1005] Bump worklets, fix failing tests --- package-lock.json | 18 ++++-------------- package.json | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6a913a50d8ae..f9f19e18fa8a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,7 +136,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "0.5.0", + "react-native-worklets": "^0.5.1", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -32159,16 +32159,6 @@ "react-native": "*" } }, - "node_modules/react-native-screens/node_modules/react-native-is-edge-to-edge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", - "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-share": { "version": "11.0.2", "license": "MIT", @@ -32295,9 +32285,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.0.tgz", - "integrity": "sha512-+/tbUCBEVchxe5xxFvvdC18PRZFCoxq1emcjIezXodElFguk1MaBYpbqABvZHBGrrEOo9k99h4Jbt0i9ArCbxg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", + "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", diff --git a/package.json b/package.json index 34b765182f539..9386613de9024 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "0.5.0", + "react-native-worklets": "^0.5.1", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", From 466c7256612b4174be525af18b9b70adc930b853 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 12 Sep 2025 15:32:16 +0200 Subject: [PATCH 0076/1005] Pin worklets version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 441937a93eb30..dcec2b05025da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,7 +136,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.5.1", + "react-native-worklets": "0.5.1", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", diff --git a/package.json b/package.json index caf39aa479891..55f06c86d5180 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "^0.5.1", + "react-native-worklets": "0.5.1", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", From 0aef90329246f7ea5874da741f94a4cd2738624c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 12 Sep 2025 15:32:30 +0200 Subject: [PATCH 0077/1005] Add rn-worklets to babel transforming --- config/webpack/webpack.common.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index d51c31f59d96c..f6c3f7e0c7b4b 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -29,6 +29,7 @@ const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWe const includeModules = [ 'react-native-animatable', 'react-native-reanimated', + 'react-native-worklets', 'react-native-picker-select', 'react-native-web', 'react-native-webview', From 93ff430e70f9a16ef2967340355d1e14d64caf80 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 12 Sep 2025 15:38:28 +0200 Subject: [PATCH 0078/1005] Temporary testing to fix web --- .../react-native-reanimated+4.1.0+002+temp.patch | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch diff --git a/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch b/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch new file mode 100644 index 0000000000000..dd2632f717ab8 --- /dev/null +++ b/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js b/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js +index 8a41ccf..c78e40e 100644 +--- a/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js ++++ b/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js +@@ -1,6 +1,6 @@ + 'use strict'; + +-import { Component } from 'react'; ++import React, {Component} from 'react'; + import { Platform, StyleSheet } from 'react-native'; + import { IS_JEST, ReanimatedError, SHOULD_BE_USE_WEB } from "../../common/index.js"; + import { getViewInfo } from "../../createAnimatedComponent/getViewInfo.js"; From 7c97a2a2b9d120b20761b73f100c3b18bda66513 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Fri, 12 Sep 2025 22:52:11 +0700 Subject: [PATCH 0079/1005] Fix - The composer loses focus after returning to Inbox from Account tab --- src/components/FocusTrap/FocusTrapForScreen/index.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index fb58356e7717a..18aa582c74b43 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -37,7 +37,7 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) { return ( Date: Sat, 13 Sep 2025 04:49:35 +0000 Subject: [PATCH 0080/1005] fix: added dependencies useOnyx hooks in the usePolicyData hook and updates related tests. --- src/hooks/usePolicyData/index.ts | 36 ++++++++++++++++++-------------- tests/unit/usePolicyData.test.ts | 9 ++++---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index b1297372ec470..eec2882531b47 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -18,25 +18,29 @@ function usePolicyData(policyID?: string): PolicyData { const policy = usePolicy(policyID); const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); - const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); - const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}, [policyID]); + const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}, [policyID]); - const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { - canBeMissing: true, - selector: (allReports) => { - if (!policyID || !allReports || !allReportsTransactionsAndViolations) { - return {}; - } - // Filter reports to only include those that belong to the specified policy and have associated transactions - return Object.keys(allReportsTransactionsAndViolations).reduce>((acc, reportID) => { - const policyReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if (policyReport?.policyID === policyID) { - acc[reportID] = policyReport; + const [reports] = useOnyx( + ONYXKEYS.COLLECTION.REPORT, + { + canBeMissing: true, + selector: (allReports) => { + if (!policyID || !allReports || !allReportsTransactionsAndViolations) { + return {}; } - return acc; - }, {}); + // Filter reports to only include those that belong to the specified policy and have associated transactions + return Object.keys(allReportsTransactionsAndViolations).reduce>((acc, reportID) => { + const policyReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + if (policyReport?.policyID === policyID) { + acc[reportID] = policyReport; + } + return acc; + }, {}); + }, }, - }); + [policyID, allReportsTransactionsAndViolations], + ); const transactionsAndViolations = useMemo(() => { if (!reports || !allReportsTransactionsAndViolations) { diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index 840e27bbfe4ed..b946077d1a5a9 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -14,8 +14,7 @@ import createRandomPolicyTags from '../utils/collections/policyTags'; import {createAdminRoom, createAnnounceRoom} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -// Mock data ids - +// Mock data id const mockPolicy: Policy = createRandomPolicy(0); const mockPolicyTagLists: PolicyTagLists = createRandomPolicyTags('Tags', 8); const mockPolicyCategories: PolicyCategories = createRandomPolicyCategories(8); @@ -71,14 +70,14 @@ describe('usePolicyData', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); initOnyxDerivedValues(); - return waitForBatchedUpdates(); }); beforeEach(() => { - return Onyx.clear().then(waitForBatchedUpdates); + Onyx.clear(); + return waitForBatchedUpdates(); }); - test('returns reports filtered by a policy ID that exists in the onyx', async () => { + test('returns data given a policy ID that exists in the onyx', async () => { await Onyx.multiSet({ ...reportsCollection, ...reportActionsCollection, From b1b033fdeaac5dd859bea07e580893487b911c4f Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Mon, 15 Sep 2025 22:15:45 +0700 Subject: [PATCH 0081/1005] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index c4b1e99d3d5f0..134054494d6e3 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -124,14 +124,24 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - if (matchingFullScreenRoute && lastFullScreenRoute && matchingFullScreenRoute.name !== lastFullScreenRoute.name) { - const isMatchingRoutePreloaded = currentState.preloadedRoutes.some((preloadedRoute) => preloadedRoute.name === matchingFullScreenRoute.name); + const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes?.at(-1); + if ( + matchingFullScreenRoute && + lastFullScreenRoute && + (matchingFullScreenRoute.name !== lastFullScreenRoute.name || lastRouteInMatchingFullScreen?.name !== lastRouteInLastFullScreenRoute?.name) + ) { + const isMatchingRoutePreloaded = currentState.preloadedRoutes.some( + (preloadedRoute) => + preloadedRoute.name === matchingFullScreenRoute.name && + (!lastRouteInMatchingFullScreen?.name || + (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name)), + ); if (isMatchingRoutePreloaded) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { - const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); const additionalAction = StackActions.push(matchingFullScreenRoute.name, { screen: lastRouteInMatchingFullScreen?.name, params: lastRouteInMatchingFullScreen?.params, From aa80b3d7b1235882e06f8aaab9424382acf579d1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Sep 2025 09:01:44 +0200 Subject: [PATCH 0082/1005] Remove temporary patch and update reanimated to fix bundle on web --- package-lock.json | 8 ++++---- package.json | 2 +- .../react-native-reanimated+4.1.0+002+temp.patch | 12 ------------ 3 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch diff --git a/package-lock.json b/package-lock.json index dcec2b05025da..d871c1e9a6055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "4.1.0", + "react-native-reanimated": "^4.1.0-jsx-fix", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", @@ -32030,9 +32030,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.0.tgz", - "integrity": "sha512-L8FqZn8VjZyBaCUMYFyx1Y+T+ZTbblaudpxReOXJ66RnOf52g6UM4Pa/IjwLD1XAw1FUxLRQrtpdjbkEc74FiQ==", + "version": "4.1.0-jsx-fix", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.0-jsx-fix.tgz", + "integrity": "sha512-kDfdzTvuHlqNkaP4W46qh1RTeyD7JhAMuNenz/9mFBAQmgajpVaXthma52j4EtXZgNfPbXHnH43NQ/OZrdQA4Q==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", diff --git a/package.json b/package.json index 55f06c86d5180..4b9e591023152 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "4.1.0", + "react-native-reanimated": "^4.1.0-jsx-fix", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", diff --git a/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch b/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch deleted file mode 100644 index dd2632f717ab8..0000000000000 --- a/patches/react-native-reanimated/react-native-reanimated+4.1.0+002+temp.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js b/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js -index 8a41ccf..c78e40e 100644 ---- a/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js -+++ b/node_modules/react-native-reanimated/lib/module/css/component/AnimatedComponent.js -@@ -1,6 +1,6 @@ - 'use strict'; - --import { Component } from 'react'; -+import React, {Component} from 'react'; - import { Platform, StyleSheet } from 'react-native'; - import { IS_JEST, ReanimatedError, SHOULD_BE_USE_WEB } from "../../common/index.js"; - import { getViewInfo } from "../../createAnimatedComponent/getViewInfo.js"; From 0116a6957295ca89ee73073031f4755f75432ae0 Mon Sep 17 00:00:00 2001 From: Mohammad Jafarinejad <71210799+mohammadjafarinejad@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:57:00 +0400 Subject: [PATCH 0083/1005] feat: add reimbursable case to computeReportPart for money request breakdown --- src/libs/Formula.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 96627bb284ecc..3d4ad9ec027a3 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import type {Policy, Report, Transaction} from '@src/types/onyx'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; -import {getReportTransactions} from './ReportUtils'; +import {getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils'; import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { @@ -247,6 +247,8 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { return formatDate(getOldestTransactionDate(report.reportID, context), format); case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); + case 'reimbursable': + return formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': return report.currency ?? ''; case 'policyname': From 3ae1698fafa30d3666b8abe00320fc7d19674bd9 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Fri, 19 Sep 2025 22:13:53 +0800 Subject: [PATCH 0084/1005] feat: draft solution for select empty report --- src/components/Search/index.tsx | 93 ++++++++++++++++--- .../Search/ReportListItemHeader.tsx | 6 +- .../Search/TransactionGroupListItem.tsx | 11 ++- 3 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f44e0c1c1391d..3fcb42eb01a50 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -45,6 +45,7 @@ import { isTransactionGroupListItemType, isTransactionListItemType, isTransactionMemberGroupListItemType, + isTransactionReportGroupListItemType, isTransactionWithdrawalIDGroupListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, @@ -539,7 +540,16 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } const areItemsGrouped = !!groupBy; - const flattenedItems = areItemsGrouped ? (data as TransactionGroupListItemType[]).flatMap((item) => item.transactions) : data; + const flattenedItems = areItemsGrouped + ? (data as TransactionGroupListItemType[]).flatMap((item) => { + // For empty reports, count the report itself as a selectable item + if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item)) { + return [item]; + } + // For regular reports, count all transactions + return item.transactions; + }) + : data; const areAllItemsSelected = flattenedItems.length === Object.keys(selectedTransactions).length; // If the user has selected all the expenses in their view but there are more expenses matched by the search @@ -570,6 +580,46 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } const currentTransactions = itemTransactions ?? item.transactions; + + // Handle empty reports - treat the report itself as selectable + if (currentTransactions.length === 0 && isTransactionReportGroupListItemType(item)) { + const reportKey = item.keyForList; + if (!reportKey) { + return; + } + + if (selectedTransactions[reportKey]?.isSelected) { + // Deselect the empty report + const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; + delete reducedSelectedTransactions[reportKey]; + setSelectedTransactions(reducedSelectedTransactions, data); + return; + } + + // Select the empty report + setSelectedTransactions( + { + ...selectedTransactions, + [reportKey]: { + isSelected: true, + canDelete: true, + canHold: false, + isHeld: false, + canUnhold: false, + canChangeReport: false, + action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, + reportID: item.reportID, + policyID: item.policyID, + amount: 0, + convertedAmount: 0, + convertedCurrency: '', + }, + }, + data, + ); + return; + } + if (currentTransactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) { const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; @@ -780,16 +830,37 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } if (areItemsGrouped) { - setSelectedTransactions( - Object.fromEntries( - (data as TransactionGroupListItemType[]).flatMap((item) => - item.transactions - .filter((t) => !isTransactionPendingDelete(t)) - .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)), - ), - ), - data, - ); + const allSelections = (data as TransactionGroupListItemType[]).flatMap((item) => { + // Handle empty reports - select the report itself + if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && item.keyForList) { + return [ + [ + item.keyForList, + { + isSelected: true, + canDelete: true, + canHold: false, + isHeld: false, + canUnhold: false, + canChangeReport: false, + action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, + reportID: item.reportID, + policyID: item.policyID, + amount: 0, + convertedAmount: 0, + convertedCurrency: '', + }, + ], + ]; + } + + // Handle regular reports with transactions + return item.transactions + .filter((t) => !isTransactionPendingDelete(t)) + .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)); + }); + + setSelectedTransactions(Object.fromEntries(allSelections), data); return; } diff --git a/src/components/SelectionList/Search/ReportListItemHeader.tsx b/src/components/SelectionList/Search/ReportListItemHeader.tsx index 3e236b13e6d5b..42d26faf95449 100644 --- a/src/components/SelectionList/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionList/Search/ReportListItemHeader.tsx @@ -112,11 +112,11 @@ function HeaderFirstRow({ isChecked={isSelectAllChecked} isIndeterminate={isIndeterminate} containerStyle={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!reportItem.isSelected, !!reportItem.isDisabled)]} - disabled={!!isDisabled || reportItem.isDisabledCheckbox} + disabled={!!isDisabled} accessibilityLabel={reportItem.text ?? ''} shouldStopMouseDownPropagation - style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), reportItem.isDisabledCheckbox && styles.cursorDisabled]} - /> + style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), reportItem.isDisabledCheckbox && styles.cursorDisabled]} + /> )} ({ return transactions.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); }, [transactions]); - const isSelectAllChecked = selectedItemsLength === transactions.length && transactions.length > 0; + const isEmpty = transactions.length === 0; + + const isEmptyReportSelected = isEmpty && isGroupByReports && item?.keyForList && selectedTransactions[item.keyForList]?.isSelected; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactions.length && transactions.length > 0); const isIndeterminate = selectedItemsLength > 0 && selectedItemsLength !== transactionsWithoutPendingDelete.length; const [isExpanded, setIsExpanded] = useState(false); - const isEmpty = transactions.length === 0; // Currently only the transaction report groups have transactions where the empty view makes sense const shouldDisplayEmptyView = isEmpty && isGroupByReports; const isDisabledOrEmpty = isEmpty || isDisabled; @@ -250,7 +253,7 @@ function TransactionGroupListItem({ report={groupItem as TransactionReportGroupListItemType} onSelectRow={onSelectRow} onCheckboxPress={onCheckboxPress} - isDisabled={isDisabledOrEmpty} + isDisabled={isDisabled} isFocused={isFocused} canSelectMultiple={canSelectMultiple} isSelectAllChecked={isSelectAllChecked} @@ -295,7 +298,7 @@ function TransactionGroupListItem({ } return headers[groupBy]; - }, [groupItem, onSelectRow, onCheckboxPress, isDisabledOrEmpty, isFocused, canSelectMultiple, isSelectAllChecked, isIndeterminate, groupBy]); + }, [groupItem, onSelectRow, onCheckboxPress, isDisabled, isFocused, canSelectMultiple, isSelectAllChecked, isIndeterminate, isDisabledOrEmpty, groupBy]); useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus); From 00ca266e2cfe43c9bd2b4391aa4941801940f109 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 21 Sep 2025 18:55:02 +0530 Subject: [PATCH 0085/1005] feat: added optimistic report --- src/libs/actions/IOU.ts | 107 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8e6fe31c6a721..6bc1e2b588772 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -113,7 +113,7 @@ import { isMoneyRequestAction, isReportPreviewAction, } from '@libs/ReportActionsUtils'; -import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, OptionData, TransactionDetails} from '@libs/ReportUtils'; +import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, OptimisticNewReport, OptionData, TransactionDetails} from '@libs/ReportUtils'; import { buildOptimisticActionableTrackExpenseWhisper, buildOptimisticAddCommentReportAction, @@ -133,6 +133,7 @@ import { buildOptimisticMarkedAsResolvedReportAction, buildOptimisticModifiedExpenseReportAction, buildOptimisticMoneyRequestEntities, + buildOptimisticMovedReportAction, buildOptimisticMovedTransactionAction, buildOptimisticRejectReportAction, buildOptimisticRejectReportActionComment, @@ -194,6 +195,7 @@ import { isSettled, isTestTransactionReport, isTrackExpenseReport, + populateOptimisticReportFormula, prepareOnboardingOnyxData, shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, updateReportPreview, @@ -12255,6 +12257,109 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st }); } else { rejectedToReportID = generateReportID(); + + // Create optimistic report for the rejected transaction + const optimisticReport: OptimisticNewReport = { + reportID: rejectedToReportID, + policyID: report?.policyID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: report?.ownerAccountID ?? 0, + reportName: '', + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + currency: report?.currency ?? CONST.CURRENCY.USD, + total: 0, // We're updating the report total as a common logic below + nonReimbursableTotal: 0, + parentReportID: report?.chatReportID, + lastVisibleActionCreated: DateUtils.getDBTime(), + parentReportActionID: undefined, + participants: report?.participants ?? {}, + managerID: report?.managerID, + pendingFields: {createReport: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + chatReportID: report?.chatReportID, + }; + + // Generate report name using the pattern "Expense Report #reportID" + const reportNamePattern = '{report:type} #{report:id}'; + optimisticReport.reportName = populateOptimisticReportFormula(reportNamePattern, optimisticReport, policy); + + // Create report preview action for the parent report (chat report) + const chatReport = report?.chatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`] : null; + const reportPreviewAction = buildOptimisticReportPreview( + chatReport, + optimisticReport as OnyxTypes.Report, + '', // No comment for rejected expenses + transaction, // Pass the transaction for receipt info + ); + + // Update optimistic report with parentReportActionID + optimisticReport.parentReportActionID = reportPreviewAction.reportActionID; + + // Update the report total with the transaction amount (expense reports store negative values) + optimisticReport.total = -transactionAmount; + + // Add optimistic data for the new report + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${rejectedToReportID}`, + value: optimisticReport as OnyxTypes.Report, + }); + + // Add optimistic metadata for the new report + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${rejectedToReportID}`, + value: { + isOptimisticReport: true, + }, + }); + + // Add report actions to the new optimistic report + const newReportActions: Record = {}; + + // Add the "rejected this expense" action + newReportActions[optimisticRejectReportAction.reportActionID] = optimisticRejectReportAction as OnyxTypes.ReportAction; + + // Add the rejection reason comment + newReportActions[optimisticRejectReportActionComment.reportActionID] = optimisticRejectReportActionComment as OnyxTypes.ReportAction; + + // Add a "moved this expense" action to show where it came from + const movedExpenseAction = buildOptimisticMovedReportAction( + report?.policyID ?? '', + report?.policyID ?? '', + report?.chatReportID ?? '', + reportID, + report?.reportName ?? '', + false, // isFromIOU + ) as OnyxTypes.ReportAction; + newReportActions[movedExpenseAction.reportActionID] = movedExpenseAction; + + // Add all report actions to the new report + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`, + value: newReportActions, + }); + + // Add report preview action to parent report (chat report) + if (report?.chatReportID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.chatReportID}`, + value: { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }, + }); + + // Update parent report's lastVisibleActionCreated + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`, + value: { + lastVisibleActionCreated: reportPreviewAction.created, + }, + }); + } } optimisticData.push( { From a409d56a752682a09fd22f2cb51a1a50387c25c0 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 22 Sep 2025 00:01:25 +0800 Subject: [PATCH 0086/1005] fix: type err --- src/components/Search/index.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 3fcb42eb01a50..2baec5969d5d1 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -540,17 +540,17 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } const areItemsGrouped = !!groupBy; - const flattenedItems = areItemsGrouped - ? (data as TransactionGroupListItemType[]).flatMap((item) => { + const totalSelectableItemsCount = areItemsGrouped + ? (data as TransactionGroupListItemType[]).reduce((count, item) => { // For empty reports, count the report itself as a selectable item if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item)) { - return [item]; + return count + 1; } // For regular reports, count all transactions - return item.transactions; - }) - : data; - const areAllItemsSelected = flattenedItems.length === Object.keys(selectedTransactions).length; + return count + item.transactions.length; + }, 0) + : data.length; + const areAllItemsSelected = totalSelectableItemsCount === Object.keys(selectedTransactions).length; // If the user has selected all the expenses in their view but there are more expenses matched by the search // give them the option to select all matching expenses @@ -609,7 +609,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS canChangeReport: false, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, reportID: item.reportID, - policyID: item.policyID, + policyID: item.policyID ?? '', amount: 0, convertedAmount: 0, convertedCurrency: '', @@ -830,7 +830,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } if (areItemsGrouped) { - const allSelections = (data as TransactionGroupListItemType[]).flatMap((item) => { + const allSelections: Array<[string, SelectedTransactionInfo]> = (data as TransactionGroupListItemType[]).flatMap((item) => { // Handle empty reports - select the report itself if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && item.keyForList) { return [ @@ -845,7 +845,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS canChangeReport: false, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, reportID: item.reportID, - policyID: item.policyID, + policyID: item.policyID ?? '', amount: 0, convertedAmount: 0, convertedCurrency: '', @@ -859,7 +859,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS .filter((t) => !isTransactionPendingDelete(t)) .map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)); }); - setSelectedTransactions(Object.fromEntries(allSelections), data); return; From 9644d7a7151c8029f93776a2e95690c11c3dc8bb Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 22 Sep 2025 00:40:34 +0800 Subject: [PATCH 0087/1005] fix: lint --- src/components/Search/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 2baec5969d5d1..0248c51cad642 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -609,7 +609,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS canChangeReport: false, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, reportID: item.reportID, - policyID: item.policyID ?? '', + policyID: item.policyID ?? CONST.POLICY.ID_FAKE, amount: 0, convertedAmount: 0, convertedCurrency: '', @@ -845,7 +845,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS canChangeReport: false, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, reportID: item.reportID, - policyID: item.policyID ?? '', + policyID: item.policyID ?? CONST.POLICY.ID_FAKE, amount: 0, convertedAmount: 0, convertedCurrency: '', From 2e808b98a14d4dfbae1376bdde0168df65dab931 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 22 Sep 2025 00:56:25 +0800 Subject: [PATCH 0088/1005] fix: refactor --- src/components/Search/SearchList/index.tsx | 42 +++++++++++---- src/components/Search/index.tsx | 54 +++++++++++-------- .../Search/ReportListItemHeader.tsx | 4 +- 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 450aeecdc7801..59c96a9d3b672 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -177,15 +177,35 @@ function SearchList({ } return data; }, [data, groupBy]); - const flattenedItemsWithoutPendingDelete = useMemo(() => flattenedItems.filter((t) => t?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [flattenedItems]); - - const selectedItemsLength = useMemo( - () => - flattenedItems.reduce((acc, item) => { - return item?.isSelected ? acc + 1 : acc; - }, 0), - [flattenedItems], - ); + const selectedItemsLength = useMemo(() => { + if (groupBy && isTransactionGroupListItemArray(data)) { + return data.reduce((acc, item) => { + if (item.transactions.length === 0) { + return acc + (item.isSelected ? 1 : 0); + } + + return ( + acc + + item.transactions.reduce((transactionAcc, transaction) => { + return transactionAcc + (transaction.isSelected ? 1 : 0); + }, 0) + ); + }, 0); + } + + return flattenedItems.reduce((acc, item) => { + return acc + (item?.isSelected ? 1 : 0); + }, 0); + }, [data, flattenedItems, groupBy]); + + const totalItems = useMemo(() => { + return data.reduce((acc, item) => { + if ('transactions' in item && item.transactions?.length) { + return acc + item.transactions.length; + } + return acc + 1; + }, 0); + }, [data]); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -361,7 +381,7 @@ function SearchList({ const tableHeaderVisible = (canSelectMultiple || !!SearchTableHeader) && (!groupBy || groupBy === CONST.SEARCH.GROUP_BY.REPORTS); const selectAllButtonVisible = canSelectMultiple && !SearchTableHeader; - const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === flattenedItemsWithoutPendingDelete.length; + const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === totalItems; return ( @@ -371,7 +391,7 @@ function SearchList({ 0 && selectedItemsLength !== flattenedItemsWithoutPendingDelete.length} + isIndeterminate={selectedItemsLength > 0 && selectedItemsLength !== totalItems} onPress={() => { onAllCheckboxPress(); }} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 0248c51cad642..5be9f10b18276 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -8,7 +8,14 @@ import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader, {getExpenseHeaders} from '@components/SelectionList/SearchTableHeader'; -import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type { + ReportActionListItemType, + SearchListItem, + SelectionListHandle, + TransactionGroupListItemType, + TransactionListItemType, + TransactionReportGroupListItemType, +} from '@components/SelectionList/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; import useLocalize from '@hooks/useLocalize'; @@ -110,6 +117,26 @@ function mapTransactionItemToSelectedEntry( ]; } +function mapEmptyReportToSelectedEntry(item: TransactionReportGroupListItemType): [string, SelectedTransactionInfo] { + return [ + item.keyForList ?? '', + { + isSelected: true, + canDelete: true, + canHold: false, + isHeld: false, + canUnhold: false, + canChangeReport: false, + action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, + reportID: item.reportID, + policyID: item.policyID ?? CONST.POLICY.ID_FAKE, + amount: 0, + convertedAmount: 0, + convertedCurrency: '', + }, + ]; +} + function mapToTransactionItemWithAdditionalInfo( item: TransactionListItemType, selectedTransactions: SelectedTransactions, @@ -146,8 +173,9 @@ function mapToItemWithAdditionalInfo(item: SearchListItem, selectedTransactions: mapToTransactionItemWithAdditionalInfo(transaction, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight, hash), ), isSelected: - item?.transactions?.length > 0 && - item.transactions?.filter((t) => !isTransactionPendingDelete(t)).every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple), + item?.transactions?.length > 0 + ? item.transactions?.filter((t) => !isTransactionPendingDelete(t)).every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple) + : !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple), hash, }; } @@ -833,25 +861,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const allSelections: Array<[string, SelectedTransactionInfo]> = (data as TransactionGroupListItemType[]).flatMap((item) => { // Handle empty reports - select the report itself if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && item.keyForList) { - return [ - [ - item.keyForList, - { - isSelected: true, - canDelete: true, - canHold: false, - isHeld: false, - canUnhold: false, - canChangeReport: false, - action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, - reportID: item.reportID, - policyID: item.policyID ?? CONST.POLICY.ID_FAKE, - amount: 0, - convertedAmount: 0, - convertedCurrency: '', - }, - ], - ]; + return [mapEmptyReportToSelectedEntry(item)]; } // Handle regular reports with transactions diff --git a/src/components/SelectionList/Search/ReportListItemHeader.tsx b/src/components/SelectionList/Search/ReportListItemHeader.tsx index 42d26faf95449..4a4c5436e7f55 100644 --- a/src/components/SelectionList/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionList/Search/ReportListItemHeader.tsx @@ -115,8 +115,8 @@ function HeaderFirstRow({ disabled={!!isDisabled} accessibilityLabel={reportItem.text ?? ''} shouldStopMouseDownPropagation - style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), reportItem.isDisabledCheckbox && styles.cursorDisabled]} - /> + style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), reportItem.isDisabledCheckbox && styles.cursorDisabled]} + /> )} Date: Mon, 22 Sep 2025 01:01:06 +0800 Subject: [PATCH 0089/1005] chore: reduce duplicated code --- src/components/Search/index.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 5be9f10b18276..9ff7ef8a7e683 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -624,24 +624,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } - // Select the empty report + const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(item); setSelectedTransactions( { ...selectedTransactions, - [reportKey]: { - isSelected: true, - canDelete: true, - canHold: false, - isHeld: false, - canUnhold: false, - canChangeReport: false, - action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, - reportID: item.reportID, - policyID: item.policyID ?? CONST.POLICY.ID_FAKE, - amount: 0, - convertedAmount: 0, - convertedCurrency: '', - }, + [reportKey]: emptyReportSelection, }, data, ); From 1e05e52b7e395639cb5ece1054e977870ec1762c Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 22 Sep 2025 01:14:16 +0800 Subject: [PATCH 0090/1005] feat: support delete empty report --- src/libs/actions/Search.ts | 44 ++++++++++++++++++++++++++++++++- src/pages/Search/SearchPage.tsx | 13 +++++----- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index ead77cd4fd5c9..bf7b6f9fffe6d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {FormOnyxValues} from '@components/Form/types'; -import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; +import type {PaymentData, SearchQueryJSON, SelectedTransactionInfo} from '@components/Search/types'; import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/SelectionList/types'; import * as API from '@libs/API'; import type {ExportSearchItemsToCSVParams, ExportSearchWithTemplateParams, ReportExportParams, SubmitReportParams} from '@libs/API/parameters'; @@ -28,6 +28,7 @@ import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type Nullable from '@src/types/utils/Nullable'; +import {deleteAppReport} from './Report'; function handleActionButtonPress( hash: number, @@ -516,6 +517,46 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, failureData, finallyData}); } +function deleteSelectedItemsOnSearch(hash: number, selectedTransactions: Record) { + const transactionIDList: string[] = []; + const reportIDList: string[] = []; + + Object.keys(selectedTransactions).forEach((key) => { + const selectedItem = selectedTransactions[key]; + if (selectedItem.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === selectedItem.reportID) { + reportIDList.push(selectedItem.reportID); + } else { + transactionIDList.push(key); + } + }); + + if (transactionIDList.length > 0) { + deleteMoneyRequestOnSearch(hash, transactionIDList); + } + + if (reportIDList.length > 0) { + reportIDList.forEach((reportID) => { + deleteAppReport(reportID); + }); + + const {optimisticData: loadingOptimisticData} = getOnyxLoadingData(hash); + const optimisticData: OnyxUpdate[] = [ + ...loadingOptimisticData, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: Object.fromEntries( + reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]), + ) as Partial, + }, + }, + ]; + + Onyx.update(optimisticData); + } +} + type Params = Record; function exportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDList}: ExportSearchItemsToCSVParams, onDownloadFailed: () => void) { @@ -605,6 +646,7 @@ export { search, updateSearchResultsWithTransactionThreadReportID, deleteMoneyRequestOnSearch, + deleteSelectedItemsOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 6307ac6628135..c8340853574ca 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -32,7 +32,7 @@ import {confirmReadyToOpenApp} from '@libs/actions/App'; import {searchInServer} from '@libs/actions/Report'; import { approveMoneyRequestOnSearch, - deleteMoneyRequestOnSearch, + deleteSelectedItemsOnSearch, exportSearchItemsToCSV, getLastPolicyPaymentMethod, payMoneyRequestOnSearch, @@ -529,17 +529,18 @@ function SearchPage({route}: SearchPageProps) { areAllMatchingItemsSelected, isOffline, selectedReports, + activePolicy, queryJSON, + integrationsExportTemplates, + csvExportLayouts, clearSelectedTransactions, + beginExportWithTemplate, + policies, lastPaymentMethods, theme.icon, styles.colorMuted, styles.fontWeightNormal, styles.textWrap, - beginExportWithTemplate, - integrationsExportTemplates, - csvExportLayouts, - policies, ]); const handleDeleteExpenses = () => { @@ -552,7 +553,7 @@ function SearchPage({route}: SearchPageProps) { // Translations copy for delete modal depends on amount of selected items, // We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural InteractionManager.runAfterInteractions(() => { - deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); + deleteSelectedItemsOnSearch(hash, selectedTransactions); clearSelectedTransactions(); }); }; From 9065324757ad5ff8be52e9714d79858f90ff562b Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 22 Sep 2025 01:18:59 +0800 Subject: [PATCH 0091/1005] chore: optimize --- src/libs/actions/Search.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index bf7b6f9fffe6d..9143aeea30119 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -538,22 +538,6 @@ function deleteSelectedItemsOnSearch(hash: number, selectedTransactions: Record< reportIDList.forEach((reportID) => { deleteAppReport(reportID); }); - - const {optimisticData: loadingOptimisticData} = getOnyxLoadingData(hash); - const optimisticData: OnyxUpdate[] = [ - ...loadingOptimisticData, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: Object.fromEntries( - reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]), - ) as Partial, - }, - }, - ]; - - Onyx.update(optimisticData); } } From 6348dfcd93aea9ccd1de90fde6bf99e2ac0a157f Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Mon, 22 Sep 2025 01:07:49 +0530 Subject: [PATCH 0092/1005] refactor: remove type cast --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 6bc1e2b588772..004a61b6c3e00 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12331,7 +12331,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st reportID, report?.reportName ?? '', false, // isFromIOU - ) as OnyxTypes.ReportAction; + ); newReportActions[movedExpenseAction.reportActionID] = movedExpenseAction; // Add all report actions to the new report From f8c84ea0f9e48e403bc602ee43dcf698283170e0 Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Wed, 24 Sep 2025 15:17:16 +0200 Subject: [PATCH 0093/1005] refactor: isolates clearPolicyTagListErrors from Onyx.connect data --- src/libs/actions/Policy/Tag.ts | 10 +- .../tags/WorkspaceTagsSettingsPage.tsx | 2 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 2 +- tests/actions/PolicyTagTest.ts | 199 +++++++++++++++++- 4 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index e16f03f7e3c1e..8691f07d603a3 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -506,8 +506,14 @@ function clearPolicyTagListErrorField(policyID: string, tagListIndex: number, er }); } -function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { - const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(tagListIndex); +type ClearPolicyTagListErrorsProps = { + policyID: string; + tagListIndex: number; + policyTags: OnyxEntry; +}; + +function clearPolicyTagListErrors({policyID, tagListIndex, policyTags}: ClearPolicyTagListErrorsProps) { + const policyTag = PolicyUtils.getTagLists(policyTags ?? {})?.at(tagListIndex); if (!policyTag) { return; diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 5e0d1fe9c7ae3..2e8125481120b 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -73,7 +73,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { {!isMultiLevelTags && ( clearPolicyTagListErrors(policyID, policyTagLists.at(0)?.orderWeight ?? 0)} + onClose={() => clearPolicyTagListErrors({policyID, tagListIndex: policyTagLists.at(0)?.orderWeight ?? 0, policyTags})} pendingAction={policyTags?.[policyTagLists.at(0)?.name ?? '']?.pendingAction} errorRowStyles={styles.mh5} > diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 37bc66e96b148..2517ed1e52bda 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -386,7 +386,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { )} clearPolicyTagListErrors(policyID, currentPolicyTag.orderWeight)} + onClose={() => clearPolicyTagListErrors({policyID, tagListIndex: currentPolicyTag.orderWeight, policyTags})} pendingAction={currentPolicyTag.pendingAction} errorRowStyles={styles.mh5} > diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 0093cda730239..14ed8a6f5e27a 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -2,7 +2,16 @@ import {renderHook, waitFor} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; -import {clearPolicyTagErrors, createPolicyTag, deletePolicyTags, renamePolicyTag, renamePolicyTagList, setPolicyRequiresTag, setWorkspaceTagEnabled} from '@libs/actions/Policy/Tag'; +import { + clearPolicyTagErrors, + clearPolicyTagListErrors, + createPolicyTag, + deletePolicyTags, + renamePolicyTag, + renamePolicyTagList, + setPolicyRequiresTag, + setWorkspaceTagEnabled, +} from '@libs/actions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, PolicyTags} from '@src/types/onyx'; @@ -823,6 +832,194 @@ describe('actions/Policy', () => { }); }); + describe('ClearPolicyTagListErrors', () => { + it('should clear errors for a tag list', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + + // Add errors to the tag list + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + errors: {field1: 'Error on tag list'}, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Verify that errors are cleared from the tag list + expect(updatedPolicyTags?.[tagListName]).toBeDefined(); + expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); + // Other properties should remain unchanged + expect(updatedPolicyTags?.[tagListName]?.name).toBe(tagListName); + expect(updatedPolicyTags?.[tagListName]?.orderWeight).toBe(0); + expect(Object.keys(updatedPolicyTags?.[tagListName]?.tags ?? {}).length).toBe(2); + }); + + it('should not modify Onyx data when tag list does not exist at given index', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + // Try to clear errors for a non-existent tag list (invalid index) + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 99, policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // The policy tags should remain unchanged + expect(updatedPolicyTags).toEqual(fakePolicyTags); + }); + + it('should not modify Onyx data when tag list name is empty', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + + // Remove the name property from the tag list + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + name: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // The policy tags should remain unchanged + expect(updatedPolicyTags).toEqual(fakePolicyTags); + }); + + it('should clear multiple errors from a tag list', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 3); + + // Add multiple errors to the tag list + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + errors: { + field1: 'Error 1', + field2: 'Error 2', + field3: 'Error 3', + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Verify that all errors are cleared + expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); + }); + + it('should handle multiple tag lists correctly', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName1 = 'Tag list 1'; + const tagListName2 = 'Tag list 2'; + + const fakePolicyTags: PolicyTagLists = { + [tagListName1]: { + name: tagListName1, + orderWeight: 0, + required: false, + tags: { + tag1: {name: 'tag1', enabled: true}, + }, + errors: {field: 'Error on list 1'}, + }, + [tagListName2]: { + name: tagListName2, + orderWeight: 1, + required: false, + tags: { + tag2: {name: 'tag2', enabled: true}, + }, + errors: {field: 'Error on list 2'}, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + // Clear errors only for the second tag list + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 1, policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Verify that only the second list has errors cleared + expect(updatedPolicyTags?.[tagListName1]?.errors).toEqual({field: 'Error on list 1'}); + expect(updatedPolicyTags?.[tagListName2]?.errors).toBeUndefined(); + }); + + it('should work with data from useOnyx hook', async () => { + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + + // Add errors to the tag list + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + errors: {field: 'Test error'}, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); + + await waitFor(() => { + expect(result.current[0]).toBeDefined(); + }); + + clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: result.current[0]}); + await waitForBatchedUpdates(); + + // Verify errors are cleared + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + expect(updatedPolicyTags?.[tagListName]).toBeDefined(); + expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); + expect(updatedPolicyTags?.[tagListName]?.name).toBe(tagListName); + expect(updatedPolicyTags?.[tagListName]?.orderWeight).toBe(0); + }); + }); + describe('ClearPolicyTagErrors', () => { it('should clear errors for a tag', async () => { const fakePolicy = createRandomPolicy(0); From 9d210880283eab2e6f9392a63ccee7cc5a89f65e Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 25 Sep 2025 02:26:35 +1000 Subject: [PATCH 0094/1005] fix: offline transaction mapping --- src/libs/actions/IOU.ts | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8e1926a84ba5b..68eaf4072e913 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12065,6 +12065,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st const reportAction = getIOUActionForReportID(reportID, transactionID); const childReportID = reportAction?.childReportID; + const reportOwnerLogin = allPersonalDetails[report?.ownerAccountID ?? 0]?.login ?? ''; let movedToReport; let rejectedToReportID; @@ -12290,6 +12291,8 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st optimisticReport as OnyxTypes.Report, '', // No comment for rejected expenses transaction, // Pass the transaction for receipt info + rejectedToReportID, // childReportID + undefined // reportPreviewReportActionID - will be added when backend supports it ); // Update optimistic report with parentReportActionID @@ -12302,7 +12305,12 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st optimisticData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${rejectedToReportID}`, - value: optimisticReport as OnyxTypes.Report, + value: { + ...optimisticReport, + parentReportID: report?.chatReportID, + parentReportActionID: reportPreviewAction.reportActionID, + chatReportID: report?.chatReportID, + } as OnyxTypes.Report, }); // Add optimistic metadata for the new report @@ -12317,6 +12325,25 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st // Add report actions to the new optimistic report const newReportActions: Record = {}; + // Add CREATED action for the new report (matches online response) + const createdAction = buildOptimisticCreatedReportAction( + reportOwnerLogin, + DateUtils.getDBTime() + ); + newReportActions[createdAction.reportActionID] = createdAction; + + // Add IOU action for the moved transaction (matches online response) + const iouAction = buildOptimisticIOUReportAction({ + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: -transactionAmount, + currency: report?.currency ?? CONST.CURRENCY.USD, + comment: typeof transaction?.comment === 'string' ? transaction.comment : '', + participants: [{accountID: report?.ownerAccountID ?? 0}, {accountID: 0}], + transactionID, + iouReportID: rejectedToReportID, + }); + newReportActions[iouAction.reportActionID] = iouAction; + // Add the "rejected this expense" action newReportActions[optimisticRejectReportAction.reportActionID] = optimisticRejectReportAction as OnyxTypes.ReportAction; @@ -12341,6 +12368,33 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st value: newReportActions, }); + + // Add personal details for participants (matches online response) + const optimisticPersonalDetails: Record = {}; + Object.keys(optimisticReport.participants ?? {}).forEach(accountID => { + const personalDetail = allPersonalDetails[accountID]; + if (personalDetail) { + optimisticPersonalDetails[accountID] = personalDetail; + } + }); + + if (Object.keys(optimisticPersonalDetails).length > 0) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticPersonalDetails, + }); + } + + // Add report name value pairs for proper linking (matches online response) + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${rejectedToReportID}`, + value: { + parentReportID: report?.chatReportID, + }, + }); + // Add report preview action to parent report (chat report) if (report?.chatReportID) { optimisticData.push({ From 401fd6daa391e2c59ef554939d860ee2d851996c Mon Sep 17 00:00:00 2001 From: Mohammad Jafarinejad <71210799+mohammadjafarinejad@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:59:05 +0400 Subject: [PATCH 0095/1005] feat: enhance report formula part computation with reimbursable amount calculation --- src/libs/Formula.ts | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 3d4ad9ec027a3..ad55269db1d5c 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import type {Policy, Report, Transaction} from '@src/types/onyx'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; -import {getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils'; +import {getReportTransactions} from './ReportUtils'; import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { @@ -248,7 +248,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'reimbursable': - return formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, getCurrencySymbol(report.currency ?? '') ?? report.currency); + return formatAmount(getReportReimbursableAmount(report.reportID, context), getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': return report.currency ?? ''; case 'policyname': @@ -416,6 +416,47 @@ function formatAmount(amount: number | undefined, currency: string | undefined): return formattedAmount; } +/** + * Get all transactions for a report, including any context transaction. + * Updates an existing transaction if it matches the context or adds it if new. + */ +function getAllReportTransactionsWithContext(reportID: string, context?: FormulaContext): Transaction[] { + const transactions = [...getReportTransactions(reportID)]; + const contextTransaction = context?.transaction; + + if (contextTransaction?.transactionID && contextTransaction.reportID === reportID) { + const transactionIndex = transactions.findIndex((transaction) => transaction?.transactionID === contextTransaction.transactionID); + if (transactionIndex >= 0) { + transactions[transactionIndex] = contextTransaction; + } else { + transactions.push(contextTransaction); + } + } + + return transactions; +} + +/** + * Calculate the reimbursable amount for a report, similar to getMoneyRequestSpendBreakdown + */ +function getReportReimbursableAmount(reportID: string, context?: FormulaContext): number { + const transactions = getAllReportTransactionsWithContext(reportID, context); + + let total = 0; + let nonReimbursable = 0; + + transactions.forEach((transaction) => { + const amount = Math.abs(transaction.amount ?? 0); + total += amount; + if (!transaction.reimbursable) { + nonReimbursable += amount; + } + }); + + const reimbursable = total - nonReimbursable; + return Math.abs(reimbursable); // Return positive value for display +} + /** * Get the date of the oldest report action for a given report */ From 4eedf01c52502f60ba95755ead7d74a9e23c6e6e Mon Sep 17 00:00:00 2001 From: Mohammad Jafarinejad <71210799+mohammadjafarinejad@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:41:46 +0400 Subject: [PATCH 0096/1005] feat: add tests for reimbursable amounts in report computation --- tests/unit/FormulaTest.ts | 78 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 1298f0414b35d..163c6cad4b425 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -99,7 +99,7 @@ describe('CustomFormula', () => { }); describe('compute()', () => { - const mockContext: FormulaContext = { + let mockContext: FormulaContext = { report: { reportID: '123', reportName: '', @@ -232,6 +232,82 @@ describe('CustomFormula', () => { const result = compute('Report with type after 4 spaces {report:type}-and no space after computed part', mockContext); expect(result).toBe('Report with type after 4 spaces Expense Report-and no space after computed part'); }); + + describe('report:reimbursable', () => { + let mockedTransactions: Transaction[] = []; + + beforeEach(() => { + mockContext = { + report: {reportID: '123', currency: 'USD'} as Report, + policy: null as unknown as Policy, + }; + + jest.clearAllMocks(); + mockReportUtils.getReportTransactions.mockImplementation(() => mockedTransactions); + }); + + test('sums reimbursable amounts across transactions', () => { + mockedTransactions = [ + {transactionID: 'trans1', amount: 10000, reimbursable: true, merchant: 'A'}, + {transactionID: 'trans2', amount: 5000, reimbursable: false, merchant: 'B'}, + {transactionID: 'trans3', amount: 2500, reimbursable: true, merchant: 'C'}, + ] as Transaction[]; + + expect(compute('{report:reimbursable}', mockContext)).toBe('$125.00'); + }); + + test('uses context transaction to override reimbursable flag', () => { + mockedTransactions = [ + {transactionID: 'trans1', amount: 10000, reimbursable: true, merchant: 'A'}, + {transactionID: 'trans2', amount: 5000, reimbursable: false, merchant: 'B'}, + ] as Transaction[]; + + mockContext = { + report: {reportID: '123', currency: 'USD'} as Report, + policy: null as unknown as Policy, + transaction: {transactionID: 'x2', reportID: '123', amount: 5000, reimbursable: true, merchant: 'B'} as Transaction, + }; + + expect(compute('{report:reimbursable}', mockContext)).toBe('$150.00'); + }); + + test('includes new context transaction not in list', () => { + mockedTransactions = [{transactionID: 'trans1', amount: 5000, reimbursable: true, merchant: 'A'}] as Transaction[]; + + mockContext = { + report: {reportID: '123', currency: 'USD'} as Report, + policy: null as unknown as Policy, + transaction: {transactionID: 'trans2', reportID: '123', amount: 2500, reimbursable: true, merchant: 'B'} as Transaction, + }; + + expect(compute('{report:reimbursable}', mockContext)).toBe('$75.00'); + }); + + test('returns $0.00 when no reimbursable transactions', () => { + mockedTransactions = [{transactionID: 'trans1', amount: 10000, reimbursable: false, merchant: 'A'}] as Transaction[]; + expect(compute('{report:reimbursable}', mockContext)).toBe('$0.00'); + }); + + test('returns $0.00 when there are no transactions', () => { + mockedTransactions = []; + expect(compute('{report:reimbursable}', mockContext)).toBe('$0.00'); + }); + + test('handles undefined amounts', () => { + mockedTransactions = [ + {transactionID: 'trans1', amount: undefined, reimbursable: true, merchant: 'A'}, + {transactionID: 'trans2', amount: 5000, reimbursable: true, merchant: 'B'}, + ] as Transaction[]; + + expect(compute('{report:reimbursable}', mockContext)).toBe('$50.00'); + }); + + test('calls getReportTransactions with reportID', () => { + mockedTransactions = []; + compute('{report:reimbursable}', mockContext); + expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('123'); + }); + }); }); describe('Edge Cases', () => { From 543ed1de369238b4a2847f20bdaa41d40603654a Mon Sep 17 00:00:00 2001 From: Olgierd Date: Thu, 25 Sep 2025 19:19:06 +0200 Subject: [PATCH 0097/1005] Removal of backTo --- src/ROUTES.ts | 41 +++++++++++++++---- src/SCREENS.ts | 7 ++++ src/components/SettlementButton/index.tsx | 34 ++++++++++++++- .../ModalStackNavigators/index.tsx | 11 +++++ .../Navigators/RightModalNavigator.tsx | 4 ++ src/libs/Navigation/linkingConfig/config.ts | 10 +++++ src/libs/Navigation/types.ts | 26 ++++++++++++ ...rchMoneyRequestReportVerifyAccountPage.tsx | 14 +++++++ .../Search/SearchReportVerifyAccountPage.tsx | 14 +++++++ .../Search/SearchRootVerifyAccountPage.tsx | 9 ++++ .../home/report/ReportVerifyAccountPage.tsx | 14 +++++++ .../MoneyRequestCreateVerifyAccountPage.tsx | 14 +++++++ ...questStepConfirmationVerifyAccountPage.tsx | 18 ++++++++ 13 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx create mode 100644 src/pages/Search/SearchReportVerifyAccountPage.tsx create mode 100644 src/pages/Search/SearchRootVerifyAccountPage.tsx create mode 100644 src/pages/home/report/ReportVerifyAccountPage.tsx create mode 100644 src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx create mode 100644 src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fed1194852e3a..b8bb5f1892ef0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,6 +60,7 @@ const ROUTES = { return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const; }, }, + SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`, SEARCH_SAVED_SEARCH_RENAME: { route: 'search/saved-search/rename', getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const, @@ -83,6 +84,10 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + SEARCH_REPORT_VERIFY_ACCOUNT: { + route: `search/view/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}`, + }, SEARCH_MONEY_REQUEST_REPORT: { route: 'search/r/:reportID', getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { @@ -92,6 +97,10 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: { + route: `search/r/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/r/${reportID}/${VERIFY_ACCOUNT}`, + }, SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: { route: 'search/r/:reportID/hold', getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { @@ -493,6 +502,10 @@ const ROUTES = { return getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo); }, }, + REPORT_VERIFY_ACCOUNT: { + route: `r/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}`, + }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -691,8 +704,15 @@ const ROUTES = { }, MONEY_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID/:backToReport?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => - `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport ?? ''}` as const, + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => { + const optionalRoutePart = backToReport !== undefined ? `/${backToReport}` : ''; + return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}${optionalRoutePart}` as const; + }, + }, + MONEY_REQUEST_CREATE_VERIFY_ACCOUNT: { + route: `:action/:iouType/start/:transactionID/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => + `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${VERIFY_ACCOUNT}`, }, MONEY_REQUEST_STEP_SEND_FROM: { route: 'create/:iouType/from/:transactionID/:reportID', @@ -708,12 +728,19 @@ const ROUTES = { }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID/:backToReport?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => { + let optionalRoutePart = ''; + if (backToReport !== undefined || participantsAutoAssigned !== undefined) { + optionalRoutePart = `/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`; + } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam( - `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`, - backTo, - ), + return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}`, backTo); + }, + }, + MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT: { + route: `:action/:iouType/confirmation/:transactionID/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => + `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${VERIFY_ACCOUNT}`, }, MONEY_REQUEST_STEP_AMOUNT: { route: ':action/:iouType/amount/:transactionID/:reportID/:reportActionID?/:pageIndex?/:backToReport?', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b4baa0ef4ab97..fd08bc3a54e8f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -40,9 +40,12 @@ const SCREENS = { }, SEARCH: { ROOT: 'Search_Root', + ROOT_VERIFY_ACCOUNT: 'Search_Root_Verify_Account', MONEY_REQUEST_REPORT: 'Search_Money_Request_Report', + MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: 'Search_Money_Request_Report_Verify_Account', MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: 'Search_Money_Request_Report_Hold_Transactions', REPORT_RHP: 'Search_Report_RHP', + REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account', ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', ADVANCED_FILTERS_GROUP_BY_RHP: 'Search_Advanced_Filters_GroupBy_RHP', @@ -245,6 +248,7 @@ const SCREENS = { ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense', SCHEDULE_CALL: 'ScheduleCall', REPORT_CHANGE_APPROVER: 'Report_Change_Approver', + REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', MERGE_TRANSACTION: 'MergeTransaction', }, PUBLIC_CONSOLE_DEBUG: 'Console_Debug', @@ -257,9 +261,11 @@ const SCREENS = { MONEY_REQUEST: { CREATE: 'Money_Request_Create', + CREATE_VERIFY_ACCOUNT: 'Create_Verify_Account', HOLD: 'Money_Request_Hold_Reason', REJECT: 'Money_Request_Reject_Reason', STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', + STEP_CONFIRMATION_VERIFY_ACCOUNT: 'Money_Request_Step_Confirmation_Verify_Account', START: 'Money_Request_Start', STEP_UPGRADE: 'Money_Request_Step_Upgrade', STEP_AMOUNT: 'Money_Request_Step_Amount', @@ -763,6 +769,7 @@ const SCREENS = { REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info', REFERRAL_DETAILS: 'Referral_Details', + REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', SHARE: { ROOT: 'Share_Root', diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index bd5d6125bf27a..a6e0184b09e6a 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -375,8 +375,38 @@ function SettlementButton({ const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute())); - return; + const activeRoute = Navigation.getActiveRoute(); + + if (activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''}))) { + Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT); + return; + } + if (reportID && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID}))) { + Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + return; + } + if (reportID && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}))) { + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + return; + } + if (activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID))) { + Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)); + return; + } + if (reportID && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID))) { + Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + return; + } + if (activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID))) { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), + ); + return; + } + if (activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID))) { + Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)); + return; + } } if (policy && shouldRestrictUserBillableActions(policy.id)) { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 46bbfe11fd520..e20f7d3b5b7c3 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -32,6 +32,7 @@ import type { ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, ReportSettingsNavigatorParamList, + ReportVerifyAccountNavigatorParamList, RoomMembersNavigatorParamList, ScheduleCallParamList, SearchAdvancedFiltersParamList, @@ -136,7 +137,9 @@ function createModalStackNavigator(screens: Scr const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.MONEY_REQUEST.START]: () => require('../../../../pages/iou/request/IOURequestRedirectToStartPage').default, [SCREENS.MONEY_REQUEST.CREATE]: () => require('../../../../pages/iou/request/IOURequestStartPage').default, + [SCREENS.MONEY_REQUEST.CREATE_VERIFY_ACCOUNT]: () => require('../../../../pages/iou/request/MoneyRequestCreateVerifyAccountPage').default, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: () => require('../../../../pages/iou/request/step/IOURequestStepConfirmation').default, + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: () => require('../../../../pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage').default, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepAmount').default, [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxAmountPage').default, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxRatePage').default, @@ -244,6 +247,10 @@ const TaskModalStackNavigator = createModalStackNavigator require('../../../../pages/tasks/TaskAssigneeSelectorModal').default, }); +const ReportVerifyAccountModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ReportVerifyAccountPage').default, +}); + const ReportDescriptionModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_DESCRIPTION_ROOT]: () => require('../../../../pages/ReportDescriptionPage').default, }); @@ -805,6 +812,9 @@ const MergeTransactionStackNavigator = createModalStackNavigator({ [SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default, + [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchRootVerifyAccountPage').default, + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchReportVerifyAccountPage').default, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchMoneyRequestReportVerifyAccountPage').default, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default, @@ -921,6 +931,7 @@ export { DomainCardModalStackNavigator, SplitDetailsModalStackNavigator, TaskModalStackNavigator, + ReportVerifyAccountModalStackNavigator, WalletStatementStackNavigator, TransactionDuplicateStackNavigator, SearchReportModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index f4a7a441a62f9..5336a9c473635 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -125,6 +125,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION} component={ModalStackNavigators.ReportDescriptionModalStackNavigator} /> + ['config'] = { }, }, }, + [SCREENS.MONEY_REQUEST.CREATE_VERIFY_ACCOUNT]: ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.route, [SCREENS.MONEY_REQUEST.DISTANCE_CREATE]: { path: ROUTES.DISTANCE_REQUEST_CREATE.route, exact: true, @@ -1345,6 +1346,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: ROUTES.MONEY_REQUEST_STEP_CURRENCY.route, [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, @@ -1484,6 +1486,11 @@ const config: LinkingOptions['config'] = { [SCREENS.REFERRAL_DETAILS]: ROUTES.REFERRAL_DETAILS_MODAL.route, }, }, + [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: { + screens: { + [SCREENS.REPORT_VERIFY_ACCOUNT]: ROUTES.REPORT_VERIFY_ACCOUNT.route, + }, + }, [SCREENS.RIGHT_MODAL.TRAVEL]: { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, @@ -1506,7 +1513,10 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { screens: { + [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT, [SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route, + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.route, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c431b82e8eedb..40642af357c06 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -81,6 +81,12 @@ type ConsoleNavigatorParamList = { }; }; +type ReportVerifyAccountNavigatorParamList = { + [SCREENS.REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; +}; + type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SHARE_CODE]: undefined; [SCREENS.SETTINGS.PROFILE.PRONOUNS]: undefined; @@ -1445,6 +1451,12 @@ type MoneyRequestNavigatorParamList = { backToReport?: string; reportActionID?: string; }; + [SCREENS.MONEY_REQUEST.CREATE_VERIFY_ACCOUNT]: { + action: IOUAction; + iouType: IOUType; + transactionID: string; + reportID: string; + }; [SCREENS.MONEY_REQUEST.START]: { iouType: IOUType; reportID: string; @@ -1480,6 +1492,12 @@ type MoneyRequestNavigatorParamList = { participantsAutoAssigned?: string; backToReport?: string; }; + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: { + action: IOUAction; + iouType: IOUType; + transactionID: string; + reportID: string; + }; [SCREENS.MONEY_REQUEST.STEP_SCAN]: { action: IOUAction; iouType: IOUType; @@ -1812,6 +1830,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams; [SCREENS.SETTINGS.SHARE_CODE]: undefined; + [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_REPORT_WORKSPACE_SELECTION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_CHANGE_WORKSPACE]: NavigatorScreenParams; @@ -2249,6 +2268,12 @@ type SearchReportParamList = { reportActionID?: string; backTo?: Routes; }; + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: { /** ID of the transaction the page was opened for */ transactionID: string; @@ -2433,6 +2458,7 @@ export type { ProfileNavigatorParamList, PublicScreensParamList, ReferralDetailsNavigatorParamList, + ReportVerifyAccountNavigatorParamList, ReimbursementAccountNavigatorParamList, ReimbursementAccountEnterSignerInfoNavigatorParamList, NewReportWorkspaceSelectionNavigatorParamList, diff --git a/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..965e6d2a1b364 --- /dev/null +++ b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SearchReportParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type SearchMoneyRequestReportVerifyAccountPageParamList = PlatformStackScreenProps; + +function SearchMoneyRequestReportVerifyAccountPage({route}: SearchMoneyRequestReportVerifyAccountPageParamList) { + return ; +} + +export default SearchMoneyRequestReportVerifyAccountPage; diff --git a/src/pages/Search/SearchReportVerifyAccountPage.tsx b/src/pages/Search/SearchReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..2150f37ae25f1 --- /dev/null +++ b/src/pages/Search/SearchReportVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SearchReportParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type SearchReportVerifyAccountPageParamList = PlatformStackScreenProps; + +function SearchReportVerifyAccountPage({route}: SearchReportVerifyAccountPageParamList) { + return ; +} + +export default SearchReportVerifyAccountPage; diff --git a/src/pages/Search/SearchRootVerifyAccountPage.tsx b/src/pages/Search/SearchRootVerifyAccountPage.tsx new file mode 100644 index 0000000000000..9b6fa120d4a5a --- /dev/null +++ b/src/pages/Search/SearchRootVerifyAccountPage.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; + +function SearchRootVerifyAccountPage() { + return ; +} + +export default SearchRootVerifyAccountPage; diff --git a/src/pages/home/report/ReportVerifyAccountPage.tsx b/src/pages/home/report/ReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..a905068635389 --- /dev/null +++ b/src/pages/home/report/ReportVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReportVerifyAccountNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ReportVerifyAccountPageProps = PlatformStackScreenProps; + +function ReportVerifyAccountPage({route}: ReportVerifyAccountPageProps) { + return ; +} + +export default ReportVerifyAccountPage; diff --git a/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx b/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx new file mode 100644 index 0000000000000..9b4f66147306c --- /dev/null +++ b/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type MoneyRequestCreateVerifyAccountPageParamList = PlatformStackScreenProps; + +function MoneyRequestCreateVerifyAccountPage({route}: MoneyRequestCreateVerifyAccountPageParamList) { + return ; +} + +export default MoneyRequestCreateVerifyAccountPage; diff --git a/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx new file mode 100644 index 0000000000000..69c650c83bff7 --- /dev/null +++ b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type MoneyRequestStepConfirmationVerifyAccountPageParamList = PlatformStackScreenProps; + +function MoneyRequestStepConfirmationVerifyAccountPage({route}: MoneyRequestStepConfirmationVerifyAccountPageParamList) { + return ( + + ); +} + +export default MoneyRequestStepConfirmationVerifyAccountPage; From 0c68d1ce7e4d16abfeb6dd2d1227db391106277e Mon Sep 17 00:00:00 2001 From: Olgierd Date: Fri, 26 Sep 2025 13:41:53 +0200 Subject: [PATCH 0098/1005] Fix of RoutesValidationError in ROUTES --- src/ROUTES.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b8bb5f1892ef0..06a1ec3d87a69 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,7 +60,7 @@ const ROUTES = { return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const; }, }, - SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`, + SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}` as const, SEARCH_SAVED_SEARCH_RENAME: { route: 'search/saved-search/rename', getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const, @@ -86,7 +86,7 @@ const ROUTES = { }, SEARCH_REPORT_VERIFY_ACCOUNT: { route: `search/view/:reportID/${VERIFY_ACCOUNT}`, - getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const, }, SEARCH_MONEY_REQUEST_REPORT: { route: 'search/r/:reportID', @@ -99,7 +99,7 @@ const ROUTES = { }, SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: { route: `search/r/:reportID/${VERIFY_ACCOUNT}`, - getRoute: (reportID: string) => `search/r/${reportID}/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/r/${reportID}/${VERIFY_ACCOUNT}` as const, }, SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: { route: 'search/r/:reportID/hold', @@ -504,7 +504,7 @@ const ROUTES = { }, REPORT_VERIFY_ACCOUNT: { route: `r/:reportID/${VERIFY_ACCOUNT}`, - getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -712,7 +712,7 @@ const ROUTES = { MONEY_REQUEST_CREATE_VERIFY_ACCOUNT: { route: `:action/:iouType/start/:transactionID/:reportID/${VERIFY_ACCOUNT}`, getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => - `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${VERIFY_ACCOUNT}`, + `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${VERIFY_ACCOUNT}` as const, }, MONEY_REQUEST_STEP_SEND_FROM: { route: 'create/:iouType/from/:transactionID/:reportID', @@ -734,13 +734,13 @@ const ROUTES = { optionalRoutePart = `/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`; } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}`, backTo); + return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}` as const, backTo); }, }, MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT: { route: `:action/:iouType/confirmation/:transactionID/:reportID/${VERIFY_ACCOUNT}`, getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => - `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${VERIFY_ACCOUNT}`, + `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${VERIFY_ACCOUNT}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { route: ':action/:iouType/amount/:transactionID/:reportID/:reportActionID?/:pageIndex?/:backToReport?', From 56bbf8430921e960d612517bbd99b8b98dc96aab Mon Sep 17 00:00:00 2001 From: Olgierd Date: Fri, 26 Sep 2025 16:46:53 +0200 Subject: [PATCH 0099/1005] Fix backTo type error in SearchHoldReasonPage --- src/pages/Search/SearchHoldReasonPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 8f483429c35e3..df244d25c5ecd 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -15,7 +15,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; -function SearchHoldReasonPage({route}: PlatformStackScreenProps>) { +function SearchHoldReasonPage({ + route, +}: PlatformStackScreenProps) { const {translate} = useLocalize(); const {backTo = '', reportID} = route.params ?? {}; const context = useSearchContext(); From 5b95f734e6221b5ae84f08f0c72003825652b847 Mon Sep 17 00:00:00 2001 From: Mohammad Jafarinejad <71210799+mohammadjafarinejad@users.noreply.github.com> Date: Fri, 26 Sep 2025 23:44:40 +0400 Subject: [PATCH 0100/1005] refactor: revert to previous reimbursable amount implementation --- src/libs/Formula.ts | 45 +--------------------- tests/unit/FormulaTest.ts | 78 +-------------------------------------- 2 files changed, 3 insertions(+), 120 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index c34c2f75ae456..aec7c5bdb0fb2 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import type {Policy, Report, Transaction} from '@src/types/onyx'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; -import {getReportTransactions} from './ReportUtils'; +import {getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils'; import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { @@ -251,7 +251,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'reimbursable': - return formatAmount(getReportReimbursableAmount(report.reportID, context), getCurrencySymbol(report.currency ?? '') ?? report.currency); + return formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': return report.currency ?? ''; case 'policyname': @@ -419,47 +419,6 @@ function formatAmount(amount: number | undefined, currency: string | undefined): return formattedAmount; } -/** - * Get all transactions for a report, including any context transaction. - * Updates an existing transaction if it matches the context or adds it if new. - */ -function getAllReportTransactionsWithContext(reportID: string, context?: FormulaContext): Transaction[] { - const transactions = [...getReportTransactions(reportID)]; - const contextTransaction = context?.transaction; - - if (contextTransaction?.transactionID && contextTransaction.reportID === reportID) { - const transactionIndex = transactions.findIndex((transaction) => transaction?.transactionID === contextTransaction.transactionID); - if (transactionIndex >= 0) { - transactions[transactionIndex] = contextTransaction; - } else { - transactions.push(contextTransaction); - } - } - - return transactions; -} - -/** - * Calculate the reimbursable amount for a report, similar to getMoneyRequestSpendBreakdown - */ -function getReportReimbursableAmount(reportID: string, context?: FormulaContext): number { - const transactions = getAllReportTransactionsWithContext(reportID, context); - - let total = 0; - let nonReimbursable = 0; - - transactions.forEach((transaction) => { - const amount = Math.abs(transaction.amount ?? 0); - total += amount; - if (!transaction.reimbursable) { - nonReimbursable += amount; - } - }); - - const reimbursable = total - nonReimbursable; - return Math.abs(reimbursable); // Return positive value for display -} - /** * Get the date of the oldest report action for a given report */ diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 163c6cad4b425..1298f0414b35d 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -99,7 +99,7 @@ describe('CustomFormula', () => { }); describe('compute()', () => { - let mockContext: FormulaContext = { + const mockContext: FormulaContext = { report: { reportID: '123', reportName: '', @@ -232,82 +232,6 @@ describe('CustomFormula', () => { const result = compute('Report with type after 4 spaces {report:type}-and no space after computed part', mockContext); expect(result).toBe('Report with type after 4 spaces Expense Report-and no space after computed part'); }); - - describe('report:reimbursable', () => { - let mockedTransactions: Transaction[] = []; - - beforeEach(() => { - mockContext = { - report: {reportID: '123', currency: 'USD'} as Report, - policy: null as unknown as Policy, - }; - - jest.clearAllMocks(); - mockReportUtils.getReportTransactions.mockImplementation(() => mockedTransactions); - }); - - test('sums reimbursable amounts across transactions', () => { - mockedTransactions = [ - {transactionID: 'trans1', amount: 10000, reimbursable: true, merchant: 'A'}, - {transactionID: 'trans2', amount: 5000, reimbursable: false, merchant: 'B'}, - {transactionID: 'trans3', amount: 2500, reimbursable: true, merchant: 'C'}, - ] as Transaction[]; - - expect(compute('{report:reimbursable}', mockContext)).toBe('$125.00'); - }); - - test('uses context transaction to override reimbursable flag', () => { - mockedTransactions = [ - {transactionID: 'trans1', amount: 10000, reimbursable: true, merchant: 'A'}, - {transactionID: 'trans2', amount: 5000, reimbursable: false, merchant: 'B'}, - ] as Transaction[]; - - mockContext = { - report: {reportID: '123', currency: 'USD'} as Report, - policy: null as unknown as Policy, - transaction: {transactionID: 'x2', reportID: '123', amount: 5000, reimbursable: true, merchant: 'B'} as Transaction, - }; - - expect(compute('{report:reimbursable}', mockContext)).toBe('$150.00'); - }); - - test('includes new context transaction not in list', () => { - mockedTransactions = [{transactionID: 'trans1', amount: 5000, reimbursable: true, merchant: 'A'}] as Transaction[]; - - mockContext = { - report: {reportID: '123', currency: 'USD'} as Report, - policy: null as unknown as Policy, - transaction: {transactionID: 'trans2', reportID: '123', amount: 2500, reimbursable: true, merchant: 'B'} as Transaction, - }; - - expect(compute('{report:reimbursable}', mockContext)).toBe('$75.00'); - }); - - test('returns $0.00 when no reimbursable transactions', () => { - mockedTransactions = [{transactionID: 'trans1', amount: 10000, reimbursable: false, merchant: 'A'}] as Transaction[]; - expect(compute('{report:reimbursable}', mockContext)).toBe('$0.00'); - }); - - test('returns $0.00 when there are no transactions', () => { - mockedTransactions = []; - expect(compute('{report:reimbursable}', mockContext)).toBe('$0.00'); - }); - - test('handles undefined amounts', () => { - mockedTransactions = [ - {transactionID: 'trans1', amount: undefined, reimbursable: true, merchant: 'A'}, - {transactionID: 'trans2', amount: 5000, reimbursable: true, merchant: 'B'}, - ] as Transaction[]; - - expect(compute('{report:reimbursable}', mockContext)).toBe('$50.00'); - }); - - test('calls getReportTransactions with reportID', () => { - mockedTransactions = []; - compute('{report:reimbursable}', mockContext); - expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('123'); - }); - }); }); describe('Edge Cases', () => { From 0d5cc5942fe1795158da003d4da6d4a953802c51 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 26 Sep 2025 23:31:29 +0300 Subject: [PATCH 0101/1005] Apply suggestions from code review: Removing whitespace Co-authored-by: Carlos Alvarez --- src/hooks/usePolicyData/index.ts | 8 +------- src/libs/ReportUtils.ts | 7 +------ src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 ++ src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 2 ++ tests/perf-test/ReportUtils.perf-test.ts | 5 ----- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index eec2882531b47..ce60bced727c4 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -13,14 +13,11 @@ import type PolicyData from './types'; * @param policyID The ID of the policy to retrieve data for. * @returns An object containing policy data */ - function usePolicyData(policyID?: string): PolicyData { const policy = usePolicy(policyID); const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); - const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}, [policyID]); const [categories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}, [policyID]); - const [reports] = useOnyx( ONYXKEYS.COLLECTION.REPORT, { @@ -29,6 +26,7 @@ function usePolicyData(policyID?: string): PolicyData { if (!policyID || !allReports || !allReportsTransactionsAndViolations) { return {}; } + // Filter reports to only include those that belong to the specified policy and have associated transactions return Object.keys(allReportsTransactionsAndViolations).reduce>((acc, reportID) => { const policyReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; @@ -41,12 +39,10 @@ function usePolicyData(policyID?: string): PolicyData { }, [policyID, allReportsTransactionsAndViolations], ); - const transactionsAndViolations = useMemo(() => { if (!reports || !allReportsTransactionsAndViolations) { return {}; } - return Object.keys(reports).reduce((acc, reportID) => { if (allReportsTransactionsAndViolations[reportID]) { acc[reportID] = allReportsTransactionsAndViolations[reportID]; @@ -54,10 +50,8 @@ function usePolicyData(policyID?: string): PolicyData { return acc; }, {}); }, [reports, allReportsTransactionsAndViolations]); - return { transactionsAndViolations, - tags: tags ?? {}, categories: categories ?? {}, policy: policy as OnyxValueWithOfflineFeedback, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bc5bbca234d91..2ef38a2300c40 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1876,7 +1876,7 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { * Updates optimistic transaction violations to OnyxData for the given policy and categories onyx update. * * @param onyxData - The OnyxData object to push updates to - * @param policyData - The current policy + * @param policyData - The current policy Data * @param policyUpdate - Changed policy properties, if none pass empty object * @param policyCategoriesUpdate - Changed categories properties, if none pass empty object * @param policyTagsUpdate - Changed tag properties, if none pass empty object @@ -1944,18 +1944,13 @@ function pushTransactionViolationsOnyxData( if (isInvoiceReport(report)) { continue; } - const reportTransactionsAndViolations = policyData.transactionsAndViolations[report.reportID]; - if (isEmptyObject(reportTransactionsAndViolations)) { continue; } - const {transactions, violations} = reportTransactionsAndViolations; - for (const transaction of Object.values(transactions)) { const existingViolations = violations[transaction.transactionID]; - const optimisticViolations = ViolationsUtils.getViolationsOnyxData( transaction, existingViolations ?? [], diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index c8c8728872668..93323b7640c36 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -534,6 +534,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (policy === undefined) { return; } + // Disable the selected tags setWorkspaceTagEnabled(policyData, tagsToDisable, 0); }, @@ -550,6 +551,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (policy === undefined) { return; } + // Enable the selected tags setWorkspaceTagEnabled(policyData, tagsToEnable, 0); }, diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 2fbcd128091ea..15f26ca5a9001 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -290,6 +290,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { if (policy === undefined) { return; } + // Disable the selected tags setWorkspaceTagEnabled(policyData, tagsToDisable, orderWeight); }, @@ -306,6 +307,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { if (policy === undefined) { return; } + // Enable the selected tags setWorkspaceTagEnabled(policyData, tagsToEnable, orderWeight); }, diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 8520560e2cce5..116a7244b818b 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -265,7 +265,6 @@ describe('ReportUtils', () => { const policyTags = createRandomPolicyTags('Tags', 8); const policyCategories = createRandomPolicyCategories(8); - await Onyx.multiSet({ ...reportCollection, ...transactionCollection, @@ -274,13 +273,9 @@ describe('ReportUtils', () => { [ONYXKEYS.COLLECTION.POLICY_TAGS]: {[policy.id]: policyTags}, [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: {[policy.id]: policyCategories}, }); - await waitForBatchedUpdates(); - const {result: policyData} = renderHook(() => usePolicyData(policy.id)); - const onyxData = {optimisticData: [], failureData: []}; - await measureFunction(() => pushTransactionViolationsOnyxData(onyxData, policyData.current, policyOptimisticData)); }); From 73538c27c000cb13c65fda76a90dbb655791b4de Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 26 Sep 2025 23:35:12 +0300 Subject: [PATCH 0102/1005] Removing whitespace Co-authored-by: Carlos Alvarez --- src/libs/actions/Policy/Category.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 68989d13faaa4..dea0bf338767f 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -637,7 +637,6 @@ function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: const policy = policyData.policy; const policyID = policy.id; const policyCategoryToUpdate = policyData.categories?.[policyCategory.oldName]; - const policyCategoryApproverRule = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], policyCategory.oldName); const policyCategoryExpenseRule = CategoryUtils.getCategoryExpenseRule(policy?.rules?.expenseRules ?? [], policyCategory.oldName); const approvalRules = policy?.rules?.approvalRules ?? []; From e42a8f690060e919155831d11ad7353f785bc009 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 26 Sep 2025 20:52:04 +0000 Subject: [PATCH 0103/1005] Refactor: Remove unnecessary policy checks before category and tag operations --- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 6 ------ .../categories/CategoryRequireReceiptsOverPage.tsx | 3 --- .../workspace/categories/CategorySettingsPage.tsx | 4 +--- src/pages/workspace/categories/EditCategoryPage.tsx | 10 +++++----- .../workspace/categories/WorkspaceCategoriesPage.tsx | 9 --------- .../categories/WorkspaceCategoriesSettingsPage.tsx | 3 --- src/pages/workspace/tags/EditTagPage.tsx | 2 +- src/pages/workspace/tags/TagSettingsPage.tsx | 4 +--- src/pages/workspace/tags/WorkspaceEditTagsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 10 +--------- src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx | 3 --- src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 7 +------ 12 files changed, 11 insertions(+), 52 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 9c0a66db73e76..3b2b5c50d34bc 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -258,9 +258,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabledAction: onDisabledOrganizeSwitchPress, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { - if (policyData.policy === undefined) { - return; - } enablePolicyCategories(policyData, isEnabled, true); }, }, @@ -273,9 +270,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro pendingAction: policy?.pendingFields?.areTagsEnabled, disabledAction: onDisabledOrganizeSwitchPress, action: (isEnabled: boolean) => { - if (policyData.policy === undefined) { - return; - } enablePolicyTags(policyData, isEnabled); }, }, diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx index faf5d0602bc98..f0a0cd40a465c 100644 --- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -89,9 +89,6 @@ function CategoryRequireReceiptsOverPage({ sections={[{data: requireReceiptsOverListData}]} ListItem={RadioListItem} onSelectRow={(item) => { - if (policyData.policy === undefined) { - return; - } if (typeof item.value === 'number') { setPolicyCategoryReceiptsRequired(policyData, categoryName, item.value); } else { diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 22bdda71ca242..5ddcd4e8fcedf 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -136,9 +136,7 @@ function CategorySettingsPage({ }; const deleteCategory = () => { - if (policyData.policy !== undefined) { - deleteWorkspaceCategories(policyData, [categoryName]); - } + deleteWorkspaceCategories(policyData, [categoryName]); setDeleteCategoryConfirmModalVisible(false); navigateBack(); }; diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 7d87bbdf2377f..0ebe985766601 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -23,6 +23,7 @@ type EditCategoryPageProps = function EditCategoryPage({route}: EditCategoryPageProps) { const {backTo, policyID, categoryName: currentCategoryName} = route.params; const policyData = usePolicyData(policyID); + const {categories: policyCategories} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_EDIT; @@ -34,23 +35,22 @@ function EditCategoryPage({route}: EditCategoryPageProps) { if (!newCategoryName) { errors.categoryName = translate('workspace.categories.categoryRequiredError'); - } else if (policyData.categories?.[newCategoryName] && currentCategoryName !== newCategoryName) { + } else if (policyCategories?.[newCategoryName] && currentCategoryName !== newCategoryName) { errors.categoryName = translate('workspace.categories.existingCategoryError'); } else if ([...newCategoryName].length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units. errors.categoryName = translate('common.error.characterLimitExceedCounter', {length: [...newCategoryName].length, limit: CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH}); } - return errors; }, - [policyData.categories, currentCategoryName, translate], + [policyCategories, currentCategoryName, translate], ); const editCategory = useCallback( (values: FormOnyxValues) => { const newCategoryName = values.categoryName.trim(); // Do not call the API if the edited category name is the same as the current category name - if (policyData.policy !== undefined && currentCategoryName !== newCategoryName) { + if (currentCategoryName !== newCategoryName) { renamePolicyCategory(policyData, {oldName: currentCategoryName, newName: values.categoryName}); } @@ -93,7 +93,7 @@ function EditCategoryPage({route}: EditCategoryPageProps) { onSubmit={editCategory} validateEdit={validate} categoryName={currentCategoryName} - policyCategories={policyData.categories} + policyCategories={policyCategories} /> diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 9049e6f1113fd..1931a3721b34c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -138,9 +138,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const updateWorkspaceCategoryEnabled = useCallback( (value: boolean, categoryName: string) => { - if (policyData.policy === undefined) { - return; - } setWorkspaceCategoryEnabled(policyData, {[categoryName]: {name: categoryName, enabled: value}}); }, [policyData], @@ -360,9 +357,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return; } setSelectedCategories([]); - if (policyData.policy === undefined || Object.keys(categoriesToDisable).length === 0) { - return; - } setWorkspaceCategoryEnabled(policyData, categoriesToDisable); }, }); @@ -385,9 +379,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedCategories([]); - if (policyData.policy === undefined || Object.keys(categoriesToEnable).length === 0) { - return; - } setWorkspaceCategoryEnabled(policyData, categoriesToEnable); }, }); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 83543fa29bf49..0005460ee1cd6 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -48,9 +48,6 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet const updateWorkspaceRequiresCategory = useCallback( (value: boolean) => { - if (policyData.policy === undefined) { - return; - } setWorkspaceRequiresCategory(policyData, value); }, [policyData], diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index e45680db9369b..3a26336a39857 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -63,7 +63,7 @@ function EditTagPage({route}: EditTagPageProps) { (values: FormOnyxValues) => { const tagName = values.tagName.trim(); // Do not call the API if the edited tag name is the same as the current tag name - if (policyData.policy !== undefined && currentTagName !== tagName) { + if (currentTagName !== tagName) { renamePolicyTag(policyData, {oldName: route.params.tagName, newName: values.tagName.trim()}, route.params.orderWeight); } Keyboard.dismiss(); diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index e91d922a1585a..d30d6ced11b4a 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -77,9 +77,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { } const deleteTagAndHideModal = () => { - if (policyData.policy !== undefined) { - deletePolicyTags(policyData, [currentPolicyTag.name]); - } + deletePolicyTags(policyData, [currentPolicyTag.name]); setIsDeleteTagModalOpen(false); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }; diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index d9a877d472a0c..e0f5ff7898c69 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -64,7 +64,7 @@ function WorkspaceEditTagsPage({route}: WorkspaceEditTagsPageProps) { const updateTagListName = useCallback( (values: FormOnyxValues) => { - if (policyData.policy !== undefined && values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { + if (values[INPUT_IDS.POLICY_TAGS_NAME] !== tagListName) { renamePolicyTagList(policyData, {oldName: tagListName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, orderWeight); } goBackToTagsSettings(); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 3f6c6ba3bab41..4035cbda47bd5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -201,9 +201,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policyData.policy === undefined) { - return; - } setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, 0); }, [policyData], @@ -211,9 +208,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const updateWorkspaceRequiresTag = useCallback( (value: boolean, orderWeight: number) => { - if (policyData.policy === undefined) { - return; - } setPolicyTagsRequired(policyData, value, orderWeight); }, [policyData], @@ -376,9 +370,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const deleteTags = () => { - if (policyData.policy !== undefined) { - deletePolicyTags(policyData, selectedTags); - } + deletePolicyTags(policyData, selectedTags); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 68c3faff0dbf9..5149ba1041810 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -61,9 +61,6 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const hasEnabledOptions = hasEnabledOptionsUtil(Object.values(policyData.tags ?? {}).flatMap(({tags}) => Object.values(tags))); const updateWorkspaceRequiresTag = useCallback( (value: boolean) => { - if (policyData.policy === undefined) { - return; - } setPolicyRequiresTag(policyData, value); }, [policyData], diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 099d7f22a540b..38703a606455c 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -113,9 +113,6 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { - if (policyData.policy === undefined) { - return; - } setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, orderWeight); }, [policyData, orderWeight], @@ -214,9 +211,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const deleteTags = () => { - if (policyData.policy !== undefined && selectedTags.length > 0) { - deletePolicyTags(policyData, selectedTags); - } + deletePolicyTags(policyData, selectedTags); setIsDeleteTagsConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { From c7818be1809bfe9af086b58d48e25de55442a15e Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 26 Sep 2025 21:13:36 +0000 Subject: [PATCH 0104/1005] refactore: removed duplicate dependency 'transactions' prop from the createTransaction callback in IOURequestStepConfirmation --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 035dd4545f261..9d38098efd460 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -982,7 +982,6 @@ function IOURequestStepConfirmation({ currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, - transactions, transactionTaxCode, transactionTaxAmount, policy, From 8960b9c66fd56218175f97f7e4cbf3d13e0a0b2b Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Sun, 28 Sep 2025 00:31:35 +0700 Subject: [PATCH 0105/1005] use useSearchSelector hook and remove unused and duplicated code --- .../FilterDropdowns/UserSelectPopup.tsx | 88 ++++---------- .../SearchFiltersParticipantsSelector.tsx | 115 +++++------------- .../SelectionListWithSections/types.ts | 4 + src/hooks/useSearchSelector.base.ts | 56 +++++---- 4 files changed, 91 insertions(+), 172 deletions(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 4e1d610df97dd..6c0497cd61ec6 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -1,37 +1,31 @@ import {accountIDSelector} from '@selectors/Session'; import isEmpty from 'lodash/isEmpty'; -import React, {memo, useCallback, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {SectionListData} from 'react-native'; import Button from '@components/Button'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionListWithSections'; import UserSelectionListItem from '@components/SelectionListWithSections/Search/UserSelectionListItem'; -import type {SelectionListHandle} from '@components/SelectionListWithSections/types'; +import type {Section, SelectionListHandle} from '@components/SelectionListWithSections/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import memoize from '@libs/memoize'; -import type {Option, Section} from '@libs/OptionsListUtils'; -import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; +import {getParticipantsOption} from '@libs/OptionsListUtils'; +import type {Option} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; function getSelectedOptionData(option: Option) { - return {...option, reportID: `${option.reportID}`, selected: true}; + return {...option, reportID: `${option.reportID ?? -1}`, selected: true, isSelected: true}; } -const optionsMatch = (opt1: Option, opt2: Option) => { - // Below is just a boolean expression. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return (opt1.accountID && opt1.accountID === opt2?.accountID) || (opt1.reportID && opt1.reportID === opt2?.reportID); -}; - -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'UserSelectPopup.getValidOptions'}); +type Sections = Array>>; type UserSelectPopupProps = { /** The currently selected users */ @@ -48,14 +42,11 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) const selectionListRef = useRef(null); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {options} = useOptionsList(); const personalDetails = usePersonalDetails(); const {windowHeight} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: accountIDSelector}); const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const [searchTerm, setSearchTerm] = useState(''); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const initialSelectedOptions = useMemo(() => { return value.reduce((acc, id) => { @@ -64,7 +55,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) return acc; } - const optionData = getSelectedOptionData(participant); + const optionData = getSelectedOptionData(getParticipantsOption(participant, personalDetails)); if (optionData) { acc.push(optionData); } @@ -73,47 +64,17 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) }, []); }, [value, personalDetails]); - const [selectedOptions, setSelectedOptions] = useState(initialSelectedOptions); - - const cleanSearchTerm = searchTerm.trim().toLowerCase(); - - const selectedAccountIDs = useMemo(() => { - return new Set(selectedOptions.map((option) => option.accountID).filter(Boolean)); - }, [selectedOptions]); - - const optionsList = useMemo(() => { - return memoizedGetValidOptions( - { - reports: options.reports, - personalDetails: options.personalDetails, - }, - { - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeCurrentUser: true, - }, - ); - }, [options.reports, options.personalDetails]); - - const filteredOptions = useMemo(() => { - return filterAndOrderOptions(optionsList, cleanSearchTerm, countryCode, { - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - canInviteUser: false, - }); - }, [optionsList, cleanSearchTerm, countryCode]); + const {searchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, selectedOptionsForDisplay} = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, + initialSelected: initialSelectedOptions, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, + includeUserToInvite: false, + includeCurrentUser: true, + }); const listData = useMemo(() => { - const personalDetailList = filteredOptions.personalDetails.map((participant) => ({ - ...participant, - isSelected: selectedAccountIDs.has(participant.accountID), - })); - - const recentReportsList = filteredOptions.recentReports.map((report) => ({ - ...report, - isSelected: selectedAccountIDs.has(report.accountID), - })); - - const combined = [...personalDetailList, ...recentReportsList]; + const combined = [...selectedOptionsForDisplay, ...availableOptions.personalDetails, ...availableOptions.recentReports]; combined.sort((a, b) => { // selected items first @@ -135,10 +96,10 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) }); return combined; - }, [filteredOptions, accountID, selectedAccountIDs]); + }, [availableOptions.personalDetails, availableOptions.recentReports, selectedOptionsForDisplay, accountID]); const {sections, headerMessage} = useMemo(() => { - const newSections: Section[] = [ + const newSections: Sections = [ { title: '', data: listData, @@ -156,13 +117,11 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) }, [listData, translate]); const selectUser = useCallback( - (option: Option) => { - const isSelected = selectedOptions.some((selected) => optionsMatch(selected, option)); - - setSelectedOptions((prev) => (isSelected ? prev.filter((selected) => !optionsMatch(selected, option)) : [...prev, getSelectedOptionData(option)])); + (option: OptionData) => { + toggleSelection(option); selectionListRef?.current?.scrollToIndex(0, true); }, - [selectedOptions], + [toggleSelection], ); const applyChanges = useCallback(() => { @@ -195,6 +154,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) onSelectRow={selectUser} onChangeText={setSearchTerm} isLoadingNewOptions={isLoadingNewOptions} + showLoadingPlaceholder={!areOptionsInitialized} /> diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 287d60b5586c7..ed8387ee57085 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -1,16 +1,16 @@ import reportsSelector from '@selectors/Attributes'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionListWithSections'; import UserSelectionListItem from '@components/SelectionListWithSections/Search/UserSelectionListItem'; +import type {Sections} from '@components/SelectionListWithSections/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import useSearchSelector from '@hooks/useSearchSelector'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import memoize from '@libs/memoize'; -import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; -import type {Option, Section} from '@libs/OptionsListUtils'; +import {formatSectionsFromSearchTerm} from '@libs/OptionsListUtils'; +import type {Option} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; @@ -19,19 +19,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SearchFilterPageFooterButtons from './SearchFilterPageFooterButtons'; -const defaultListOptions = { - userToInvite: null, - recentReports: [], - personalDetails: [], - currentUserOption: null, - headerMessage: '', -}; - -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'}); - function getSelectedOptionData(option: Option): OptionData { // eslint-disable-next-line rulesdir/no-default-id-values - return {...option, selected: true, reportID: option.reportID ?? '-1'}; + return {...option, selected: true, reportID: option.reportID ?? '-1', isSelected: true}; } type SearchFiltersParticipantsSelectorProps = { @@ -43,59 +33,32 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {canBeMissing: false, initWithStoredValues: false}); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: reportsSelector}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const [selectedOptions, setSelectedOptions] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); + + const {searchTerm, setSearchTerm, availableOptions, selectedOptions, setSelectedOptions, toggleSelection, areOptionsInitialized} = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, + includeUserToInvite: true, + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, + includeRecentReports: true, + shouldInitialize: didScreenTransitionEnd, + includeCurrentUser: true, + }); + const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); - const defaultOptions = useMemo(() => { + const {sections, headerMessage} = useMemo(() => { + const newSections: Sections = []; if (!areOptionsInitialized) { - return defaultListOptions; + return {sections: [], headerMessage: undefined}; } - return memoizedGetValidOptions( - { - reports: options.reports, - personalDetails: options.personalDetails, - }, - { - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeCurrentUser: true, - }, - ); - }, [areOptionsInitialized, options.personalDetails, options.reports]); - - const unselectedOptions = useMemo(() => { - return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID))); - }, [defaultOptions, selectedOptions]); - - const chatOptions = useMemo(() => { - const filteredOptions = filterAndOrderOptions(unselectedOptions, cleanSearchTerm, countryCode, { - selectedOptions, - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, - canInviteUser: false, - }); + const chatOptions = {...availableOptions}; + const currentUserOption = chatOptions.currentUserOption; - const {currentUserOption} = unselectedOptions; - - // Ensure current user is not in personalDetails when they should be excluded if (currentUserOption) { - filteredOptions.personalDetails = filteredOptions.personalDetails.filter((detail) => detail.accountID !== currentUserOption.accountID); - } - - return filteredOptions; - }, [unselectedOptions, cleanSearchTerm, selectedOptions, countryCode]); - - const {sections, headerMessage} = useMemo(() => { - const newSections: Section[] = []; - if (!areOptionsInitialized) { - return {sections: [], headerMessage: undefined}; + chatOptions.personalDetails = chatOptions.personalDetails.filter((detail) => detail.accountID !== currentUserOption.accountID); } const formattedResults = formatSectionsFromSearchTerm( @@ -133,7 +96,10 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }); } - newSections.push(formattedResults.section); + newSections.push({ + ...formattedResults.section, + data: formattedResults.section.data.map(getSelectedOptionData), + }); newSections.push({ title: '', @@ -154,11 +120,11 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: sections: newSections, headerMessage: message, }; - }, [areOptionsInitialized, cleanSearchTerm, selectedOptions, chatOptions, personalDetails, reportAttributesDerived, translate]); + }, [areOptionsInitialized, availableOptions, cleanSearchTerm, selectedOptions, personalDetails, reportAttributesDerived, translate]); const resetChanges = useCallback(() => { setSelectedOptions([]); - }, []); + }, [setSelectedOptions]); const applyChanges = useCallback(() => { const selectedAccountIDs = selectedOptions.map((option) => (option.accountID ? option.accountID.toString() : undefined)).filter(Boolean) as string[]; @@ -191,27 +157,10 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }, [initialAccountIDs, personalDetails]); const handleParticipantSelection = useCallback( - (option: Option) => { - const foundOptionIndex = selectedOptions.findIndex((selectedOption: Option) => { - if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { - return true; - } - - if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { - return true; - } - - return false; - }); - - if (foundOptionIndex < 0) { - setSelectedOptions([...selectedOptions, getSelectedOptionData(option)]); - } else { - const newSelectedOptions = [...selectedOptions.slice(0, foundOptionIndex), ...selectedOptions.slice(foundOptionIndex + 1)]; - setSelectedOptions(newSelectedOptions); - } + (option: OptionData) => { + toggleSelection(option); }, - [selectedOptions], + [toggleSelection], ); const footerContent = useMemo( diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index d89984a74fd91..0fede868f5831 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -19,6 +19,7 @@ import type {AnimatedStyle} from 'react-native-reanimated'; import type {SearchRouterItem} from '@components/Search/SearchAutocompleteList'; import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types'; import type {ForwardedFSClassProps} from '@libs/Fullstory/types'; +import type {OptionData} from '@libs/ReportUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import type UnreportedExpenseListItem from '@pages/UnreportedExpenseListItem'; import type SpendCategorySelectorListItem from '@pages/workspace/categories/SpendCategorySelectorListItem'; @@ -573,6 +574,8 @@ type Section = { shouldShow?: boolean; }; +type Sections = Array>>; + type LoadingPlaceholderComponentProps = { shouldStyleAsTable?: boolean; fixedNumItems?: number; @@ -994,4 +997,5 @@ export type { SplitListItemType, SearchListItem, UnreportedExpenseListItemType, + Sections, }; diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index f19b75c33cd08..27158385e6803 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -32,6 +32,9 @@ type UseSearchSelectorConfig = { /** Whether to include recent reports (for getMemberInviteOptions) */ includeRecentReports?: boolean; + /** Whether to include current user */ + includeCurrentUser?: boolean; + /** Enable phone contacts integration */ enablePhoneContacts?: boolean; @@ -109,6 +112,14 @@ type UseSearchSelectorReturn = { onListEndReached: () => void; }; +const isOptionMatch = (opt1: OptionData, opt2: OptionData) => { + return ( + (opt1.accountID && opt1.accountID === opt2.accountID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison + (opt1.reportID && opt1.reportID !== '-1' && opt1.reportID === opt2.reportID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison + (opt1.login && opt1.login === opt2.login) + ); +}; + /** * Base hook that provides search functionality with selection logic for option lists. * This contains the core logic without platform-specific dependencies. @@ -126,6 +137,7 @@ function useSearchSelectorBase({ initialSelected, shouldInitialize = true, contactOptions, + includeCurrentUser = false, }: UseSearchSelectorConfig): UseSearchSelectorReturn { const {options: defaultOptions, areOptionsInitialized} = useOptionsList({ shouldInitialize, @@ -173,6 +185,7 @@ function useSearchSelectorBase({ maxElements: maxResults, searchString: computedSearchTerm, includeUserToInvite, + includeCurrentUser, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL: return getValidOptions(optionsWithContacts, { @@ -182,20 +195,27 @@ function useSearchSelectorBase({ maxElements: maxResults, includeUserToInvite, loginsToExclude: excludeLogins, + includeCurrentUser, }); default: return getEmptyOptions(); } - }, [areOptionsInitialized, optionsWithContacts, betas, computedSearchTerm, maxResults, searchContext, includeUserToInvite, excludeLogins, includeRecentReports, getValidOptionsConfig]); + }, [ + areOptionsInitialized, + searchContext, + optionsWithContacts, + betas, + computedSearchTerm, + maxResults, + includeUserToInvite, + excludeLogins, + includeRecentReports, + includeCurrentUser, + getValidOptionsConfig, + ]); const isOptionSelected = useMemo(() => { - return (option: OptionData) => - selectedOptions.some( - (selected) => - (selected.accountID && selected.accountID === option.accountID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.reportID && selected.reportID === option.reportID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.login && selected.login === option.login), - ); + return (option: OptionData) => selectedOptions.some((selected) => isOptionMatch(selected, option)); }, [selectedOptions]); const searchOptions = useMemo(() => { @@ -243,23 +263,9 @@ function useSearchSelectorBase({ return; } - const isSelected = selectedOptions.some( - (selected) => - (selected.accountID && selected.accountID === option.accountID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.reportID && selected.reportID === option.reportID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.login && selected.login === option.login), - ); - - const newSelected = isSelected - ? selectedOptions.filter( - (selected) => - !( - (selected.accountID && selected.accountID === option.accountID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.reportID && selected.reportID === option.reportID) || // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- this is boolean comparison - (selected.login && selected.login === option.login) - ), - ) - : [...selectedOptions, {...option, isSelected: true}]; + const isSelected = selectedOptions.some((selected) => isOptionMatch(selected, option)); + + const newSelected = isSelected ? selectedOptions.filter((selected) => !isOptionMatch(selected, option)) : [...selectedOptions, {...option, isSelected: true}]; setSelectedOptions(newSelected); onSelectionChange?.(newSelected); From 6994cd74c3c1d1dd8558888cfdab94fdc5451f78 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Sun, 28 Sep 2025 00:38:58 +0700 Subject: [PATCH 0106/1005] disable lint rule --- src/components/Search/FilterDropdowns/UserSelectPopup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 6c0497cd61ec6..44cbb019f5f40 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -22,6 +22,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; function getSelectedOptionData(option: Option) { + // eslint-disable-next-line rulesdir/no-default-id-values return {...option, reportID: `${option.reportID ?? -1}`, selected: true, isSelected: true}; } From 4c5a6077cf2ce6f308a25a8177aa292781ce6d83 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Sun, 28 Sep 2025 23:52:38 +0800 Subject: [PATCH 0107/1005] chore: handle pending delete case --- src/components/Search/SearchList/index.tsx | 4 +++- .../Search/TransactionGroupListItem.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 95024e2e704ce..d522d4c093af9 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -38,6 +38,7 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import navigationRef from '@libs/Navigation/navigationRef'; +import {isTransactionPendingDelete} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -201,7 +202,8 @@ function SearchList({ const totalItems = useMemo(() => { return data.reduce((acc, item) => { if ('transactions' in item && item.transactions?.length) { - return acc + item.transactions.length; + const transactions = item.transactions.filter((transaction) => !isTransactionPendingDelete(transaction)); + return acc + transactions.length; } return acc + 1; }, 0); diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx index 105e257ac4dd7..b939c7d86304b 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx @@ -126,7 +126,7 @@ function TransactionGroupListItem({ const isEmptyReportSelected = isEmpty && isGroupByReports && item?.keyForList && selectedTransactions[item.keyForList]?.isSelected; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactions.length && transactions.length > 0); + const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactionsWithoutPendingDelete.length && transactionsWithoutPendingDelete.length > 0); const isIndeterminate = selectedItemsLength > 0 && selectedItemsLength !== transactionsWithoutPendingDelete.length; const [isExpanded, setIsExpanded] = useState(false); From 3af1876ccf08c32e66499a7c43e4052fdaae9d85 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Sun, 28 Sep 2025 23:56:19 +0800 Subject: [PATCH 0108/1005] fix: revert code --- .../SelectionListWithSections/Search/ReportListItemHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index a05195134e6ce..fdc87acf221af 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -112,7 +112,7 @@ function HeaderFirstRow({ isChecked={isSelectAllChecked} isIndeterminate={isIndeterminate} containerStyle={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!reportItem.isSelected, !!reportItem.isDisabled)]} - disabled={!!isDisabled} + disabled={!!isDisabled || reportItem.isDisabledCheckbox} accessibilityLabel={reportItem.text ?? ''} shouldStopMouseDownPropagation style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), reportItem.isDisabledCheckbox && styles.cursorDisabled]} From 2eee88e432f547920863aa42c1f78bc0b913fbf2 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 29 Sep 2025 00:13:35 +0800 Subject: [PATCH 0109/1005] fix: preserve selection --- src/components/Search/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 090489a24c56b..e398e1f2a4e02 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -443,6 +443,19 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (!Object.hasOwn(transactionGroup, 'transactions') || !('transactions' in transactionGroup)) { return; } + + if (transactionGroup.transactions.length === 0 && isTransactionReportGroupListItemType(transactionGroup)) { + const reportKey = transactionGroup.keyForList; + if (reportKey && (Object.keys(selectedTransactions).includes(reportKey) || areAllMatchingItemsSelected)) { + const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(transactionGroup); + newTransactionList[reportKey] = { + ...emptyReportSelection, + isSelected: areAllMatchingItemsSelected || selectedTransactions[reportKey]?.isSelected, + }; + } + return; + } + transactionGroup.transactions.forEach((transaction) => { if (!Object.keys(selectedTransactions).includes(transaction.transactionID) && !areAllMatchingItemsSelected) { return; From c7f7e6ad7cde94ef32b43cf393e94179957b0c4e Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:57:41 +0000 Subject: [PATCH 0110/1005] Fix wrong input label --- src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx | 5 +---- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 5 +---- .../workspace/members/WorkspaceInviteMessageComponent.tsx | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 537dd3026e46a..cf356dc3c8171 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -117,9 +117,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { Navigation.goBack(); }; - const shouldShowSearchInput = policy?.employeeList; - const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; - const membersDetails = useMemo(() => { let membersList: ListItem[] = []; if (!policy?.employeeList) { @@ -219,7 +216,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { > {translate('workspace.companyCards.whoNeedsCardAssigned')} { let membersList: ListItem[] = []; if (!policy?.employeeList) { @@ -171,7 +168,7 @@ function AssigneeStep({policy, stepNames, startStepIndex}: AssigneeStepProps) { > {translate('workspace.card.issueNewCard.whoNeedsCard')} Navigation.dismissModal()); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.dismissModal); return; } From dbbe9e851bd00856a55c88aeb5e09a021ca6e514 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:27:37 +0000 Subject: [PATCH 0111/1005] fix selection list in company cards page --- .../companyCards/assignCard/AssigneeStep.tsx | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index cf356dc3c8171..1e6e47ce287d3 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -1,7 +1,6 @@ import React, {useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import SelectionList from '@components/SelectionListWithSections'; @@ -51,16 +50,11 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { const isEditing = assignCard?.isEditing; const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); - const [shouldShowError, setShouldShowError] = useState(false); - const selectMember = (assignee: ListItem) => { + const submit = (assignee: ListItem) => { + let nextStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; Keyboard.dismiss(); setSelectedMember(assignee.login ?? ''); - setShouldShowError(false); - }; - - const submit = () => { - let nextStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; if (selectedMember === assignCard?.data?.email) { setAssignCardStepAndData({ currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep, @@ -69,11 +63,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { return; } - if (!selectedMember || (!searchValue && selectedMember !== policy?.employeeList?.[selectedMember]?.email)) { - setShouldShowError(true); - return; - } - if (userToInvite?.login === selectedMember) { setAssignCardStepAndData({ currentStep: CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER, @@ -222,20 +211,9 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { sections={sections} headerMessage={headerMessage} ListItem={UserListItem} - onSelectRow={selectMember} - initiallyFocusedOptionKey={selectedMember} + onSelectRow={submit} shouldUpdateFocusedIndex addBottomSafeAreaPadding - footerContent={ - - } showLoadingPlaceholder={!areOptionsInitialized} /> From 18f38aebec6cd6734f92346d484650529432d303 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:46:42 +0000 Subject: [PATCH 0112/1005] fix existing users results --- src/libs/UseOptionsUtils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libs/UseOptionsUtils.ts b/src/libs/UseOptionsUtils.ts index 234d0c40fbb4a..8ea255dd61379 100644 --- a/src/libs/UseOptionsUtils.ts +++ b/src/libs/UseOptionsUtils.ts @@ -5,6 +5,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useOnyx from '@hooks/useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {searchInServer} from './actions/Report'; import memoize from './memoize'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from './OptionsListUtils'; @@ -27,6 +28,14 @@ function useOptions() { setIsLoading(false); }, [isLoading, optionsList.reports, optionsList.personalDetails]); + useEffect(() => { + if (!debouncedSearchValue.length) { + return; + } + + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); + const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( { From 96cfe3f98871bb42d6f011c95378f6dd781ec797 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Mon, 29 Sep 2025 17:49:14 +0200 Subject: [PATCH 0113/1005] Cleaned getRoutes code --- src/ROUTES.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 06a1ec3d87a69..a7937cf92279e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -705,8 +705,10 @@ const ROUTES = { MONEY_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => { - const optionalRoutePart = backToReport !== undefined ? `/${backToReport}` : ''; - return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}${optionalRoutePart}` as const; + if (backToReport) { + return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport}` as const; + } + return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const; }, }, MONEY_REQUEST_CREATE_VERIFY_ACCOUNT: { @@ -730,8 +732,11 @@ const ROUTES = { route: ':action/:iouType/confirmation/:transactionID/:reportID/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => { let optionalRoutePart = ''; - if (backToReport !== undefined || participantsAutoAssigned !== undefined) { - optionalRoutePart = `/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`; + if (backToReport !== undefined) { + optionalRoutePart += `/${backToReport}`; + } + if (participantsAutoAssigned !== undefined) { + optionalRoutePart += '?participantsAutoAssigned=true'; } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}` as const, backTo); From b2894ba3d060a7b9ca4023e7dc6c622adf233761 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Mon, 29 Sep 2025 18:37:37 +0200 Subject: [PATCH 0114/1005] Create SettlementButtonUtils to separate logic and reduce number of if statements --- src/components/SettlementButton/index.tsx | 35 ++----------- src/libs/SettlementButtonUtils.ts | 62 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 src/libs/SettlementButtonUtils.ts diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index a6e0184b09e6a..3116700fb84c3 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -30,6 +30,7 @@ import { isInvoiceReport as isInvoiceReportUtil, isIOUReport, } from '@libs/ReportUtils'; +import handleRouteVerification from '@libs/SettlementButtonUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {approveMoneyRequest} from '@userActions/IOU'; @@ -375,38 +376,8 @@ function SettlementButton({ const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { if (!isUserValidated) { - const activeRoute = Navigation.getActiveRoute(); - - if (activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''}))) { - Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT); - return; - } - if (reportID && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID}))) { - Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - return; - } - if (reportID && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}))) { - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - return; - } - if (activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID))) { - Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)); - return; - } - if (reportID && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID))) { - Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - return; - } - if (activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID))) { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), - ); - return; - } - if (activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID))) { - Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)); - return; - } + handleRouteVerification(reportID ?? '', chatReportID); + return; } if (policy && shouldRestrictUserBillableActions(policy.id)) { diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts new file mode 100644 index 0000000000000..6fb4797ed38b7 --- /dev/null +++ b/src/libs/SettlementButtonUtils.ts @@ -0,0 +1,62 @@ +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import Navigation from './Navigation/Navigation'; + +type RouteMapping = { + /** Condition that determines if this route mapping applies to the current active route */ + check: (activeRoute: string) => boolean; + + /** Navigates to the appropriate verification route when the check condition is met */ + navigate: () => void; +}; + +const handleRouteVerification = (reportID: string, chatReportID: string): boolean => { + const routeMappings: RouteMapping[] = [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), + }, + { + check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID}))), + navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}))), + navigate: () => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, + { + check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID))), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), + ), + }, + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + }, + ]; + + const activeRoute = Navigation.getActiveRoute(); + const matchedRoute = routeMappings.find((mapping) => mapping.check(activeRoute)); + + if (matchedRoute) { + matchedRoute.navigate(); + return true; + } + + return false; +}; + +export default handleRouteVerification; From 0f6004cfcc52f002e4d6af87d57c3c30242c52d9 Mon Sep 17 00:00:00 2001 From: gijoe0295 <153004152+gijoe0295@users.noreply.github.com> Date: Tue, 30 Sep 2025 03:40:54 +0700 Subject: [PATCH 0115/1005] fix Central pane changes to Wallet after clicking Update address in workspace chat --- src/ROUTES.ts | 4 ++++ src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 +++ .../settings/Wallet/ExpensifyCardPage.tsx | 11 ++++++----- .../settings/Wallet/WalletPage/CardDetails.tsx | 18 +++++++++++++++--- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f07e469188b04..80ed76274fc21 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -267,6 +267,10 @@ const ROUTES = { route: 'settings/card/:cardID?', getRoute: (cardID: string) => `settings/card/${cardID}` as const, }, + SETTINGS_DOMAIN_CARD_UPDATE_ADDRESS: { + route: 'settings/card/:cardID/update-address', + getRoute: (cardID: string) => `settings/card/${cardID}/update-address` as const, + }, SETTINGS_REPORT_FRAUD: { route: 'settings/wallet/card/:cardID/report-virtual-fraud', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index d5ede296b7287..05052882b7482 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -348,6 +348,7 @@ const SCREENS = { DOMAIN_CARD: { DOMAIN_CARD_DETAIL: 'Domain_Card_Detail', DOMAIN_CARD_REPORT_FRAUD: 'Domain_Card_Report_Fraud', + DOMAIN_CARD_UPDATE_ADDRESS: 'Domain_Card_Update_Address', }, SETTINGS_TAGS: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index cbc6722e0a303..05ab49168f38a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -283,6 +283,7 @@ const ExpensifyCardModalStackNavigator = createModalStackNavigator({ const DomainCardModalStackNavigator = createModalStackNavigator({ [SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage').default, [SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, + [SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default, }); const ReportParticipantsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 736b5ce1768b8..1e73ff39d5aa6 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1219,6 +1219,9 @@ const config: LinkingOptions['config'] = { [SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: { path: ROUTES.SETTINGS_DOMAIN_CARD_REPORT_FRAUD.route, }, + [SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS]: { + path: ROUTES.SETTINGS_DOMAIN_CARD_UPDATE_ADDRESS.route, + }, }, }, [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: { diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 6434b3454071a..96f4d8f4d18a6 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -64,11 +64,8 @@ function getLimitTypeTranslationKeys(limitType: ValueOf()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); @@ -243,6 +240,8 @@ function ExpensifyCardPage({ expiration={formatCardExpiration(cardsDetails[card.cardID]?.expiration ?? '')} cvv={cardsDetails[card.cardID]?.cvv} domain={domain} + cardID={card.cardID} + route={route} /> ) : ( <> @@ -300,6 +299,8 @@ function ExpensifyCardPage({ ) : ( <> diff --git a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx index 5af5265009511..c173610cff79d 100644 --- a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx +++ b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx @@ -6,10 +6,13 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getFormattedAddress} from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {PrivatePersonalDetails} from '@src/types/onyx'; const defaultPrivatePersonalDetails: PrivatePersonalDetails = { @@ -24,7 +27,7 @@ const defaultPrivatePersonalDetails: PrivatePersonalDetails = { ], }; -type CardDetailsProps = { +type CardDetailsProps = Omit, 'navigation'> & { /** Card number */ pan?: string; @@ -36,9 +39,12 @@ type CardDetailsProps = { /** Domain name */ domain: string; + + /** Card ID */ + cardID: number; }; -function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsProps) { +function CardDetails({pan = '', expiration = '', cvv = '', domain, cardID, route}: CardDetailsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); @@ -81,7 +87,13 @@ function CardDetails({pan = '', expiration = '', cvv = '', domain}: CardDetailsP /> Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS.getRoute(domain))} + onPress={() => { + if (route.name === SCREENS.SETTINGS.WALLET.DOMAIN_CARD) { + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS.getRoute(domain)); + return; + } + Navigation.navigate(ROUTES.SETTINGS_DOMAIN_CARD_UPDATE_ADDRESS.getRoute(cardID.toString())); + }} > {translate('cardPage.cardDetails.updateAddress')} From 09ef892fe33d9b217932d21717b2ab722fbf1cb2 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 30 Sep 2025 12:09:50 +0700 Subject: [PATCH 0116/1005] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 134054494d6e3..cb92446ba8837 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -131,7 +131,8 @@ export default function linkTo(navigation: NavigationContainerRef From e7565e5d6fec4eb828eca6a91151b8ca54877489 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Wed, 1 Oct 2025 00:39:36 +1000 Subject: [PATCH 0117/1005] fix:add reportPreviewReportActionID --- src/libs/API/parameters/RejectMoneyRequestParams.ts | 1 + src/libs/actions/IOU.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/API/parameters/RejectMoneyRequestParams.ts b/src/libs/API/parameters/RejectMoneyRequestParams.ts index a49c17bac363e..815c3ef38efe6 100644 --- a/src/libs/API/parameters/RejectMoneyRequestParams.ts +++ b/src/libs/API/parameters/RejectMoneyRequestParams.ts @@ -3,6 +3,7 @@ type RejectMoneyRequestParams = { reportID: string; comment: string; rejectedToReportID?: string; + reportPreviewReportActionID?: string; rejectedActionReportActionID: string; rejectedCommentReportActionID: string; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 87bfa210f7c6e..f720401c4910f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12104,6 +12104,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st let movedToReport; let rejectedToReportID; let urlToNavigateBack; + let reportPreviewAction: OnyxTypes.ReportAction | undefined; const hasMultipleExpenses = getReportTransactions(reportID).length > 1; @@ -12320,7 +12321,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st // Create report preview action for the parent report (chat report) const chatReport = report?.chatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`] : null; - const reportPreviewAction = buildOptimisticReportPreview( + reportPreviewAction = buildOptimisticReportPreview( chatReport, optimisticReport as OnyxTypes.Report, '', // No comment for rejected expenses @@ -12670,6 +12671,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st reportID, comment, rejectedToReportID, + reportPreviewReportActionID: reportPreviewAction?.reportActionID, rejectedActionReportActionID: optimisticRejectReportAction.reportActionID, rejectedCommentReportActionID: optimisticRejectReportActionComment.reportActionID, }; From 5155579e6a75c53a89a290a4b7cc74f47b304408 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 10:54:41 +0200 Subject: [PATCH 0118/1005] Warn about navigation fail --- src/libs/SettlementButtonUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 6fb4797ed38b7..96b7ae12b3a7c 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -1,5 +1,6 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import Log from './Log'; import Navigation from './Navigation/Navigation'; type RouteMapping = { @@ -10,7 +11,7 @@ type RouteMapping = { navigate: () => void; }; -const handleRouteVerification = (reportID: string, chatReportID: string): boolean => { +const handleRouteVerification = (reportID: string, chatReportID: string) => { const routeMappings: RouteMapping[] = [ { check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), @@ -53,10 +54,9 @@ const handleRouteVerification = (reportID: string, chatReportID: string): boolea if (matchedRoute) { matchedRoute.navigate(); - return true; + } else { + Log.warn('Failed to navigate to the correct path'); } - - return false; }; export default handleRouteVerification; From 42a6ab99c0d23b1ebd332c9b78d5f5d8af3f0656 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 10:59:44 +0200 Subject: [PATCH 0119/1005] Add props type SearchHoldReasonPageProps --- src/pages/Search/SearchHoldReasonPage.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index df244d25c5ecd..e5392f2209d29 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -15,9 +15,12 @@ import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; -function SearchHoldReasonPage({ - route, -}: PlatformStackScreenProps) { +type SearchHoldReasonPageProps = PlatformStackScreenProps< + SearchReportParamList, + typeof SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP | typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS +>; + +function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); const {backTo = '', reportID} = route.params ?? {}; const context = useSearchContext(); From 2ed0b49be702824ccb1ca2fb19e85115f6293792 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 11:07:03 +0200 Subject: [PATCH 0120/1005] Name props type correctly --- .../Search/SearchMoneyRequestReportVerifyAccountPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx index 965e6d2a1b364..73defbf18072b 100644 --- a/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx @@ -5,9 +5,9 @@ import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type SearchMoneyRequestReportVerifyAccountPageParamList = PlatformStackScreenProps; +type SearchMoneyRequestReportVerifyAccountPageProps = PlatformStackScreenProps; -function SearchMoneyRequestReportVerifyAccountPage({route}: SearchMoneyRequestReportVerifyAccountPageParamList) { +function SearchMoneyRequestReportVerifyAccountPage({route}: SearchMoneyRequestReportVerifyAccountPageProps) { return ; } From 7787465e6dc32e44a4dad86d6ac71b0dd4870e21 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 11:07:59 +0200 Subject: [PATCH 0121/1005] Name props type correctly --- src/pages/Search/SearchReportVerifyAccountPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchReportVerifyAccountPage.tsx b/src/pages/Search/SearchReportVerifyAccountPage.tsx index 2150f37ae25f1..5c4eb6fc7e203 100644 --- a/src/pages/Search/SearchReportVerifyAccountPage.tsx +++ b/src/pages/Search/SearchReportVerifyAccountPage.tsx @@ -5,9 +5,9 @@ import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type SearchReportVerifyAccountPageParamList = PlatformStackScreenProps; +type SearchReportVerifyAccountPageProps = PlatformStackScreenProps; -function SearchReportVerifyAccountPage({route}: SearchReportVerifyAccountPageParamList) { +function SearchReportVerifyAccountPage({route}: SearchReportVerifyAccountPageProps) { return ; } From 27bba3d57ff8b10bcc5fedd664386a0d7614d789 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 11:10:30 +0200 Subject: [PATCH 0122/1005] Name props type correctly --- src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx | 4 ++-- .../step/MoneyRequestStepConfirmationVerifyAccountPage.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx b/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx index 9b4f66147306c..e6b3e696a3e84 100644 --- a/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx +++ b/src/pages/iou/request/MoneyRequestCreateVerifyAccountPage.tsx @@ -5,9 +5,9 @@ import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type MoneyRequestCreateVerifyAccountPageParamList = PlatformStackScreenProps; +type MoneyRequestCreateVerifyAccountPageProps = PlatformStackScreenProps; -function MoneyRequestCreateVerifyAccountPage({route}: MoneyRequestCreateVerifyAccountPageParamList) { +function MoneyRequestCreateVerifyAccountPage({route}: MoneyRequestCreateVerifyAccountPageProps) { return ; } diff --git a/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx index 69c650c83bff7..178e4d147672c 100644 --- a/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx +++ b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx @@ -5,9 +5,9 @@ import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type MoneyRequestStepConfirmationVerifyAccountPageParamList = PlatformStackScreenProps; +type MoneyRequestStepConfirmationVerifyAccountPageProps = PlatformStackScreenProps; -function MoneyRequestStepConfirmationVerifyAccountPage({route}: MoneyRequestStepConfirmationVerifyAccountPageParamList) { +function MoneyRequestStepConfirmationVerifyAccountPage({route}: MoneyRequestStepConfirmationVerifyAccountPageProps) { return ( Date: Wed, 1 Oct 2025 11:20:06 +0200 Subject: [PATCH 0123/1005] Remove unnecessary as const --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a7937cf92279e..35e79cd1877a4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,7 +60,7 @@ const ROUTES = { return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const; }, }, - SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}` as const, + SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`, SEARCH_SAVED_SEARCH_RENAME: { route: 'search/saved-search/rename', getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const, From 4dd0cf31562eda3320a570b53316ea79e7d0016c Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 11:43:07 +0200 Subject: [PATCH 0124/1005] Make string reportID to not have a default --- src/libs/SettlementButtonUtils.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 96b7ae12b3a7c..3c5c75472a8fa 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,27 +11,42 @@ type RouteMapping = { navigate: () => void; }; -const handleRouteVerification = (reportID: string, chatReportID: string) => { +const handleRouteVerification = (reportID: string | undefined, chatReportID: string) => { const routeMappings: RouteMapping[] = [ { check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), }, { - check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID}))), - navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + }, }, { - check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}))), - navigate: () => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + }, }, { check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), }, { - check: (activeRoute: string) => !!(reportID && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID))), - navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + }, }, { check: (activeRoute: string) => From dadcb86fb18c4d55ef67ca30fbe74ce19a956ae8 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 11:43:26 +0200 Subject: [PATCH 0125/1005] Make string reportID to not have a default --- src/components/SettlementButton/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 3116700fb84c3..e6335d53a2cff 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -376,7 +376,7 @@ function SettlementButton({ const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { if (!isUserValidated) { - handleRouteVerification(reportID ?? '', chatReportID); + handleRouteVerification(reportID, chatReportID); return; } From 057d05720bd4bb797acb5481dc967290093c88eb Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 12:18:53 +0200 Subject: [PATCH 0126/1005] Rename function for better description --- src/components/SettlementButton/index.tsx | 4 ++-- src/libs/SettlementButtonUtils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index e6335d53a2cff..f1924fd46ca93 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -30,7 +30,7 @@ import { isInvoiceReport as isInvoiceReportUtil, isIOUReport, } from '@libs/ReportUtils'; -import handleRouteVerification from '@libs/SettlementButtonUtils'; +import handleUnvalidatedUserNavigation from '@libs/SettlementButtonUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {approveMoneyRequest} from '@userActions/IOU'; @@ -376,7 +376,7 @@ function SettlementButton({ const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { if (!isUserValidated) { - handleRouteVerification(reportID, chatReportID); + handleUnvalidatedUserNavigation(reportID, chatReportID); return; } diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 3c5c75472a8fa..86137ae4fea92 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,7 +11,7 @@ type RouteMapping = { navigate: () => void; }; -const handleRouteVerification = (reportID: string | undefined, chatReportID: string) => { +const handleUnvalidatedUserNavigation = (reportID: string | undefined, chatReportID: string) => { const routeMappings: RouteMapping[] = [ { check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), @@ -74,4 +74,4 @@ const handleRouteVerification = (reportID: string | undefined, chatReportID: str } }; -export default handleRouteVerification; +export default handleUnvalidatedUserNavigation; From 0d2be778e9de55d6a2a40fec06259a0bf50f7bf9 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Wed, 1 Oct 2025 16:01:05 +0200 Subject: [PATCH 0127/1005] Extract RouteMappings logic from handleUnvalidatedUserNavigation --- src/libs/SettlementButtonUtils.ts | 104 +++++++++++++++--------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 86137ae4fea92..f5e208e043518 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,67 +11,67 @@ type RouteMapping = { navigate: () => void; }; -const handleUnvalidatedUserNavigation = (reportID: string | undefined, chatReportID: string) => { - const routeMappings: RouteMapping[] = [ - { - check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), - navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), - }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - }, - }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - }, +const getRouteMappings = (reportID: string | undefined, chatReportID: string): RouteMapping[] => [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), + }, + { + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); }, - { - check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), - navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, + { + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); - }, + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, + { + check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), + navigate: () => { + if (reportID === undefined) { + return; + } + Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); }, - { - check: (activeRoute: string) => - activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - navigate: () => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), - ), - }, - { - check: (activeRoute: string) => - activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - navigate: () => - Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - }, - ]; + }, + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), + ), + }, + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + }, +]; +const handleUnvalidatedUserNavigation = (reportID: string | undefined, chatReportID: string) => { const activeRoute = Navigation.getActiveRoute(); - const matchedRoute = routeMappings.find((mapping) => mapping.check(activeRoute)); + const matchedRoute = getRouteMappings(reportID, chatReportID).find((mapping) => mapping.check(activeRoute)); if (matchedRoute) { matchedRoute.navigate(); - } else { - Log.warn('Failed to navigate to the correct path'); + return; } + Log.warn('Failed to navigate to the correct path'); }; export default handleUnvalidatedUserNavigation; From e0334b985cfb0f22d96d19a0bdf7d318f8020d12 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Thu, 2 Oct 2025 10:09:10 +0200 Subject: [PATCH 0128/1005] Add SettlementButtonUtils tests --- tests/unit/SettlementButtonUtilsTest.ts | 107 ++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/unit/SettlementButtonUtilsTest.ts diff --git a/tests/unit/SettlementButtonUtilsTest.ts b/tests/unit/SettlementButtonUtilsTest.ts new file mode 100644 index 0000000000000..6008f41aefea2 --- /dev/null +++ b/tests/unit/SettlementButtonUtilsTest.ts @@ -0,0 +1,107 @@ +import Navigation from '@libs/Navigation/Navigation'; +import handleUnvalidatedUserNavigation from '@libs/SettlementButtonUtils'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +jest.mock('@libs/Navigation/Navigation'); + +describe('SettlementButtonUtils', () => { + const mockReportID = '123456789'; + const mockChatReportID = '987654321'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('navigate to ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_ROOT', () => { + const mockActiveRoute = ROUTES.SEARCH_ROOT.getRoute({query: ''}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT); + }); + + it('navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_REPORT', () => { + const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + }); + + it('do not navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { + const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(undefined, mockChatReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); + + it('navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_MONEY_REQUEST_REPORT', () => { + const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + }); + + it('do not navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { + const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(undefined, mockChatReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); + + it('match ROUTES.SEARCH_MONEY_REQUEST_REPORT over ROUTES.REPORT_WITH_ID', () => { + // Should match the first applicable route when multiple conditions could match + const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledTimes(1); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + }); + + it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(chatReportID)', () => { + const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockChatReportID); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); + expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + }); + + it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID)', () => { + const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); + }); + + it('do not navigate when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID) and reportID is undefined', () => { + const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(undefined, mockChatReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); + + it('navigate to ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_STEP_CONFIRMATION', () => { + const mockActiveRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + ); + }); + + it('navigate to ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_CREATE', () => { + const mockActiveRoute = ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).toHaveBeenCalledWith( + ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + ); + }); + + it('when no route mapping matches, user should not be navigated', () => { + (Navigation.getActiveRoute as jest.Mock).mockReturnValue('/just/unmatched/route'); + handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); +}); From 37aa99290d51d571b2c23932d585f1d13f2858b7 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 2 Oct 2025 16:28:45 +0300 Subject: [PATCH 0129/1005] fix: update deleteWorkspaceCategories to accept an additional parameter --- src/libs/actions/Policy/Category.ts | 3 ++- tests/actions/PolicyCategoryTest.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 40febe585eac9..31ef84f68ed44 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1002,7 +1002,8 @@ function deleteWorkspaceCategories(policyData: PolicyData, categoryNamesToDelete const optimisticPolicyData: Partial = shouldDisableRequiresCategory ? { requiresCategory: false, - } : {} + } + : {}; const onyxData: OnyxData = { optimisticData: [ diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index e4e1cd53981c4..4a9122d3f36a5 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -225,7 +225,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete); + Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete, false); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ From c2a4d8168cc4b3fe7c0d57614e21b7a370bec1e5 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Thu, 2 Oct 2025 19:54:48 +0200 Subject: [PATCH 0130/1005] Divide route mappings from getRouteMappings into nonReportId and reportId mappings --- src/libs/SettlementButtonUtils.ts | 92 +++++++++++++++---------------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index f5e208e043518..4f26acd412add 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,57 +11,53 @@ type RouteMapping = { navigate: () => void; }; -const getRouteMappings = (reportID: string | undefined, chatReportID: string): RouteMapping[] => [ - { - check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), - navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), - }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); +const getRouteMappings = (reportID: string | undefined, chatReportID: string): RouteMapping[] => { + const nonReportIdRouteMappings = [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), }, - }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), + ), }, - }, - { - check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), - navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), - }, - { - check: (activeRoute: string) => reportID !== undefined && activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), - navigate: () => { - if (reportID === undefined) { - return; - } - Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)); + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), }, - }, - { - check: (activeRoute: string) => - activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - navigate: () => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), - ), - }, - { - check: (activeRoute: string) => - activeRoute.includes(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - navigate: () => - Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), - }, -]; + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, + ]; + + if (reportID === undefined) { + return nonReportIdRouteMappings; + } + + const reportIdRouteMappings = [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + ]; + + return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; +}; const handleUnvalidatedUserNavigation = (reportID: string | undefined, chatReportID: string) => { const activeRoute = Navigation.getActiveRoute(); From 93fae1b0630f9b19fec59527f837d51918cf75b1 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Thu, 2 Oct 2025 20:06:35 +0200 Subject: [PATCH 0131/1005] Move optional props to the end of both handleUnvalidatedUserNavigation and getRouteMappings function declarations --- src/components/SettlementButton/index.tsx | 2 +- src/libs/SettlementButtonUtils.ts | 6 +++--- tests/unit/SettlementButtonUtilsTest.ts | 24 +++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index fdc5c21490ce9..d78a5c2396b78 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -376,7 +376,7 @@ function SettlementButton({ const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { if (!isUserValidated) { - handleUnvalidatedUserNavigation(reportID, chatReportID); + handleUnvalidatedUserNavigation(chatReportID, reportID); return; } diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 4f26acd412add..9f2f59b37cb82 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,7 +11,7 @@ type RouteMapping = { navigate: () => void; }; -const getRouteMappings = (reportID: string | undefined, chatReportID: string): RouteMapping[] => { +const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping[] => { const nonReportIdRouteMappings = [ { check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), @@ -59,9 +59,9 @@ const getRouteMappings = (reportID: string | undefined, chatReportID: string): R return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; }; -const handleUnvalidatedUserNavigation = (reportID: string | undefined, chatReportID: string) => { +const handleUnvalidatedUserNavigation = (chatReportID: string, reportID?: string) => { const activeRoute = Navigation.getActiveRoute(); - const matchedRoute = getRouteMappings(reportID, chatReportID).find((mapping) => mapping.check(activeRoute)); + const matchedRoute = getRouteMappings(chatReportID, reportID).find((mapping) => mapping.check(activeRoute)); if (matchedRoute) { matchedRoute.navigate(); diff --git a/tests/unit/SettlementButtonUtilsTest.ts b/tests/unit/SettlementButtonUtilsTest.ts index 6008f41aefea2..c4aca575b9723 100644 --- a/tests/unit/SettlementButtonUtilsTest.ts +++ b/tests/unit/SettlementButtonUtilsTest.ts @@ -16,35 +16,35 @@ describe('SettlementButtonUtils', () => { it('navigate to ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_ROOT', () => { const mockActiveRoute = ROUTES.SEARCH_ROOT.getRoute({query: ''}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT); }); it('navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_REPORT', () => { const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); }); it('do not navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(undefined, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); it('navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_MONEY_REQUEST_REPORT', () => { const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); }); it('do not navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(undefined, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); @@ -52,7 +52,7 @@ describe('SettlementButtonUtils', () => { // Should match the first applicable route when multiple conditions could match const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledTimes(1); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); @@ -61,7 +61,7 @@ describe('SettlementButtonUtils', () => { it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(chatReportID)', () => { const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockChatReportID); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); }); @@ -69,7 +69,7 @@ describe('SettlementButtonUtils', () => { it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID)', () => { const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); }); @@ -77,14 +77,14 @@ describe('SettlementButtonUtils', () => { it('do not navigate when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID) and reportID is undefined', () => { const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(undefined, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); it('navigate to ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_STEP_CONFIRMATION', () => { const mockActiveRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith( ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), ); @@ -93,7 +93,7 @@ describe('SettlementButtonUtils', () => { it('navigate to ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_CREATE', () => { const mockActiveRoute = ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).toHaveBeenCalledWith( ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), ); @@ -101,7 +101,7 @@ describe('SettlementButtonUtils', () => { it('when no route mapping matches, user should not be navigated', () => { (Navigation.getActiveRoute as jest.Mock).mockReturnValue('/just/unmatched/route'); - handleUnvalidatedUserNavigation(mockReportID, mockChatReportID); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); }); From a907e4f3babf1cbf9c1215d71e0ce62d160d3aa7 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Fri, 3 Oct 2025 14:05:15 +0200 Subject: [PATCH 0132/1005] Add a jsdoc comment for both getRouteMappings and handleUnvalidatedUserNavigation functions from SettlementButtonUtils --- src/libs/SettlementButtonUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 9f2f59b37cb82..1896267a9bfc7 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -11,6 +11,13 @@ type RouteMapping = { navigate: () => void; }; +/** + * Retrieves an array of available RouteMappings for an unvalidated user. + * Each mapping contains a `check` function that determines whether the activeRoute matches the given mapping and a `navigate` function that executes navigation to the corresponding route. + * @param chatReportID - The chat or workspace ID from which the unvalidated user makes a payment via SettlementButton + * @param reportID - The expense report ID that the user pays using SettlementButton (optional) + * @return An array of available RouteMappings suitable for an unvalidated user + */ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping[] => { const nonReportIdRouteMappings = [ { @@ -59,6 +66,9 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; }; +/** + * Handles SettlementButton navigation for unvalidated users based on the active route and current chatID, reportID (optional). + */ const handleUnvalidatedUserNavigation = (chatReportID: string, reportID?: string) => { const activeRoute = Navigation.getActiveRoute(); const matchedRoute = getRouteMappings(chatReportID, reportID).find((mapping) => mapping.check(activeRoute)); From 2ff77f44146792f6fcdb2f50d2c366f0c06b82f7 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 3 Oct 2025 15:47:22 +0200 Subject: [PATCH 0133/1005] bump live-markdown to version supporting worklets & remove patch --- package-lock.json | 33 ++- package.json | 4 +- ...y+react-native-live-markdown+0.1.302.patch | 218 ------------------ .../react-native-live-markdown/details.md | 9 - 4 files changed, 23 insertions(+), 241 deletions(-) delete mode 100644 patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch delete mode 100644 patches/@expensify/react-native-live-markdown/details.md diff --git a/package-lock.json b/package-lock.json index d871c1e9a6055..712b21f6f6de8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.302", + "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", "@expensify/react-native-wallet": "^0.1.5", "@expo/metro-runtime": "^5.0.4", "@firebase/app": "^0.13.2", @@ -136,7 +136,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "0.5.1", + "react-native-worklets": "0.6.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -3831,9 +3831,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.302", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.302.tgz", - "integrity": "sha512-0mTy7A6myWwBviQAI47LvcmxGQ+x3O4b7Vt9JJyTXcp9b8N6wPsBhe9YMjzBLqGFYP00ucGNV13P64R9XLglag==", + "version": "0.1.305", + "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", + "integrity": "sha512-zojPCX+2B13MTkcdiShpdUZVcCzcq8lOCP1Szfu3Z35znjRD8d+KT+Ue1lnJQYHpxD+cGyEgiNwGeiC0f2DlBg==", "license": "MIT", "workspaces": [ "./example", @@ -3846,7 +3846,16 @@ "expensify-common": ">=2.0.148", "react": "*", "react-native": "*", - "react-native-reanimated": ">=3.17.0" + "react-native-reanimated": ">=3.17.0", + "react-native-worklets": "^0.6.0" + }, + "peerDependenciesMeta": { + "react-native-reanimated": { + "optional": true + }, + "react-native-worklets": { + "optional": true + } } }, "node_modules/@expensify/react-native-wallet": { @@ -32030,9 +32039,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.1.0-jsx-fix", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.0-jsx-fix.tgz", - "integrity": "sha512-kDfdzTvuHlqNkaP4W46qh1RTeyD7JhAMuNenz/9mFBAQmgajpVaXthma52j4EtXZgNfPbXHnH43NQ/OZrdQA4Q==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.2.tgz", + "integrity": "sha512-qzmQiFrvjm62pRBcj97QI9Xckc3EjgHQoY1F2yjktd0kpjhoyePeuTEXjYRCAVIy7IV/1cfeSup34+zFThFoHQ==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", @@ -32285,9 +32294,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", - "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.6.0.tgz", + "integrity": "sha512-yETMNuCcivdYWteuG4eRqgiAk2DzRCrVAaEBIEWPo4emrf3BNjadFo85L5QvyEusrX9QKE3ZEAx8U5A/nbyFgg==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", diff --git a/package.json b/package.json index 4b9e591023152..22b6c069e7d79 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.302", + "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", "@expensify/react-native-wallet": "^0.1.5", "@expo/metro-runtime": "^5.0.4", "@firebase/app": "^0.13.2", @@ -206,7 +206,7 @@ "react-native-vision-camera": "^4.7.0", "react-native-web": "0.20.0", "react-native-webview": "13.13.1", - "react-native-worklets": "0.5.1", + "react-native-worklets": "0.6.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", diff --git a/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch b/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch deleted file mode 100644 index 8d39864cce21e..0000000000000 --- a/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.302.patch +++ /dev/null @@ -1,218 +0,0 @@ -diff --git a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec -index 21de59d..da055d6 100644 ---- a/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec -+++ b/node_modules/@expensify/react-native-live-markdown/RNLiveMarkdown.podspec -@@ -5,7 +5,7 @@ react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir - react_native_minor_version = react_native_json['version'].split('.')[1].to_i - - pods_root = Pod::Config.instance.project_pods_root --react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-reanimated/package.json')"`) -+react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`) - react_native_reanimated_node_modules_dir_from_pods_root = Pathname.new(react_native_reanimated_node_modules_dir).relative_path_from(pods_root).to_s - - package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -@@ -24,7 +24,7 @@ Pod::Spec.new do |s| - - s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" - -- s.dependency "RNReanimated/worklets" -+ s.dependency "RNWorklets" - - s.xcconfig = { - "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}", -diff --git a/node_modules/@expensify/react-native-live-markdown/android/build.gradle b/node_modules/@expensify/react-native-live-markdown/android/build.gradle -index 001a23c..232263c 100644 ---- a/node_modules/@expensify/react-native-live-markdown/android/build.gradle -+++ b/node_modules/@expensify/react-native-live-markdown/android/build.gradle -@@ -1,3 +1,5 @@ -+import org.apache.tools.ant.taskdefs.condition.Os -+ - buildscript { - repositories { - google() -@@ -68,6 +70,15 @@ def getReactNativeMinorVersion() { - - def REACT_NATIVE_MINOR_VERSION = getReactNativeMinorVersion() - -+def toPlatformFileString(String path) { -+ if (Os.isFamily(Os.FAMILY_WINDOWS)) { -+ path = path.replace(File.separatorChar, '/' as char) -+ } -+ return path -+} -+ -+def reactNativeRootDir = resolveReactNativeDirectory() -+ - android { - if (supportsNamespace()) { - namespace "com.expensify.livemarkdown" -@@ -94,6 +105,7 @@ android { - arguments "-DANDROID_STL=c++_shared", - "-DANDROID_TOOLCHAIN=clang", - "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", -+ "-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}", - "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" - abiFilters (*reactNativeArchitectures()) - } -@@ -175,7 +187,7 @@ repositories { - dependencies { - implementation "com.facebook.react:react-android" // version substituted by RNGP - implementation "com.facebook.react:hermes-android" // version substituted by RNGP -- implementation project(":react-native-reanimated") -+ implementation project(":react-native-worklets") - } - - // This fixes linking errors due to undefined symbols from libworklets.so. -@@ -183,6 +195,6 @@ dependencies { - // like a header-only library. During build, config files are not regenerated - // because the cache key does not change and AGP thinks that they are up-to-date. - afterEvaluate { -- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabDebugPackage") -- prepareKotlinBuildScriptModel.dependsOn(":react-native-reanimated:prefabReleasePackage") -+ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabDebugPackage") -+ prepareKotlinBuildScriptModel.dependsOn(":react-native-worklets:prefabReleasePackage") - } -diff --git a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt -index e5ba0ea..c0456eb 100644 ---- a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt -+++ b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/CMakeLists.txt -@@ -4,6 +4,9 @@ cmake_minimum_required(VERSION 3.13) - - set(CMAKE_VERBOSE_MAKEFILE on) - -+include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake") -+add_compile_options(${folly_FLAGS}) -+ - add_compile_options(-fvisibility=hidden -fexceptions -frtti) - - string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}") -@@ -15,16 +18,16 @@ file(GLOB CPP_SRC CONFIGURE_DEPENDS "${CPP_DIR}/*.cpp") - - add_library(${CMAKE_PROJECT_NAME} SHARED ${ANDROID_SRC} ${CPP_SRC}) - --target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR}) -+target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR} "${REACT_NATIVE_DIR}/ReactCommon/jsiexecutor") - - find_package(fbjni REQUIRED CONFIG) - find_package(ReactAndroid REQUIRED CONFIG) --find_package(react-native-reanimated REQUIRED CONFIG) -+find_package(react-native-worklets REQUIRED CONFIG) - - target_link_libraries( - ${CMAKE_PROJECT_NAME} - fbjni::fbjni - ReactAndroid::jsi - ReactAndroid::reactnative -- react-native-reanimated::worklets -+ react-native-worklets::worklets - ) -diff --git a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -index 9d585e3..fe46961 100644 ---- a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -+++ b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -@@ -19,7 +19,7 @@ @implementation MarkdownParser { - const auto &markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); - jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); - -- std::shared_ptr markdownWorklet; -+ std::shared_ptr markdownWorklet; - try { - markdownWorklet = expensify::livemarkdown::getMarkdownWorklet([parserId intValue]); - } catch (const std::out_of_range &error) { -diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp -index 56fd6de..67f93eb 100644 ---- a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp -+++ b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.cpp -@@ -17,11 +17,11 @@ std::shared_ptr getMarkdownRuntime() { - return globalMarkdownWorkletRuntime; - } - --std::unordered_map> globalMarkdownShareableWorklets; -+std::unordered_map> globalMarkdownShareableWorklets; - std::mutex globalMarkdownShareableWorkletsMutex; - int nextParserId = 1; - --const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet) { -+const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet) { - assert(markdownWorklet != nullptr); - auto parserId = nextParserId++; - std::unique_lock lock(globalMarkdownShareableWorkletsMutex); -@@ -34,7 +34,7 @@ void unregisterMarkdownWorklet(const int parserId) { - globalMarkdownShareableWorklets.erase(parserId); - } - --std::shared_ptr getMarkdownWorklet(const int parserId) { -+std::shared_ptr getMarkdownWorklet(const int parserId) { - std::unique_lock lock(globalMarkdownShareableWorkletsMutex); - return globalMarkdownShareableWorklets.at(parserId); - } -diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h -index 1edfb45..e181726 100644 ---- a/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h -+++ b/node_modules/@expensify/react-native-live-markdown/cpp/MarkdownGlobal.h -@@ -14,11 +14,11 @@ void setMarkdownRuntime(const std::shared_ptr &markdownWorkletRu - - std::shared_ptr getMarkdownRuntime(); - --const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet); -+const int registerMarkdownWorklet(const std::shared_ptr &markdownWorklet); - - void unregisterMarkdownWorklet(const int parserId); - --std::shared_ptr getMarkdownWorklet(const int parserId); -+std::shared_ptr getMarkdownWorklet(const int parserId); - - } // namespace livemarkdown - } // namespace expensify -diff --git a/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp b/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp -index 5332e30..f1f25a5 100644 ---- a/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp -+++ b/node_modules/@expensify/react-native-live-markdown/cpp/RuntimeDecorator.cpp -@@ -23,7 +23,7 @@ void injectJSIBindings(jsi::Runtime &rt) { - jsi::PropNameID::forAscii(rt, "jsi_registerMarkdownWorklet"), - 1, - [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { -- auto parserId = registerMarkdownWorklet(extractShareableOrThrow(rt, args[0])); -+ auto parserId = registerMarkdownWorklet(extractSerializableOrThrow(rt, args[0])); - return jsi::Value(parserId); - })); - -diff --git a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts b/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts -index e6ad088..90f2487 100644 ---- a/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts -+++ b/node_modules/@expensify/react-native-live-markdown/lib/typescript/src/MarkdownTextInput.d.ts -@@ -2,7 +2,7 @@ import { TextInput } from 'react-native'; - import React from 'react'; - import type { TextInputProps } from 'react-native'; - import type { WorkletRuntime } from 'react-native-reanimated'; --import type { ShareableRef, WorkletFunction } from 'react-native-reanimated/lib/typescript/commonTypes'; -+import type { ShareableRef, WorkletFunction } from 'react-native-worklets'; - import type { PartialMarkdownStyle } from './styleUtils'; - import type { InlineImagesInputProps, MarkdownRange } from './commonTypes'; - declare global { -diff --git a/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx b/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx -index 82ee895..55c0e90 100644 ---- a/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx -+++ b/node_modules/@expensify/react-native-live-markdown/src/MarkdownTextInput.tsx -@@ -3,7 +3,7 @@ import React from 'react'; - import type {TextInputProps} from 'react-native'; - import {createWorkletRuntime, makeShareableCloneRecursive} from 'react-native-reanimated'; - import type {WorkletRuntime} from 'react-native-reanimated'; --import type {ShareableRef, WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; -+import type {ShareableRef, WorkletFunction} from 'react-native-worklets'; - - import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; - import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; -diff --git a/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts b/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts -index 6e14e25..38d92bb 100644 ---- a/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts -+++ b/node_modules/@expensify/react-native-live-markdown/src/parseExpensiMark.ts -@@ -4,7 +4,7 @@ import {Platform} from 'react-native'; - import {ExpensiMark} from 'expensify-common'; - import {unescapeText} from 'expensify-common/dist/utils'; - import {decode} from 'html-entities'; --import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes'; -+import type {WorkletFunction} from 'react-native-worklets'; - import type {MarkdownType, MarkdownRange} from './commonTypes'; - import {groupRanges, sortRanges, splitRangesOnEmojis} from './rangeUtils'; - diff --git a/patches/@expensify/react-native-live-markdown/details.md b/patches/@expensify/react-native-live-markdown/details.md deleted file mode 100644 index a209fe239a53c..0000000000000 --- a/patches/@expensify/react-native-live-markdown/details.md +++ /dev/null @@ -1,9 +0,0 @@ - -# `@expensify/react-native-live-markdown` patches - -### [@expensify+react-native-live-markdown+0.1.302.patch](@expensify+react-native-live-markdown+0.1.302.patch) - -- Reason: This is a temporary patch to make the app buildable and testable with live-markdown. The plan is to bump live-markdown first, and then update reanimated. This patch is not meant for production — it exists only for testing purposes. -- Upstream PR/issue: 🛑 -- E/App issue: 🛑 -- PR Introducing Patch: [Upgrade Reanimated to v4](https://github.com/Expensify/App/pull/69469) From a3112abad5040395458dbc72636d20909b3783b2 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 3 Oct 2025 16:05:14 +0200 Subject: [PATCH 0134/1005] update Podfile.lock --- ios/Podfile.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c645efba42c92..4a3c0ff053001 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2841,7 +2841,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.304): + - RNLiveMarkdown (0.1.305): - DoubleConversion - glog - hermes-engine @@ -2971,7 +2971,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (4.1.0): + - RNReanimated (4.1.2): - DoubleConversion - glog - hermes-engine @@ -2994,10 +2994,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.1.0) + - RNReanimated/reanimated (= 4.1.2) - RNWorklets - Yoga - - RNReanimated/reanimated (4.1.0): + - RNReanimated/reanimated (4.1.2): - DoubleConversion - glog - hermes-engine @@ -3020,10 +3020,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.1.0) + - RNReanimated/reanimated/apple (= 4.1.2) - RNWorklets - Yoga - - RNReanimated/reanimated/apple (4.1.0): + - RNReanimated/reanimated/apple (4.1.2): - DoubleConversion - glog - hermes-engine @@ -3177,7 +3177,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNWorklets (0.5.0): + - RNWorklets (0.6.0): - DoubleConversion - glog - hermes-engine @@ -3200,9 +3200,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets (= 0.5.0) + - RNWorklets/worklets (= 0.6.0) - Yoga - - RNWorklets/worklets (0.5.0): + - RNWorklets/worklets (0.6.0): - DoubleConversion - glog - hermes-engine @@ -3225,9 +3225,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets/apple (= 0.5.0) + - RNWorklets/worklets/apple (= 0.6.0) - Yoga - - RNWorklets/worklets/apple (0.5.0): + - RNWorklets/worklets/apple (0.6.0): - DoubleConversion - glog - hermes-engine @@ -3917,18 +3917,18 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 61d3e94abb866ee955563ddf073e405e3f541359 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 7a023e8ca721643cb5a9297e279e4b085093137c + RNLiveMarkdown: 2090300f9993f0babf741715e3a0cfe56943cfbb RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: f3875edd847c82d770d1b57852b36d45b3ea8a01 RNNitroSQLite: e36968e81bbcab6c9e97a35e74ae34c7420d734c RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 223b1b3895100ae576756fa8e0d09e49354f9bdf + RNReanimated: 2721090eb0e662bf05b84701eae6de7a4eead177 RNScreens: 0dcbe4c37daf0ec9b98bc5a655b1303e58e676ff RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 9346c68a0d5c5e4e74d82d664b745f28e6cfc1cb - RNWorklets: 18e24f98345960e357e8e0f7908c6b67704c34a2 + RNWorklets: f7b2c643f9c3fcdf4c7049f237bcf5d308eaaa93 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c From 6a519b73f44e8a9eeacc330706c0d7f1d670a887 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Fri, 3 Oct 2025 17:36:47 +0200 Subject: [PATCH 0135/1005] Reduce code repetitiveness by using test.each in SettlementButtonUtilsTest --- tests/unit/SettlementButtonUtilsTest.ts | 130 +++++++++++------------- 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/tests/unit/SettlementButtonUtilsTest.ts b/tests/unit/SettlementButtonUtilsTest.ts index c4aca575b9723..84ffab384c335 100644 --- a/tests/unit/SettlementButtonUtilsTest.ts +++ b/tests/unit/SettlementButtonUtilsTest.ts @@ -13,43 +13,77 @@ describe('SettlementButtonUtils', () => { jest.clearAllMocks(); }); - it('navigate to ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_ROOT', () => { - const mockActiveRoute = ROUTES.SEARCH_ROOT.getRoute({query: ''}); + // handleUnvalidatedUserNavigation navigates to the correct route + it.each([ + { + description: 'navigate to ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_ROOT', + mockActiveRoute: ROUTES.SEARCH_ROOT.getRoute({query: ''}), + expectedRouteToNavigate: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT, + }, + { + description: 'navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_REPORT', + mockActiveRoute: ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}), + expectedRouteToNavigate: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_MONEY_REQUEST_REPORT', + mockActiveRoute: ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}), + expectedRouteToNavigate: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(chatReportID)', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockChatReportID), + expectedRouteToNavigate: ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID), + }, + { + description: 'navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID)', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockReportID), + expectedRouteToNavigate: ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_STEP_CONFIRMATION', + mockActiveRoute: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + expectedRouteToNavigate: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute( + CONST.IOU.ACTION.CREATE, + CONST.IOU.TYPE.PAY, + CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + mockChatReportID, + ), + }, + { + description: 'navigate to ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_CREATE', + mockActiveRoute: ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + expectedRouteToNavigate: ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + }, + ])('$description', ({mockActiveRoute, expectedRouteToNavigate}) => { (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT); - }); - - it('navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_REPORT', () => { - const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); - }); - - it('do not navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { - const mockActiveRoute = ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID); - expect(Navigation.navigate).not.toHaveBeenCalled(); - }); - - it('navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_MONEY_REQUEST_REPORT', () => { - const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + expect(Navigation.navigate).toHaveBeenCalledWith(expectedRouteToNavigate); + expect(Navigation.navigate).toHaveBeenCalledTimes(1); }); - it('do not navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when reportID is undefined', () => { - const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); + // handleUnvalidatedUserNavigation does not navigate to the route that require reportID, when reportID is undefined + it.each([ + { + description: 'do not navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when reportID is undefined', + mockActiveRoute: ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}), + }, + { + description: 'do not navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when reportID is undefined', + mockActiveRoute: ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}), + }, + { + description: 'do not navigate when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID) and reportID is undefined', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockReportID), + }, + ])('$description', ({mockActiveRoute}) => { (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); handleUnvalidatedUserNavigation(mockChatReportID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); + // handleUnvalidatedUserNavigation matches the first applicable route when multiple conditions could match it('match ROUTES.SEARCH_MONEY_REQUEST_REPORT over ROUTES.REPORT_WITH_ID', () => { - // Should match the first applicable route when multiple conditions could match const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); @@ -58,47 +92,7 @@ describe('SettlementButtonUtils', () => { expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); }); - it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(chatReportID)', () => { - const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockChatReportID); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); - expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); - }); - - it('navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID)', () => { - const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); - expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID)); - }); - - it('do not navigate when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID) and reportID is undefined', () => { - const mockActiveRoute = ROUTES.REPORT_WITH_ID.getRoute(mockReportID); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID); - expect(Navigation.navigate).not.toHaveBeenCalled(); - }); - - it('navigate to ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_STEP_CONFIRMATION', () => { - const mockActiveRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith( - ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), - ); - }); - - it('navigate to ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_CREATE', () => { - const mockActiveRoute = ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID); - (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); - handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); - expect(Navigation.navigate).toHaveBeenCalledWith( - ROUTES.MONEY_REQUEST_CREATE_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), - ); - }); - + // handleUnvalidatedUserNavigation does not navigate when no route mapping matches it('when no route mapping matches, user should not be navigated', () => { (Navigation.getActiveRoute as jest.Mock).mockReturnValue('/just/unmatched/route'); handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); From 2226044b46750f9343d3346073f92efd882224f1 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 5 Oct 2025 00:25:04 +0000 Subject: [PATCH 0136/1005] Add missing Onyx.set call to update policy in PolicyTagTest --- tests/actions/PolicyTagTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 1df21e74baadb..deef60c0634fa 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -515,7 +515,7 @@ describe('actions/Policy', () => { const tagName = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}).at(0) ?? ''; mockFetch?.pause?.(); - + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); From f12ea0acf276f3ebbd3b6f15efc1d264241252c4 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Sun, 5 Oct 2025 23:30:26 +0800 Subject: [PATCH 0137/1005] fix: delete state --- src/components/Search/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 512cb24cd9820..f4df05af7ecdf 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -450,6 +450,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS if (transactionGroup.transactions.length === 0 && isTransactionReportGroupListItemType(transactionGroup)) { const reportKey = transactionGroup.keyForList; + if (transactionGroup.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } if (reportKey && (Object.keys(selectedTransactions).includes(reportKey) || areAllMatchingItemsSelected)) { const [, emptyReportSelection] = mapEmptyReportToSelectedEntry(transactionGroup); newTransactionList[reportKey] = { @@ -566,6 +569,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS ? (data as TransactionGroupListItemType[]).reduce((count, item) => { // For empty reports, count the report itself as a selectable item if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item)) { + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return count; + } return count + 1; } // For regular reports, count all transactions @@ -610,6 +616,10 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + if (selectedTransactions[reportKey]?.isSelected) { // Deselect the empty report const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; @@ -842,6 +852,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const allSelections: Array<[string, SelectedTransactionInfo]> = (data as TransactionGroupListItemType[]).flatMap((item) => { // Handle empty reports - select the report itself if (item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && item.keyForList) { + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return []; + } return [mapEmptyReportToSelectedEntry(item)]; } From 6c9a50bc4baf51b73f9b6d314653411fc9df0319 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 6 Oct 2025 12:26:09 +0200 Subject: [PATCH 0138/1005] Bump reanimated to 4.1.2 --- package-lock.json | 2 +- package.json | 2 +- ...ive-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename patches/react-native-reanimated/{react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch => react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch} (100%) diff --git a/package-lock.json b/package-lock.json index ac130b21fc960..7362628a53c73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "12.5.3", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^4.1.0-jsx-fix", + "react-native-reanimated": "4.1.2", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", diff --git a/package.json b/package.json index 82423069646e9..5500173bdaecc 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#07d60d78d4772d47afd7a744940fc6b6d1881806", "react-native-plaid-link-sdk": "12.5.3", "react-native-qrcode-svg": "6.3.14", - "react-native-reanimated": "^4.1.0-jsx-fix", + "react-native-reanimated": "4.1.2", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.4.0", diff --git a/patches/react-native-reanimated/react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch b/patches/react-native-reanimated/react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch similarity index 100% rename from patches/react-native-reanimated/react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch rename to patches/react-native-reanimated/react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch From 805688d160da6516f9294d47f1cade6a363cf469 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 6 Oct 2025 12:28:49 +0200 Subject: [PATCH 0139/1005] Fix patch name --- patches/react-native-reanimated/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/react-native-reanimated/details.md b/patches/react-native-reanimated/details.md index 33055066aaa6f..b9e9bd2a6cde3 100644 --- a/patches/react-native-reanimated/details.md +++ b/patches/react-native-reanimated/details.md @@ -1,7 +1,7 @@ # `react-native-reanimated` patches -### [react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+4.1.0+001+catch-all-exceptions-on-stoi.patch) +### [react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+4.1.2+001+catch-all-exceptions-on-stoi.patch) - Reason: Reanimated wasn't able to catch an exception here, so the catch clause was broadened. - Upstream PR/issue: 🛑 From d4405acb66c02a12f134b772226bd95c0b7410ed Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Mon, 6 Oct 2025 13:38:56 +0200 Subject: [PATCH 0140/1005] refactor: refactors test comments for ClearPolicyTagListErrors in PolicyTagTest.ts --- tests/actions/PolicyTagTest.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 14ed8a6f5e27a..8e7043ce2e943 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -834,11 +834,11 @@ describe('actions/Policy', () => { describe('ClearPolicyTagListErrors', () => { it('should clear errors for a tag list', async () => { + // Given a policy with a tag list that has errors const fakePolicy = createRandomPolicy(0); const tagListName = 'Test tag list'; const fakePolicyTags = createRandomPolicyTags(tagListName, 2); - // Add errors to the tag list fakePolicyTags[tagListName] = { ...fakePolicyTags[tagListName], errors: {field1: 'Error on tag list'}, @@ -846,6 +846,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + // When clearing the errors from the tag list clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); await waitForBatchedUpdates(); @@ -855,23 +856,23 @@ describe('actions/Policy', () => { callback: (val) => (updatedPolicyTags = val), }); - // Verify that errors are cleared from the tag list + // Then the errors should be cleared while other properties remain unchanged expect(updatedPolicyTags?.[tagListName]).toBeDefined(); expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); - // Other properties should remain unchanged expect(updatedPolicyTags?.[tagListName]?.name).toBe(tagListName); expect(updatedPolicyTags?.[tagListName]?.orderWeight).toBe(0); expect(Object.keys(updatedPolicyTags?.[tagListName]?.tags ?? {}).length).toBe(2); }); it('should not modify Onyx data when tag list does not exist at given index', async () => { + // Given a policy with a tag list const fakePolicy = createRandomPolicy(0); const tagListName = 'Test tag list'; const fakePolicyTags = createRandomPolicyTags(tagListName, 2); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - // Try to clear errors for a non-existent tag list (invalid index) + // When attempting to clear errors for a non-existent tag list using an invalid index clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 99, policyTags: fakePolicyTags}); await waitForBatchedUpdates(); @@ -881,16 +882,16 @@ describe('actions/Policy', () => { callback: (val) => (updatedPolicyTags = val), }); - // The policy tags should remain unchanged + // Then the policy tags should remain unchanged because the index is invalid expect(updatedPolicyTags).toEqual(fakePolicyTags); }); it('should not modify Onyx data when tag list name is empty', async () => { + // Given a policy with a tag list that has an empty name const fakePolicy = createRandomPolicy(0); const tagListName = 'Test tag list'; const fakePolicyTags = createRandomPolicyTags(tagListName, 2); - // Remove the name property from the tag list fakePolicyTags[tagListName] = { ...fakePolicyTags[tagListName], name: '', @@ -898,6 +899,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + // When attempting to clear errors for the tag list with empty name clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); await waitForBatchedUpdates(); @@ -907,16 +909,16 @@ describe('actions/Policy', () => { callback: (val) => (updatedPolicyTags = val), }); - // The policy tags should remain unchanged + // Then the policy tags should remain unchanged because the tag list name is empty expect(updatedPolicyTags).toEqual(fakePolicyTags); }); it('should clear multiple errors from a tag list', async () => { + // Given a policy with a tag list that has multiple errors const fakePolicy = createRandomPolicy(0); const tagListName = 'Test tag list'; const fakePolicyTags = createRandomPolicyTags(tagListName, 3); - // Add multiple errors to the tag list fakePolicyTags[tagListName] = { ...fakePolicyTags[tagListName], errors: { @@ -928,6 +930,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + // When clearing errors from the tag list clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: fakePolicyTags}); await waitForBatchedUpdates(); @@ -937,11 +940,12 @@ describe('actions/Policy', () => { callback: (val) => (updatedPolicyTags = val), }); - // Verify that all errors are cleared + // Then all errors should be cleared from the tag list expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); }); it('should handle multiple tag lists correctly', async () => { + // Given a policy with multiple tag lists that both have errors const fakePolicy = createRandomPolicy(0); const tagListName1 = 'Tag list 1'; const tagListName2 = 'Tag list 2'; @@ -969,7 +973,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - // Clear errors only for the second tag list + // When clearing errors only for the second tag list clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 1, policyTags: fakePolicyTags}); await waitForBatchedUpdates(); @@ -979,17 +983,17 @@ describe('actions/Policy', () => { callback: (val) => (updatedPolicyTags = val), }); - // Verify that only the second list has errors cleared + // Then only the second list should have errors cleared while the first list keeps its errors expect(updatedPolicyTags?.[tagListName1]?.errors).toEqual({field: 'Error on list 1'}); expect(updatedPolicyTags?.[tagListName2]?.errors).toBeUndefined(); }); it('should work with data from useOnyx hook', async () => { + // Given a policy with a tag list that has errors and is accessed via useOnyx hook const fakePolicy = createRandomPolicy(0); const tagListName = 'Test tag list'; const fakePolicyTags = createRandomPolicyTags(tagListName, 2); - // Add errors to the tag list fakePolicyTags[tagListName] = { ...fakePolicyTags[tagListName], errors: {field: 'Test error'}, @@ -1003,16 +1007,17 @@ describe('actions/Policy', () => { expect(result.current[0]).toBeDefined(); }); + // When clearing errors using data from the useOnyx hook clearPolicyTagListErrors({policyID: fakePolicy.id, tagListIndex: 0, policyTags: result.current[0]}); await waitForBatchedUpdates(); - // Verify errors are cleared let updatedPolicyTags: PolicyTagLists | undefined; await TestHelper.getOnyxData({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, callback: (val) => (updatedPolicyTags = val), }); + // Then the errors should be cleared and other properties should remain unchanged expect(updatedPolicyTags?.[tagListName]).toBeDefined(); expect(updatedPolicyTags?.[tagListName]?.errors).toBeUndefined(); expect(updatedPolicyTags?.[tagListName]?.name).toBe(tagListName); From 7e69199c04b955dcfb7bdc9b3dcddab9cb340d9f Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Mon, 6 Oct 2025 22:09:38 +0800 Subject: [PATCH 0141/1005] fix: refactoring code --- src/components/Search/SearchList/index.tsx | 34 ++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index d522d4c093af9..6377cc851ea7e 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -178,28 +178,38 @@ function SearchList({ } return data; }, [data, groupBy]); + + const emptyReports = useMemo(() => { + if (groupBy && isTransactionGroupListItemArray(data)) { + return data.filter((item) => item.transactions.length === 0); + } + return []; + }, [data, groupBy]); + const selectedItemsLength = useMemo(() => { if (groupBy && isTransactionGroupListItemArray(data)) { - return data.reduce((acc, item) => { - if (item.transactions.length === 0) { - return acc + (item.isSelected ? 1 : 0); - } + const selectedEmptyReports = emptyReports.reduce((acc, item) => { + return acc + (item.isSelected ? 1 : 0); + }, 0); - return ( - acc + - item.transactions.reduce((transactionAcc, transaction) => { - return transactionAcc + (transaction.isSelected ? 1 : 0); - }, 0) - ); + const selectedTransactions = flattenedItems.reduce((acc, item) => { + return acc + (item?.isSelected ? 1 : 0); }, 0); + + return selectedEmptyReports + selectedTransactions; } return flattenedItems.reduce((acc, item) => { return acc + (item?.isSelected ? 1 : 0); }, 0); - }, [data, flattenedItems, groupBy]); + }, [data, flattenedItems, groupBy, emptyReports]); const totalItems = useMemo(() => { + if (groupBy && isTransactionGroupListItemArray(data)) { + const nonPendingTransactions = flattenedItems.filter((transaction) => !isTransactionPendingDelete(transaction as TransactionListItemType)); + return emptyReports.length + nonPendingTransactions.length; + } + return data.reduce((acc, item) => { if ('transactions' in item && item.transactions?.length) { const transactions = item.transactions.filter((transaction) => !isTransactionPendingDelete(transaction)); @@ -207,7 +217,7 @@ function SearchList({ } return acc + 1; }, 0); - }, [data]); + }, [data, groupBy, flattenedItems, emptyReports]); const {translate} = useLocalize(); const {isOffline} = useNetwork(); From 45ee8ab45a5c8ce83b67b2b7a0f1be8a25a9aa26 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Mon, 6 Oct 2025 18:35:16 +0200 Subject: [PATCH 0142/1005] Move SettlementButton payment methods to SettlementButtonUtils for React Compiler compliance --- src/components/SettlementButton/index.tsx | 23 ++----------------- src/libs/SettlementButtonUtils.ts | 27 ++++++++++++++++++++++- tests/unit/SettlementButtonUtilsTest.ts | 2 +- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index d78a5c2396b78..349b6bcf2283f 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -30,7 +30,7 @@ import { isInvoiceReport as isInvoiceReportUtil, isIOUReport, } from '@libs/ReportUtils'; -import handleUnvalidatedUserNavigation from '@libs/SettlementButtonUtils'; +import {getSettlementButtonPaymentMethods, handleUnvalidatedUserNavigation} from '@libs/SettlementButtonUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {approveMoneyRequest} from '@userActions/IOU'; @@ -174,26 +174,7 @@ function SettlementButton({ const paymentButtonOptions = useMemo(() => { const buttonOptions = []; - const paymentMethods = { - [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { - text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), - icon: Expensicons.User, - value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, - shouldUpdateSelectedIndex: false, - }, - [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { - text: translate('iou.settleBusiness', {formattedAmount: ''}), - icon: Expensicons.Building, - value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, - shouldUpdateSelectedIndex: false, - }, - [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.CheckCircle, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - shouldUpdateSelectedIndex: false, - }, - }; + const paymentMethods = getSettlementButtonPaymentMethods(hasActivatedWallet, translate); const approveButtonOption = { text: translate('iou.approve', {formattedAmount}), diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 1896267a9bfc7..cc7a77fa321a6 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -1,3 +1,5 @@ +import * as Expensicons from '@components/Icon/Expensicons'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import Log from './Log'; @@ -80,4 +82,27 @@ const handleUnvalidatedUserNavigation = (chatReportID: string, reportID?: string Log.warn('Failed to navigate to the correct path'); }; -export default handleUnvalidatedUserNavigation; +const getSettlementButtonPaymentMethods = (hasActivatedWallet: boolean, translate: LocaleContextProps['translate']) => { + return { + [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { + text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), + icon: Expensicons.User, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + shouldUpdateSelectedIndex: false, + }, + [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { + text: translate('iou.settleBusiness', {formattedAmount: ''}), + icon: Expensicons.Building, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + shouldUpdateSelectedIndex: false, + }, + [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.CheckCircle, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + shouldUpdateSelectedIndex: false, + }, + }; +}; + +export {handleUnvalidatedUserNavigation, getSettlementButtonPaymentMethods}; diff --git a/tests/unit/SettlementButtonUtilsTest.ts b/tests/unit/SettlementButtonUtilsTest.ts index 84ffab384c335..fe69201da0359 100644 --- a/tests/unit/SettlementButtonUtilsTest.ts +++ b/tests/unit/SettlementButtonUtilsTest.ts @@ -1,5 +1,5 @@ import Navigation from '@libs/Navigation/Navigation'; -import handleUnvalidatedUserNavigation from '@libs/SettlementButtonUtils'; +import {handleUnvalidatedUserNavigation} from '@libs/SettlementButtonUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; From a60857d3066e020f57f17f1ca8c88813b14a7381 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 6 Oct 2025 17:22:55 +0000 Subject: [PATCH 0143/1005] Fix import order in PolicyTagTest to ensure proper module resolution --- tests/actions/PolicyTagTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 8f9d8a8a617b5..f724a061aa535 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -1,7 +1,7 @@ import {renderHook, waitFor} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; -import OnyxListItemProvider from '@components/OnyxListItemProvider'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; import useOnyx from '@hooks/useOnyx'; import usePolicyData from '@hooks/usePolicyData'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; From 5dcebc1c515098c4540a07dddae1c1599c394bbc Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 7 Oct 2025 11:28:50 +0700 Subject: [PATCH 0144/1005] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 45 ++++++++++++++------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index cb92446ba8837..02bbba34358c2 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,6 +64,34 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } +function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1); + + const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name; + const isEqualLastRouteInFullScreenRoute = + !lastRouteInMatchingFullScreen?.name || !lastRouteInLastFullScreenRoute?.name || lastRouteInMatchingFullScreen.name === lastRouteInLastFullScreenRoute.name; + + return isEqualFullScreenRoute && isEqualLastRouteInFullScreenRoute; +} + +/** Check whether the route has been preloaded */ +function isRoutePreloaded(currentState: PlatformStackNavigationState, matchingFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + + const preloadedRoutes = currentState.preloadedRoutes; + + return preloadedRoutes.some((preloadedRoute) => { + const isMatchingFullScreenRoute = preloadedRoute.name === matchingFullScreenRoute.name; + + // Compare the last route of the preloadedRoute and the last route of the matchingFullScreenRoute to ensure the preloaded route is accepted when matching subroutes as well + const isMatchingLastRoute = + !lastRouteInMatchingFullScreen?.name || (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name); + + return isMatchingFullScreenRoute && isMatchingLastRoute; + }); +} + export default function linkTo(navigation: NavigationContainerRef | null, path: Route, options?: LinkToOptions) { if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); @@ -127,20 +155,9 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes?.at(-1); - if ( - matchingFullScreenRoute && - lastFullScreenRoute && - (matchingFullScreenRoute.name !== lastFullScreenRoute.name || - (lastRouteInMatchingFullScreen?.name && lastRouteInLastFullScreenRoute?.name && lastRouteInMatchingFullScreen.name !== lastRouteInLastFullScreenRoute.name)) - ) { - const isMatchingRoutePreloaded = currentState.preloadedRoutes.some( - (preloadedRoute) => - preloadedRoute.name === matchingFullScreenRoute.name && - (!lastRouteInMatchingFullScreen?.name || - (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name)), - ); - if (isMatchingRoutePreloaded) { + + if (matchingFullScreenRoute && lastFullScreenRoute && !areFullScreenRoutesEqual(matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { + if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { const additionalAction = StackActions.push(matchingFullScreenRoute.name, { From f1d972758482105d8b137cc3cbd5225b390d39d4 Mon Sep 17 00:00:00 2001 From: Olgierd Date: Tue, 7 Oct 2025 11:05:18 +0200 Subject: [PATCH 0145/1005] Add VERIFY_ACCOUNT screens to SEARCH_TO_RHP mapping for correct relation to the central search screen --- src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index 2489d81fbf723..9dc84d5798b2f 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -4,10 +4,12 @@ import SCREENS from '@src/SCREENS'; // This file is used to define RHP screens that are in relation to the search screen. const SEARCH_TO_RHP: Partial> = { [SCREENS.SEARCH.ROOT]: [ + SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT, SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_GROUP_BY_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP, SCREENS.SEARCH.REPORT_RHP, + SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT, SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP, SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_RHP, From 186102616cf17fc79f91e1f826967b168097946b Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 7 Oct 2025 11:41:09 +0200 Subject: [PATCH 0146/1005] bump live-markdown --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2fe66b44406c..42102afd541b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", + "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#a8a5765f98ec1c2e0422b215eebb23d8cc8b09f0", "@expensify/react-native-wallet": "^0.1.5", "@expo/metro-runtime": "^5.0.4", "@firebase/app": "^0.13.2", @@ -3836,8 +3836,8 @@ }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.305", - "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", - "integrity": "sha512-zojPCX+2B13MTkcdiShpdUZVcCzcq8lOCP1Szfu3Z35znjRD8d+KT+Ue1lnJQYHpxD+cGyEgiNwGeiC0f2DlBg==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#a8a5765f98ec1c2e0422b215eebb23d8cc8b09f0", + "integrity": "sha512-j2ahhF7n1UDdrTqwSvdyk5t6XVaOvgdy2VimMOBTKVmXEr6M9DZz+8Hz5S+SP4mW1oA3/pr7PEITGrRNCSTZ8A==", "license": "MIT", "workspaces": [ "./example", diff --git a/package.json b/package.json index 02d0e932f6bab..8c83d19eed35e 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#2e125b8ee4268424bed20989cd73bb7f8707d6fe", + "@expensify/react-native-live-markdown": "git+https://github.com/Expensify/react-native-live-markdown.git#a8a5765f98ec1c2e0422b215eebb23d8cc8b09f0", "@expensify/react-native-wallet": "^0.1.5", "@expo/metro-runtime": "^5.0.4", "@firebase/app": "^0.13.2", From 52500baaa0129252fbe732583c20079643e465ef Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Tue, 7 Oct 2025 22:33:19 +0500 Subject: [PATCH 0147/1005] remove Auditor option from bulk action in worksapce members page --- src/pages/workspace/WorkspaceMembersPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 17838d3620c69..34d280355a6c3 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -51,7 +51,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; import {getAccountIDsByLogins, getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; -import {getMemberAccountIDsForWorkspace, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminUtils} from '@libs/PolicyUtils'; +import {getMemberAccountIDsForWorkspace, isControlPolicy, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminUtils} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; @@ -617,7 +617,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers options.push(adminOption); } - if (hasAtLeastOneNonAuditorRole) { + if (hasAtLeastOneNonAuditorRole && isControlPolicy(policy)) { options.push(auditorOption); } From 94bba4f51353e20b75b12a7708a73e5a7721786e Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Tue, 7 Oct 2025 22:37:01 +0500 Subject: [PATCH 0148/1005] remove Auditor from invite member role and member deatail page for non ControlPolicy --- .../workspace/WorkspaceInviteMessageRolePage.tsx | 16 ++++++++++------ .../members/WorkspaceMemberDetailsPage.tsx | 14 +++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessageRolePage.tsx b/src/pages/workspace/WorkspaceInviteMessageRolePage.tsx index 37cb7ecee09f1..488b7915ed45a 100644 --- a/src/pages/workspace/WorkspaceInviteMessageRolePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessageRolePage.tsx @@ -13,7 +13,7 @@ import {setWorkspaceInviteRoleDraft} from '@libs/actions/Policy/Member'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {goBackFromInvalidPolicy} from '@libs/PolicyUtils'; +import {goBackFromInvalidPolicy, isControlPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -42,8 +42,8 @@ function WorkspaceInviteMessageRolePage({policy, route}: WorkspaceInviteMessageR const viewportOffsetTop = useViewportOffsetTop(); const isOnyxLoading = isLoadingOnyxValue(roleResult); - const roleItems: ListItemType[] = useMemo( - () => [ + const roleItems: ListItemType[] = useMemo(() => { + const items: ListItemType[] = [ { value: CONST.POLICY.ROLE.ADMIN, text: translate('common.admin'), @@ -65,9 +65,13 @@ function WorkspaceInviteMessageRolePage({policy, route}: WorkspaceInviteMessageR isSelected: role === CONST.POLICY.ROLE.USER, keyForList: CONST.POLICY.ROLE.USER, }, - ], - [role, translate], - ); + ]; + + if (!isControlPolicy(policy)) { + return items.filter((item) => item.value !== CONST.POLICY.ROLE.AUDITOR); + } + return items; + }, [role, translate, policy]); return ( [ + const roleItems: ListItemType[] = useMemo(() => { + const items: ListItemType[] = [ { value: CONST.POLICY.ROLE.ADMIN, text: translate('common.admin'), @@ -160,9 +160,13 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM isSelected: member?.role === CONST.POLICY.ROLE.USER, keyForList: CONST.POLICY.ROLE.USER, }, - ], - [member?.role, translate], - ); + ]; + + if (isControlPolicy(policy)) { + return items; + } + return member?.role === CONST.POLICY.ROLE.AUDITOR ? items : items.filter((item) => item.value !== CONST.POLICY.ROLE.AUDITOR); + }, [member?.role, translate, policy]); useEffect(() => { if (!prevMember || prevMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { From 7831df41f4a0afe54eb36427f0a1cd1e91c39a2c Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Tue, 7 Oct 2025 22:40:12 +0500 Subject: [PATCH 0149/1005] feat: check if imported spreadsheet has auditor role, direct user to upgrade --- .../ImportedMembersConfirmationPage.tsx | 60 +++++++++++-------- .../workspace/members/ImportedMembersPage.tsx | 9 ++- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index ebae5ede3e9bf..19eb733cfd55a 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; +import {InteractionManager} from 'react-native'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; @@ -24,7 +25,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getAccountIDsByLogins} from '@libs/PersonalDetailsUtils'; -import {isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; +import {isControlPolicy, isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import WorkspaceMemberDetailsRoleSelectionModal from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; import type {ListItemType} from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; @@ -52,7 +53,9 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag useEffect(() => { return () => { - clearImportedSpreadsheetMemberData(); + InteractionManager.runAfterInteractions(() => { + clearImportedSpreadsheetMemberData(); + }); }; }, []); @@ -100,29 +103,36 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag setIsRoleSelectionModalVisible(false); }; - const roleItems: ListItemType[] = [ - { - value: CONST.POLICY.ROLE.ADMIN, - text: translate('common.admin'), - alternateText: translate('workspace.common.adminAlternateText'), - isSelected: role === CONST.POLICY.ROLE.ADMIN, - keyForList: CONST.POLICY.ROLE.ADMIN, - }, - { - value: CONST.POLICY.ROLE.AUDITOR, - text: translate('common.auditor'), - alternateText: translate('workspace.common.auditorAlternateText'), - isSelected: role === CONST.POLICY.ROLE.AUDITOR, - keyForList: CONST.POLICY.ROLE.AUDITOR, - }, - { - value: CONST.POLICY.ROLE.USER, - text: translate('common.member'), - alternateText: translate('workspace.common.memberAlternateText'), - isSelected: role === CONST.POLICY.ROLE.USER, - keyForList: CONST.POLICY.ROLE.USER, - }, - ]; + const roleItems: ListItemType[] = useMemo(() => { + const items: ListItemType[] = [ + { + value: CONST.POLICY.ROLE.ADMIN, + text: translate('common.admin'), + alternateText: translate('workspace.common.adminAlternateText'), + isSelected: role === CONST.POLICY.ROLE.ADMIN, + keyForList: CONST.POLICY.ROLE.ADMIN, + }, + { + value: CONST.POLICY.ROLE.AUDITOR, + text: translate('common.auditor'), + alternateText: translate('workspace.common.auditorAlternateText'), + isSelected: role === CONST.POLICY.ROLE.AUDITOR, + keyForList: CONST.POLICY.ROLE.AUDITOR, + }, + { + value: CONST.POLICY.ROLE.USER, + text: translate('common.member'), + alternateText: translate('workspace.common.memberAlternateText'), + isSelected: role === CONST.POLICY.ROLE.USER, + keyForList: CONST.POLICY.ROLE.USER, + }, + ]; + + if (!isControlPolicy(policy)) { + return items.filter((item) => item.value !== CONST.POLICY.ROLE.AUDITOR); + } + return items; + }, [role, translate, policy]); if (!spreadsheet || !importedSpreadsheetMemberData) { return ; diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index d0d11b36090cd..e631104a6006e 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -80,14 +80,19 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const columns = Object.values(spreadsheet?.columns ?? {}); const containsAdvanceApprovalColumns = columns.includes(CONST.CSV_IMPORT_COLUMNS.SUBMIT_TO) || columns.includes(CONST.CSV_IMPORT_COLUMNS.APPROVE_TO); + const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); + const hasAuditorRole = + membersRolesColumn !== -1 && + spreadsheet?.data?.[membersRolesColumn]?.some( + (role, index) => (containsHeader ? spreadsheet?.data?.[membersRolesColumn]?.[index + 1] : (role ?? '')) === CONST.POLICY.ROLE.AUDITOR, + ); - if (containsAdvanceApprovalColumns && !isControlPolicy(policy)) { + if ((containsAdvanceApprovalColumns || hasAuditorRole) && !isControlPolicy(policy)) { Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(route.params.policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, Navigation.getActiveRoute())); return; } const membersEmailsColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.EMAIL); - const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); const membersEmails = spreadsheet?.data[membersEmailsColumn].map((email) => email); const membersRoles = membersRolesColumn !== -1 ? spreadsheet?.data[membersRolesColumn].map((role) => role) : []; const membersSubmitsToColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.SUBMIT_TO); From 336b6abb2be1cc98b6c3205243637cbd66eae7fc Mon Sep 17 00:00:00 2001 From: thelullabyy Date: Wed, 8 Oct 2025 00:43:21 +0700 Subject: [PATCH 0150/1005] fix: Account - RBR is shown after remove secondary contact --- .../ValidateCodeActionForm/index.tsx | 9 ------- .../Contacts/ContactMethodDetailsPage.tsx | 27 +++++++++++++------ .../MergeAccounts/AccountValidatePage.tsx | 18 +++++++++++++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/components/ValidateCodeActionForm/index.tsx b/src/components/ValidateCodeActionForm/index.tsx index 88a5ba5d614d6..c5a551ef41b34 100644 --- a/src/components/ValidateCodeActionForm/index.tsx +++ b/src/components/ValidateCodeActionForm/index.tsx @@ -36,15 +36,6 @@ function ValidateCodeActionForm({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [shouldSkipInitialValidation]); - useEffect(() => { - return () => { - if (!isUnmounted.current) { - return; - } - clearError(); - }; - }, [clearError]); - return ( {descriptionPrimary} diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index f897cdc68300f..a2e79b1f346f4 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -59,6 +59,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); + const didClearError = useRef(false); const {formatPhoneNumber, translate} = useLocalize(); const themeStyles = useThemeStyles(); @@ -164,6 +165,23 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo]); + const clearError = useCallback(() => { + // When removing unverified contact methods, the ValidateCodeActionForm unmounts and triggers clearError. + // This causes loginData to become an object, which makes sendValidateCode trigger, so we add this check to prevent clearing the error. + if (!loginData?.partnerUserID) { + return; + } + clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent'); + }, [contactMethod, loginData?.partnerUserID, validateLoginError]); + + useEffect(() => { + if (isLoadingOnyxValues || didClearError.current) { + return; + } + didClearError.current = true; + clearError(); + }, [clearError, isLoadingOnyxValues]); + useEffect(() => { setIsValidateCodeFormVisible(!loginData?.validatedDate); }, [loginData?.validatedDate, loginData?.errorFields?.addedLogin]); @@ -343,14 +361,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { hasMagicCodeBeenSent={hasMagicCodeBeenSent} handleSubmitForm={(validateCode) => validateSecondaryLogin(loginList, contactMethod, validateCode, formatPhoneNumber)} validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')} - clearError={() => { - // When removing unverified contact methods, the ValidateCodeActionForm unmounts and triggers clearError. - // This causes loginData to become an object, which makes sendValidateCode trigger, so we add this check to prevent clearing the error. - if (!loginData.partnerUserID) { - return; - } - clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent'); - }} + clearError={clearError} sendValidateCode={() => { if (!loginData.partnerUserID) { return; diff --git a/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx b/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx index 596126cf6217d..b472b6f03d809 100644 --- a/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx +++ b/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx @@ -30,6 +30,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const getMergeErrorPage = (err: string): ValueOf | null => { if (err.includes('403')) { @@ -92,6 +93,13 @@ function AccountValidatePage() { const privateSubscription = usePrivateSubscription(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [, loginListResult] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); + const [, sessionResult] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); + const [, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS, {canBeMissing: true}); + const [, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP, {canBeMissing: true}); + const [, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {canBeMissing: true}); + const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); + const {params} = useRoute>(); const email = params.login ?? ''; @@ -151,6 +159,16 @@ function AccountValidatePage() { return unsubscribe; }, [navigation]); + const didClearError = useRef(false); + + useEffect(() => { + if (isLoadingOnyxValues || didClearError.current) { + return; + } + didClearError.current = true; + clearMergeWithValidateCode(); + }, [isLoadingOnyxValues]); + const authenticationErrorKey = getAuthenticationErrorKey(latestError); const validateCodeError = !errorPage && authenticationErrorKey ? {authError: translate(authenticationErrorKey)} : undefined; From 4c00162b9c2dc58d4187c9952b01dc8362905114 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Wed, 8 Oct 2025 00:17:29 +0500 Subject: [PATCH 0151/1005] fix lint --- .../workspace/members/ImportedMembersConfirmationPage.tsx | 3 +-- src/pages/workspace/members/ImportedMembersPage.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index 19eb733cfd55a..2d370d1c4351e 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import {InteractionManager} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index e631104a6006e..7c30eb9559da5 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -84,7 +84,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const hasAuditorRole = membersRolesColumn !== -1 && spreadsheet?.data?.[membersRolesColumn]?.some( - (role, index) => (containsHeader ? spreadsheet?.data?.[membersRolesColumn]?.[index + 1] : (role ?? '')) === CONST.POLICY.ROLE.AUDITOR, + (role, index) => (containsHeader ? spreadsheet?.data?.[membersRolesColumn]?.at(index + 1) : (role ?? '')) === CONST.POLICY.ROLE.AUDITOR, ); if ((containsAdvanceApprovalColumns || hasAuditorRole) && !isControlPolicy(policy)) { From f98a3f23004121eb7c6366125bb9d4a56638547b Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Wed, 8 Oct 2025 01:18:51 +0500 Subject: [PATCH 0152/1005] fix failing check --- src/pages/workspace/members/ImportedMembersPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 7c30eb9559da5..d9f1865f72325 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -83,9 +83,9 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); const hasAuditorRole = membersRolesColumn !== -1 && - spreadsheet?.data?.[membersRolesColumn]?.some( - (role, index) => (containsHeader ? spreadsheet?.data?.[membersRolesColumn]?.at(index + 1) : (role ?? '')) === CONST.POLICY.ROLE.AUDITOR, - ); + spreadsheet?.data + ?.at(membersRolesColumn) + ?.some((role, index) => (containsHeader ? spreadsheet?.data?.at(membersRolesColumn)?.at(index + 1) : (role ?? '')) === CONST.POLICY.ROLE.AUDITOR); if ((containsAdvanceApprovalColumns || hasAuditorRole) && !isControlPolicy(policy)) { Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(route.params.policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, Navigation.getActiveRoute())); From 57d66da35d748d6d416dda81b4154d181211df16 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 8 Oct 2025 17:16:06 +0530 Subject: [PATCH 0153/1005] Remove Onyx.connect() for the key: ONYXKEYS.PERSONAL_DETAILS_LIST in src/libs/actions/Task.ts --- src/libs/actions/QuickActionNavigation.ts | 6 ++-- src/libs/actions/Task.ts | 33 ++++++++++--------- src/pages/home/report/ReportFooter.tsx | 2 +- .../FloatingActionButtonAndPopover.tsx | 8 ++++- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 8 +++-- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/QuickActionNavigation.ts b/src/libs/actions/QuickActionNavigation.ts index 4f9ecab58b927..0a4433897fbbc 100644 --- a/src/libs/actions/QuickActionNavigation.ts +++ b/src/libs/actions/QuickActionNavigation.ts @@ -1,5 +1,6 @@ import {generateReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; import type {DistanceExpenseType} from '@src/types/onyx/IOU'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import type QuickAction from '@src/types/onyx/QuickAction'; @@ -12,6 +13,7 @@ type NavigateToQuickActionParams = { quickAction: QuickAction; selectOption: (onSelected: () => void, shouldRestrictAction: boolean) => void; lastDistanceExpenseType?: DistanceExpenseType; + targetAccountPersonalDetails?: PersonalDetails | null; }; function getQuickActionRequestType(action: QuickActionName | undefined, lastDistanceExpenseType?: DistanceExpenseType): IOURequestType | undefined { @@ -34,7 +36,7 @@ function getQuickActionRequestType(action: QuickActionName | undefined, lastDist } function navigateToQuickAction(params: NavigateToQuickActionParams) { - const {isValidReport, quickAction, selectOption, lastDistanceExpenseType} = params; + const {isValidReport, quickAction, selectOption, lastDistanceExpenseType, targetAccountPersonalDetails} = params; const reportID = isValidReport && quickAction?.chatReportID ? quickAction?.chatReportID : generateReportID(); const requestType = getQuickActionRequestType(quickAction?.action, lastDistanceExpenseType); @@ -53,7 +55,7 @@ function navigateToQuickAction(params: NavigateToQuickActionParams) { selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, reportID, undefined, true), false); break; case CONST.QUICK_ACTIONS.ASSIGN_TASK: - selectOption(() => startOutCreateTaskQuickAction(isValidReport ? reportID : '', quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID), false); + selectOption(() => startOutCreateTaskQuickAction(isValidReport ? reportID : '', quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, targetAccountPersonalDetails), false); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: case CONST.QUICK_ACTIONS.TRACK_SCAN: diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 6b3778910764a..acf23607337bd 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -54,12 +54,6 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (value) => (allPersonalDetails = value), -}); - let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, @@ -814,7 +808,7 @@ function setAssigneeChatReport(chatReport: OnyxTypes.Report, isOptimisticReport } } -function setNewOptimisticAssignee(assigneeLogin: string, assigneeAccountID: number) { +function setNewOptimisticAssignee(assigneeLogin: string, assigneeAccountID: number, assigneePersonalDetails?: OnyxTypes.PersonalDetails | null) { const report: ReportUtils.OptimisticChatReport = ReportUtils.buildOptimisticChatReport({ participantList: [assigneeAccountID, currentUserAccountID], reportName: '', @@ -827,8 +821,8 @@ function setNewOptimisticAssignee(assigneeLogin: string, assigneeAccountID: numb const optimisticPersonalDetailsListAction: OnyxTypes.PersonalDetails = { accountID: assigneeAccountID, - avatar: allPersonalDetails?.[assigneeAccountID]?.avatar, - displayName: allPersonalDetails?.[assigneeAccountID]?.displayName ?? assigneeLogin, + avatar: assigneePersonalDetails?.avatar, + displayName: assigneePersonalDetails?.displayName ?? assigneeLogin, login: assigneeLogin, }; Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[assigneeAccountID]: optimisticPersonalDetailsListAction}); @@ -843,6 +837,7 @@ function setNewOptimisticAssignee(assigneeLogin: string, assigneeAccountID: numb function setAssigneeValue( assigneeEmail: string, assigneeAccountID: number, + assigneePersonalDetails?: OnyxTypes.PersonalDetails | null, shareToReportID?: string, chatReport?: OnyxEntry, isCurrentUser = false, @@ -863,7 +858,7 @@ function setAssigneeValue( } // If chat report is still not found we need to build new optimistic chat report if (!report) { - report = setNewOptimisticAssignee(assigneeEmail, assigneeAccountID).assigneeReport; + report = setNewOptimisticAssignee(assigneeEmail, assigneeAccountID, assigneePersonalDetails).assigneeReport; } const reportMetadata = ReportUtils.getReportMetadata(report?.reportID); @@ -905,14 +900,20 @@ function setParentReportID(parentReportID: string) { /** * Clears out the task info from the store and navigates to the NewTaskDetails page */ -function clearOutTaskInfoAndNavigate(reportID?: string, chatReport?: OnyxEntry, accountID = 0, skipConfirmation = false) { +function clearOutTaskInfoAndNavigate( + reportID?: string, + chatReport?: OnyxEntry, + assigneeAccountID = 0, + assigneePersonalDetails?: OnyxTypes.PersonalDetails | null, + skipConfirmation = false, +) { clearOutTaskInfo(skipConfirmation); if (reportID && reportID !== '0') { setParentReportID(reportID); } - if (accountID > 0) { - const accountLogin = allPersonalDetails?.[accountID]?.login ?? ''; - setAssigneeValue(accountLogin, accountID, reportID, chatReport, accountID === currentUserAccountID, skipConfirmation); + if (assigneeAccountID > 0) { + const accountLogin = assigneePersonalDetails?.login ?? ''; + setAssigneeValue(accountLogin, assigneeAccountID, assigneePersonalDetails, reportID, chatReport, assigneeAccountID === currentUserAccountID, skipConfirmation); } Navigation.navigate(ROUTES.NEW_TASK_DETAILS.getRoute(Navigation.getReportRHPActiveRoute())); } @@ -920,12 +921,12 @@ function clearOutTaskInfoAndNavigate(reportID?: string, chatReport?: OnyxEntry value?.login === mentionWithDomain) ?? undefined; if (!Object.keys(assignee ?? {}).length) { const assigneeAccountID = generateAccountID(mentionWithDomain); - const optimisticDataForNewAssignee = setNewOptimisticAssignee(mentionWithDomain, assigneeAccountID); + const optimisticDataForNewAssignee = setNewOptimisticAssignee(mentionWithDomain, assigneeAccountID, assignee); assignee = optimisticDataForNewAssignee.assignee; assigneeChatReport = optimisticDataForNewAssignee.assigneeReport; } diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index b7ccb7a4cc8d6..4de4d4649ccd2 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -364,7 +364,13 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref showDelegateNoAccessModal(); return; } - navigateToQuickAction({isValidReport, quickAction, selectOption, lastDistanceExpenseType}); + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails: personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + }); }); }; return [ diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 404618b208129..f5c7e90f6a950 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -5,7 +5,7 @@ import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, useSession} from '@components/OnyxListItemProvider'; +import {useBetas, usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionListWithSections'; @@ -118,6 +118,8 @@ function TaskAssigneeSelectorModal() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, debouncedSearchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); + const allPersonalDetails = usePersonalDetails(); + const report: OnyxEntry = useMemo(() => { if (!route.params?.reportID) { return; @@ -189,6 +191,7 @@ function TaskAssigneeSelectorModal() { const assigneeChatReport = setAssigneeValue( option?.login ?? '', option?.accountID ?? CONST.DEFAULT_NUMBER_ID, + allPersonalDetails?.[option?.accountID ?? CONST.DEFAULT_NUMBER_ID], report.reportID, undefined, // passing null as report because for editing task the report will be task details report page not the actual report where task was created isCurrentUser({...option, accountID: option?.accountID ?? CONST.DEFAULT_NUMBER_ID, login: option?.login ?? ''}), @@ -205,6 +208,7 @@ function TaskAssigneeSelectorModal() { setAssigneeValue( option?.login ?? '', option.accountID ?? CONST.DEFAULT_NUMBER_ID, + allPersonalDetails?.[option?.accountID ?? CONST.DEFAULT_NUMBER_ID], task?.shareDestination ?? '', undefined, // passing null as report is null in this condition isCurrentUser({...option, accountID: option?.accountID ?? CONST.DEFAULT_NUMBER_ID, login: option?.login ?? undefined}), @@ -215,7 +219,7 @@ function TaskAssigneeSelectorModal() { }); } }, - [session?.accountID, task?.shareDestination, report, backTo], + [report, allPersonalDetails, session?.accountID, task?.shareDestination, backTo], ); const handleBackButtonPress = useCallback(() => Navigation.goBack(!route.params?.reportID ? ROUTES.NEW_TASK.getRoute(backTo) : backTo), [route.params, backTo]); From b7974f31c29b00b7cbe3caed6a265b73cb5613ac Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 8 Oct 2025 21:28:34 +0700 Subject: [PATCH 0154/1005] implement NewChatPage --- .../FilterDropdowns/UserSelectPopup.tsx | 1 + .../SearchFiltersParticipantsSelector.tsx | 1 + src/hooks/useSearchSelector.base.ts | 4 ++ src/pages/NewChatPage.tsx | 54 +++++++------------ 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 44cbb019f5f40..38c4b6e433275 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -70,6 +70,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, initialSelected: initialSelectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, includeUserToInvite: false, includeCurrentUser: true, }); diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index ed8387ee57085..67699a7308ead 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -39,6 +39,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: const {searchTerm, setSearchTerm, availableOptions, selectedOptions, setSelectedOptions, toggleSelection, areOptionsInitialized} = useSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, includeUserToInvite: true, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeRecentReports: true, diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 27ca596bcb240..978f47e8be1a4 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -87,6 +87,9 @@ type UseSearchSelectorReturn = { /** Function to update search term */ setSearchTerm: (value: string) => void; + /** Debounced search term */ + debouncedSearchTerm: string; + /** Filtered and optimized search options with selection state */ searchOptions: Options; @@ -298,6 +301,7 @@ function useSearchSelectorBase({ contactState: undefined, onListEndReached, selectedOptionsForDisplay, + debouncedSearchTerm, }; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 152e9c05c789b..79d0646c5e322 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -16,30 +16,20 @@ import type {ListItem, SelectionListHandle} from '@components/SelectionListWithS import UserListItem from '@components/SelectionListWithSections/UserListItem'; import useContactImport from '@hooks/useContactImport'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebouncedState from '@hooks/useDebouncedState'; import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useSearchSelector from '@hooks/useSearchSelector'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/actions/Report'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; -import memoize from '@libs/memoize'; import Navigation from '@libs/Navigation/Navigation'; import type {Option, Section} from '@libs/OptionsListUtils'; -import { - filterAndOrderOptions, - filterSelectedOptions, - formatSectionsFromSearchTerm, - getFirstKeyForList, - getHeaderMessage, - getPersonalDetailSearchTerms, - getUserToInviteOption, - getValidOptions, -} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatSectionsFromSearchTerm, getFirstKeyForList, getHeaderMessage, getPersonalDetailSearchTerms, getUserToInviteOption} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -55,46 +45,38 @@ type SelectedOption = ListItem & reportID?: string; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'NewChatPage.getValidOptions'}); - function useOptions() { - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + // const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + // const [selectedOptions, setSelectedOptions] = useState([]); const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {canBeMissing: true}); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const personalData = useCurrentUserPersonalDetails(); const focusTimeoutRef = useRef(null); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const {contacts} = useContactImport(); const {options: listOptions, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); + const {contacts} = useContactImport(); - const defaultOptions = useMemo(() => { - const filteredOptions = memoizedGetValidOptions( - { - reports: listOptions.reports ?? [], - personalDetails: (listOptions.personalDetails ?? []).concat(contacts), - }, - { - betas: betas ?? [], - includeSelfDM: true, - }, - ); - return filteredOptions; - }, [betas, listOptions.personalDetails, listOptions.reports, contacts]); - - const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]); + const {selectedOptions, setSelectedOptions, searchTerm, setSearchTerm, availableOptions, debouncedSearchTerm} = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, + includeCurrentUser: true, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + shouldInitialize: didScreenTransitionEnd, + includeUserToInvite: true, + contactOptions: contacts, + }); const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(unselectedOptions, debouncedSearchTerm, countryCode, { + const filteredOptions = filterAndOrderOptions(availableOptions, searchTerm, countryCode, { selectedOptions, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); return filteredOptions; - }, [debouncedSearchTerm, unselectedOptions, selectedOptions, countryCode]); + }, [availableOptions, searchTerm, countryCode, selectedOptions]); + const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const headerMessage = useMemo(() => { return getHeaderMessage( @@ -147,7 +129,7 @@ function useOptions() { }); }); setSelectedOptions(newSelectedOptions); - }, [newGroupDraft?.participants, listOptions.personalDetails, personalData.accountID]); + }, [newGroupDraft?.participants, listOptions.personalDetails, personalData.accountID, setSelectedOptions]); return { ...options, From 1fbeee2969be7063176ed936c9de8e955f94df9b Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 8 Oct 2025 21:44:18 +0700 Subject: [PATCH 0155/1005] fix ts check --- src/hooks/useSearchSelector.base.ts | 7 +++++++ src/pages/NewChatPage.tsx | 12 +++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 978f47e8be1a4..a9d34b9f2c71c 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -41,6 +41,9 @@ type UseSearchSelectorConfig = { /** Enable phone contacts integration */ enablePhoneContacts?: boolean; + /** Whether to include self DM */ + includeSelfDM?: boolean; + /** Additional configuration for getValidOptions function */ getValidOptionsConfig?: Partial; @@ -145,6 +148,7 @@ function useSearchSelectorBase({ shouldInitialize = true, contactOptions, includeCurrentUser = false, + includeSelfDM = false, }: UseSearchSelectorConfig): UseSearchSelectorReturn { const {options: defaultOptions, areOptionsInitialized} = useOptionsList({ shouldInitialize, @@ -194,6 +198,7 @@ function useSearchSelectorBase({ searchString: computedSearchTerm, includeUserToInvite, includeCurrentUser, + includeSelfDM, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL: return getValidOptions(optionsWithContacts, { @@ -205,6 +210,7 @@ function useSearchSelectorBase({ includeUserToInvite, loginsToExclude: excludeLogins, includeCurrentUser, + includeSelfDM, }); default: return getEmptyOptions(); @@ -222,6 +228,7 @@ function useSearchSelectorBase({ includeCurrentUser, maxRecentReportsToShow, getValidOptionsConfig, + includeSelfDM, ]); const isOptionSelected = useMemo(() => { diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 79d0646c5e322..80c11f9c8f2e3 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -40,10 +40,7 @@ import KeyboardUtils from '@src/utils/keyboard'; const excludedGroupEmails: string[] = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -type SelectedOption = ListItem & - Omit & { - reportID?: string; - }; +type SelectedOption = ListItem & OptionData; function useOptions() { // const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -61,11 +58,11 @@ function useOptions() { const {selectedOptions, setSelectedOptions, searchTerm, setSearchTerm, availableOptions, debouncedSearchTerm} = useSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, - includeCurrentUser: true, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, shouldInitialize: didScreenTransitionEnd, includeUserToInvite: true, contactOptions: contacts, + includeSelfDM: true, }); const options = useMemo(() => { @@ -178,7 +175,7 @@ function NewChatPage({ref}: NewChatPageProps) { const formatResults = formatSectionsFromSearchTerm( debouncedSearchTerm, - selectedOptions as OptionData[], + selectedOptions, recentReports, personalDetails, undefined, @@ -236,7 +233,8 @@ function NewChatPage({ref}: NewChatPageProps) { if (isOptionInList) { newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID}]; + // eslint-disable-next-line rulesdir/no-default-id-values + newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID ?? '-1'}]; selectionListRef?.current?.scrollToIndex(0, true); } From 31d2e153e2a461d59c5f9710ce35a2f9a223fcb5 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 8 Oct 2025 21:51:21 +0700 Subject: [PATCH 0156/1005] run prettier --- src/pages/NewChatPage.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 80c11f9c8f2e3..2176409f46ddf 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -173,16 +173,7 @@ function NewChatPage({ref}: NewChatPageProps) { const sectionsList: Section[] = []; let firstKey = ''; - const formatResults = formatSectionsFromSearchTerm( - debouncedSearchTerm, - selectedOptions, - recentReports, - personalDetails, - undefined, - undefined, - undefined, - reportAttributesDerived, - ); + const formatResults = formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails, undefined, undefined, undefined, reportAttributesDerived); sectionsList.push(formatResults.section); if (!firstKey) { From fbd4eaaa26506d170fdf30ce6b02d661b757f5b3 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Thu, 9 Oct 2025 19:43:10 +0800 Subject: [PATCH 0157/1005] fix: optimizing --- src/components/Search/SearchList/index.tsx | 24 +++++++--------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index bde8e88994f68..ecba167c4e64c 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -39,7 +39,6 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import navigationRef from '@libs/Navigation/navigationRef'; -import {isTransactionPendingDelete} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -188,36 +187,27 @@ function SearchList({ }, [data, groupBy]); const selectedItemsLength = useMemo(() => { + const selectedTransactions = flattenedItems.reduce((acc, item) => { + return acc + (item?.isSelected ? 1 : 0); + }, 0); + if (groupBy && isTransactionGroupListItemArray(data)) { const selectedEmptyReports = emptyReports.reduce((acc, item) => { return acc + (item.isSelected ? 1 : 0); }, 0); - const selectedTransactions = flattenedItems.reduce((acc, item) => { - return acc + (item?.isSelected ? 1 : 0); - }, 0); - return selectedEmptyReports + selectedTransactions; } - return flattenedItems.reduce((acc, item) => { - return acc + (item?.isSelected ? 1 : 0); - }, 0); + return selectedTransactions; }, [data, flattenedItems, groupBy, emptyReports]); const totalItems = useMemo(() => { if (groupBy && isTransactionGroupListItemArray(data)) { - const nonPendingTransactions = flattenedItems.filter((transaction) => !isTransactionPendingDelete(transaction as TransactionListItemType)); - return emptyReports.length + nonPendingTransactions.length; + return emptyReports.length + flattenedItems.length; } - return data.reduce((acc, item) => { - if ('transactions' in item && item.transactions?.length) { - const transactions = item.transactions.filter((transaction) => !isTransactionPendingDelete(transaction)); - return acc + transactions.length; - } - return acc + 1; - }, 0); + return flattenedItems.length; }, [data, groupBy, flattenedItems, emptyReports]); const {translate} = useLocalize(); From e4186b528c5bb5268453b56aa81542e6a52a2a2d Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 10 Oct 2025 03:00:41 +0700 Subject: [PATCH 0158/1005] fix: reduce the PDF download steps by one click --- src/CONST/index.ts | 1 + src/components/MoneyReportHeader.tsx | 32 ++++++++++++++-------------- src/languages/en.ts | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a107b734a2ccd..d62b63b4d9b17 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3242,6 +3242,7 @@ const CONST = { ACTIVITY_INDICATOR_SIZE: { LARGE: 'large', + SMALL: 'small', }, QR_CODE_SIZE: { diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 374adc355c08c..202a845b8bb5e 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1136,6 +1136,14 @@ function MoneyReportHeader({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [transactionThreadReportID]); + useEffect(() => { + if(!isPDFModalVisible || !reportPDFFilename || reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR || isDownloadingPDF) { + return; + } + downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? ''); + setIsPDFModalVisible(false); + }, [isPDFModalVisible, reportPDFFilename, isDownloadingPDF, moneyRequestReport?.reportName]); + const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout; const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : ''; @@ -1477,37 +1485,29 @@ function MoneyReportHeader({ innerContainerStyle={styles.pv0} > - - + +
+ {messagePDF} - - {messagePDF} - {!reportPDFFilename && ( + + {true && ( )} - {!!reportPDFFilename && reportPDFFilename !== 'error' && ( - + + ); + + case 'loading': + return ; + + default: + return ; + } +} + +type VerifyDomainPageProps = PlatformStackScreenProps; + function VerifyDomainPage({route}: VerifyDomainPageProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -55,7 +81,7 @@ function VerifyDomainPage({route}: VerifyDomainPageProps) { return; } getDomainValidationCode(accountID); - }); + }, [accountID]); return ( {translate('domain.verifyDomain.addTXTRecord')} - - getDomainValidationCode(accountID)} /> @@ -116,7 +142,7 @@ function VerifyDomainPage({route}: VerifyDomainPageProps) { validateDomain(accountID)} - message={domain?.validationError} + message={domain?.validationError ?? undefined} isAlertVisible={!!domain?.validationError} containerStyles={styles.mb5} isLoading={domain?.isValidationPending} diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts index ca844e3ace499..c5052df59b605 100644 --- a/src/types/onyx/Domain.ts +++ b/src/types/onyx/Domain.ts @@ -21,10 +21,10 @@ type Domain = OnyxCommon.OnyxValueWithOfflineFeedback<{ isValidationPending?: boolean; /** Whether domain validation is pending */ - validationError?: string; + validationError?: string | null; - /** Whether validation code is currently loading */ - isValidateCodeLoading?: boolean; + /** Whether validation code is currently loading or has failed/succeeded */ + validateCodeLoadingStatus?: 'loading' | 'error' | 'success'; }>; export default Domain; From e1021fc3a5df7c2fe909aa115cfb3758246dd98a Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 29 Oct 2025 01:00:31 +0700 Subject: [PATCH 0445/1005] fix Selecting hold throws an error --- src/libs/actions/IOU.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9bae78e7ed157..efa381d914173 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12756,6 +12756,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st const reportAction = getIOUActionForReportID(reportID, transactionID); const childReportID = reportAction?.childReportID; + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`]; let movedToReport; let rejectedToReportID; @@ -13045,6 +13046,14 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st [reportPreviewAction.reportActionID]: reportPreviewAction, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + parentReportActionID: iouAction.reportActionID, + parentReportID: rejectedToReportID, + }, + }, ); successData.push( { @@ -13110,6 +13119,14 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st [reportPreviewAction.reportActionID]: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: { + parentReportActionID: transactionThreadReport?.parentReportActionID, + parentReportID: transactionThreadReport?.parentReportID, + }, + }, ); } optimisticData.push( From 323928baa3716a5a33ab1227ccd1c2b646a6f5b0 Mon Sep 17 00:00:00 2001 From: Stephanie Elliott <31225194+stephanieelliott@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:45:25 -1000 Subject: [PATCH 0446/1005] Update Commercial-feeds.md --- .../new-expensify/connect-credit-cards/Commercial-feeds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md b/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md index 9c83ba20c1c5a..684ab5511283e 100644 --- a/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md +++ b/docs/articles/new-expensify/connect-credit-cards/Commercial-feeds.md @@ -1,7 +1,7 @@ --- title: Commercial Card Feeds description: Learn how to set up and manage commercial card feeds (Visa, Mastercard, Amex) in Expensify. -keywords: [New Expensify, commercial feed, Mastercard feed, Visa feed, Amex feed, company cards, corporate cards, CDF, VCF, GH1025m control account] +keywords: [New Expensify, commercial feed, Mastercard feed, Visa feed, Amex feed, company cards, corporate cards, CDF, VCF, GH1025, control account] --- Commercial feeds are the most reliable way to import company card expenses. These feeds are not affected by login credential changes or banking UI updates, making them ideal for growing teams and finance admins. From bf9359dc849b59a8f4f30f24d89a251095f42d2a Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 29 Oct 2025 02:03:00 +0700 Subject: [PATCH 0447/1005] fix: incorrect owner account ID --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index efa381d914173..cb08e3327308b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12977,7 +12977,7 @@ function rejectMoneyRequest(transactionID: string, reportID: string, comment: st const newExpenseReport = buildOptimisticExpenseReport( report.chatReportID, report?.policyID, - userAccountID, + report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID, transactionAmount, getCurrency(transaction), transactionAmount, From b8da880e18420d8ad5284815da6907b5517ba186 Mon Sep 17 00:00:00 2001 From: Carlos Miceli Date: Tue, 28 Oct 2025 18:49:04 -0300 Subject: [PATCH 0448/1005] remove call migrated to auth --- src/libs/actions/Report.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2e688ea78a95e..9734767cd6205 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -215,7 +215,7 @@ import {setDownload} from './Download'; import {close} from './Modal'; import navigateFromNotification from './navigateFromNotification'; import {getAll} from './PersistedRequests'; -import {addMembersToWorkspace, buildAddMembersToWorkspaceOnyxData, buildRoomMembersOnyxData} from './Policy/Member'; +import {buildAddMembersToWorkspaceOnyxData, buildRoomMembersOnyxData} from './Policy/Member'; import {createPolicyExpenseChats} from './Policy/Policy'; import { createUpdateCommentMatcher, @@ -4552,30 +4552,6 @@ function resolveActionableMentionWhisper( return; } - if (ReportActionsUtils.isActionableMentionWhisper(reportAction) && resolution === CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE) { - const actionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction); - - const policyID = policy?.id; - - if (actionOriginalMessage && policyID) { - const currentUserDetails = allPersonalDetails?.[getCurrentUserAccountID()]; - const welcomeNoteSubject = `# ${currentUserDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`; - // eslint-disable-next-line @typescript-eslint/no-deprecated - const welcomeNote = Localize.translateLocal('workspace.common.welcomeNote'); - const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); - - const invitees: Record = {}; - actionOriginalMessage.inviteeEmails?.forEach((email, index) => { - if (!email) { - return; - } - invitees[email] = actionOriginalMessage.inviteeAccountIDs?.at(index) ?? CONST.DEFAULT_NUMBER_ID; - }); - - addMembersToWorkspace(invitees, `${welcomeNoteSubject}\n\n${welcomeNote}`, policyID, policyMemberAccountIDs, CONST.POLICY.ROLE.USER, formatPhoneNumber); - } - } - const message = ReportActionsUtils.getReportActionMessage(reportAction); if (!message) { return; From eb7bccae548c7d5fc16262bed990f4e373c57a99 Mon Sep 17 00:00:00 2001 From: Carlos Miceli Date: Tue, 28 Oct 2025 19:03:10 -0300 Subject: [PATCH 0449/1005] linting --- src/libs/actions/Report.ts | 5 +---- src/pages/home/report/PureReportActionItem.tsx | 6 ------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 9734767cd6205..1f9c40504ada6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -72,7 +72,6 @@ import {getMicroSecondOnyxErrorWithTranslationKey, getMicroSecondTranslationErro import fileDownload from '@libs/fileDownload'; import HttpUtils from '@libs/HttpUtils'; import isPublicScreenRoute from '@libs/isPublicScreenRoute'; -import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; @@ -4544,9 +4543,7 @@ function resolveActionableMentionWhisper( reportID: string | undefined, reportAction: OnyxEntry, resolution: ValueOf | ValueOf, - formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], isReportArchived: boolean | undefined, - policy?: OnyxEntry, ) { if (!reportAction || !reportID) { return; @@ -4634,7 +4631,7 @@ function resolveActionableMentionConfirmWhisper( formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], isReportArchived: boolean, ) { - resolveActionableMentionWhisper(reportID, reportAction, resolution, formatPhoneNumber, isReportArchived, undefined); + resolveActionableMentionWhisper(reportID, reportAction, resolution, isReportArchived); } function resolveActionableReportMentionWhisper( diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 09466a3c31380..94e2fafc3e9ce 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -910,9 +910,7 @@ function PureReportActionItem({ reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, - formatPhoneNumber, isOriginalReportArchived, - policy, ), isMediumSized: true, }); @@ -927,9 +925,7 @@ function PureReportActionItem({ reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, - formatPhoneNumber, isOriginalReportArchived, - policy, ), isMediumSized: true, }, @@ -941,9 +937,7 @@ function PureReportActionItem({ reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, - formatPhoneNumber, isOriginalReportArchived, - policy, ), isMediumSized: true, }, From 2ace41f223ae942bfec21cc35105825715257c79 Mon Sep 17 00:00:00 2001 From: Carlos Miceli Date: Tue, 28 Oct 2025 19:07:42 -0300 Subject: [PATCH 0450/1005] prettier --- .../home/report/PureReportActionItem.tsx | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 94e2fafc3e9ce..8586e4bffd9e7 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -906,12 +906,7 @@ function PureReportActionItem({ text: 'actionableMentionWhisperOptions.inviteToSubmitExpense', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`, onPress: () => - resolveActionableMentionWhisper( - reportActionReportID, - action, - CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, - isOriginalReportArchived, - ), + resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, isOriginalReportArchived), isMediumSized: true, }); } @@ -920,25 +915,13 @@ function PureReportActionItem({ { text: 'actionableMentionWhisperOptions.inviteToChat', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, - onPress: () => - resolveActionableMentionWhisper( - reportActionReportID, - action, - CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, - isOriginalReportArchived, - ), + onPress: () => resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, isOriginalReportArchived), isMediumSized: true, }, { text: 'actionableMentionWhisperOptions.nothing', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => - resolveActionableMentionWhisper( - reportActionReportID, - action, - CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, - isOriginalReportArchived, - ), + onPress: () => resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, isOriginalReportArchived), isMediumSized: true, }, ); From c44cb907eb1c05fed10b6b90cd2c7f6477931eca Mon Sep 17 00:00:00 2001 From: Carlos Miceli Date: Tue, 28 Oct 2025 19:09:22 -0300 Subject: [PATCH 0451/1005] type checks --- src/pages/home/report/PureReportActionItem.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 8586e4bffd9e7..8a1c12488e166 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -341,9 +341,7 @@ type PureReportActionItemProps = { reportId: string | undefined, reportAction: OnyxEntry, resolution: ValueOf, - formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], isReportArchived: boolean, - policy: OnyxEntry, ) => void; /** Whether the provided report is a closed expense report with no expenses */ From f0d64b16eee000b70a0abc0066c6736a3941d0af Mon Sep 17 00:00:00 2001 From: Carlos Miceli Date: Tue, 28 Oct 2025 19:35:20 -0300 Subject: [PATCH 0452/1005] lint again --- src/pages/home/report/PureReportActionItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 8a1c12488e166..b30b4eff4edcf 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -18,7 +18,6 @@ import {Eye} from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; -import type {LocaleContextProps} from '@components/LocaleContextProvider'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; From 2f6ddeb518092472e7c863864e17f91fc5032c4c Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 28 Oct 2025 16:22:16 -0700 Subject: [PATCH 0453/1005] updated UI / functionality to match design --- src/CONST/index.ts | 3 ++ .../SplitListItem.tsx | 7 ++- .../SelectionListWithSections/types.ts | 19 +++++++- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- src/pages/iou/SplitExpensePage.tsx | 46 ++++++++++++------- 14 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ededd81cf5090..6b5a982932fb2 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7317,6 +7317,9 @@ const CONST = { INVITE_ACCOUNTANT: 'inviteAccountant', ADD_EXPENSE_APPROVALS: 'addExpenseApprovals', }, + ENHANCED_SECTIONS: { + SPLIT_EXPENSE_ACTIONS: 'splitExpenseActions', + }, } as const; const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [ diff --git a/src/components/SelectionListWithSections/SplitListItem.tsx b/src/components/SelectionListWithSections/SplitListItem.tsx index c5d89b7d28513..ac497ab3c3142 100644 --- a/src/components/SelectionListWithSections/SplitListItem.tsx +++ b/src/components/SelectionListWithSections/SplitListItem.tsx @@ -13,7 +13,7 @@ import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {ListItem, SplitListItemProps, SplitListItemType} from './types'; +import type {EnhancedSectionListItem, ListItem, SplitListItemProps, SplitListItemType} from './types'; function SplitListItem({ item, @@ -56,6 +56,11 @@ function SplitListItem({ onInputFocus(index); }, [onInputFocus, index]); + const enhancedItem = item as Partial; + if (enhancedItem.key === CONST.ENHANCED_SECTIONS.SPLIT_EXPENSE_ACTIONS && enhancedItem.component) { + return enhancedItem.component; + } + return ( = { /** Title of the section */ @@ -621,9 +623,21 @@ type SectionWithIndexOffset = Section & { indexOffset?: number; }; +type EnhancedSectionListItem = ListItem & { + /** Key to identify the type of enhanced section */ + key: (typeof CONST.ENHANCED_SECTIONS)[keyof typeof CONST.ENHANCED_SECTIONS]; + /** The component to render for this enhanced section */ + component: React.JSX.Element; +}; + +type EnhancedSectionListType = { + /** Enhanced type for sections data used for adding items at the bottom of the list sections (e.g. action buttons).*/ + data: EnhancedSectionListItem[]; +}; + type SelectionListProps = Partial & { /** Sections for the section list */ - sections: Array> | typeof CONST.EMPTY_ARRAY; + sections: Array> | Array | EnhancedSectionListType> | typeof CONST.EMPTY_ARRAY; /** List of selected items */ selectedItems?: string[]; @@ -997,6 +1011,7 @@ type SearchListItem = TransactionListItemType | TransactionGroupListItemType | R export type { BaseListItemProps, + EnhancedSectionListItem, SelectionListProps, ButtonOrCheckBoxRoles, ExtendedTargetedEvent, diff --git a/src/languages/de.ts b/src/languages/de.ts index a7fd40e72dba7..1d78d2313c2ef 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1125,7 +1125,7 @@ const translations = { splitExpense: 'Ausgabe aufteilen', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} von ${merchant}`, addSplit: 'Split hinzufügen', - splitEvenly: 'Gleichmäßig aufteilen', + makeSplitsEven: 'Aufteilungen angleichen', editSplits: 'Splits bearbeiten', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Der Gesamtbetrag ist ${amount} höher als die ursprüngliche Ausgabe.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Der Gesamtbetrag ist ${amount} weniger als die ursprüngliche Ausgabe.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index e2d1b205fbc22..1d36ead6b85dd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1103,7 +1103,7 @@ const translations = { splitExpense: 'Split expense', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} from ${merchant}`, addSplit: 'Add split', - splitEvenly: 'Split evenly', + makeSplitsEven: 'Make splits even', editSplits: 'Edit splits', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Total amount is ${amount} greater than the original expense.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Total amount is ${amount} less than the original expense.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 643193b7cdd41..d59d953b480c4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1092,7 +1092,7 @@ const translations = { splitExpense: 'Dividir gasto', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Añadir división', - splitEvenly: 'Dividir equitativamente', + makeSplitsEven: 'Igualar divisiones', editSplits: 'Editar divisiones', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `El importe total es ${amount} mayor que el gasto original.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `El importe total es ${amount} menor que el gasto original.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2b718ff05f60a..2680c5b248bab 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1122,7 +1122,7 @@ const translations = { splitExpense: 'Fractionner la dépense', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Ajouter une répartition', - splitEvenly: 'Répartir équitablement', + makeSplitsEven: 'Uniformiser les répartitions', editSplits: 'Modifier les répartitions', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Le montant total est de ${amount} supérieur à la dépense initiale.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Le montant total est de ${amount} inférieur à la dépense originale.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 524641e06a150..96fd12fa32554 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1117,7 +1117,7 @@ const translations = { splitExpense: 'Dividi spesa', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} da ${merchant}`, addSplit: 'Aggiungi divisione', - splitEvenly: 'Dividi equamente', + makeSplitsEven: 'Uniformare le suddivisioni', editSplits: 'Modifica suddivisioni', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `L'importo totale è ${amount} maggiore della spesa originale.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `L'importo totale è ${amount} inferiore alla spesa originale.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b10927ad74afa..7a99fb0ec89fb 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1119,7 +1119,7 @@ const translations = { splitExpense: '経費を分割', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${merchant}から${amount}`, addSplit: '分割を追加', - splitEvenly: '均等に分割', + makeSplitsEven: '分割を均等にする', editSplits: '分割を編集', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `合計金額は元の経費よりも${amount}多いです。`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `合計金額は元の経費よりも${amount}少なくなっています。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 646abe91fe030..45b5629603fea 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1117,7 +1117,7 @@ const translations = { splitExpense: 'Uitgave splitsen', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} van ${merchant}`, addSplit: 'Splits toevoegen', - splitEvenly: 'Gelijk verdelen', + makeSplitsEven: 'Verdelingen gelijk maken', editSplits: 'Splits bewerken', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Het totale bedrag is ${amount} meer dan de oorspronkelijke uitgave.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Het totale bedrag is ${amount} minder dan de oorspronkelijke uitgave.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 852aa21a02d2f..bc7decad3dfef 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1117,7 +1117,7 @@ const translations = { splitExpense: 'Podziel wydatek', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} od ${merchant}`, addSplit: 'Dodaj podział', - splitEvenly: 'Podziel po równo', + makeSplitsEven: 'Wyrównaj podziały', editSplits: 'Edytuj podziały', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Całkowita kwota jest o ${amount} większa niż pierwotny wydatek.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Całkowita kwota jest o ${amount} mniejsza niż pierwotny wydatek.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d02f14aef7a59..545003316510d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1116,7 +1116,7 @@ const translations = { splitExpense: 'Dividir despesa', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Adicionar divisão', - splitEvenly: 'Dividir igualmente', + makeSplitsEven: 'Tornar as divisões iguais', editSplits: 'Editar divisões', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `O valor total é ${amount} maior que a despesa original.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `O valor total é ${amount} a menos que a despesa original.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c3a37632cb472..acb69c7070d42 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1111,7 +1111,7 @@ const translations = { splitExpense: '拆分费用', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `来自${merchant}的${amount}`, addSplit: '添加分账', - splitEvenly: '平均分配', + makeSplitsEven: '使拆分均等', editSplits: '编辑拆分', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `总金额比原始费用多${amount}。`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `总金额比原始费用少 ${amount}。`, diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index b5f68cef0647e..3700529a74e65 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -8,6 +8,7 @@ import ConfirmModal from '@components/ConfirmModal'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import {useSearchContext} from '@components/Search/SearchContext'; import SelectionList from '@components/SelectionListWithSections'; @@ -119,7 +120,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { addSplitExpenseField(transaction, draftTransaction); }, [draftTransaction, transaction, transactionID]); - const onSplitEvenly = useCallback(() => { + const onMakeSplitsEven = useCallback(() => { if (!draftTransaction) { return; } @@ -288,29 +289,43 @@ function SplitExpensePage({route}: SplitExpensePageProps) { getTranslatedText, ]); - const shouldShowSplitEvenly = useMemo(() => childTransactions.length === 0, [childTransactions.length]); + const shouldShowMakeSplitsEven = useMemo(() => childTransactions.length === 0, [childTransactions.length]); - const headerContent = useMemo( + const ActionButtons = useMemo( () => ( - - - - ); - - case 'loading': - return ; - - default: - return ; - } -} - type VerifyDomainPageProps = PlatformStackScreenProps; function VerifyDomainPage({route}: VerifyDomainPageProps) { @@ -111,16 +87,32 @@ function VerifyDomainPage({route}: VerifyDomainPageProps) { - - + + {translate('domain.verifyDomain.addTXTRecord')} - getDomainValidationCode(accountID)} - /> - - + + {domain?.validateCodeLoadingStatus !== 'error' && ( + + )} + + + {domain?.validateCodeLoadingStatus === 'error' && ( + + +