Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
20 changes: 18 additions & 2 deletions src/components/ReportActionItem/MoneyRequestReceiptView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import usePermissions from '@hooks/usePermissions';
import {usePersonalDetail} from '@hooks/usePersonalDetails';
import usePrevious from '@hooks/usePrevious';
import useReceiptTranslation from '@hooks/useReceiptTranslation';
import useRelatedReportActions from '@hooks/useRelatedReportActions';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useReportsParentHierarchy from '@hooks/useReportsParentHierarchy';
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
Expand Down Expand Up @@ -178,6 +179,7 @@ function MoneyRequestReceiptView({

const originalReportID = useOriginalReportID(report?.reportID, parentReportAction);
const reportsParentHierarchy = useReportsParentHierarchy(report?.reportID);
const relatedReportActions = useRelatedReportActions(reportsParentHierarchy, originalReportID, parentReportAction?.childReportID);
const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction);
const [chatIOUReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReport?.chatReportID)}`);
const iouReportTransactionsCollection = useReportTransactionsCollection(iouReport?.reportID);
Expand Down Expand Up @@ -564,15 +566,29 @@ function MoneyRequestReceiptView({
return;
}
clearError(linkedTransactionID);
clearAllRelatedReportActionErrors({reportID: report.reportID, reportAction: parentReportAction, originalReportID, isOffline, reports: reportsParentHierarchy});
clearAllRelatedReportActionErrors({
reportID: report.reportID,
reportAction: parentReportAction,
originalReportID,
isOffline,
reports: reportsParentHierarchy,
reportActions: relatedReportActions,
});
return;
}
if (!isEmptyObject(transactionAndReportActionErrors)) {
revert(transaction, getLastModifiedExpense(report?.reportID));
}
if (!isEmptyObject(errorsWithoutReportCreation)) {
clearError(transaction.transactionID);
clearAllRelatedReportActionErrors({reportID: report.reportID, reportAction: parentReportAction, originalReportID, isOffline, reports: reportsParentHierarchy});
clearAllRelatedReportActionErrors({
reportID: report.reportID,
reportAction: parentReportAction,
originalReportID,
isOffline,
reports: reportsParentHierarchy,
reportActions: relatedReportActions,
});
}
if (!isEmptyObject(reportCreationError)) {
if (isInNarrowPaneModal) {
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useRelatedReportActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {ReportHierarchyInfo} from '@userActions/ClearReportActionErrors';

import ONYXKEYS from '@src/ONYXKEYS';
import {relatedReportActionsSelector} from '@src/selectors/ReportAction';
import type {ReportActions} from '@src/types/onyx';

import type {OnyxCollection} from 'react-native-onyx';

import useOnyx from './useOnyx';

/**
* Subscribes to the report actions `clearAllRelatedReportActionErrors` walks when an error is dismissed: the actions
* of every report in `reportsParentHierarchy` (see `useReportsParentHierarchy`), of the original report holding the
* dismissed action, and of the threads below `childRootReportID` that hang off errored actions. The selector only
* looks up those keys and returns each report's actions by reference, so a write to an unrelated report action costs
* a few lookups rather than a deep comparison over every action in the account.
*/
function useRelatedReportActions(
reportsParentHierarchy: OnyxCollection<ReportHierarchyInfo>,
originalReportID: string | undefined,
childRootReportID: string | undefined,
): OnyxCollection<ReportActions> {
const reportIDs = Object.keys(reportsParentHierarchy ?? {}).map((key) => key.slice(ONYXKEYS.COLLECTION.REPORT.length));
const [relatedReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
selector: relatedReportActionsSelector([...reportIDs, originalReportID], childRootReportID),
});
return relatedReportActions;
}

export default useRelatedReportActions;
19 changes: 11 additions & 8 deletions src/libs/actions/ClearReportActionErrors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {getLinkedTransactionID, getReportAction, getReportActionMessage, getVisibleReportActionErrors, isCreatedTaskReportAction, isRejectedAction} from '@libs/ReportActionsUtils';
import {getLinkedTransactionID, getReportActionMessage, getVisibleReportActionErrors, isCreatedTaskReportAction, isRejectedAction} from '@libs/ReportActionsUtils';
import {getOriginalReportID} from '@libs/ReportUtils';
import {buildOptimisticSnapshotData} from '@libs/SearchQueryUtils';

Expand Down Expand Up @@ -33,7 +33,7 @@ Onyx.connectWithoutView({
},
});

function clearReportActionErrors(reportAction: ReportAction, originalReportID: string | undefined, keys?: string[]) {
function clearReportActionErrors(reportAction: ReportAction, originalReportID: string | undefined, keys?: string[], reportActions?: OnyxCollection<OnyxTypes.ReportActions>) {
if (!reportAction?.reportActionID) {
return;
}
Expand All @@ -52,7 +52,7 @@ function clearReportActionErrors(reportAction: ReportAction, originalReportID: s
// Also clean up any orphaned optimistic reject actions (REJECTED/REJECTED_TO_SUBMITTER)
// from the same report. When a rejection fails, the comment action gets the error but the
// corresponding reject action remains with pendingAction ADD and no errors.
const siblingActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`];
const siblingActions = (reportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`];
if (siblingActions) {
for (const [actionID, siblingAction] of Object.entries(siblingActions)) {
if (
Expand Down Expand Up @@ -125,23 +125,24 @@ type ClearAllRelatedReportActionErrorsParams = {
ignore?: IgnoreDirection;
keys?: string[];
reports?: OnyxCollection<ReportHierarchyInfo>;
reportActions?: OnyxCollection<OnyxTypes.ReportActions>;
};

function clearAllRelatedReportActionErrors({reportID, reportAction, originalReportID, isOffline, ignore, keys, reports}: ClearAllRelatedReportActionErrorsParams) {
function clearAllRelatedReportActionErrors({reportID, reportAction, originalReportID, isOffline, ignore, keys, reports, reportActions}: ClearAllRelatedReportActionErrorsParams) {
// Default to the errors the user can see. Propagating a hidden one, e.g. the mirrored pay failure, would clear
// or delete the action it was mirrored from without the user ever seeing it.
const errorKeys = keys ?? Object.keys(getVisibleReportActionErrors(reportAction ?? undefined) ?? {});
if (!reportAction || errorKeys.length === 0 || !reportID) {
return;
}

clearReportActionErrors(reportAction, originalReportID, keys);
clearReportActionErrors(reportAction, originalReportID, keys, reportActions);

const report = (reports ?? allReports)?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.parentReportID && report?.parentReportActionID && ignore !== 'parent') {
const parentReportAction = getReportAction(report.parentReportID, report.parentReportActionID);
const parentReportActions = (reportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`] ?? {};
const parentReportAction = parentReportActions[report.parentReportActionID];
const parentErrorKeys = Object.keys(parentReportAction?.errors ?? {}).filter((err) => errorKeys.includes(err));
const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`] ?? {};
const parentOriginalReportID = getOriginalReportID(report.parentReportID, parentReportAction, parentReportActions, isOffline);

clearAllRelatedReportActionErrors({
Expand All @@ -152,11 +153,12 @@ function clearAllRelatedReportActionErrors({reportID, reportAction, originalRepo
ignore: 'child',
keys: parentErrorKeys,
reports,
reportActions,
});
}

if (reportAction.childReportID && ignore !== 'child') {
const childActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction.childReportID}`] ?? {};
const childActions = (reportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction.childReportID}`] ?? {};
for (const action of Object.values(childActions)) {
const childErrorKeys = Object.keys(action.errors ?? {}).filter((err) => errorKeys.includes(err));
const childOriginalReportID = getOriginalReportID(reportAction.childReportID, action, childActions, isOffline);
Expand All @@ -168,6 +170,7 @@ function clearAllRelatedReportActionErrors({reportID, reportAction, originalRepo
ignore: 'parent',
keys: childErrorKeys,
reports,
reportActions,
});
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/selectors/ReportAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,47 @@ function getReceiptScanFailedIOUActionDataSelector(
const transactionThreadReportIDSelector = (transactionID: string | undefined) => (reportActions: OnyxEntry<ReportActions>) =>
transactionID ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transactionID)?.childReportID : undefined;

/**
* Creates a selector returning the report actions `clearAllRelatedReportActionErrors` walks: the actions of every
* report in `reportIDs` (the report whose error is dismissed, its ancestors and the original report holding the
* dismissed action), plus the threads below `childRootReportID` that hang off actions with errors.
* The walk returns early from an action without errors, so following only errored actions downward is exact.
* Only those keys are looked up and each report's actions are returned by reference, so a write to an unrelated
* report action costs a few lookups, and a report whose actions are unchanged compares by reference.
*/
function relatedReportActionsSelector(reportIDs: Array<string | undefined>, childRootReportID: string | undefined) {
return function selectRelatedReportActions(reportActions: OnyxCollection<ReportActions>): OnyxCollection<ReportActions> {
const result: OnyxCollection<ReportActions> = {};
const visitedReportIDs = new Set<string>();
const collect = (reportID: string | undefined, shouldFollowErroredThreads: boolean) => {
// The visited set also terminates the walk when thread links form a cycle
if (!reportID || visitedReportIDs.has(reportID)) {
return;
}
visitedReportIDs.add(reportID);
const actions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
if (!actions) {
return;
}
result[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = actions;
if (!shouldFollowErroredThreads) {
return;
}
for (const action of Object.values(actions)) {
if (action.childReportID && Object.keys(action.errors ?? {}).length > 0) {
collect(action.childReportID, true);
}
}
};
// The child root is usually also one of `reportIDs`, so descend from it before the plain lookups mark it visited
collect(childRootReportID, true);
for (const reportID of reportIDs) {
collect(reportID, false);
}
return result;
};
}

/** Returns the ID of the author's newest comment in the report that can show the feedback prompt. */
const getLatestFeedbackActionIDByAuthorSelector = (authorAccountID: number | undefined) => (reportActions: OnyxEntry<ReportActions>) =>
authorAccountID ? getLatestConciergeFeedbackActionIDFromReportActions(reportActions, authorAccountID) : undefined;
Expand Down Expand Up @@ -240,6 +281,7 @@ export {
getReportActionByIDSelector,
getReceiptScanFailedIOUActionDataSelector,
reportVisibleActionsSelector,
relatedReportActionsSelector,
forwardedSinceLastSubmitReportIDsSelector,
transactionThreadReportIDSelector,
getParentReportActionWithoutThreadRepliesSelector,
Expand Down
Loading
Loading