Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/libs/actions/Policy/Tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -473,13 +473,40 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) {
},
};

const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_TAGS> = {
const tagsToDeleteSet = new Set(tagsToDelete);
const codingRules = policyData.policy?.rules?.codingRules ?? {};
const updatedCodingRules: Record<string, Partial<CodingRule>> = {};
const failureCodingRules: Record<string, Partial<CodingRule>> = {};

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<typeof ONYXKEYS.COLLECTION.POLICY_TAGS | typeof ONYXKEYS.COLLECTION.POLICY> = {
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: [
{
Expand Down Expand Up @@ -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,
},
},
},
]
: []),
],
};

Expand Down
144 changes: 143 additions & 1 deletion tests/actions/PolicyTagTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
} 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';
Expand Down Expand Up @@ -1169,6 +1169,148 @@
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]];

Check failure on line 1180 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1180 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access

// Set up coding rules that reference the tag being deleted
fakePolicy.rules = {
codingRules: {
rule1: {
filters: {left: 'merchant', operator: 'eq', right: 'test'},
tag: tagNames[0],

Check failure on line 1187 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1187 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access
},
rule2: {
filters: {left: 'merchant', operator: 'eq', right: 'other'},
tag: tagNames[1],

Check failure on line 1191 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1191 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access
},
},
};

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]);

Check failure on line 1219 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1219 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access

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]];

Check failure on line 1232 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1232 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access

fakePolicy.rules = {
codingRules: {
rule1: {
filters: {left: 'merchant', operator: 'eq', right: 'test'},
tag: tagNames[0],

Check failure on line 1238 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1238 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access
},
},
};

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]);

Check failure on line 1267 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1267 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access
});

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]];

Check failure on line 1277 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1277 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access

// 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],

Check failure on line 1284 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1284 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access
},
},
};

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]);

Check failure on line 1309 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Prefer using the `.at()` method for array element access

Check failure on line 1309 in tests/actions/PolicyTagTest.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer using the `.at()` method for array element access

await mockFetch?.resume?.();
await waitForBatchedUpdates();
});
});

describe('ClearPolicyTagListErrors', () => {
Expand Down
Loading