From af89dea357d9213260b348d284f7d2fd1fafbf2c Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 16 Nov 2025 00:55:02 +0000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 a5b19dd7a2d44ba82b5a29c40153cb56c45e5123 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 18 Nov 2025 23:33:17 +0300 Subject: [PATCH 8/8] refactor tagLists --- src/hooks/usePolicyData/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/usePolicyData/index.ts b/src/hooks/usePolicyData/index.ts index 4fd908f153ff5..7af00f30094f7 100644 --- a/src/hooks/usePolicyData/index.ts +++ b/src/hooks/usePolicyData/index.ts @@ -52,7 +52,7 @@ function usePolicyData(policyID: string): PolicyData { }, [reports, allReportsTransactionsAndViolations]); return { transactionsAndViolations, - tagLists, + tagLists: tagLists ?? {}, categories: categories ?? {}, policyID, policy,