Skip to content

Commit 8ef84fc

Browse files
authored
Merge pull request Expensify#89153 from yuvrajangadsingh/fix/88160-v2
fix: dedup RBR/Fix badge on child expense report under accessible workspace chat
2 parents a0ac909 + 7ed3b28 commit 8ef84fc

5 files changed

Lines changed: 110 additions & 7 deletions

File tree

src/libs/OptionsListUtils/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import {
123123
getDeletedParentActionMessageForChatReport,
124124
getDeletedTransactionMessage,
125125
getDisplayNameForParticipant,
126+
getEffectiveReportErrors,
126127
getIcons,
127128
getMovedActionMessage,
128129
getMovedTransactionMessage,
@@ -1071,7 +1072,7 @@ function createOption({
10711072

10721073
// Type/category flags already set in initialization above, but update brickRoadIndicator
10731074
const reportAttribute = reportAttributesDerived?.[report.reportID];
1074-
result.allReportErrors = reportAttribute?.reportErrors ?? {};
1075+
result.allReportErrors = getEffectiveReportErrors(reportAttribute);
10751076
result.brickRoadIndicator = !isEmptyObject(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : (reportAttribute?.brickRoadStatus ?? '');
10761077

10771078
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- below is a boolean expression

src/libs/ReportUtils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9452,6 +9452,19 @@ function getReceiptUploadErrorReason(
94529452
return null;
94539453
}
94549454

9455+
/**
9456+
* Returns the report's effective errors for LHN/option-list consumers that key off `reportErrors`.
9457+
* When the child's RBR-worthy state is being propagated up to an accessible parent workspace chat,
9458+
* the per-report pass in `reportAttributes.ts` suppresses the child's `brickRoadStatus`. Mirror that
9459+
* here so consumers that read `reportErrors` directly (`shouldDisplayReportInLHN`, OptionsListUtils
9460+
* `brickRoadIndicator`) don't promote the child's errors into a duplicate RBR — the parent chat row
9461+
* carries the indicator instead.
9462+
*/
9463+
function getEffectiveReportErrors(attributes: ReportAttributesDerivedValue['reports'][string] | undefined): Errors {
9464+
const isPropagatingToAccessibleParent = !!attributes?.needsParentChatErrorPropagation && !attributes?.brickRoadStatus;
9465+
return isPropagatingToAccessibleParent ? {} : (attributes?.reportErrors ?? {});
9466+
}
9467+
94559468
function hasReportErrorsOtherThanFailedReceipt(
94569469
report: Report,
94579470
chatReport: OnyxEntry<Report>,
@@ -9460,7 +9473,7 @@ function hasReportErrorsOtherThanFailedReceipt(
94609473
transactions: OnyxCollection<Transaction>,
94619474
reportAttributes?: ReportAttributesDerivedValue['reports'],
94629475
) {
9463-
const allReportErrors = reportAttributes?.[report?.reportID]?.reportErrors ?? {};
9476+
const allReportErrors = getEffectiveReportErrors(reportAttributes?.[report?.reportID]);
94649477
const transactionReportActions = getAllReportActions(report.reportID);
94659478
const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, transactionReportActions, undefined);
94669479
let doesTransactionThreadReportHasViolations = false;
@@ -13636,6 +13649,7 @@ export {
1363613649
buildOptimisticChangeFieldAction,
1363713650
isPolicyRelatedReport,
1363813651
hasReportErrorsOtherThanFailedReceipt,
13652+
getEffectiveReportErrors,
1363913653
getAllReportErrors,
1364013654
getAllReportActionsErrorsAndReportActionThatRequiresAttention,
1364113655
hasInvoiceReports,

src/libs/actions/OnyxDerived/configs/reportAttributes.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
22
import {getIsOffline} from '@libs/NetworkState';
33
import {computeReportName} from '@libs/ReportNameUtils';
4-
import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isValidReport} from '@libs/ReportUtils';
4+
import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isPolicyAdmin, isPolicyExpenseChat, isValidReport} from '@libs/ReportUtils';
55
import SidebarUtils from '@libs/SidebarUtils';
66
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
77
import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils';
@@ -283,6 +283,7 @@ export default createOnyxDerivedValueConfig({
283283
let brickRoadStatus;
284284
let actionBadge;
285285
let actionTargetReportActionID;
286+
let needsParentChatErrorPropagation = false;
286287
const reasonAndReportAction = SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad(
287288
report,
288289
chatReport,
@@ -297,9 +298,19 @@ export default createOnyxDerivedValueConfig({
297298
);
298299
// if report has errors or violations, show red dot
299300
if (reasonAndReportAction) {
300-
brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
301-
actionBadge = CONST.REPORT.ACTION_BADGE.FIX;
302-
actionTargetReportActionID = reasonAndReportAction.reportAction?.reportActionID;
301+
needsParentChatErrorPropagation = true;
302+
303+
// RBR/Fix mirrors GBR's access rule: only show on the child when the user can't already
304+
// see it on the parent workspace chat. The parent still gets ERROR/FIX through the
305+
// propagation loop below, so the actionable indicator surfaces on the workspace chat row
306+
// (which is where C+ wants it). Skips when the chat parent isn't accessible to the user.
307+
const chatPolicy = chatReport?.policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${chatReport.policyID}`] : undefined;
308+
const isChildOfAccessiblePolicyExpenseChat = !!chatReport && isPolicyExpenseChat(chatReport) && (!!chatReport.isOwnPolicyExpenseChat || isPolicyAdmin(chatPolicy));
309+
if (!isChildOfAccessiblePolicyExpenseChat) {
310+
brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
311+
actionBadge = CONST.REPORT.ACTION_BADGE.FIX;
312+
actionTargetReportActionID = reasonAndReportAction.reportAction?.reportActionID;
313+
}
303314
}
304315
// if report does not have error, check if it should show green dot
305316
if (brickRoadStatus !== CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && requiresAttention) {
@@ -330,6 +341,7 @@ export default createOnyxDerivedValueConfig({
330341
actionTargetReportActionID,
331342
reportErrors,
332343
oneTransactionThreadReportID,
344+
needsParentChatErrorPropagation,
333345
};
334346

335347
return acc;
@@ -346,8 +358,15 @@ export default createOnyxDerivedValueConfig({
346358

347359
// If this is an IOU report and its calculated attributes have an error,
348360
// then we need to mark its parent chat report.
361+
// We read `needsParentChatErrorPropagation` rather than `brickRoadStatus` because the per-report
362+
// pass suppresses the child's own brickRoadStatus when the parent workspace chat is accessible —
363+
// we still need to propagate the error up so the parent shows the indicator.
349364
const attributes = reportAttributes[report.reportID];
350-
if (report.chatReportID && report.reportID !== report.chatReportID && attributes?.brickRoadStatus === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
365+
if (
366+
report.chatReportID &&
367+
report.reportID !== report.chatReportID &&
368+
(attributes?.needsParentChatErrorPropagation || attributes?.brickRoadStatus === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)
369+
) {
351370
chatReportIDsWithErrors.add(report.chatReportID);
352371
}
353372
}

src/types/onyx/DerivedValues.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ type ReportAttributes = {
4545
* The reportID of the one-transaction thread report, if applicable.
4646
*/
4747
oneTransactionThreadReportID?: string;
48+
49+
/**
50+
* True when this report (typically a child expense report) has an RBR-worthy reason that should
51+
* propagate up to its parent workspace chat. Set by the per-report pass; consumed by the propagation
52+
* loop. We track it separately from `brickRoadStatus` because we suppress the child's own RBR/Fix badge
53+
* when the parent workspace chat is accessible (so we can't read `brickRoadStatus` to drive propagation).
54+
*/
55+
needsParentChatErrorPropagation?: boolean;
4856
};
4957

5058
/**

tests/unit/ReportUtilsTest.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import {
8585
getDeletedTransactionMessage,
8686
getDisplayNameForParticipant,
8787
getDisplayNamesWithTooltips,
88+
getEffectiveReportErrors,
8889
getHarvestOriginalReportID,
8990
getIconsForParticipants,
9091
getIndicatedMissingPaymentMethod,
@@ -17341,6 +17342,66 @@ describe('ReportUtils', () => {
1734117342
});
1734217343
});
1734317344

17345+
describe('getEffectiveReportErrors', () => {
17346+
const childErrors = {
17347+
error1: 'Some real error',
17348+
};
17349+
17350+
it('returns empty when attributes is undefined', () => {
17351+
expect(getEffectiveReportErrors(undefined)).toEqual({});
17352+
});
17353+
17354+
it('returns the report errors when not propagating to parent', () => {
17355+
const attributes = {
17356+
reportName: 'Report',
17357+
isEmpty: false,
17358+
brickRoadStatus: undefined,
17359+
requiresAttention: false,
17360+
reportErrors: childErrors,
17361+
needsParentChatErrorPropagation: false,
17362+
};
17363+
expect(getEffectiveReportErrors(attributes)).toEqual(childErrors);
17364+
});
17365+
17366+
it('returns empty when child is propagating to an accessible parent (brickRoadStatus suppressed)', () => {
17367+
const attributes = {
17368+
reportName: 'Child expense report',
17369+
isEmpty: false,
17370+
// brickRoadStatus suppressed by reportAttributes.ts because the parent workspace chat is accessible
17371+
brickRoadStatus: undefined,
17372+
requiresAttention: false,
17373+
reportErrors: childErrors,
17374+
needsParentChatErrorPropagation: true,
17375+
};
17376+
expect(getEffectiveReportErrors(attributes)).toEqual({});
17377+
});
17378+
17379+
it('returns the report errors when child has its own brickRoadStatus (not propagating)', () => {
17380+
const attributes = {
17381+
reportName: 'Child IOU on personal DM',
17382+
isEmpty: false,
17383+
// brickRoadStatus stayed because parent is not an accessible workspace chat (e.g. DM-based IOU)
17384+
brickRoadStatus: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR,
17385+
requiresAttention: false,
17386+
reportErrors: childErrors,
17387+
needsParentChatErrorPropagation: true,
17388+
};
17389+
expect(getEffectiveReportErrors(attributes)).toEqual(childErrors);
17390+
});
17391+
17392+
it('returns empty when reportErrors is missing', () => {
17393+
const attributes = {
17394+
reportName: 'Report',
17395+
isEmpty: false,
17396+
brickRoadStatus: undefined,
17397+
requiresAttention: false,
17398+
reportErrors: {},
17399+
needsParentChatErrorPropagation: false,
17400+
};
17401+
expect(getEffectiveReportErrors(attributes)).toEqual({});
17402+
});
17403+
});
17404+
1734417405
describe('buildTransactionThread', () => {
1734517406
const chatReportID = '7001';
1734617407
const expenseReportID = '7002';

0 commit comments

Comments
 (0)