From a5c25f5fe4e580b3456b85ebd8214e6d6fbc5486 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 1 Apr 2026 17:53:13 +0700 Subject: [PATCH 1/2] fix: System message with no tag is shown --- src/libs/actions/Policy/Tag.ts | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 970672d77b8a..49e1413a0f47 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -34,7 +34,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report, ReportAction} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import type {ApprovalRule} from '@src/types/onyx/Policy'; +import type {ApprovalRule, CodingRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; type CreatePolicyTagParams = { @@ -473,13 +473,40 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { }, }; - const onyxData: OnyxData = { + const tagsToDeleteSet = new Set(tagsToDelete); + const codingRules = policyData.policy?.rules?.codingRules ?? {}; + const updatedCodingRules: Record> = {}; + const failureCodingRules: Record> = {}; + + for (const [ruleID, rule] of Object.entries(codingRules)) { + if (rule?.tag && tagsToDeleteSet.has(rule.tag)) { + updatedCodingRules[ruleID] = {tag: null as unknown as string}; + failureCodingRules[ruleID] = {tag: rule.tag}; + } + } + + const hasCodingRuleUpdates = Object.keys(updatedCodingRules).length > 0; + + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: policyTagsOptimisticData, }, + ...(hasCodingRuleUpdates + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const, + value: { + rules: { + codingRules: updatedCodingRules, + }, + }, + }, + ] + : []), ], successData: [ { @@ -516,6 +543,19 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) { }, }, }, + ...(hasCodingRuleUpdates + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const, + value: { + rules: { + codingRules: failureCodingRules, + }, + }, + }, + ] + : []), ], }; From 8ce37cfeb3d9bf7d6a2889db3539d61ca3891fea Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 3 Apr 2026 10:28:01 +0700 Subject: [PATCH 2/2] fix: test --- tests/actions/PolicyTagTest.ts | 144 ++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 78ecb9788fd6..45779fba8351 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -23,7 +23,7 @@ import { } from '@libs/actions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; +import type {Policy, PolicyTagLists, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; import createRandomPolicy from '../utils/collections/policies'; import createRandomPolicyTags from '../utils/collections/policyTags'; import * as TestHelper from '../utils/TestHelper'; @@ -1169,6 +1169,148 @@ describe('actions/Policy', () => { expect(updatePolicyTags?.[tagListName]?.tags[tagName]).toBeFalsy(); } }); + + it('should clear coding rule tag references when deleting tags referenced by coding rules', async () => { + const fakePolicy = createRandomPolicy(0); + fakePolicy.areTagsEnabled = true; + + const tagListName = 'Fake tag'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + const tagNames = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}); + const tagsToDelete = [tagNames[0]]; + + // Set up coding rules that reference the tag being deleted + fakePolicy.rules = { + codingRules: { + rule1: { + filters: {left: 'merchant', operator: 'eq', right: 'test'}, + tag: tagNames[0], + }, + rule2: { + filters: {left: 'merchant', operator: 'eq', right: 'other'}, + tag: tagNames[1], + }, + }, + }; + + mockFetch?.pause?.(); + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + await act(async () => { + await waitForBatchedUpdates(); + }); + deletePolicyTags(policyData.current, tagsToDelete); + + await waitForBatchedUpdates(); + + // Verify optimistic data: coding rule for deleted tag should have tag nullified + let updatedPolicy: Policy | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (val) => (updatedPolicy = val), + }); + + // Onyx merge with null removes the key, so tag becomes undefined + expect(updatedPolicy?.rules?.codingRules?.rule1?.tag).toBeUndefined(); + // Rule2 references a tag that was NOT deleted, so it should remain unchanged + expect(updatedPolicy?.rules?.codingRules?.rule2?.tag).toBe(tagNames[1]); + + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + }); + + it('should restore coding rule tag references on api failure', async () => { + const fakePolicy = createRandomPolicy(0); + fakePolicy.areTagsEnabled = true; + + const tagListName = 'Fake tag'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + const tagNames = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}); + const tagsToDelete = [tagNames[0]]; + + fakePolicy.rules = { + codingRules: { + rule1: { + filters: {left: 'merchant', operator: 'eq', right: 'test'}, + tag: tagNames[0], + }, + }, + }; + + mockFetch?.pause?.(); + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + await act(async () => { + await waitForBatchedUpdates(); + }); + + mockFetch?.fail?.(); + deletePolicyTags(policyData.current, tagsToDelete); + await waitForBatchedUpdates(); + + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + // Verify failure data: coding rule tag should be restored + let updatedPolicy: Policy | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (val) => (updatedPolicy = val), + }); + + expect(updatedPolicy?.rules?.codingRules?.rule1?.tag).toBe(tagNames[0]); + }); + + it('should not add coding rule updates when no coding rules reference deleted tags', async () => { + const fakePolicy = createRandomPolicy(0); + fakePolicy.areTagsEnabled = true; + + const tagListName = 'Fake tag'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + const tagNames = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}); + const tagsToDelete = [tagNames[0]]; + + // Coding rule references a different tag that is NOT being deleted + fakePolicy.rules = { + codingRules: { + rule1: { + filters: {left: 'merchant', operator: 'eq', right: 'test'}, + tag: tagNames[1], + }, + }, + }; + + mockFetch?.pause?.(); + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + const {result: policyData} = renderHook(() => usePolicyData(fakePolicy.id), {wrapper: OnyxListItemProvider}); + await act(async () => { + await waitForBatchedUpdates(); + }); + deletePolicyTags(policyData.current, tagsToDelete); + + await waitForBatchedUpdates(); + + // Verify the coding rule tag reference is unchanged + let updatedPolicy: Policy | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (val) => (updatedPolicy = val), + }); + + expect(updatedPolicy?.rules?.codingRules?.rule1?.tag).toBe(tagNames[1]); + + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + }); }); describe('ClearPolicyTagListErrors', () => {