From 7eee7fac3fe0ca102b8b9a3c0682c3ead4861b2a Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Sat, 27 Jun 2026 02:08:10 -0400 Subject: [PATCH] fix: The floating action badge scrolls to the wrong report --- .../OnyxDerived/configs/reportAttributes.ts | 83 ++++++++++++++++--- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index afb7937405c8..036502c1b10c 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,14 +1,26 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getReportPreviewAction} from '@libs/actions/IOU/MoneyRequestBuilder'; import {getIsOffline} from '@libs/NetworkState'; import {getLinkedTransactionID} from '@libs/ReportActionsUtils'; import {computeReportName} from '@libs/ReportNameUtils'; -import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isPolicyAdmin, isPolicyExpenseChat, isValidReport} from '@libs/ReportUtils'; +import { + generateIsEmptyReport, + generateReportAttributes, + hasViolations, + hasVisibleReportFieldViolations, + isArchivedReport, + isOpenReport, + isPolicyAdmin, + isPolicyExpenseChat, + isProcessingReport, + isValidReport, +} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Policy, ReportAttributesDerivedValue} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportAttributesDerivedValue} from '@src/types/onyx'; let previousDisplayNames: Record = {}; let previousPersonalDetails: OnyxEntry | undefined; @@ -374,39 +386,86 @@ export default createOnyxDerivedValueConfig({ ); // Propagate errors from IOU reports to their parent chat reports. - const chatReportIDsWithErrors = new Set(); + const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID; + const currentUserEmail = session?.email ?? ''; + const erroredChildReportIDsByChat = new Map(); + const childReportIDsByChat = new Map(); for (const report of Object.values(reports)) { - if (!report?.reportID) { + if (!report?.reportID || !report.chatReportID || report.reportID === report.chatReportID) { continue; } + const childReportIDs = childReportIDsByChat.get(report.chatReportID) ?? []; + childReportIDs.push(report.reportID); + childReportIDsByChat.set(report.chatReportID, childReportIDs); + // If this is an IOU report and its calculated attributes have an error, // then we need to mark its parent chat report. // We read `needsParentChatErrorPropagation` rather than `brickRoadStatus` because the per-report // pass suppresses the child's own brickRoadStatus when the parent workspace chat is accessible — // we still need to propagate the error up so the parent shows the indicator. const attributes = reportAttributes[report.reportID]; - if ( - report.chatReportID && - report.reportID !== report.chatReportID && - (attributes?.needsParentChatErrorPropagation || attributes?.brickRoadStatus === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) - ) { - chatReportIDsWithErrors.add(report.chatReportID); + if (attributes?.needsParentChatErrorPropagation || attributes?.brickRoadStatus === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) { + const erroredChildReportIDs = erroredChildReportIDsByChat.get(report.chatReportID) ?? []; + erroredChildReportIDs.push(report.reportID); + erroredChildReportIDsByChat.set(report.chatReportID, erroredChildReportIDs); } } // Apply the error status to the parent chat reports. - for (const chatReportID of chatReportIDsWithErrors) { + for (const [chatReportID, erroredChildReportIDs] of erroredChildReportIDsByChat) { if (!reportAttributes[chatReportID]) { continue; } + const chatAttributes = reportAttributes[chatReportID]; + let actionTargetReportActionID = chatAttributes.actionTargetReportActionID; + + // Returns the report-preview action ID of the oldest child in `reportIDs` matching `predicate` + // (oldest by preview-action creation time), or undefined when none match. + const getOldestPreviewActionID = (reportIDs: string[] | undefined, predicate?: (childReport: OnyxEntry) => boolean) => { + let oldestCreated: string | undefined; + let targetReportActionID: string | undefined; + for (const childReportID of reportIDs ?? []) { + if (predicate && !predicate(reports[`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`])) { + continue; + } + const reportPreviewAction = getReportPreviewAction(chatReportID, childReportID); + if (!reportPreviewAction) { + continue; + } + if (oldestCreated === undefined || reportPreviewAction.created < oldestCreated) { + oldestCreated = reportPreviewAction.created; + targetReportActionID = reportPreviewAction.reportActionID; + } + } + return targetReportActionID; + }; + + const isActionable = (childReport: OnyxEntry) => isOpenReport(childReport) || isProcessingReport(childReport); + // Open/processing and the user still needs to fix a violation on it. Violations are read directly from + // transactionViolations so this works even when owner data is absent (e.g. masked Onyx exports). + const needsViolationFix = (childReport: OnyxEntry) => { + if (!childReport || !isActionable(childReport)) { + return false; + } + const childPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${childReport.policyID}`]; + return hasViolations(childReport.reportID, transactionViolations, currentUserAccountID, currentUserEmail, true, undefined, childReport, childPolicy); + }; + + actionTargetReportActionID = + getOldestPreviewActionID(erroredChildReportIDs, isActionable) ?? + getOldestPreviewActionID(childReportIDsByChat.get(chatReportID), needsViolationFix) ?? + getOldestPreviewActionID(erroredChildReportIDs) ?? + actionTargetReportActionID; + // Clone the entry before mutating — it may be a reference carried over from // currentValue.reports that wasn't recomputed in this incremental run. reportAttributes[chatReportID] = { - ...reportAttributes[chatReportID], + ...chatAttributes, brickRoadStatus: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, actionBadge: CONST.REPORT.ACTION_BADGE.FIX, + actionTargetReportActionID, }; }