Skip to content

Commit ccfea93

Browse files
committed
Fix optimistic tag and category violations on the Require fields page
Signed-off-by: krishna2323 <belivethatkg@gmail.com>
1 parent fac7f35 commit ccfea93

4 files changed

Lines changed: 40 additions & 17 deletions

File tree

src/libs/PolicyUtils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,14 +2012,15 @@ function hasDependentTags(policy: OnyxEntry<Policy>, policyTagList: OnyxEntry<Po
20122012
if (!policy?.hasMultipleTagLists) {
20132013
return false;
20142014
}
2015-
return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags).some((tag) => !!tag.rules?.parentTagsFilter || !!tag.parentTagsFilter));
2015+
// An empty tag list arrives without the `tags` key, despite the type.
2016+
return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags ?? {}).some((tag) => !!tag.rules?.parentTagsFilter || !!tag.parentTagsFilter));
20162017
}
20172018

20182019
function hasIndependentTags(policy: OnyxEntry<Policy>, policyTagList: OnyxEntry<PolicyTagLists>) {
20192020
if (!policy?.hasMultipleTagLists || hasDependentTags(policy, policyTagList)) {
20202021
return false;
20212022
}
2022-
return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags).length > 0);
2023+
return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags ?? {}).length > 0);
20232024
}
20242025

20252026
/**

src/libs/actions/Policy/Category.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,8 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode:
12091209
API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData);
12101210
}
12111211

1212-
function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean) {
1212+
/** Pass shouldRecomputeViolations = false when tag Required changes in the same save: each recompute SETs violations from the pre-save snapshot, so two overwrite each other. */
1213+
function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: boolean, shouldRecomputeViolations = true) {
12131214
const policyID = policyData.policy?.id;
12141215
const policyOptimisticData: Partial<Policy> = {
12151216
requiresCategory,
@@ -1258,7 +1259,9 @@ function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory:
12581259
],
12591260
};
12601261

1261-
pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData);
1262+
if (shouldRecomputeViolations) {
1263+
pushTransactionViolationsOnyxData(onyxData, policyData, policyOptimisticData);
1264+
}
12621265

12631266
const parameters = {
12641267
policyID,

src/libs/actions/Policy/Tag.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,10 +1058,12 @@ function renamePolicyTagList(policyID: string, policyTagListName: {oldName: stri
10581058
API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData);
10591059
}
10601060

1061-
function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean) {
1061+
/** extraPolicyUpdate folds a caller's same-save requiresCategory change into this action's single violation recompute. */
1062+
function setPolicyRequiresTag(policyData: PolicyData, requiresTag: boolean, extraPolicyUpdate: Partial<Policy> = {}) {
10621063
const policyID = policyData.policy?.id;
10631064

10641065
const policyOptimisticData: Partial<Policy> = {
1066+
...extraPolicyUpdate,
10651067
requiresTag,
10661068
// A manual toggle is explicit, so any pending switch-level restore intent is no longer needed.
10671069
pendingRequiresTagRestore: null,
@@ -1266,7 +1268,7 @@ function setPolicyTagsRequired(policyData: PolicyData, requiresTag: boolean, tag
12661268
* key, the last request would overwrite the others — e.g. requiring Region while clearing Department would drop the
12671269
* Missing Region violation the first half just added.
12681270
*/
1269-
function setPolicyTagLevelsRequired(policyData: PolicyData, requiredByOrderWeight: Record<number, boolean>) {
1271+
function setPolicyTagLevelsRequired(policyData: PolicyData, requiredByOrderWeight: Record<number, boolean>, extraPolicyUpdate: Partial<Policy> = {}) {
12701272
const policyID = policyData.policy?.id;
12711273
if (!policyID || !policyData.tags) {
12721274
return;
@@ -1282,6 +1284,11 @@ function setPolicyTagLevelsRequired(policyData: PolicyData, requiredByOrderWeigh
12821284
combinedTagsUpdate[tagList.name] = {required: requiredByOrderWeight[tagList.orderWeight]};
12831285
}
12841286

1287+
// ViolationsUtils gates every tag violation behind policy.requiresTag, so mirror what the backend derives from the
1288+
// levels instead of waiting for the next policy refresh.
1289+
const requiresTag = Object.values(policyData.tags).some((tagList) => requiredByOrderWeight[tagList.orderWeight] ?? !!tagList.required);
1290+
const policyUpdate: Partial<Policy> = {...extraPolicyUpdate, requiresTag};
1291+
12851292
const buildOnyxData = (tagLists: Array<PolicyTagLists[string]>, isRequired: boolean, shouldRecomputeViolations: boolean) => {
12861293
const optimisticValue: Record<string, Partial<PolicyTagLists[string]>> = {};
12871294
const successValue: Record<string, Partial<PolicyTagLists[string]>> = {};
@@ -1301,14 +1308,21 @@ function setPolicyTagLevelsRequired(policyData: PolicyData, requiredByOrderWeigh
13011308
};
13021309
}
13031310

1304-
const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_TAGS> = {
1311+
const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.POLICY_TAGS> = {
13051312
optimisticData: [{onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: optimisticValue}],
13061313
successData: [{onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: successValue}],
13071314
failureData: [{onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: failureValue}],
13081315
};
13091316

1317+
// Only the request that owns the recompute writes the derived policy flags, so the other can't revert them.
13101318
if (shouldRecomputeViolations) {
1311-
pushTransactionViolationsOnyxData(onyxData, policyData, {}, {}, combinedTagsUpdate);
1319+
onyxData.optimisticData?.push({onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: policyUpdate});
1320+
onyxData.failureData?.push({
1321+
onyxMethod: Onyx.METHOD.MERGE,
1322+
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
1323+
value: {requiresTag: policyData.policy?.requiresTag ?? false},
1324+
});
1325+
pushTransactionViolationsOnyxData(onyxData, policyData, policyUpdate, {}, combinedTagsUpdate);
13121326
}
13131327

13141328
return onyxData;

src/pages/workspace/rules/RulesRequireFieldsPage.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function RulesRequireFieldsPage({
5757
const isCategoryFeatureDisabled = !policy?.areCategoriesEnabled;
5858
const isCategoryToggleDisabled = isCategoryFeatureDisabled || !hasEnabledCategories || isConnectedToAccounting;
5959

60-
const hasEnabledTags = hasEnabledOptions(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags)));
60+
const hasEnabledTags = hasEnabledOptions(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags ?? {})));
6161
const isTagFeatureDisabled = !policy?.areTagsEnabled;
6262
// A connection owns the tag lists, not whether an expense must carry one, so unlike Categories it doesn't lock this.
6363
const isTagToggleDisabled = isTagFeatureDisabled || !hasEnabledTags;
@@ -107,16 +107,21 @@ function RulesRequireFieldsPage({
107107
return;
108108
}
109109

110-
if (categoryRequired !== initialCategoryRequired) {
111-
setWorkspaceRequiresCategory(policyData, categoryRequired);
110+
const hasCategoryChange = categoryRequired !== initialCategoryRequired;
111+
const hasTagLevelChanges = hasPerLevelTagRequired && changedTagLevels.length > 0;
112+
const hasSingleTagChange = !hasPerLevelTagRequired && tagRequired !== initialTagRequired;
113+
const categoryUpdateForTagRecompute = hasCategoryChange ? {requiresCategory: categoryRequired} : {};
114+
115+
if (hasCategoryChange) {
116+
// With a tag change in the same save, the tag action owns the one violation recompute and carries requiresCategory into it.
117+
setWorkspaceRequiresCategory(policyData, categoryRequired, !hasTagLevelChanges && !hasSingleTagChange);
112118
}
113119

114-
if (hasPerLevelTagRequired) {
115-
// All changed levels go in one call so violations are recomputed once from the combined end state. Every changed
116-
// level has a pending value by definition, and it can only be the opposite of the saved one.
117-
setPolicyTagLevelsRequired(policyData, Object.fromEntries(changedTagLevels.map((tagList) => [tagList.orderWeight, !tagList.required])));
118-
} else if (tagRequired !== initialTagRequired) {
119-
setPolicyRequiresTag(policyData, tagRequired);
120+
if (hasTagLevelChanges) {
121+
// One call for every changed level, so violations are recomputed once from the combined end state.
122+
setPolicyTagLevelsRequired(policyData, Object.fromEntries(changedTagLevels.map((tagList) => [tagList.orderWeight, !tagList.required])), categoryUpdateForTagRecompute);
123+
} else if (hasSingleTagChange) {
124+
setPolicyRequiresTag(policyData, tagRequired, categoryUpdateForTagRecompute);
120125
}
121126

122127
Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);

0 commit comments

Comments
 (0)