diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index d4407015c4ec..69171310f946 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -490,8 +490,15 @@ function clearPolicyTagErrors({policyID, tagName, tagListIndex, policyTags}: Cle }); } -function clearPolicyTagListErrorField(policyID: string, tagListIndex: number, errorField: string) { - const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.at(tagListIndex); +type ClearPolicyTagListErrorFieldProps = { + policyID: string; + tagListIndex: number; + errorField: string; + policyTags: OnyxEntry; +}; + +function clearPolicyTagListErrorField({policyID, tagListIndex, errorField, policyTags}: ClearPolicyTagListErrorFieldProps) { + const policyTag = PolicyUtils.getTagLists(policyTags ?? {})?.at(tagListIndex); if (!policyTag) { return; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 91a9bb43a431..cad3d7f11255 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -379,7 +379,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} - onCloseError={() => clearPolicyTagListErrorField(policyID, route.params.orderWeight, 'required')} + onCloseError={() => clearPolicyTagListErrorField({policyID, tagListIndex: route.params.orderWeight, errorField: 'required', policyTags})} disabled={!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)} showLockIcon={isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])} /> diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index c53e6547a3c7..d1aac4939607 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -6,6 +6,7 @@ import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import { buildOptimisticPolicyRecentlyUsedTags, clearPolicyTagErrors, + clearPolicyTagListErrorField, createPolicyTag, deletePolicyTags, renamePolicyTag, @@ -1022,6 +1023,137 @@ describe('actions/Policy', () => { }); }); + describe('ClearPolicyTagListErrorField', () => { + it('should clear specific error field from tag list', async () => { + // Given a policy with a tag list that has multiple error fields + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 2); + + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + errorFields: { + name: {genericError: 'Name error'}, + required: {genericError: 'Required error'}, + maxTagsSelected: {genericError: 'Max tags error'}, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + // When clearing only the 'required' error field from the tag list + clearPolicyTagListErrorField({policyID: fakePolicy.id, tagListIndex: 0, errorField: 'required', policyTags: fakePolicyTags}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Then only the 'required' error field should be cleared while other error fields remain + expect(updatedPolicyTags?.[tagListName]).toBeDefined(); + expect(updatedPolicyTags?.[tagListName].errorFields?.required).toBeUndefined(); + expect(updatedPolicyTags?.[tagListName].errorFields?.name).toEqual({genericError: 'Name error'}); + expect(updatedPolicyTags?.[tagListName].errorFields?.maxTagsSelected).toEqual({genericError: 'Max tags error'}); + }); + + it('should not modify Onyx data when tag list does not exist', async () => { + // Given a policy with no tag lists + const fakePolicy = createRandomPolicy(0); + const fakePolicyTags = {}; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + // When attempting to clear an error field from a non-existent tag list + expect(() => { + clearPolicyTagListErrorField({policyID: fakePolicy.id, tagListIndex: 0, errorField: 'required', policyTags: fakePolicyTags}); + }).not.toThrow(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Then the policy tags should remain unchanged because the tag list does not exist + expect(updatedPolicyTags).toEqual(fakePolicyTags); + }); + + it('should not modify Onyx data when tag list has no name', async () => { + // Given a policy with a tag list that has an empty name and error fields + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 1); + + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + name: '', + errorFields: { + required: {genericError: 'This error should not be cleared'}, + name: {genericError: 'This error should also remain'}, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + // When attempting to clear an error field from a tag list with no name + expect(() => { + clearPolicyTagListErrorField({policyID: fakePolicy.id, tagListIndex: 0, errorField: 'required', policyTags: fakePolicyTags}); + }).not.toThrow(); + + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Then the error fields should remain unchanged because the tag list name is empty + expect(updatedPolicyTags?.[tagListName].errorFields?.required).toEqual({genericError: 'This error should not be cleared'}); + expect(updatedPolicyTags?.[tagListName].errorFields?.name).toEqual({genericError: 'This error should also remain'}); + expect(updatedPolicyTags?.[tagListName].name).toBe(''); + }); + + it('should work with data from useOnyx hook', async () => { + // Given a policy with a tag list that has error fields and is accessed via useOnyx hook + const fakePolicy = createRandomPolicy(0); + const tagListName = 'Test tag list'; + const fakePolicyTags = createRandomPolicyTags(tagListName, 1); + + fakePolicyTags[tagListName] = { + ...fakePolicyTags[tagListName], + errorFields: { + required: {genericError: 'Required field error'}, + name: {genericError: 'Name field error'}, + }, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`)); + + await waitFor(() => { + expect(result.current[0]).toBeDefined(); + }); + + // When clearing the 'name' error field using data from the useOnyx hook + clearPolicyTagListErrorField({policyID: fakePolicy.id, tagListIndex: 0, errorField: 'name', policyTags: result.current[0]}); + await waitForBatchedUpdates(); + + let updatedPolicyTags: PolicyTagLists | undefined; + await TestHelper.getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + callback: (val) => (updatedPolicyTags = val), + }); + + // Then only the 'name' error field should be cleared while the 'required' error field remains + expect(updatedPolicyTags?.[tagListName].errorFields?.name).toBeUndefined(); + expect(updatedPolicyTags?.[tagListName].errorFields?.required).toEqual({genericError: 'Required field error'}); + }); + }); + describe('buildOptimisticPolicyRecentlyUsedTags', () => { it('should return empty object when transactionTags is undefined', () => { const result = buildOptimisticPolicyRecentlyUsedTags({