From af89dea357d9213260b348d284f7d2fd1fafbf2c Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 00:55:02 +0000 Subject: [PATCH 01/13] Refactor Policy Tag Management Functions - Updated `createPolicyTag` to accept `policyData` instead of `policyID` and `policyTags`, simplifying the function signature and improving data handling. - Refactored `setWorkspaceTagEnabled`, `setWorkspaceTagRequired`, `deletePolicyTags`, and `renamePolicyTag` to utilize `tagLists` directly from `policyData`, enhancing consistency across tag operations. - Adjusted optimistic data structures to align with the new `tagLists` format, ensuring accurate state management during API interactions. - Modified tests to accommodate changes in function signatures and data structures, ensuring robust coverage for tag management functionalities. - Updated `IOURequestStepCategory` and `WorkspaceCreateTagPage` to use the new `usePolicyData` hook for fetching policy data, improving data retrieval efficiency. --- src/hooks/usePolicyData/index.ts | 19 +- src/hooks/usePolicyData/types.ts | 9 +- src/libs/actions/Policy/Category.ts | 43 ++-- src/libs/actions/Policy/Tag.ts | 218 ++++++++---------- .../request/step/IOURequestStepCategory.tsx | 2 +- .../workspace/tags/WorkspaceCreateTagPage.tsx | 18 +- tests/actions/PolicyTagTest.ts | 146 ++++++------ 7 files changed, 220 insertions(+), 235 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index b67328916b7f2..b09b6cf343281 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; @@ -14,8 +14,7 @@ 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); +function usePolicyData(policyID: string): PolicyData { const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); // Stable selector for useOnyx to avoid defining the selector inline @@ -37,15 +36,16 @@ function usePolicyData(policyID?: string): PolicyData { [policyID, allReportsTransactionsAndViolations], ); - const [tags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}, [policyID]); + const policy = usePolicy(policyID); + const [tagLists] = 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: reportsSelectorCallback}, [policyID, allReportsTransactionsAndViolations]); const transactionsAndViolations = useMemo(() => { if (!reports || !allReportsTransactionsAndViolations) { return {}; } - return Object.keys(reports).reduce((acc, reportID) => { - if (allReportsTransactionsAndViolations[reportID]) { + return Object.entries(reports).reduce((acc, [reportID]) => { + if (Object.keys(allReportsTransactionsAndViolations[reportID].transactions).length > 0) { acc[reportID] = allReportsTransactionsAndViolations[reportID]; } return acc; @@ -53,10 +53,11 @@ function usePolicyData(policyID?: string): PolicyData { }, [reports, allReportsTransactionsAndViolations]); return { transactionsAndViolations, - tags: tags ?? {}, + tagLists: tagLists, categories: categories ?? {}, - policy: policy as OnyxValueWithOfflineFeedback, - reports: Object.values(reports ?? {}) as Array>, + policyID, + policy, + reports: Object.values(reports ?? {}), }; } diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts index df3367cd1fccc..e64789a38a332 100644 --- a/src/hooks/usePolicyData/types.ts +++ b/src/hooks/usePolicyData/types.ts @@ -1,12 +1,13 @@ +import {OnyxEntry} from 'react-native-onyx'; 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; + policyID: string; + policy: OnyxEntry; + tagLists: OnyxEntry; categories: PolicyCategories; - reports: Array>; + reports: Array>; transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; }; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 1381f643fa8ed..f95c6b1de62e6 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -321,8 +321,8 @@ function setWorkspaceCategoryEnabled( setupCategoryTaskParentReport: OnyxEntry, currentUserAccountID: number, ) { - const policyID = policyData.policy?.id; - const policyCategoriesOptimisticData = { + const {policyID} = policyData; + const categoriesOptimisticData = { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { acc[key] = { ...categoriesToUpdate[key], @@ -342,7 +342,7 @@ function setWorkspaceCategoryEnabled( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: policyCategoriesOptimisticData, + value: categoriesOptimisticData, }, ], successData: [ @@ -386,7 +386,7 @@ function setWorkspaceCategoryEnabled( ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, categoriesOptimisticData); appendSetupCategoriesOnboardingData(onyxData, setupCategoryTaskReport, setupCategoryTaskParentReport, isSetupCategoriesTaskParentReportArchived, currentUserAccountID); const parameters = { @@ -467,9 +467,9 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st } function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string, maxAmountNoReceipt: number) { - const policyID = policyData.policy?.id; - const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; - const policyCategoriesOptimisticData = { + const {policyID, categories} = policyData; + const originalMaxAmountNoReceipt = categories[categoryName]?.maxAmountNoReceipt; + const categoriesOptimisticData = { [categoryName]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { @@ -484,7 +484,7 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: policyCategoriesOptimisticData, + value: categoriesOptimisticData, }, ], successData: [ @@ -520,7 +520,7 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, categoriesOptimisticData); const parameters: SetPolicyCategoryReceiptsRequiredParams = { policyID, @@ -532,9 +532,9 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: } function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string) { - const policyID = policyData.policy?.id; + const {policyID} = policyData; const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; - const policyCategoriesOptimisticData = { + const categoriesOptimisticData = { [categoryName]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { @@ -549,7 +549,7 @@ function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryNa { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: policyCategoriesOptimisticData, + value: categoriesOptimisticData, }, ], successData: [ @@ -585,7 +585,7 @@ function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryNa ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, policyCategoriesOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, categoriesOptimisticData); const parameters: RemovePolicyCategoryReceiptsRequiredParams = { policyID, @@ -634,9 +634,8 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[]) } function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: string; newName: string}) { - const policy = policyData.policy; - const policyID = policy.id; - const policyCategoryToUpdate = policyData.categories?.[policyCategory.oldName]; + const {policyID, policy, categories} = policyData; + const policyCategoryToUpdate = categories?.[policyCategory.oldName]; const policyCategoryApproverRule = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], policyCategory.oldName); const policyCategoryExpenseRule = CategoryUtils.getCategoryExpenseRule(policy?.rules?.expenseRules ?? [], policyCategory.oldName); @@ -681,7 +680,7 @@ function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: mccGroup: updatedMccGroup, }; - const policyCategoriesOptimisticData = { + const categoriesOptimisticData = { [policyCategory.newName]: { ...policyCategoryToUpdate, errors: null, @@ -702,7 +701,7 @@ function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [policyCategory.oldName]: null, - ...policyCategoriesOptimisticData, + ...categoriesOptimisticData, }, }, { @@ -765,7 +764,7 @@ function renamePolicyCategory(policyData: PolicyData, policyCategory: {oldName: return acc; }, {}); - pushTransactionViolationsOnyxData(onyxData, {...policyData, categories: policyCategories}, policyOptimisticData, policyCategoriesOptimisticData); + pushTransactionViolationsOnyxData(onyxData, {...policyData, categories: policyCategories}, policyOptimisticData, categoriesOptimisticData); const parameters = { policyID, @@ -912,7 +911,7 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: } function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean) { - const policyID = policyData.policy?.id; + const {policyID} = policyData; const policyOptimisticData: Partial = { requiresCategory, errors: { @@ -999,7 +998,7 @@ function deleteWorkspaceCategories( setupCategoryTaskParentReport: OnyxEntry, currentUserAccountID: number, ) { - const policyID = policyData.policy?.id; + const {policyID} = policyData; const optimisticPolicyCategoriesData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; return acc; @@ -1060,7 +1059,7 @@ function deleteWorkspaceCategories( } function enablePolicyCategories(policyData: PolicyData, enabled: boolean, shouldGoBack = true) { - const policyID = policyData.policy?.id; + const {policyID} = policyData; const policyUpdate: Partial = { areCategoriesEnabled: enabled, pendingFields: { diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 8e491aa423f57..47bb9828c62fe 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -125,27 +125,29 @@ function updateImportSpreadsheetData(tagsLength: number): OnyxData { return onyxData; } -function createPolicyTag(policyID: string, tagName: string, policyTags: PolicyTagLists = {}) { - const policyTag = PolicyUtils.getTagLists(policyTags)?.at(0) ?? ({} as PolicyTagList); +function createPolicyTag(policyData: PolicyData, tagName: string) { + const {policyID, tagLists} = policyData; + const tagList = PolicyUtils.getTagLists(tagLists)?.at(0) ?? ({} as PolicyTagList); const newTagName = PolicyUtils.escapeTagName(tagName); + const tagListsOptimisticData = { + [tagList.name]: { + tags: { + [newTagName]: { + name: newTagName, + enabled: true, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + }; const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: { - [policyTag.name]: { - tags: { - [newTagName]: { - name: newTagName, - enabled: true, - errors: null, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - }, - }, + value: tagListsOptimisticData, }, ], successData: [ @@ -153,7 +155,7 @@ function createPolicyTag(policyID: string, tagName: string, policyTags: PolicyTa onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { + [tagList.name]: { tags: { [newTagName]: { errors: null, @@ -169,7 +171,7 @@ function createPolicyTag(policyID: string, tagName: string, policyTags: PolicyTa onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { + [tagList.name]: { tags: { [newTagName]: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -186,6 +188,9 @@ function createPolicyTag(policyID: string, tagName: string, policyTags: PolicyTa tags: JSON.stringify([{name: newTagName}]), }; + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); + + API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); } @@ -202,39 +207,40 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { } function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record, tagListIndex: number) { - const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); + if (tagListIndex === -1) { + return; + } + const {policyID, policy, tagLists} = policyData; + const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); - if (!policyTag || tagListIndex === -1 || !policyData.policy) { + if (!tagList || !policy) { return; } - const policyID = policyData.policy?.id; - const policyTagsOptimisticData = { - ...Object.keys(tagsToUpdate).reduce((acc, key) => { - acc[key] = { - ...policyTag.tags[key], - ...tagsToUpdate[key], - errors: null, - pendingFields: { - ...policyTag.tags[key]?.pendingFields, - enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }; + const tagListsOptimisticData = { + [tagList.name]: { + ...Object.keys(tagsToUpdate).reduce((acc, key) => { + acc[key] = { + ...tagList.tags[key], + ...tagsToUpdate[key], + errors: null, + pendingFields: { + ...tagList.tags[key]?.pendingFields, + enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + pendingAction: 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: { - [policyTag.name]: { - tags: policyTagsOptimisticData, - }, - }, + value: tagListsOptimisticData, }, ], successData: [ @@ -242,15 +248,15 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { acc[key] = { - ...policyTag.tags[key], + ...tagList.tags[key], ...tagsToUpdate[key], errors: null, pendingFields: { - ...policyTag.tags[key].pendingFields, + ...tagList.tags[key].pendingFields, enabled: null, }, pendingAction: null, @@ -268,15 +274,15 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { acc[key] = { - ...policyTag.tags[key], + ...tagList.tags[key], ...tagsToUpdate[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), pendingFields: { - ...policyTag.tags[key].pendingFields, + ...tagList.tags[key].pendingFields, enabled: null, }, pendingAction: null, @@ -291,17 +297,7 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { - if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { + const tagListsOptimisticData = { + ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { + if (tagListIndexes.includes(value.orderWeight)) { acc[key] = { ...acc[key], required: isRequired, @@ -343,7 +336,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, + value: tagListsOptimisticData, }, ], successData: [ @@ -351,8 +344,8 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.keys(policyData.tags).reduce((acc, key) => { - if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { + ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { + if (tagListIndexes.includes(value.orderWeight)) { acc[key] = { ...acc[key], errors: undefined, @@ -374,7 +367,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.keys(policyData.tags).reduce((acc, key) => { + ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { acc[key] = { ...acc[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -390,7 +383,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); const parameters: SetPolicyTagListsRequired = { policyID, @@ -402,15 +395,15 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ } function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { - const policyID = policyData.policy?.id; - const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(0); + const {policyID, tagLists} = policyData; + const tagList = PolicyUtils.getTagLists(tagLists)?.at(0); - if (!policyTag) { + if (!tagList) { return; } - const policyTagsOptimisticData: Record>>> = { - [policyTag.name]: { + const tagListsOptimisticData: Record>>> = { + [tagList.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; @@ -425,7 +418,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, + value: tagListsOptimisticData, }, ], successData: [ @@ -433,7 +426,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { + [tagList.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = null; @@ -449,13 +442,13 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { + [tagList.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = { pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.deleteFailureMessage'), - enabled: !!policyTag?.tags[tagName]?.enabled, + enabled: !!tagList?.tags[tagName]?.enabled, }; return acc; }, {}), @@ -466,7 +459,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); const parameters = { policyID, @@ -565,8 +558,8 @@ function clearPolicyTagListErrors({policyID, tagListIndex, policyTags}: ClearPol } function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; newName: string}, tagListIndex: number) { - const policyID = policyData.policy?.id; - const tagList = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); + const {policyID, policy, tagLists} = policyData; + const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); if (!tagList) { return; } @@ -575,7 +568,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne const newTagName = PolicyUtils.escapeTagName(policyTag.newName); const policyTagRule = PolicyUtils.getTagApproverRule(policyID, oldTagName); - const approvalRules = policyData.policy?.rules?.approvalRules ?? []; + const approvalRules = policy?.rules?.approvalRules ?? []; const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); // Its related by name, so the corresponding rule has to be updated to handle offline scenario @@ -599,7 +592,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne }, }; - const policyTagsOptimisticData: Record>>> = { + const tagListsOptimisticData: Record>>> = { [tagList?.name]: { tags: { [oldTagName]: null, @@ -623,7 +616,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, + value: tagListsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -674,7 +667,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, policyTagsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, tagListsOptimisticData); const parameters: RenamePolicyTagsParams = { policyID, @@ -687,7 +680,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne } function enablePolicyTags(policyData: PolicyData, enabled: boolean) { - const policyID = policyData.policy?.id; + const {policyID, tagLists} = policyData; const policyOptimisticData = { areTagsEnabled: enabled, pendingFields: { @@ -728,15 +721,7 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { ], }; - if (Object.keys(policyData.tags).length === 0) { - const defaultTagList: PolicyTagLists = { - Tag: { - name: 'Tag', - orderWeight: 0, - required: false, - tags: {}, - }, - }; + if (!tagLists || Object.keys(tagLists).length === 0) { onyxData.optimisticData?.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, @@ -747,18 +732,18 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); - pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, defaultTagList); + pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, CONST.POLICY.DEFAULT_TAG_LIST); } else if (!enabled) { - const policyTag = PolicyUtils.getTagLists(policyData.tags).at(0); + const tagList = PolicyUtils.getTagLists(tagLists).at(0); - if (!policyTag) { + if (!tagList) { return; } - const policyTagsOptimisticData: Record> = { - [policyTag.name]: { + const tagListsOptimisticData: Record> = { + [tagList.name]: { tags: Object.fromEntries( - Object.keys(policyTag.tags).map((tagName) => [ + Object.keys(tagLists ?? {}).map((tagName) => [ tagName, { enabled: false, @@ -772,7 +757,7 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, + value: tagListsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -783,7 +768,7 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { }, ); - pushTransactionViolationsOnyxData(onyxData, policyData, {...policyOptimisticData, requiresTag: false}, {}, policyTagsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {...policyOptimisticData, requiresTag: false}, {}, tagListsOptimisticData); } else { pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData); } @@ -890,9 +875,9 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet ); } -function renamePolicyTagList(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number) { - const newName = policyTagListName.newName; - const oldName = policyTagListName.oldName; +function renamePolicyTagList(policyID: string, tagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number) { + const newName = tagListName.newName; + const oldName = tagListName.oldName; const oldPolicyTags = policyTags?.[oldName] ?? {}; const onyxData: OnyxData = { optimisticData: [ @@ -942,7 +927,7 @@ function renamePolicyTagList(policyID: string, policyTagListName: {oldName: stri } function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { - const policyID = policyData.policy?.id; + const {policyID, tagLists} = policyData; const policyOptimisticData: Partial = { requiresTag, errors: {requiresTag: null}, @@ -989,7 +974,7 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { }; const getUpdatedTagsData = (required: boolean): PolicyTagLists => ({ - ...Object.keys(policyData.tags).reduce((acc, key) => { + ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { acc[key] = { ...acc[key], required, @@ -1019,14 +1004,13 @@ 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) { + const {policyID, tagLists} = policyData; + const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); + if (!tagList || !tagList.name) { return; } - - const policyID = policyData.policy?.id; - const policyTagsOptimisticData = { - [policyTag.name]: { + const tagListsOptimisticData = { + [tagList.name]: { required: requiresTag, pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, errorFields: {required: null}, @@ -1038,7 +1022,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: policyTagsOptimisticData, + value: tagListsOptimisticData, }, ], successData: [ @@ -1046,7 +1030,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { + [tagList.name]: { pendingFields: {required: null}, }, }, @@ -1057,8 +1041,8 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [policyTag.name]: { - required: policyTag.required, + [tagList.name]: { + required: tagList.required, pendingFields: {required: null}, errorFields: { required: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -1069,7 +1053,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); const parameters: SetPolicyTagsRequired = { policyID, diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 1250e2970da78..e5ac16dc780a5 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -70,7 +70,7 @@ function IOURequestStepCategory({ const report = reportReal ?? reportDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; - const policyData = usePolicyData(policy?.id); + const policyData = usePolicyData(policyID ?? CONST.DEFAULT_NUMBER_ID.toString()); const {currentSearchHash} = useSearchContext(); const isEditing = action === CONST.IOU.ACTION.EDIT; const isEditingSplit = (iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.SPLIT_EXPENSE) && isEditing; diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index a607308aabe0a..e46e9b11842d4 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -23,25 +23,27 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTagForm'; +import usePolicyData from '@hooks/usePolicyData'; type WorkspaceCreateTagPageProps = | PlatformStackScreenProps | PlatformStackScreenProps; function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { - const policyID = route.params.policyID; - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const {policyID, backTo} = route.params; + const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE; + const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); - const backTo = route.params.backTo; - const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE; + const policyData = usePolicyData(policyID); + const {tagLists} = policyData; const validate = useCallback( (values: FormOnyxValues) => { const errors: FormInputErrors = {}; const tagName = escapeTagName(values.tagName.trim()); - const {tags} = getTagList(policyTags, 0); + const {tags} = getTagList(tagLists, 0); if (!isRequiredFulfilled(tagName)) { errors.tagName = translate('workspace.tags.tagRequiredError'); @@ -56,16 +58,16 @@ function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { return errors; }, - [policyTags, translate], + [tagLists, translate], ); const createTag = useCallback( (values: FormOnyxValues) => { - createPolicyTag(policyID, values.tagName.trim(), policyTags); + createPolicyTag(policyData, values.tagName.trim()); Keyboard.dismiss(); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }, - [policyID, policyTags, isQuickSettingsFlow, backTo], + [policyData, isQuickSettingsFlow, backTo], ); return ( diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 5886a84e49ebf..c3af17136a4e1 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -28,6 +28,8 @@ import createRandomPolicyTags from '../utils/collections/policyTags'; import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import { getTagList } from '@libs/PolicyUtils'; +import PolicyData from '@hooks/usePolicyData/types'; OnyxUpdateManager(); @@ -287,11 +289,16 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + mockFetch.pause(); await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + // When creating a new tag - createPolicyTag(fakePolicy.id, newTagName, fakePolicyTags); + createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback @@ -321,12 +328,16 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + mockFetch.pause(); await waitForBatchedUpdates(); mockFetch.fail(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When the API fails - createPolicyTag(fakePolicy.id, newTagName, fakePolicyTags); + createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); mockFetch.resume(); await waitForBatchedUpdates(); @@ -344,11 +355,15 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + mockFetch.pause(); await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + // When adding the first tag - createPolicyTag(fakePolicy.id, newTagName, {}); + createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); // Then the tag should be created in a new list with pending state so the user sees immediate feedback @@ -391,14 +406,13 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); await waitForBatchedUpdates(); - const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); - + const {result : policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { - expect(result.current[0]).toBeDefined(); + expect(getTagList(policyData?.current?.tagLists, 0)).toBeDefined(); }); // When using data from useOnyx hook - createPolicyTag(fakePolicy.id, newTagName, result.current[0] ?? {}); + createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback @@ -586,16 +600,13 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + // When the tag is renamed - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -643,16 +654,13 @@ describe('actions/Policy', () => { mockFetch.fail(); + await waitForBatchedUpdates(); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + // When the tag rename fails - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -682,19 +690,14 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, existingPolicyTags); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When trying to rename a tag with invalid index - const policyData = { - policy: fakePolicy, - tags: {}, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; expect(() => { renamePolicyTag( - policyData, + policyData.current, { oldName: 'oldTag', newName: 'newTag', @@ -742,30 +745,26 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); + const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + // When the tag is renamed - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, }, 0, ); + await waitForBatchedUpdates(); - + // Then the approval rule should be updated with the new tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); - - expect(updatedPolicy?.rules?.approvalRules).toHaveLength(1); - expect(updatedPolicy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(newTagName); + rerender(fakePolicy.id); + expect(policyData.current.policy?.rules?.approvalRules).toHaveLength(1); + expect(policyData.current.policy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(newTagName); mockFetch.resume(); await waitForBatchedUpdates(); @@ -789,7 +788,7 @@ describe('actions/Policy', () => { const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { - expect(policyData.current.policy).toBeDefined(); + expect(policyData?.current?.policy).toBeDefined(); }); // When renaming tag with data from usePolicyData @@ -1708,22 +1707,21 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData.current.policy?.areTagsEnabled).toBe(true); - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And a default tag list should be created - const defaultTag = Object.values(policyData.current?.tags ?? {}).at(0); - expect(defaultTag?.name).toBe('Tag'); - expect(defaultTag?.orderWeight).toBe(0); - expect(defaultTag?.required).toBe(false); - expect(defaultTag?.tags).toEqual({}); - + const defaultTagList = Object.values(policyData.current?.tagLists ?? {}).at(0); + expect(defaultTagList?.name).toBe('Tag'); + expect(defaultTagList?.orderWeight).toBe(0); + expect(defaultTagList?.required).toBe(false); + expect(defaultTagList?.tags).toEqual({}); mockFetch.resume(); await waitForBatchedUpdates(); rerender(fakePolicy.id); // And after API success, pending fields should be cleared - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); }); it('should disable tags and update existing tag list', async () => { @@ -1749,13 +1747,13 @@ describe('actions/Policy', () => { // Then the policy should be updated optimistically rerender(fakePolicy.id); - expect(policyData.current.policy?.areTagsEnabled).toBe(false); - expect(policyData.current.policy?.requiresTag).toBe(false); - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData?.current?.policy?.areTagsEnabled).toBe(false); + expect(policyData?.current?.policy?.requiresTag).toBe(false); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And all tags should be disabled Object.keys(existingTags).forEach((tagName) => { - expect(policyData.current?.tags?.[tagListName]?.tags[tagName]?.enabled).toBe(false); + expect(policyData.current?.tagLists?.[tagListName]?.tags[tagName]?.enabled).toBe(false); }); await mockFetch.resume(); @@ -1764,7 +1762,7 @@ describe('actions/Policy', () => { // And after API success, pending fields should be cleared rerender(fakePolicy.id); - expect(policyData.current.policy?.pendingFields).toBeDefined(); + expect(policyData?.current?.policy?.pendingFields).toBeDefined(); }); it('should reset changes when API returns error', async () => { @@ -1789,9 +1787,9 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // After the API request failure, the policy should be reset to original state - expect(policyData.current.policy.areTagsEnabled).toBe(false); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); - expect(policyData.current.tags).toMatchObject({}); + expect(policyData.current?.policy?.areTagsEnabled).toBe(false); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData.current?.tagLists).toMatchObject({}); }); it('should work with data from useOnyx hook', async () => { @@ -1806,15 +1804,15 @@ describe('actions/Policy', () => { const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - expect(policyData.current.policy).toBeDefined(); + expect(policyData?.current?.policy).toBeDefined(); enablePolicyTags(policyData.current, true); await waitForBatchedUpdates(); rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData.current.policy.areTagsEnabled).toBe(true); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); await mockFetch.resume(); await waitForBatchedUpdates(); @@ -1822,11 +1820,11 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // And after API success, policy should be enabled - expect(policyData.current.policy.areTagsEnabled).toBe(true); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); // And default tag list should be created - expect(policyData.current.tags.Tag).toBeDefined(); + expect(policyData?.current?.tagLists?.Tag).toBeDefined(); }); }); @@ -1857,7 +1855,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as required with pending fields - let updatedPolicyTags = policyData.current.tags; + let updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); // Check optimistic data - pendingFields should be set @@ -1869,7 +1867,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); rerender(fakePolicy.id); // Then after API success, pending fields should be cleared - updatedPolicyTags = policyData.current.tags; + updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); @@ -1900,7 +1898,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as not required with pending fields - let updatedPolicyTags = policyData.current.tags; + let updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); // Check optimistic data - pendingFields should be set @@ -1913,7 +1911,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then after API success, pending fields should be cleared - updatedPolicyTags = policyData.current.tags; + updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); @@ -1948,7 +1946,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be restored to original state with error - const updatedPolicyTags = policyData.current.tags; + const updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); expect(updatedPolicyTags?.[tagListName]?.errorFields?.required).toBeTruthy(); @@ -1986,7 +1984,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as required - const updatedPolicyTags = policyData.current.tags; + const updatedPolicyTags = policyData?.current?.tagLists; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); // Check optimistic data - pendingFields should be set From 6e58a092615db512546397de75e3d3bfd1f3fe4c Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 01:17:48 +0000 Subject: [PATCH 02/13] refactor: lint --- src/hooks/usePolicyData/index.ts | 7 +++---- src/hooks/usePolicyData/types.ts | 2 +- src/pages/workspace/tags/WorkspaceCreateTagPage.tsx | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index b09b6cf343281..4fd908f153ff5 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -1,12 +1,11 @@ import {useCallback, useMemo} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Report} from '@src/types/onyx'; import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; -import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type PolicyData from './types'; /** @@ -53,7 +52,7 @@ function usePolicyData(policyID: string): PolicyData { }, [reports, allReportsTransactionsAndViolations]); return { transactionsAndViolations, - tagLists: tagLists, + tagLists, categories: categories ?? {}, policyID, policy, diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts index e64789a38a332..0598e23c3e815 100644 --- a/src/hooks/usePolicyData/types.ts +++ b/src/hooks/usePolicyData/types.ts @@ -1,4 +1,4 @@ -import {OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Policy, PolicyCategories, PolicyTagLists, Report} from '@src/types/onyx'; import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/DerivedValues'; diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index e46e9b11842d4..21f0f40502abc 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -8,7 +8,6 @@ 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 useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; From c1d32ef5ef51ed8df84614d04399b6bd1eb8ee50 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 02:04:06 +0000 Subject: [PATCH 03/13] refactor: update PolicyData type to use OnyxEntry for categories --- src/hooks/usePolicyData/types.ts | 2 +- src/libs/actions/Policy/Category.ts | 22 +++++++++++-------- src/libs/actions/Policy/Tag.ts | 5 ++--- .../workspace/tags/WorkspaceCreateTagPage.tsx | 4 ++-- tests/actions/PolicyTagTest.ts | 13 +++++------ 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts index 0598e23c3e815..509587cf6886c 100644 --- a/src/hooks/usePolicyData/types.ts +++ b/src/hooks/usePolicyData/types.ts @@ -6,7 +6,7 @@ type PolicyData = { policyID: string; policy: OnyxEntry; tagLists: OnyxEntry; - categories: PolicyCategories; + categories: OnyxEntry; reports: Array>; transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; }; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index f95c6b1de62e6..6c804be669290 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -321,7 +321,11 @@ function setWorkspaceCategoryEnabled( setupCategoryTaskParentReport: OnyxEntry, currentUserAccountID: number, ) { - const {policyID} = policyData; + const {policyID, categories} = policyData; + if (!categories) { + Log.warn('setWorkspaceCategoryEnabled invalid params', {categories}); + return; + } const categoriesOptimisticData = { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { acc[key] = { @@ -371,7 +375,7 @@ function setWorkspaceCategoryEnabled( value: { ...Object.keys(categoriesToUpdate).reduce((acc, key) => { acc[key] = { - ...policyData.categories[key], + ...categories[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), pendingFields: { enabled: null, @@ -468,7 +472,7 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string, maxAmountNoReceipt: number) { const {policyID, categories} = policyData; - const originalMaxAmountNoReceipt = categories[categoryName]?.maxAmountNoReceipt; + const originalMaxAmountNoReceipt = categories?.[categoryName]?.maxAmountNoReceipt; const categoriesOptimisticData = { [categoryName]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -532,8 +536,8 @@ function setPolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: } function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryName: string) { - const {policyID} = policyData; - const originalMaxAmountNoReceipt = policyData.categories[categoryName]?.maxAmountNoReceipt; + const {policyID, categories} = policyData; + const originalMaxAmountNoReceipt = categories?.[categoryName]?.maxAmountNoReceipt; const categoriesOptimisticData = { [categoryName]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -998,14 +1002,14 @@ function deleteWorkspaceCategories( setupCategoryTaskParentReport: OnyxEntry, currentUserAccountID: number, ) { - const {policyID} = policyData; + const {policyID, categories} = policyData; const optimisticPolicyCategoriesData = categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; return acc; }, {}); const shouldDisableRequiresCategory = !hasEnabledOptions( - Object.values(policyData.categories).filter((category) => !categoryNamesToDelete.includes(category.name) && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), + Object.values(categories ?? {}).filter((category) => !categoryNamesToDelete.includes(category.name) && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), ); const optimisticPolicyData: Partial = shouldDisableRequiresCategory ? { @@ -1059,7 +1063,7 @@ function deleteWorkspaceCategories( } function enablePolicyCategories(policyData: PolicyData, enabled: boolean, shouldGoBack = true) { - const {policyID} = policyData; + const {policyID, categories} = policyData; const policyUpdate: Partial = { areCategoriesEnabled: enabled, pendingFields: { @@ -1103,7 +1107,7 @@ function enablePolicyCategories(policyData: PolicyData, enabled: boolean, should if (!enabled) { policyCategoriesUpdate = Object.fromEntries( - Object.entries(policyData.categories).map(([categoryName]) => [ + Object.entries(categories ?? {}).map(([categoryName]) => [ categoryName, { enabled: false, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 47bb9828c62fe..33d0964fff8a1 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -190,7 +190,6 @@ function createPolicyTag(policyData: PolicyData, tagName: string) { pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); - API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); } @@ -367,7 +366,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { + ...Object.keys(tagLists ?? {}).reduce((acc, key) => { acc[key] = { ...acc[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -974,7 +973,7 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { }; const getUpdatedTagsData = (required: boolean): PolicyTagLists => ({ - ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { + ...Object.keys(tagLists ?? {}).reduce((acc, key) => { acc[key] = { ...acc[key], required, diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index 21f0f40502abc..176be61755752 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.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 usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -22,7 +23,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTagForm'; -import usePolicyData from '@hooks/usePolicyData'; type WorkspaceCreateTagPageProps = | PlatformStackScreenProps @@ -66,7 +66,7 @@ function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { Keyboard.dismiss(); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }, - [policyData, isQuickSettingsFlow, backTo], + [policyID, policyData, isQuickSettingsFlow, backTo], ); return ( diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index c3af17136a4e1..c49c97dda42f0 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -20,6 +20,7 @@ import { setPolicyTagsRequired, setWorkspaceTagEnabled, } from '@libs/actions/Policy/Tag'; +import {getTagList} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; @@ -28,8 +29,6 @@ import createRandomPolicyTags from '../utils/collections/policyTags'; import * as TestHelper from '../utils/TestHelper'; import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import { getTagList } from '@libs/PolicyUtils'; -import PolicyData from '@hooks/usePolicyData/types'; OnyxUpdateManager(); @@ -330,7 +329,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - + mockFetch.pause(); await waitForBatchedUpdates(); mockFetch.fail(); @@ -406,7 +405,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); await waitForBatchedUpdates(); - const {result : policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { expect(getTagList(policyData?.current?.tagLists, 0)).toBeDefined(); }); @@ -748,7 +747,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - + // When the tag is renamed renamePolicyTag( policyData.current, @@ -758,9 +757,9 @@ describe('actions/Policy', () => { }, 0, ); - + await waitForBatchedUpdates(); - + // Then the approval rule should be updated with the new tag name rerender(fakePolicy.id); expect(policyData.current.policy?.rules?.approvalRules).toHaveLength(1); From a869d59a59f53fd8d8fbd3faed6c9c6432d75daf Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 02:58:49 +0000 Subject: [PATCH 04/13] refactor: update policyData to use tagLists instead of tags across tag-related pages --- src/libs/ReportUtils.ts | 36 ++++++++++--------- src/pages/workspace/tags/EditTagPage.tsx | 2 +- src/pages/workspace/tags/TagSettingsPage.tsx | 2 +- .../workspace/tags/WorkspaceTagsPage.tsx | 2 +- .../tags/WorkspaceTagsSettingsPage.tsx | 2 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 3 +- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 105d3c5a628ce..b5866fff8bb86 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,7 +14,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {OriginalMessageChangePolicy, OriginalMessageExportIntegration, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage'; -import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest'; +import type {PartialDeep, SetRequired, TupleToUnion, ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {FallbackAvatar, IntacctSquare, NetSuiteExport, NetSuiteSquare, QBDSquare, QBOExport, QBOSquare, SageIntacctExport, XeroExport, XeroSquare} from '@components/Icon/Expensicons'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -1972,9 +1972,13 @@ function pushTransactionViolationsOnyxData( categoriesUpdate: Record> = {}, tagListsUpdate: Record> = {}, ) { + const {policy, tagLists, categories} = policyData; + if (policy === undefined) { + return; + } const nonInvoiceReportTransactionsAndViolations = policyData.reports.reduce((acc, report) => { // Skipping invoice reports since they should not have any category or tag violations - if (isInvoiceReport(report)) { + if (report === undefined || isInvoiceReport(report)) { return acc; } const reportTransactionsAndViolations = policyData.transactionsAndViolations[report.reportID]; @@ -2000,41 +2004,41 @@ function pushTransactionViolationsOnyxData( } // Merge the existing policy with the optimistic updates - const optimisticPolicy = isPolicyUpdateEmpty ? policyData.policy : {...policyData.policy, ...policyUpdate}; + const optimisticPolicy = isPolicyUpdateEmpty ? policy : {...policy, ...policyUpdate}; // Merge the existing categories with the optimistic updates const optimisticCategories = isCategoriesUpdateEmpty - ? policyData.categories + ? (categories ?? {}) : { - ...Object.fromEntries(Object.entries(policyData.categories).filter(([categoryName]) => !(categoryName in categoriesUpdate) || !!categoriesUpdate[categoryName])), - ...Object.entries(categoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { + ...Object.fromEntries(Object.entries(categories ?? {}).filter(([categoryName]) => !(categoryName in categoriesUpdate) || !!categoriesUpdate[categoryName])), + ...Object.entries(categoriesUpdate).reduce>((acc, [categoryName, categoryUpdate]) => { if (!categoryUpdate) { return acc; } - acc[categoryName] = { - ...(policyData.categories?.[categoryName] ?? {}), + acc[categoryName] = categories?.[categoryName] !== undefined ? { + ...categories[categoryName], ...categoryUpdate, - }; + } : categoryUpdate; return acc; }, {}), }; // Merge the existing tag lists with the optimistic updates const optimisticTagLists = isTagListsUpdateEmpty - ? policyData.tags + ? (tagLists ?? {}) : { - ...Object.fromEntries(Object.entries(policyData.tags ?? {}).filter(([tagListName]) => !(tagListName in tagListsUpdate) || !!tagListsUpdate[tagListName])), - ...Object.entries(tagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { - if (!tagListUpdate) { + ...Object.fromEntries(Object.entries(tagLists ?? {}).filter(([tagListName]) => !(tagListName in tagListsUpdate) || !!tagListsUpdate[tagListName])), + ...Object.entries(tagListsUpdate).reduce>((acc, [tagListName, tagListUpdate]) => { + if (!tagListName || !tagListUpdate) { return acc; } - const tagList = policyData.tags?.[tagListName]; - const tags = tagList.tags ?? {}; + const tagList = tagLists?.[tagListName] + const tags = tagList?.tags ?? {}; const tagsUpdate = tagListUpdate?.tags ?? {}; acc[tagListName] = { - ...tagList, + ...(tagList ?? {}), ...tagListUpdate, tags: { ...((): PolicyTags => { diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index 3a26336a39857..dded9e2cc874f 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -30,7 +30,7 @@ type EditTagPageProps = function EditTagPage({route}: EditTagPageProps) { const {backTo, policyID} = route.params; const policyData = usePolicyData(policyID); - const {tags: policyTags} = policyData; + const {tagLists: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 00ef9c8d79c1b..7bf4321dd2af7 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -45,7 +45,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policyData = usePolicyData(policyID); - const {policy, tags: policyTags} = policyData; + const {policy, tagLists: policyTags} = policyData; const policyTag = useMemo(() => getTagListByOrderWeight(policyTags, orderWeight), [policyTags, orderWeight]); const {environmentURL} = useEnvironment(); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 206ce08234c1e..5412f657d29f7 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -90,7 +90,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const [isCannotMakeLastTagOptionalModalVisible, setIsCannotMakeLastTagOptionalModalVisible] = useState(false); const {backTo, policyID} = route.params; const policyData = usePolicyData(policyID); - const {policy, tags: policyTags} = policyData; + const {policy, tagLists: policyTags} = policyData; const isMobileSelectionModeEnabled = useMobileSelectionMode(); const {environmentURL} = useEnvironment(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index bd897207fcba6..b061f6832451a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -54,7 +54,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const backTo = route.params.backTo; const styles = useThemeStyles(); const policyData = usePolicyData(policyID); - const {tags: policyTags} = policyData; + const {tagLists: policyTags} = policyData; const {translate} = useLocalize(); const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 0a5ceaec8364f..6093f6767276a 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -68,8 +68,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const policyData = usePolicyData(policyID); - const {policy, tags: policyTags} = policyData; - + const {policy, tagLists: policyTags} = policyData; const isMobileSelectionModeEnabled = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, orderWeight), [policyTags, orderWeight]); const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); From ac6f27f8d6d1eab379cd3e8a27e889343cde5dd5 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 20:39:30 +0000 Subject: [PATCH 05/13] refactor: simplify category and tag list updates in pushTransactionViolationsOnyxData --- src/libs/ReportUtils.ts | 49 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b5866fff8bb86..a05f7486647b4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,7 +14,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {OriginalMessageChangePolicy, OriginalMessageExportIntegration, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage'; -import type {PartialDeep, SetRequired, TupleToUnion, ValueOf} from 'type-fest'; +import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {FallbackAvatar, IntacctSquare, NetSuiteExport, NetSuiteSquare, QBDSquare, QBOExport, QBOSquare, SageIntacctExport, XeroExport, XeroSquare} from '@components/Icon/Expensicons'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -2011,14 +2011,20 @@ function pushTransactionViolationsOnyxData( ? (categories ?? {}) : { ...Object.fromEntries(Object.entries(categories ?? {}).filter(([categoryName]) => !(categoryName in categoriesUpdate) || !!categoriesUpdate[categoryName])), - ...Object.entries(categoriesUpdate).reduce>((acc, [categoryName, categoryUpdate]) => { + ...Object.entries(categoriesUpdate).reduce((acc, [categoryName, categoryUpdate]) => { if (!categoryUpdate) { return acc; } - acc[categoryName] = categories?.[categoryName] !== undefined ? { - ...categories[categoryName], + const category = categories?.[categoryName]; + if (category === undefined) { + acc[categoryName] = categoryUpdate as PolicyCategory; + return acc; + } + + acc[categoryName] = { + ...category, ...categoryUpdate, - } : categoryUpdate; + } as PolicyCategory; return acc; }, {}), }; @@ -2028,29 +2034,42 @@ function pushTransactionViolationsOnyxData( ? (tagLists ?? {}) : { ...Object.fromEntries(Object.entries(tagLists ?? {}).filter(([tagListName]) => !(tagListName in tagListsUpdate) || !!tagListsUpdate[tagListName])), - ...Object.entries(tagListsUpdate).reduce>((acc, [tagListName, tagListUpdate]) => { + ...Object.entries(tagListsUpdate).reduce((acc, [tagListName, tagListUpdate]) => { if (!tagListName || !tagListUpdate) { return acc; } - const tagList = tagLists?.[tagListName] - const tags = tagList?.tags ?? {}; + const tagList = tagLists?.[tagListName]; + if (!tagList) { + acc[tagListName] = tagListUpdate as PolicyTagList; + return acc; + } + const tags = tagList?.tags; + if (!tags) { + acc[tagListName] = {...tagList, ...tagListUpdate} as PolicyTagList; + return acc; + } const tagsUpdate = tagListUpdate?.tags ?? {}; - acc[tagListName] = { - ...(tagList ?? {}), + ...tagList, ...tagListUpdate, tags: { ...((): PolicyTags => { - const optimisticTags: PolicyTags = Object.fromEntries(Object.entries(tags).filter(([tagName]) => !(tagName in tagsUpdate) || !!tagsUpdate[tagName])); + const optimisticTags: PolicyTags = Object.fromEntries( + Object.entries(tags ?? {}).filter(([tagName, tag]) => !tag || !(tagName in tagsUpdate) || !!tagsUpdate[tagName]), + ); for (const [tagName, tagUpdate] of Object.entries(tagsUpdate)) { if (!tagUpdate) { continue; } - optimisticTags[tagName] = { - ...(tags[tagName] ?? {}), - ...tagUpdate, - }; + const tag = tags?.[tagName]; + optimisticTags[tagName] = + tag !== undefined + ? { + ...tag, + ...tagUpdate, + } + : tagUpdate; } return optimisticTags; })(), From ecda7d0e888d7f0c4fe7f879d4dc30bc081eca2e Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Mon, 17 Nov 2025 22:46:25 +0300 Subject: [PATCH 06/13] refactor: deleteTask function to remove unused ancestors parameter and update related calls --- src/libs/actions/Task.ts | 8 ++++---- src/pages/ReportDetailsPage.tsx | 5 +---- src/pages/home/HeaderView.tsx | 7 ++----- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index d5d893f2317ea..daf01d647ca25 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1083,7 +1083,7 @@ function getNavigationUrlOnTaskDelete(report: OnyxEntry): stri /** * Cancels a task by setting the report state to SUBMITTED and status to CLOSED */ -function deleteTask(report: OnyxEntry, isReportArchived: boolean, currentUserAccountID: number, ancestors: ReportUtils.Ancestor[] = []) { +function deleteTask(report: OnyxEntry, isReportArchived: boolean, currentUserAccountID: number) { if (!report) { return; } @@ -1157,12 +1157,12 @@ function deleteTask(report: OnyxEntry, isReportArchived: boole parentReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - optimisticParentReportData.forEach((parentReportData) => { + for (const parentReportData of optimisticParentReportData) { if (isEmptyObject(parentReportData)) { - return; + continue; } optimisticData.push(parentReportData); - }); + } } const successData: OnyxUpdate[] = [ diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index de2f006e253d9..3900c1deeb183 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -22,7 +22,6 @@ import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; -import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDeleteTransactions from '@hooks/useDeleteTransactions'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; @@ -210,7 +209,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const shouldDisableRename = useMemo(() => shouldDisableRenameUtil(report, isReportArchived), [report, isReportArchived]); const parentNavigationSubtitleData = getParentNavigationSubtitle(report, isParentReportArchived); const base62ReportID = getBase62ReportID(Number(report.reportID)); - const ancestors = useAncestors(report); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx const chatRoomSubtitle = useMemo(() => { const subtitle = getChatRoomSubtitle(report, false, isReportArchived); @@ -810,7 +808,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const deleteTransaction = useCallback(() => { if (caseID === CASES.DEFAULT) { - deleteTask(report, isReportArchived, currentUserPersonalDetails.accountID, ancestors); + deleteTask(report, isReportArchived, currentUserPersonalDetails.accountID); return; } @@ -839,7 +837,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail removeTransaction(iouTransactionID); } }, [ - ancestors, caseID, requestParentReportAction, report, diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 1b556e8a65559..096120625bc19 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -8,6 +8,7 @@ import CaretWrapper from '@components/CaretWrapper'; import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; +import {BackArrow, DotIndicator} from '@components/Icon/Expensicons'; import LoadingBar from '@components/LoadingBar'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import OnboardingHelpDropdownButton from '@components/OnboardingHelpDropdownButton'; @@ -20,10 +21,8 @@ import HelpButton from '@components/SidePanel/HelpComponents/HelpButton'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -99,7 +98,6 @@ type HeaderViewProps = { }; function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) { - const {BackArrow, DotIndicator} = useMemoizedLazyExpensifyIcons(['BackArrow', 'DotIndicator'] as const); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); @@ -154,7 +152,6 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, const isPersonalExpenseChat = isPolicyExpenseChat && isCurrentUserSubmitter(report); const hasTeam2025Pricing = useHasTeam2025Pricing(); const subscriptionPlan = useSubscriptionPlan(); - const ancestors = useAncestors(report); const displayNamesFSClass = FS.getChatFSClass(personalDetails, report); const shouldShowSubtitle = () => { @@ -373,7 +370,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, isVisible={isDeleteTaskConfirmModalVisible} onConfirm={() => { setIsDeleteTaskConfirmModalVisible(false); - deleteTask(report, isReportArchived, currentUserPersonalDetails.accountID, ancestors); + deleteTask(report, isReportArchived, currentUserPersonalDetails.accountID); }} onCancel={() => setIsDeleteTaskConfirmModalVisible(false)} title={translate('task.deleteTask')} From 1a4f6b6a06e37b10e0ac7d1496d6510f76f4a5ad Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 18 Nov 2025 01:10:57 +0300 Subject: [PATCH 07/13] refactor: handle optional chaining for policy categories and update related tests --- .../WorkspaceCategoriesSettingsPage.tsx | 2 +- tests/actions/PolicyTagTest.ts | 89 ++++++++----------- tests/perf-test/ReportUtils.perf-test.ts | 3 +- tests/unit/usePolicyData.test.ts | 36 ++++---- 4 files changed, 57 insertions(+), 73 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index e13707d180e61..ccd6a94753ac7 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -70,7 +70,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet ); }, [policyData.policy]); - const hasEnabledCategories = hasEnabledOptions(policyData.categories); + const hasEnabledCategories = hasEnabledOptions(policyData?.categories ?? {}); const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledCategories || isConnectedToAccounting; const setNewCategory = (selectedCategory: SelectionListWithSectionsListItem, currentGroupID: string) => { diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index cf5a586bbb1c5..cc4bd75d924a2 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -20,7 +20,6 @@ import { setPolicyTagsRequired, setWorkspaceTagEnabled, } from '@libs/actions/Policy/Tag'; -import {getTagList} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; @@ -295,7 +294,6 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - // When creating a new tag createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); @@ -329,6 +327,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); mockFetch.pause(); await waitForBatchedUpdates(); @@ -353,14 +352,15 @@ describe('actions/Policy', () => { fakePolicy.areTagsEnabled = true; const newTagName = 'new tag'; - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, {}); + await waitForBatchedUpdates(); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); mockFetch.pause(); await waitForBatchedUpdates(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - // When adding the first tag createPolicyTag(policyData.current, newTagName); await waitForBatchedUpdates(); @@ -401,13 +401,13 @@ describe('actions/Policy', () => { const fakePolicyTags = createRandomPolicyTags(tagListName); mockFetch.pause(); - + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { - expect(getTagList(policyData?.current?.tagLists, 0)).toBeDefined(); + expect(policyData.current.tagLists).toBeDefined(); }); // When using data from useOnyx hook @@ -452,9 +452,10 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - await waitForBatchedUpdates(); + setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); await waitForBatchedUpdates(); @@ -509,8 +510,10 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - + mockFetch?.fail?.(); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); await waitForBatchedUpdates(); @@ -542,15 +545,8 @@ describe('actions/Policy', () => { 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}`)); - - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitForBatchedUpdates(); - - await waitFor(() => { - expect(result.current[0]).toBeDefined(); - }); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setWorkspaceTagEnabled(policyData.current, {[tagName]: {name: tagName, enabled: false}}, 0); @@ -599,11 +595,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - await waitForBatchedUpdates(); - - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - // When the tag is renamed + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTag( policyData.current, { @@ -653,11 +646,8 @@ describe('actions/Policy', () => { mockFetch.fail(); - await waitForBatchedUpdates(); - - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - // When the tag rename fails + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTag( policyData.current, { @@ -689,10 +679,9 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, existingPolicyTags); - await waitForBatchedUpdates(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When trying to rename a tag with invalid index + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); expect(() => { renamePolicyTag( @@ -746,9 +735,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); await waitForBatchedUpdates(); - const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - // When the tag is renamed + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); renamePolicyTag( policyData.current, { @@ -757,13 +745,13 @@ describe('actions/Policy', () => { }, 0, ); - await waitForBatchedUpdates(); // Then the approval rule should be updated with the new tag name - rerender(fakePolicy.id); - expect(policyData.current.policy?.rules?.approvalRules).toHaveLength(1); - expect(policyData.current.policy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(newTagName); + const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + + expect(updatedPolicy?.rules?.approvalRules).toHaveLength(1); + expect(updatedPolicy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(newTagName); mockFetch.resume(); await waitForBatchedUpdates(); @@ -787,7 +775,7 @@ describe('actions/Policy', () => { const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { - expect(policyData?.current?.policy).toBeDefined(); + expect(policyData.current.policy).toBeDefined(); }); // When renaming tag with data from usePolicyData @@ -1706,21 +1694,22 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current.policy?.areTagsEnabled).toBe(true); + expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And a default tag list should be created - const defaultTagList = Object.values(policyData.current?.tagLists ?? {}).at(0); - expect(defaultTagList?.name).toBe('Tag'); - expect(defaultTagList?.orderWeight).toBe(0); - expect(defaultTagList?.required).toBe(false); - expect(defaultTagList?.tags).toEqual({}); + const defaultTag = Object.values(policyData.current?.tagLists ?? {}).at(0); + expect(defaultTag?.name).toBe('Tag'); + expect(defaultTag?.orderWeight).toBe(0); + expect(defaultTag?.required).toBe(false); + expect(defaultTag?.tags).toEqual({}); + mockFetch.resume(); await waitForBatchedUpdates(); rerender(fakePolicy.id); // And after API success, pending fields should be cleared - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); + expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); }); it('should disable tags and update existing tag list', async () => { @@ -1746,9 +1735,9 @@ describe('actions/Policy', () => { // Then the policy should be updated optimistically rerender(fakePolicy.id); - expect(policyData?.current?.policy?.areTagsEnabled).toBe(false); - expect(policyData?.current?.policy?.requiresTag).toBe(false); - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current.policy?.areTagsEnabled).toBe(false); + expect(policyData.current.policy?.requiresTag).toBe(false); + expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And all tags should be disabled for (const tagName of Object.keys(existingTags)) { @@ -1761,7 +1750,7 @@ describe('actions/Policy', () => { // And after API success, pending fields should be cleared rerender(fakePolicy.id); - expect(policyData?.current?.policy?.pendingFields).toBeDefined(); + expect(policyData.current.policy?.pendingFields).toBeDefined(); }); it('should reset changes when API returns error', async () => { @@ -1786,9 +1775,9 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // After the API request failure, the policy should be reset to original state - expect(policyData.current?.policy?.areTagsEnabled).toBe(false); - expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); - expect(policyData.current?.tagLists).toMatchObject({}); + expect(policyData?.current?.policy?.areTagsEnabled).toBe(false); + expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData?.current?.tagLists).toMatchObject({}); }); it('should work with data from useOnyx hook', async () => { @@ -1803,7 +1792,7 @@ describe('actions/Policy', () => { const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - expect(policyData?.current?.policy).toBeDefined(); + expect(policyData.current.policy).toBeDefined(); enablePolicyTags(policyData.current, true); await waitForBatchedUpdates(); diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index ac8df3a728b6c..b4c17550f0bb9 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -229,7 +229,8 @@ describe('ReportUtils', () => { const policyData: PolicyData = { reports, - tags: createRandomPolicyTags('Tags', 8), + policyID, + tagLists: createRandomPolicyTags('Tags', 8), categories: createRandomPolicyCategories(8), // Current policy with categories and tags enabled but does not require them policy: { diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index b946077d1a5a9..05f31ca881a70 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -91,35 +91,29 @@ describe('usePolicyData', () => { 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?.policyID).toEqual(mockPolicy.id) + expect(result.current?.policy).toEqual(mockPolicy); + expect(result.current?.tagLists).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?.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 policyID = 'non_existent_policy_id' 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?.policyID).toEqual(policyID); + expect(result.current?.policy).toBeUndefined(); + + expect(result.current?.reports).toBeUndefined(); + expect(result.current?.tagLists).toBeUndefined(); + expect(result.current?.categories).toBeUndefined(); + - 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({}); + expect(result.current?.transactionsAndViolations).toEqual({}); }); }); From f8a8b16cead0ec94529c2154b89156bf32336e62 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 19 Nov 2025 01:25:33 +0300 Subject: [PATCH 08/13] fix: removing violations when policy category is created --- src/hooks/usePolicyData/index.ts | 5 +- src/hooks/usePolicyData/types.ts | 2 +- src/libs/actions/Policy/Category.ts | 7 +- src/libs/actions/Policy/Tag.ts | 217 ++++++++++-------- .../categories/CreateCategoryPage.tsx | 12 +- tests/actions/IOUTest.ts | 23 +- tests/actions/PolicyTagTest.ts | 118 +++++----- tests/actions/ReportTest.ts | 46 ++-- tests/unit/usePolicyData.test.ts | 11 +- 9 files changed, 239 insertions(+), 202 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index 4fd908f153ff5..379237954031f 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -36,7 +36,7 @@ function usePolicyData(policyID: string): PolicyData { ); const policy = usePolicy(policyID); - const [tagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}, [policyID]); + 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: reportsSelectorCallback}, [policyID, allReportsTransactionsAndViolations]); const transactionsAndViolations = useMemo(() => { @@ -50,9 +50,10 @@ function usePolicyData(policyID: string): PolicyData { return acc; }, {}); }, [reports, allReportsTransactionsAndViolations]); + return { transactionsAndViolations, - tagLists, + tags: tags ?? {}, categories: categories ?? {}, policyID, policy, diff --git a/src/hooks/usePolicyData/types.ts b/src/hooks/usePolicyData/types.ts index 509587cf6886c..4199e0ad36f08 100644 --- a/src/hooks/usePolicyData/types.ts +++ b/src/hooks/usePolicyData/types.ts @@ -5,7 +5,7 @@ import type {ReportTransactionsAndViolationsDerivedValue} from '@src/types/onyx/ type PolicyData = { policyID: string; policy: OnyxEntry; - tagLists: OnyxEntry; + tags: PolicyTagLists; categories: OnyxEntry; reports: Array>; transactionsAndViolations: ReportTransactionsAndViolationsDerivedValue; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index da9643c54ba2d..6b42a960e4481 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -602,17 +602,18 @@ function removePolicyCategoryReceiptsRequired(policyData: PolicyData, categoryNa } function createPolicyCategory( - policyID: string, + policyData: PolicyData, categoryName: string, isSetupCategoriesTaskParentReportArchived: boolean, setupCategoryTaskReport: OnyxEntry, setupCategoryTaskParentReport: OnyxEntry, currentUserAccountID: number, ) { - const onyxData = buildOptimisticPolicyCategories(policyID, [categoryName]); + const onyxData = buildOptimisticPolicyCategories(policyData.policyID, [categoryName]); appendSetupCategoriesOnboardingData(onyxData, setupCategoryTaskReport, setupCategoryTaskParentReport, isSetupCategoriesTaskParentReportArchived, currentUserAccountID); + const parameters = { - policyID, + policyID: policyData.policyID, categories: JSON.stringify([{name: categoryName}]), }; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 4c9a53ee2deb0..b8f06cf1c53a8 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -125,29 +125,27 @@ function updateImportSpreadsheetData(tagsLength: number): OnyxData { return onyxData; } -function createPolicyTag(policyData: PolicyData, tagName: string) { - const {policyID, tagLists} = policyData; - const tagList = PolicyUtils.getTagLists(tagLists)?.at(0) ?? ({} as PolicyTagList); +function createPolicyTag(policyID: string, tagName: string, policyTags: PolicyTagLists = {}) { + const policyTag = PolicyUtils.getTagLists(policyTags)?.at(0) ?? ({} as PolicyTagList); const newTagName = PolicyUtils.escapeTagName(tagName); - const tagListsOptimisticData = { - [tagList.name]: { - tags: { - [newTagName]: { - name: newTagName, - enabled: true, - errors: null, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - }, - }; const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: { + [policyTag.name]: { + tags: { + [newTagName]: { + name: newTagName, + enabled: true, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + }, }, ], successData: [ @@ -155,7 +153,7 @@ function createPolicyTag(policyData: PolicyData, tagName: string) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { + [policyTag.name]: { tags: { [newTagName]: { errors: null, @@ -171,7 +169,7 @@ function createPolicyTag(policyData: PolicyData, tagName: string) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { + [policyTag.name]: { tags: { [newTagName]: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -188,8 +186,6 @@ function createPolicyTag(policyData: PolicyData, tagName: string) { tags: JSON.stringify([{name: newTagName}]), }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); - API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); } @@ -206,40 +202,39 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { } function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record, tagListIndex: number) { - if (tagListIndex === -1) { - return; - } - const {policyID, policy, tagLists} = policyData; - const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); - if (!tagList || !policy) { + if (!policyTag || tagListIndex === -1 || !policyData.policy) { return; } - const tagListsOptimisticData = { - [tagList.name]: { - ...Object.keys(tagsToUpdate).reduce((acc, key) => { - acc[key] = { - ...tagList.tags[key], - ...tagsToUpdate[key], - errors: null, - pendingFields: { - ...tagList.tags[key]?.pendingFields, - enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }; + const policyID = policyData.policy?.id; + const policyTagsOptimisticData = { + ...Object.keys(tagsToUpdate).reduce((acc, key) => { + acc[key] = { + ...policyTag.tags[key], + ...tagsToUpdate[key], + errors: null, + pendingFields: { + ...policyTag.tags[key]?.pendingFields, + enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + pendingAction: 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: tagListsOptimisticData, + value: { + [policyTag.name]: { + tags: policyTagsOptimisticData, + }, + }, }, ], successData: [ @@ -247,15 +242,15 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { acc[key] = { - ...tagList.tags[key], + ...policyTag.tags[key], ...tagsToUpdate[key], errors: null, pendingFields: { - ...tagList.tags[key].pendingFields, + ...policyTag.tags[key].pendingFields, enabled: null, }, pendingAction: null, @@ -273,15 +268,15 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, key) => { acc[key] = { - ...tagList.tags[key], + ...policyTag.tags[key], ...tagsToUpdate[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), pendingFields: { - ...tagList.tags[key].pendingFields, + ...policyTag.tags[key].pendingFields, enabled: null, }, pendingAction: null, @@ -296,7 +291,17 @@ function setWorkspaceTagEnabled(policyData: PolicyData, tagsToUpdate: Record((acc, [key, value]) => { - if (tagListIndexes.includes(value.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, @@ -335,7 +343,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: policyTagsOptimisticData, }, ], successData: [ @@ -343,8 +351,8 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.entries(tagLists ?? {}).reduce((acc, [key, value]) => { - if (tagListIndexes.includes(value.orderWeight)) { + ...Object.keys(policyData.tags).reduce((acc, key) => { + if (tagListIndexes.includes(policyData.tags[key].orderWeight)) { acc[key] = { ...acc[key], errors: undefined, @@ -366,7 +374,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - ...Object.keys(tagLists ?? {}).reduce((acc, key) => { + ...Object.keys(policyData.tags).reduce((acc, key) => { acc[key] = { ...acc[key], errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -382,7 +390,7 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); const parameters: SetPolicyTagListsRequired = { policyID, @@ -394,15 +402,15 @@ function setWorkspaceTagRequired(policyData: PolicyData, tagListIndexes: number[ } function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { - const {policyID, tagLists} = policyData; - const tagList = PolicyUtils.getTagLists(tagLists)?.at(0); + const policyID = policyData.policy?.id; + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(0); - if (!tagList) { + if (!policyTag) { return; } - const tagListsOptimisticData: Record>>> = { - [tagList.name]: { + const policyTagsOptimisticData: Record>>> = { + [policyTag.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false}; @@ -417,7 +425,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: policyTagsOptimisticData, }, ], successData: [ @@ -425,7 +433,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { + [policyTag.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = null; @@ -441,13 +449,13 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { + [policyTag.name]: { tags: { ...tagsToDelete.reduce>>>((acc, tagName) => { acc[tagName] = { pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.deleteFailureMessage'), - enabled: !!tagList?.tags[tagName]?.enabled, + enabled: !!policyTag?.tags[tagName]?.enabled, }; return acc; }, {}), @@ -458,7 +466,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); const parameters = { policyID, @@ -557,8 +565,8 @@ function clearPolicyTagListErrors({policyID, tagListIndex, policyTags}: ClearPol } function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; newName: string}, tagListIndex: number) { - const {policyID, policy, tagLists} = policyData; - const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); + const policyID = policyData.policy?.id; + const tagList = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); if (!tagList) { return; } @@ -567,7 +575,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne 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 @@ -591,7 +599,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne }, }; - const tagListsOptimisticData: Record>>> = { + const policyTagsOptimisticData: Record>>> = { [tagList?.name]: { tags: { [oldTagName]: null, @@ -615,7 +623,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: policyTagsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -666,7 +674,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, tagListsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, policyTagsOptimisticData); const parameters: RenamePolicyTagsParams = { policyID, @@ -679,7 +687,7 @@ function renamePolicyTag(policyData: PolicyData, policyTag: {oldName: string; ne } function enablePolicyTags(policyData: PolicyData, enabled: boolean) { - const {policyID, tagLists} = policyData; + const policyID = policyData.policy?.id; const policyOptimisticData = { areTagsEnabled: enabled, pendingFields: { @@ -720,7 +728,15 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { ], }; - if (!tagLists || Object.keys(tagLists).length === 0) { + if (Object.keys(policyData.tags).length === 0) { + const defaultTagList: PolicyTagLists = { + Tag: { + name: 'Tag', + orderWeight: 0, + required: false, + tags: {}, + }, + }; onyxData.optimisticData?.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, @@ -731,18 +747,18 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: null, }); - pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, CONST.POLICY.DEFAULT_TAG_LIST); + pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData, {}, defaultTagList); } else if (!enabled) { - const tagList = PolicyUtils.getTagLists(tagLists).at(0); + const policyTag = PolicyUtils.getTagLists(policyData.tags).at(0); - if (!tagList) { + if (!policyTag) { return; } - const tagListsOptimisticData: Record> = { - [tagList.name]: { + const policyTagsOptimisticData: Record> = { + [policyTag.name]: { tags: Object.fromEntries( - Object.keys(tagLists ?? {}).map((tagName) => [ + Object.keys(policyTag.tags).map((tagName) => [ tagName, { enabled: false, @@ -756,7 +772,7 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: policyTagsOptimisticData, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -767,7 +783,7 @@ function enablePolicyTags(policyData: PolicyData, enabled: boolean) { }, ); - pushTransactionViolationsOnyxData(onyxData, policyData, {...policyOptimisticData, requiresTag: false}, {}, tagListsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {...policyOptimisticData, requiresTag: false}, {}, policyTagsOptimisticData); } else { pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData); } @@ -874,9 +890,9 @@ function importMultiLevelTags(policyID: string, spreadsheet: ImportedSpreadsheet ); } -function renamePolicyTagList(policyID: string, tagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number) { - const newName = tagListName.newName; - const oldName = tagListName.oldName; +function renamePolicyTagList(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry, tagListIndex: number) { + const newName = policyTagListName.newName; + const oldName = policyTagListName.oldName; const oldPolicyTags = policyTags?.[oldName] ?? {}; const onyxData: OnyxData = { optimisticData: [ @@ -926,7 +942,7 @@ function renamePolicyTagList(policyID: string, tagListName: {oldName: string; ne } function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { - const {policyID, tagLists} = policyData; + const policyID = policyData.policy?.id; const policyOptimisticData: Partial = { requiresTag, errors: {requiresTag: null}, @@ -973,7 +989,7 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { }; const getUpdatedTagsData = (required: boolean): PolicyTagLists => ({ - ...Object.keys(tagLists ?? {}).reduce((acc, key) => { + ...Object.keys(policyData.tags).reduce((acc, key) => { acc[key] = { ...acc[key], required, @@ -1003,13 +1019,14 @@ function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) { } function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tagListIndex: number) { - const {policyID, tagLists} = policyData; - const tagList = PolicyUtils.getTagLists(tagLists)?.at(tagListIndex); - if (!tagList || !tagList.name) { + const policyTag = PolicyUtils.getTagLists(policyData.tags)?.at(tagListIndex); + if (!policyTag || !policyTag.name) { return; } - const tagListsOptimisticData = { - [tagList.name]: { + + const policyID = policyData.policy?.id; + const policyTagsOptimisticData = { + [policyTag.name]: { required: requiresTag, pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, errorFields: {required: null}, @@ -1021,7 +1038,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - value: tagListsOptimisticData, + value: policyTagsOptimisticData, }, ], successData: [ @@ -1029,7 +1046,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { + [policyTag.name]: { pendingFields: {required: null}, }, }, @@ -1040,8 +1057,8 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [tagList.name]: { - required: tagList.required, + [policyTag.name]: { + required: policyTag.required, pendingFields: {required: null}, errorFields: { required: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), @@ -1052,7 +1069,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag ], }; - pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, tagListsOptimisticData); + pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, policyTagsOptimisticData); const parameters: SetPolicyTagsRequired = { policyID, diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index d28f49213676b..214250b9629b1 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -5,8 +5,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; -import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; +import usePolicyData from '@hooks/usePolicyData'; import {createPolicyCategory} from '@libs/actions/Policy/Category'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -23,11 +23,11 @@ type CreateCategoryPageProps = | PlatformStackScreenProps; function CreateCategoryPage({route}: CreateCategoryPageProps) { - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, {canBeMissing: true}); const styles = useThemeStyles(); const {translate} = useLocalize(); const backTo = route.params?.backTo; const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_CREATE; + const policyData = usePolicyData(route.params.policyID); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const { taskReport: setupCategoryTaskReport, @@ -38,17 +38,17 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { const createCategory = useCallback( (values: FormOnyxValues) => { createPolicyCategory( - route.params.policyID, + policyData, values.categoryName.trim(), isSetupCategoryTaskParentReportArchived, setupCategoryTaskReport, setupCategoryTaskParentReport, currentUserPersonalDetails.accountID, ); - Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo) : undefined); + Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(policyData.policyID, backTo) : undefined); }, [ - route.params.policyID, + policyData, isSetupCategoryTaskParentReportArchived, setupCategoryTaskReport, setupCategoryTaskParentReport, @@ -76,7 +76,7 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { /> diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 128520d2dc5f1..ca94df6c6537b 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1114,7 +1114,7 @@ describe('actions/IOU', () => { attendees: [], currency: CONST.CURRENCY.USD, created: '', - merchant: '', + merchant: '(none)', comment, }, shouldGenerateTransactionThreadReport: true, @@ -1425,7 +1425,7 @@ describe('actions/IOU', () => { expect(newTransaction?.reportID).toBe(iouReportID); expect(newTransaction?.amount).toBe(amount); expect(newTransaction?.comment?.comment).toBe(comment); - expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); expect(newTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -1600,7 +1600,7 @@ describe('actions/IOU', () => { expect(transaction?.reportID).toBe(iouReportID); expect(transaction?.amount).toBe(amount); expect(transaction?.comment?.comment).toBe(comment); - expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(transaction?.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -4435,7 +4435,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); // When a comment is added - addComment(thread.reportID, thread.reportID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(thread.reportID, thread.reportID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Then comment details should match the expected report action @@ -4533,7 +4533,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); - addComment(thread.reportID, thread.reportID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(thread.reportID, thread.reportID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. @@ -4582,7 +4582,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); if (IOU_REPORT_ID) { - addComment(IOU_REPORT_ID, IOU_REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(IOU_REPORT_ID, IOU_REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); } await waitForBatchedUpdates(); @@ -6354,7 +6354,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(0), reportID, amount: 0, - modifiedAmount: 0, + modifiedAmount: '', receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6366,7 +6366,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), reportID, amount: 0, - modifiedAmount: 0, + modifiedAmount: '', receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6416,7 +6416,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), reportID, amount: 0, - modifiedAmount: 0, + modifiedAmount: '', receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6708,7 +6708,7 @@ describe('actions/IOU', () => { reportID: fakeReport.reportID, transactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID, isFromGlobalCreate: true, - merchant: '(none)', + merchant: 'Expense', }; const currentDate = '2025-04-01'; @@ -6758,6 +6758,7 @@ describe('actions/IOU', () => { .then(async () => { expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`)).toStrictEqual({ ...transactionResult, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, }); }); @@ -6902,7 +6903,7 @@ describe('actions/IOU', () => { }, }); }); - expect(updatedTransaction?.modifiedAmount).toBe(0); + expect(updatedTransaction?.modifiedAmount).toBe(''); }); }); diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index cc4bd75d924a2..195e23ad1b27d 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -287,15 +287,11 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - mockFetch.pause(); await waitForBatchedUpdates(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When creating a new tag - createPolicyTag(policyData.current, newTagName); + createPolicyTag(fakePolicy.id, newTagName, fakePolicyTags); await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback @@ -325,17 +321,12 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - await waitForBatchedUpdates(); - mockFetch.pause(); await waitForBatchedUpdates(); mockFetch.fail(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When the API fails - createPolicyTag(policyData.current, newTagName); + createPolicyTag(fakePolicy.id, newTagName, fakePolicyTags); await waitForBatchedUpdates(); mockFetch.resume(); await waitForBatchedUpdates(); @@ -352,17 +343,12 @@ describe('actions/Policy', () => { fakePolicy.areTagsEnabled = true; const newTagName = 'new tag'; - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, {}); - await waitForBatchedUpdates(); - - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); mockFetch.pause(); await waitForBatchedUpdates(); // When adding the first tag - createPolicyTag(policyData.current, newTagName); + createPolicyTag(fakePolicy.id, newTagName, {}); await waitForBatchedUpdates(); // Then the tag should be created in a new list with pending state so the user sees immediate feedback @@ -401,17 +387,18 @@ describe('actions/Policy', () => { const fakePolicyTags = createRandomPolicyTags(tagListName); mockFetch.pause(); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); await waitForBatchedUpdates(); - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); + await waitFor(() => { - expect(policyData.current.tagLists).toBeDefined(); + expect(result.current[0]).toBeDefined(); }); // When using data from useOnyx hook - createPolicyTag(policyData.current, newTagName); + createPolicyTag(fakePolicy.id, newTagName, result.current[0] ?? {}); await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback @@ -452,10 +439,9 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - + await waitForBatchedUpdates(); setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); await waitForBatchedUpdates(); @@ -510,10 +496,8 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - - mockFetch?.fail?.(); - await waitForBatchedUpdates(); + mockFetch?.fail?.(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); setWorkspaceTagEnabled(policyData.current, tagsToUpdate, 0); await waitForBatchedUpdates(); @@ -545,8 +529,15 @@ describe('actions/Policy', () => { mockFetch?.pause?.(); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - await waitForBatchedUpdates(); + + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdates(); + + await waitFor(() => { + expect(result.current[0]).toBeDefined(); + }); setWorkspaceTagEnabled(policyData.current, {[tagName]: {name: tagName, enabled: false}}, 0); @@ -596,9 +587,15 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); // When the tag is renamed - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const policyData = { + policy: fakePolicy, + tags: fakePolicyTags, + categories: {}, + reports: [], + transactionsAndViolations: {}, + }; renamePolicyTag( - policyData.current, + policyData, { oldName: oldTagName, newName: newTagName, @@ -647,9 +644,15 @@ describe('actions/Policy', () => { mockFetch.fail(); // When the tag rename fails - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const policyData = { + policy: fakePolicy, + tags: fakePolicyTags, + categories: {}, + reports: [], + transactionsAndViolations: {}, + }; renamePolicyTag( - policyData.current, + policyData, { oldName: oldTagName, newName: newTagName, @@ -681,11 +684,17 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, existingPolicyTags); // When trying to rename a tag with invalid index - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const policyData = { + policy: fakePolicy, + tags: {}, + categories: {}, + reports: [], + transactionsAndViolations: {}, + }; expect(() => { renamePolicyTag( - policyData.current, + policyData, { oldName: 'oldTag', newName: 'newTag', @@ -733,12 +742,17 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); - await waitForBatchedUpdates(); // When the tag is renamed - const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + const policyData = { + policy: fakePolicy, + tags: fakePolicyTags, + categories: {}, + reports: [], + transactionsAndViolations: {}, + }; renamePolicyTag( - policyData.current, + policyData, { oldName: oldTagName, newName: newTagName, @@ -1698,7 +1712,7 @@ describe('actions/Policy', () => { expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And a default tag list should be created - const defaultTag = Object.values(policyData.current?.tagLists ?? {}).at(0); + const defaultTag = Object.values(policyData.current?.tags ?? {}).at(0); expect(defaultTag?.name).toBe('Tag'); expect(defaultTag?.orderWeight).toBe(0); expect(defaultTag?.required).toBe(false); @@ -1741,7 +1755,7 @@ describe('actions/Policy', () => { // And all tags should be disabled for (const tagName of Object.keys(existingTags)) { - expect(policyData.current?.tagLists?.[tagListName]?.tags[tagName]?.enabled).toBe(false); + expect(policyData.current?.tags?.[tagListName]?.tags[tagName]?.enabled).toBe(false); } await mockFetch.resume(); @@ -1775,9 +1789,9 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // After the API request failure, the policy should be reset to original state - expect(policyData?.current?.policy?.areTagsEnabled).toBe(false); - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); - expect(policyData?.current?.tagLists).toMatchObject({}); + expect(policyData.current.policy.areTagsEnabled).toBe(false); + expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData.current.tags).toMatchObject({}); }); it('should work with data from useOnyx hook', async () => { @@ -1799,8 +1813,8 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current.policy.areTagsEnabled).toBe(true); + expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); await mockFetch.resume(); await waitForBatchedUpdates(); @@ -1808,11 +1822,11 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // And after API success, policy should be enabled - expect(policyData?.current?.policy?.areTagsEnabled).toBe(true); - expect(policyData?.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData.current.policy.areTagsEnabled).toBe(true); + expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); // And default tag list should be created - expect(policyData?.current?.tagLists?.Tag).toBeDefined(); + expect(policyData.current.tags.Tag).toBeDefined(); }); }); @@ -1843,7 +1857,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as required with pending fields - let updatedPolicyTags = policyData?.current?.tagLists; + let updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); // Check optimistic data - pendingFields should be set @@ -1855,7 +1869,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); rerender(fakePolicy.id); // Then after API success, pending fields should be cleared - updatedPolicyTags = policyData?.current?.tagLists; + updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); @@ -1886,7 +1900,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as not required with pending fields - let updatedPolicyTags = policyData?.current?.tagLists; + let updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); // Check optimistic data - pendingFields should be set @@ -1899,7 +1913,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then after API success, pending fields should be cleared - updatedPolicyTags = policyData?.current?.tagLists; + updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); @@ -1934,7 +1948,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be restored to original state with error - const updatedPolicyTags = policyData?.current?.tagLists; + const updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(false); expect(updatedPolicyTags?.[tagListName]?.pendingFields?.required).toBeUndefined(); expect(updatedPolicyTags?.[tagListName]?.errorFields?.required).toBeTruthy(); @@ -1972,7 +1986,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the tag list should be marked as required - const updatedPolicyTags = policyData?.current?.tagLists; + const updatedPolicyTags = policyData.current.tags; expect(updatedPolicyTags?.[tagListName]?.required).toBe(true); // Check optimistic data - pendingFields should be set diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 2e5b744f181d4..6a339277c315d 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -153,7 +153,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -283,7 +283,7 @@ describe('actions/Report', () => { } // And leave a comment on a report - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Then we should expect that there is on persisted request expect(PersistedRequests.getAll().length).toBe(1); @@ -406,7 +406,7 @@ describe('actions/Report', () => { // When a new comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 1', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 1', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -418,7 +418,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 2', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 2', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -429,7 +429,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 3', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 3', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -696,7 +696,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -824,7 +824,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -915,8 +915,12 @@ describe('actions/Report', () => { comment: {comment: 'Legacy expense'}, } as unknown as OnyxTypes.Transaction); - // Call openReport with transactionID to trigger the legacy preview flow - Report.openReport(CHILD_REPORT_ID, undefined, [], undefined, undefined, false, [], undefined, TXN_ID); + // Get the transaction object from Onyx + const transaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${TXN_ID}` as const); + expect(transaction).toBeTruthy(); + + // Call openReport with transaction object to trigger the legacy preview flow + Report.openReport(CHILD_REPORT_ID, undefined, [], undefined, undefined, false, [], undefined, false, transaction ?? undefined, undefined, SELF_DM_ID); await waitForBatchedUpdates(); // Validate the correct Onyx key received the new action and existing one is preserved @@ -983,7 +987,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1062,7 +1066,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(1); @@ -1119,7 +1123,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForNetworkPromises(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1168,7 +1172,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); const file = new File([''], 'test.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1237,7 +1241,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file, 'Attachment with comment'); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file, 'Attachment with comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1320,7 +1324,7 @@ describe('actions/Report', () => { const fileB = new File(['b'], 'b.txt', {type: 'text/plain'}); const fileC = new File(['c'], 'c.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], [fileA, fileB, fileC], 'Hello world', CONST.DEFAULT_TIME_ZONE, shouldPlaySound); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [fileA, fileB, fileC], 'Hello world', CONST.DEFAULT_TIME_ZONE, shouldPlaySound); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(1); @@ -1353,7 +1357,7 @@ describe('actions/Report', () => { const fileA = new File(['a'], 'a.txt', {type: 'text/plain'}); const fileB = new File(['b'], 'b.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], [fileA, fileB], undefined, CONST.DEFAULT_TIME_ZONE, shouldPlaySound); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [fileA, fileB], undefined, CONST.DEFAULT_TIME_ZONE, shouldPlaySound); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(1); @@ -1385,7 +1389,7 @@ describe('actions/Report', () => { const REPORT_ID = '1'; const file = new File(['a'], 'a.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(0); @@ -1407,7 +1411,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await Promise.resolve(); - Report.addComment(REPORT_ID, REPORT_ID, [], 'reactions with comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'reactions with comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1504,7 +1508,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Attachment with comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Attachment with comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1576,7 +1580,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await waitForBatchedUpdates(); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -1628,7 +1632,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index 05f31ca881a70..faf91166582ff 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -91,9 +91,9 @@ describe('usePolicyData', () => { const {result} = renderHook(() => usePolicyData(mockPolicy.id), {wrapper: OnyxListItemProvider}); - expect(result.current?.policyID).toEqual(mockPolicy.id) + expect(result.current?.policyID).toEqual(mockPolicy.id); expect(result.current?.policy).toEqual(mockPolicy); - expect(result.current?.tagLists).toEqual(mockPolicyTagLists); + expect(result.current?.tags).toEqual(mockPolicyTagLists); expect(result.current?.categories).toEqual(mockPolicyCategories); expect(result.current?.reports).toHaveLength(1); @@ -103,16 +103,15 @@ describe('usePolicyData', () => { }); test('returns default empty values when policy ID does not exist in the onyx', () => { - const policyID = 'non_existent_policy_id' + const policyID = 'non_existent_policy_id'; const {result} = renderHook(() => usePolicyData('non_existent_policy_id'), {wrapper: OnyxListItemProvider}); expect(result.current?.policyID).toEqual(policyID); expect(result.current?.policy).toBeUndefined(); - + expect(result.current?.reports).toBeUndefined(); - expect(result.current?.tagLists).toBeUndefined(); + expect(result.current?.tags).toBeUndefined(); expect(result.current?.categories).toBeUndefined(); - expect(result.current?.transactionsAndViolations).toEqual({}); }); From 3fe7a5604f00dafe7d33e3fc908d858a0f3ef5aa Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 19 Nov 2025 02:33:15 +0300 Subject: [PATCH 09/13] refactor: update policyTags reference in usePolicyData and related components --- src/hooks/usePolicyData/index.ts | 2 +- .../categories/CreateCategoryPage.tsx | 12 ++---------- src/pages/workspace/tags/EditTagPage.tsx | 2 +- src/pages/workspace/tags/TagSettingsPage.tsx | 2 +- .../workspace/tags/WorkspaceCreateTagPage.tsx | 19 +++++++++---------- .../workspace/tags/WorkspaceTagsPage.tsx | 2 +- .../tags/WorkspaceTagsSettingsPage.tsx | 2 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 3 ++- tests/unit/usePolicyData.test.ts | 2 +- 9 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index 379237954031f..f95b5a22f7cad 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -54,7 +54,7 @@ function usePolicyData(policyID: string): PolicyData { return { transactionsAndViolations, tags: tags ?? {}, - categories: categories ?? {}, + categories, policyID, policy, reports: Object.values(reports ?? {}), diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 214250b9629b1..d26acca4476bf 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -5,8 +5,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; -import useThemeStyles from '@hooks/useThemeStyles'; import usePolicyData from '@hooks/usePolicyData'; +import useThemeStyles from '@hooks/useThemeStyles'; import {createPolicyCategory} from '@libs/actions/Policy/Category'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -47,15 +47,7 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { ); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(policyData.policyID, backTo) : undefined); }, - [ - policyData, - isSetupCategoryTaskParentReportArchived, - setupCategoryTaskReport, - setupCategoryTaskParentReport, - currentUserPersonalDetails.accountID, - isQuickSettingsFlow, - backTo, - ], + [policyData, isSetupCategoryTaskParentReportArchived, setupCategoryTaskReport, setupCategoryTaskParentReport, currentUserPersonalDetails.accountID, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index dded9e2cc874f..3a26336a39857 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -30,7 +30,7 @@ type EditTagPageProps = function EditTagPage({route}: EditTagPageProps) { const {backTo, policyID} = route.params; const policyData = usePolicyData(policyID); - const {tagLists: policyTags} = policyData; + const {tags: policyTags} = policyData; const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 7bf4321dd2af7..00ef9c8d79c1b 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -45,7 +45,7 @@ function TagSettingsPage({route, navigation}: TagSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policyData = usePolicyData(policyID); - const {policy, tagLists: policyTags} = policyData; + const {policy, tags: policyTags} = policyData; const policyTag = useMemo(() => getTagListByOrderWeight(policyTags, orderWeight), [policyTags, orderWeight]); const {environmentURL} = useEnvironment(); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index 176be61755752..a607308aabe0a 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -8,7 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; -import usePolicyData from '@hooks/usePolicyData'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -29,20 +29,19 @@ type WorkspaceCreateTagPageProps = | PlatformStackScreenProps; function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { - const {policyID, backTo} = route.params; - const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE; - + const policyID = route.params.policyID; + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); - const policyData = usePolicyData(policyID); - const {tagLists} = policyData; + const backTo = route.params.backTo; + const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.SETTINGS_TAG_CREATE; const validate = useCallback( (values: FormOnyxValues) => { const errors: FormInputErrors = {}; const tagName = escapeTagName(values.tagName.trim()); - const {tags} = getTagList(tagLists, 0); + const {tags} = getTagList(policyTags, 0); if (!isRequiredFulfilled(tagName)) { errors.tagName = translate('workspace.tags.tagRequiredError'); @@ -57,16 +56,16 @@ function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { return errors; }, - [tagLists, translate], + [policyTags, translate], ); const createTag = useCallback( (values: FormOnyxValues) => { - createPolicyTag(policyData, values.tagName.trim()); + createPolicyTag(policyID, values.tagName.trim(), policyTags); Keyboard.dismiss(); Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); }, - [policyID, policyData, isQuickSettingsFlow, backTo], + [policyID, policyTags, isQuickSettingsFlow, backTo], ); return ( diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 503bbb2f623c1..8f329656899f3 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -88,7 +88,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const [isCannotMakeLastTagOptionalModalVisible, setIsCannotMakeLastTagOptionalModalVisible] = useState(false); const {backTo, policyID} = route.params; const policyData = usePolicyData(policyID); - const {policy, tagLists: policyTags} = policyData; + const {policy, tags: policyTags} = policyData; const isMobileSelectionModeEnabled = useMobileSelectionMode(); const {environmentURL} = useEnvironment(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index b061f6832451a..bd897207fcba6 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -54,7 +54,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) { const backTo = route.params.backTo; const styles = useThemeStyles(); const policyData = usePolicyData(policyID); - const {tagLists: policyTags} = policyData; + const {tags: policyTags} = policyData; const {translate} = useLocalize(); const [policyTagLists, isMultiLevelTags] = useMemo(() => [getTagListsUtil(policyTags), isMultiLevelTagsUtil(policyTags)], [policyTags]); const isLoading = !getTagListsUtil(policyTags)?.at(0) || Object.keys(policyTags ?? {}).at(0) === 'undefined'; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 6093f6767276a..0a5ceaec8364f 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -68,7 +68,8 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const policyData = usePolicyData(policyID); - const {policy, tagLists: policyTags} = policyData; + const {policy, tags: policyTags} = policyData; + const isMobileSelectionModeEnabled = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, orderWeight), [policyTags, orderWeight]); const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index faf91166582ff..1a298647cf0eb 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -99,7 +99,7 @@ describe('usePolicyData', () => { expect(result.current?.reports).toHaveLength(1); expect(result.current?.reports.at(0)).toEqual(mockIOUReport); - expect(result.current.transactionsAndViolations).toEqual(expectedTransactionsAndViolations); + expect(result.current?.transactionsAndViolations).toEqual(expectedTransactionsAndViolations); }); test('returns default empty values when policy ID does not exist in the onyx', () => { From 9449a83eb47d21726b8f53008e7a1a7234d86741 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 19 Nov 2025 03:16:08 +0300 Subject: [PATCH 10/13] refactor --- src/libs/ReportUtils.ts | 2 +- tests/actions/IOUTest.ts | 23 +++++++++---------- tests/actions/ReportTest.ts | 46 +++++++++++++++++-------------------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0301ffefe6843..b413720ab54e8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1982,7 +1982,7 @@ function pushTransactionViolationsOnyxData( categoriesUpdate: Record> = {}, tagListsUpdate: Record> = {}, ) { - const {policy, tagLists, categories} = policyData; + const {policy, tags: tagLists, categories} = policyData; if (policy === undefined) { return; } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index ca94df6c6537b..128520d2dc5f1 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1114,7 +1114,7 @@ describe('actions/IOU', () => { attendees: [], currency: CONST.CURRENCY.USD, created: '', - merchant: '(none)', + merchant: '', comment, }, shouldGenerateTransactionThreadReport: true, @@ -1425,7 +1425,7 @@ describe('actions/IOU', () => { expect(newTransaction?.reportID).toBe(iouReportID); expect(newTransaction?.amount).toBe(amount); expect(newTransaction?.comment?.comment).toBe(comment); - expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); + expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); expect(newTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -1600,7 +1600,7 @@ describe('actions/IOU', () => { expect(transaction?.reportID).toBe(iouReportID); expect(transaction?.amount).toBe(amount); expect(transaction?.comment?.comment).toBe(comment); - expect(transaction?.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); + expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -4435,7 +4435,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); // When a comment is added - addComment(thread.reportID, thread.reportID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(thread.reportID, thread.reportID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Then comment details should match the expected report action @@ -4533,7 +4533,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); - addComment(thread.reportID, thread.reportID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(thread.reportID, thread.reportID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. @@ -4582,7 +4582,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); if (IOU_REPORT_ID) { - addComment(IOU_REPORT_ID, IOU_REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + addComment(IOU_REPORT_ID, IOU_REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); } await waitForBatchedUpdates(); @@ -6354,7 +6354,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(0), reportID, amount: 0, - modifiedAmount: '', + modifiedAmount: 0, receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6366,7 +6366,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), reportID, amount: 0, - modifiedAmount: '', + modifiedAmount: 0, receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6416,7 +6416,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), reportID, amount: 0, - modifiedAmount: '', + modifiedAmount: 0, receipt: { source: 'test', state: CONST.IOU.RECEIPT_STATE.SCANNING, @@ -6708,7 +6708,7 @@ describe('actions/IOU', () => { reportID: fakeReport.reportID, transactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID, isFromGlobalCreate: true, - merchant: 'Expense', + merchant: '(none)', }; const currentDate = '2025-04-01'; @@ -6758,7 +6758,6 @@ describe('actions/IOU', () => { .then(async () => { expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`)).toStrictEqual({ ...transactionResult, - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, }); }); @@ -6903,7 +6902,7 @@ describe('actions/IOU', () => { }, }); }); - expect(updatedTransaction?.modifiedAmount).toBe(''); + expect(updatedTransaction?.modifiedAmount).toBe(0); }); }); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 6a339277c315d..2e5b744f181d4 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -153,7 +153,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -283,7 +283,7 @@ describe('actions/Report', () => { } // And leave a comment on a report - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Then we should expect that there is on persisted request expect(PersistedRequests.getAll().length).toBe(1); @@ -406,7 +406,7 @@ describe('actions/Report', () => { // When a new comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 1', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 1', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -418,7 +418,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 2', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 2', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -429,7 +429,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 3', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Current User Comment 3', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -696,7 +696,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -824,7 +824,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); return waitForBatchedUpdates(); }) .then(() => { @@ -915,12 +915,8 @@ describe('actions/Report', () => { comment: {comment: 'Legacy expense'}, } as unknown as OnyxTypes.Transaction); - // Get the transaction object from Onyx - const transaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${TXN_ID}` as const); - expect(transaction).toBeTruthy(); - - // Call openReport with transaction object to trigger the legacy preview flow - Report.openReport(CHILD_REPORT_ID, undefined, [], undefined, undefined, false, [], undefined, false, transaction ?? undefined, undefined, SELF_DM_ID); + // Call openReport with transactionID to trigger the legacy preview flow + Report.openReport(CHILD_REPORT_ID, undefined, [], undefined, undefined, false, [], undefined, TXN_ID); await waitForBatchedUpdates(); // Validate the correct Onyx key received the new action and existing one is preserved @@ -987,7 +983,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1066,7 +1062,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(1); @@ -1123,7 +1119,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); await waitForNetworkPromises(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1172,7 +1168,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); const file = new File([''], 'test.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1241,7 +1237,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file, 'Attachment with comment'); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file, 'Attachment with comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1324,7 +1320,7 @@ describe('actions/Report', () => { const fileB = new File(['b'], 'b.txt', {type: 'text/plain'}); const fileC = new File(['c'], 'c.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [fileA, fileB, fileC], 'Hello world', CONST.DEFAULT_TIME_ZONE, shouldPlaySound); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], [fileA, fileB, fileC], 'Hello world', CONST.DEFAULT_TIME_ZONE, shouldPlaySound); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(1); @@ -1357,7 +1353,7 @@ describe('actions/Report', () => { const fileA = new File(['a'], 'a.txt', {type: 'text/plain'}); const fileB = new File(['b'], 'b.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [fileA, fileB], undefined, CONST.DEFAULT_TIME_ZONE, shouldPlaySound); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], [fileA, fileB], undefined, CONST.DEFAULT_TIME_ZONE, shouldPlaySound); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(1); @@ -1389,7 +1385,7 @@ describe('actions/Report', () => { const REPORT_ID = '1'; const file = new File(['a'], 'a.txt', {type: 'text/plain'}); - Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, file); + Report.addAttachmentWithComment(REPORT_ID, REPORT_ID, [], file); const relevant = (await relevantPromise) as OnyxTypes.Request[]; expect(playSoundMock).toHaveBeenCalledTimes(0); @@ -1411,7 +1407,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await Promise.resolve(); - Report.addComment(REPORT_ID, REPORT_ID, 'reactions with comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'reactions with comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1508,7 +1504,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, REPORT_ID, 'Attachment with comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Attachment with comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1580,7 +1576,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await waitForBatchedUpdates(); - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -1632,7 +1628,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + Report.addComment(REPORT_ID, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; From cb9cedd0eed3ea7cd2f81f382113a2f7a27368d4 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 19 Nov 2025 23:58:56 +0300 Subject: [PATCH 11/13] fixing tests.. --- tests/actions/PolicyCategoryTest.ts | 10 +++++++--- tests/unit/usePolicyData.test.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index c5a45ebb55068..0bcf7aa658938 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -11,6 +11,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 PolicyDistanceRateTaxRateEditPage from '@pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage'; OnyxUpdateManager(); describe('actions/PolicyCategory', () => { @@ -76,7 +77,9 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - Category.createPolicyCategory(fakePolicy.id, newCategoryName, false, undefined, undefined, CONST.DEFAULT_NUMBER_ID); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + Category.createPolicyCategory(policyData.current, newCategoryName, false, undefined, undefined, CONST.DEFAULT_NUMBER_ID); await waitForBatchedUpdates(); await new Promise((resolve) => { const connection = Onyx.connect({ @@ -121,7 +124,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - + await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.renamePolicyCategory(policyData.current, { oldName: oldCategoryName ?? '', @@ -176,7 +179,7 @@ describe('actions/PolicyCategory', () => { mockFetch?.pause?.(); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); - + await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.setWorkspaceCategoryEnabled(policyData.current, categoriesToUpdate, false, undefined, undefined, CONST.DEFAULT_NUMBER_ID); await waitForBatchedUpdates(); @@ -224,6 +227,7 @@ describe('actions/PolicyCategory', () => { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + await waitForBatchedUpdates(); const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); Category.deleteWorkspaceCategories(policyData.current, categoriesToDelete, false, undefined, undefined, CONST.DEFAULT_NUMBER_ID); await waitForBatchedUpdates(); diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index 1a298647cf0eb..29e84699e2995 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -110,7 +110,7 @@ describe('usePolicyData', () => { expect(result.current?.policy).toBeUndefined(); expect(result.current?.reports).toBeUndefined(); - expect(result.current?.tags).toBeUndefined(); + expect(result.current?.tags).toEqual([]); expect(result.current?.categories).toBeUndefined(); expect(result.current?.transactionsAndViolations).toEqual({}); From 45a1a71af02595a3287d9b7549a823d4dbcc9010 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Thu, 20 Nov 2025 02:09:05 +0300 Subject: [PATCH 12/13] Change reports expectation from undefined to empty array --- tests/unit/usePolicyData.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index 29e84699e2995..8899d2b4c26b1 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -109,7 +109,7 @@ describe('usePolicyData', () => { expect(result.current?.policyID).toEqual(policyID); expect(result.current?.policy).toBeUndefined(); - expect(result.current?.reports).toBeUndefined(); + expect(result.current?.reports).toEqual([]); expect(result.current?.tags).toEqual([]); expect(result.current?.categories).toBeUndefined(); From f6595498ff4a432f89e94aba80f0f3236dc85941 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sun, 23 Nov 2025 02:07:54 +0300 Subject: [PATCH 13/13] refactor: clean up test files for policy and tag actions --- tests/actions/PolicyCategoryTest.ts | 1 - tests/actions/PolicyTagTest.ts | 75 +++++++++++------------------ tests/unit/usePolicyData.test.ts | 29 +++++------ 3 files changed, 41 insertions(+), 64 deletions(-) diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 0bcf7aa658938..485e987e9a1a4 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -11,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 PolicyDistanceRateTaxRateEditPage from '@pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage'; OnyxUpdateManager(); describe('actions/PolicyCategory', () => { diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 195e23ad1b27d..24f8888b313c3 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -585,17 +585,13 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When the tag is renamed - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; + renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -640,19 +636,14 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); mockFetch.fail(); // When the tag rename fails - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -682,19 +673,14 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, existingPolicyTags); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When trying to rename a tag with invalid index - const policyData = { - policy: fakePolicy, - tags: {}, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; expect(() => { renamePolicyTag( - policyData, + policyData.current, { oldName: 'oldTag', newName: 'newTag', @@ -742,17 +728,12 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + await waitForBatchedUpdates(); + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); // When the tag is renamed - const policyData = { - policy: fakePolicy, - tags: fakePolicyTags, - categories: {}, - reports: [], - transactionsAndViolations: {}, - }; renamePolicyTag( - policyData, + policyData.current, { oldName: oldTagName, newName: newTagName, @@ -789,7 +770,7 @@ describe('actions/Policy', () => { const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); await waitFor(() => { - expect(policyData.current.policy).toBeDefined(); + expect(policyData.current?.policy).toBeDefined(); }); // When renaming tag with data from usePolicyData @@ -1708,8 +1689,8 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData.current.policy?.areTagsEnabled).toBe(true); - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And a default tag list should be created const defaultTag = Object.values(policyData.current?.tags ?? {}).at(0); @@ -1723,7 +1704,7 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // And after API success, pending fields should be cleared - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBeFalsy(); }); it('should disable tags and update existing tag list', async () => { @@ -1749,9 +1730,9 @@ describe('actions/Policy', () => { // Then the policy should be updated optimistically rerender(fakePolicy.id); - expect(policyData.current.policy?.areTagsEnabled).toBe(false); - expect(policyData.current.policy?.requiresTag).toBe(false); - expect(policyData.current.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current?.policy?.areTagsEnabled).toBe(false); + expect(policyData.current?.policy?.requiresTag).toBe(false); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); // And all tags should be disabled for (const tagName of Object.keys(existingTags)) { @@ -1764,7 +1745,7 @@ describe('actions/Policy', () => { // And after API success, pending fields should be cleared rerender(fakePolicy.id); - expect(policyData.current.policy?.pendingFields).toBeDefined(); + expect(policyData.current?.policy?.pendingFields).toBeDefined(); }); it('should reset changes when API returns error', async () => { @@ -1789,8 +1770,8 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // After the API request failure, the policy should be reset to original state - expect(policyData.current.policy.areTagsEnabled).toBe(false); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData.current?.policy?.areTagsEnabled).toBe(false); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); expect(policyData.current.tags).toMatchObject({}); }); @@ -1806,15 +1787,15 @@ describe('actions/Policy', () => { const {result: policyData, rerender} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); - expect(policyData.current.policy).toBeDefined(); + expect(policyData.current?.policy).toBeDefined(); enablePolicyTags(policyData.current, true); await waitForBatchedUpdates(); rerender(fakePolicy.id); // Then the policy should be updated optimistically - expect(policyData.current.policy.areTagsEnabled).toBe(true); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyData.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); await mockFetch.resume(); await waitForBatchedUpdates(); @@ -1822,8 +1803,8 @@ describe('actions/Policy', () => { rerender(fakePolicy.id); // And after API success, policy should be enabled - expect(policyData.current.policy.areTagsEnabled).toBe(true); - expect(policyData.current.policy.pendingFields?.areTagsEnabled).toBeUndefined(); + expect(policyData.current?.policy?.areTagsEnabled).toBe(true); + expect(policyData.current?.policy?.pendingFields?.areTagsEnabled).toBeUndefined(); // And default tag list should be created expect(policyData.current.tags.Tag).toBeDefined(); diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index 8899d2b4c26b1..f974fa1847e69 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -30,6 +30,7 @@ const mockTransaction = { tag: Object.values(mockPolicyTagLists).at(0)?.name, }; +const expectedReports = [mockIOUReport]; const expectedTransactionsAndViolations = { [mockIOUReport.reportID]: { transactions: { @@ -77,7 +78,7 @@ describe('usePolicyData', () => { return waitForBatchedUpdates(); }); - test('returns data given a policy ID that exists in the onyx', async () => { + it('should return the correct data given a policy ID that exists in the onyx', async () => { await Onyx.multiSet({ ...reportsCollection, ...reportActionsCollection, @@ -91,28 +92,24 @@ describe('usePolicyData', () => { const {result} = renderHook(() => usePolicyData(mockPolicy.id), {wrapper: OnyxListItemProvider}); - expect(result.current?.policyID).toEqual(mockPolicy.id); - 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); + expect(result.current?.policyID).toMatchObject(mockPolicy.id); + expect(result.current?.policy).toMatchObject(mockPolicy); + expect(result.current?.tags).toMatchObject(mockPolicyTagLists); + expect(result.current?.categories).toMatchObject(mockPolicyCategories); + expect(result.current?.reports).toMatchObject(expectedReports); + expect(result.current?.transactionsAndViolations).toMatchObject(expectedTransactionsAndViolations); }); - test('returns default empty values when policy ID does not exist in the onyx', () => { + it('should return the default values when policy ID does not exist in the onyx', () => { const policyID = 'non_existent_policy_id'; const {result} = renderHook(() => usePolicyData('non_existent_policy_id'), {wrapper: OnyxListItemProvider}); - expect(result.current?.policyID).toEqual(policyID); + expect(result.current?.policyID).toMatchObject(policyID); expect(result.current?.policy).toBeUndefined(); - - expect(result.current?.reports).toEqual([]); - expect(result.current?.tags).toEqual([]); expect(result.current?.categories).toBeUndefined(); + expect(result.current?.tags).toBeUndefined(); - expect(result.current?.transactionsAndViolations).toEqual({}); + expect(result.current?.reports).toMatchObject([]); + expect(result.current?.transactionsAndViolations).toMatchObject({}); }); });