From 58c142a723bd568e3289e5ad4c3f28c19c45c073 Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 13 Mar 2026 14:47:16 +0700 Subject: [PATCH 1/4] follow up: GBR appears when there is report field error --- src/libs/ReportUtils.ts | 26 +++++++++++++++++++ .../OnyxDerived/configs/reportAttributes.ts | 26 ++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 910024b7f1cdc..342e7ee1a1015 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9152,6 +9152,31 @@ function hasViolations( return transactions.some((transaction) => hasViolation(transaction, transactionViolations, currentUserEmailParam ?? '', currentUserAccountIDParam, report, policy, shouldShowInReview)); } +function hasVisibleReportFieldViolations(report: OnyxEntry, policy: OnyxEntry, reportViolations?: OnyxEntry): boolean { + if (!report || !policy?.fieldList || !policy?.areReportFieldsEnabled) { + return false; + } + + if (!isPaidGroupPolicyExpenseReport(report) && !isInvoiceReport(report)) { + return false; + } + + const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); + + return Object.values(fieldsByName).some((field) => { + if (field.target !== report.type) { + return false; + } + if (shouldHideSingleReportField(field)) { + return false; + } + if (isReportFieldDisabledForUser(report, field, policy)) { + return false; + } + return !!getFieldViolation(reportViolations, field); + }); +} + /** * Checks to see if a report contains a violation of type `warning` */ @@ -13255,6 +13280,7 @@ export { hasSmartscanError, hasUpdatedTotal, hasViolations, + hasVisibleReportFieldViolations, hasWarningTypeViolations, hasNoticeTypeViolations, hasAnyViolations, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 2360aff7e04b3..1cf8d7b402f9d 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,6 +1,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {computeReportName} from '@libs/ReportNameUtils'; -import {generateIsEmptyReport, generateReportAttributes, isArchivedReport, isValidReport} from '@libs/ReportUtils'; +import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isValidReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; @@ -20,7 +20,8 @@ const prepareReportKeys = (keys: string[]) => { key .replace(ONYXKEYS.COLLECTION.REPORT_METADATA, ONYXKEYS.COLLECTION.REPORT) .replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT) - .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT), + .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT) + .replace(ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT), ), ), ]; @@ -72,10 +73,11 @@ export default createOnyxDerivedValueConfig({ ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.COLLECTION.POLICY_TAGS, + ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT_METADATA, ], compute: ( - [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags], + [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags, reportViolations], {currentValue, sourceValues}, ) => { // Check if display names changed when personal details are updated @@ -114,6 +116,7 @@ export default createOnyxDerivedValueConfig({ const reportMetadataUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_METADATA] ?? {}; const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS] ?? {}; const reportNameValuePairsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS] ?? {}; + const reportViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_VIOLATIONS] ?? {}; const transactionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION]; const transactionViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]; let dataToIterate = Object.keys(reports); @@ -138,6 +141,7 @@ export default createOnyxDerivedValueConfig({ ...Object.keys(reportMetadataUpdates), ...Object.keys(reportActionsUpdates), ...Object.keys(reportNameValuePairsUpdates), + ...Object.keys(reportViolationsUpdates), ...Array.from(reportUpdatesRelatedToReportActions), ]; @@ -217,10 +221,24 @@ export default createOnyxDerivedValueConfig({ isReportArchived, }); + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const hasFieldViolations = hasVisibleReportFieldViolations(report, policy, reportViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]); + let brickRoadStatus; let actionBadge; // if report has errors or violations, show red dot - if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { + if ( + SidebarUtils.shouldShowRedBrickRoad( + report, + chatReport, + reportActionsList, + hasAnyViolations || hasFieldViolations, + reportErrors, + transactions, + transactionViolations, + !!isReportArchived, + ) + ) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } From 19ba7bbfd4b5c228d8401d0c9ee76f2b80fec69b Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 13 Mar 2026 14:56:12 +0700 Subject: [PATCH 2/4] fix ts and add test --- tests/unit/OnyxDerivedTest.tsx | 4 +- tests/unit/ReportUtilsTest.ts | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index d12448d36d7c7..028ee038fd3ba 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -123,9 +123,9 @@ describe('OnyxDerived', () => { const transaction = createRandomTransaction(1); // When the report attributes are recomputed with both report and transaction updates - reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); + reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); const reportAttributesComputedValue = reportAttributes.compute( - [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], + [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], { sourceValues: { [ONYXKEYS.COLLECTION.REPORT]: { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 9db06b91b9169..e7f81df35bd15 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -97,6 +97,7 @@ import { hasActionWithErrorsForTransaction, hasEmptyReportsForPolicy, hasReceiptError, + hasVisibleReportFieldViolations, isAllowedToApproveExpenseReport, isArchivedNonExpenseReport, isArchivedReport, @@ -13649,6 +13650,85 @@ describe('ReportUtils', () => { }); }); + describe('hasVisibleReportFieldViolations', () => { + const policyID = 'policy-field-violations'; + + const baseField: PolicyReportField = { + name: 'project', + fieldID: 'project_field', + defaultValue: '', + orderWeight: 1, + type: 'text', + deletable: true, + target: CONST.REPORT.TYPE.EXPENSE, + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }; + + const basePolicy = { + ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + areReportFieldsEnabled: true, + fieldList: { + [`expensify_${baseField.fieldID}`]: baseField, + }, + }; + + const expenseReport: Report = { + reportID: 'report-field-violations', + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + ownerAccountID: currentUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + beforeEach(async () => { + await Onyx.clear(); + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + }); + + it('should return false when policy does not have areReportFieldsEnabled enabled', () => { + const policyWithFieldsDisabled = {...basePolicy, areReportFieldsEnabled: false}; + + expect(hasVisibleReportFieldViolations(expenseReport, policyWithFieldsDisabled)).toBe(false); + }); + + it('should return false when the report is not an expense report or invoice report', () => { + const chatReport: Report = { + reportID: 'chat-report-field-violations', + type: CONST.REPORT.TYPE.CHAT, + policyID, + }; + + expect(hasVisibleReportFieldViolations(chatReport, basePolicy)).toBe(false); + }); + + it('should return true when expense report has a required field with no value', async () => { + const fieldWithNoValue: PolicyReportField = { + ...baseField, + value: null, + defaultValue: '', + }; + + const policyWithEmptyField = { + ...basePolicy, + fieldList: {[`expensify_${fieldWithNoValue.fieldID}`]: fieldWithNoValue}, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyWithEmptyField); + await waitForBatchedUpdates(); + + expect(hasVisibleReportFieldViolations(expenseReport, policyWithEmptyField)).toBe(true); + }); + }); + describe('getAddExpenseDropdownOptions', () => { const mockTranslate: LocaleContextProps['translate'] = (path, ...params) => translate(CONST.LOCALES.EN, path, ...params); const mockIcons = { From 349e728dd587eab1ac4405c1168c28f0e7189b9d Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 16 Mar 2026 19:31:51 +0700 Subject: [PATCH 3/4] fix bot comments --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a77f81e5e90ec..0c057beb54ce5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9159,7 +9159,8 @@ function hasVisibleReportFieldViolations(report: OnyxEntry, policy: Onyx return false; } - if (!isPaidGroupPolicyExpenseReport(report) && !isInvoiceReport(report)) { + const isPaidGroupPolicyReport = isExpenseReport(report) && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM); + if (!isPaidGroupPolicyReport && !isInvoiceReport(report)) { return false; } From c62a975af351c7a9eb0269086402bfe19135fd6c Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 17 Mar 2026 17:42:09 +0700 Subject: [PATCH 4/4] fix hasVisibleReportFieldViolations logic --- src/libs/ReportUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c3bf4d18b7712..a024147fd6ae6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9192,6 +9192,16 @@ function hasVisibleReportFieldViolations(report: OnyxEntry, policy: Onyx return false; } + // We only show the RBR to the submitter for expense reports + if (isPaidGroupPolicyReport && !isCurrentUserSubmitter(report)) { + return false; + } + + // Allow both open and processing reports to show RBR for field violations (expense reports only) + if (isPaidGroupPolicyReport && !isOpenOrProcessingReport(report)) { + return false; + } + const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); return Object.values(fieldsByName).some((field) => {