Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import Log from '@libs/Log';
import {selectAllTransactionsForReport, shouldDisplayReportTableView} from '@libs/MoneyRequestReportUtils';
import navigationRef from '@libs/Navigation/navigationRef';
import {getOneTransactionThreadReportID, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOneTransactionThreadReportID, isDeletedParentAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {canEditReportAction, getReportOfflinePendingActionAndErrors, isReportTransactionThread} from '@libs/ReportUtils';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import Navigation from '@navigation/Navigation';
Expand Down Expand Up @@ -95,7 +95,8 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false, canBeMissing: true});
const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report);

const {reportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID);
const {reportActions: reportActionsWithDeletedExpenses, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID);
const reportActions = reportActionsWithDeletedExpenses.filter((value) => !isDeletedParentAction(value));
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], isOffline);

const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getCleanedTagName} from '@libs/PolicyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalMessage, getReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import type {TransactionDetails} from '@libs/ReportUtils';
import {canEditMoneyRequest, getTransactionDetails, getWorkspaceIcon, isIOUReport, isPolicyExpenseChat, isReportApproved, isSettled} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
Expand Down Expand Up @@ -60,6 +60,7 @@ function TransactionPreviewContent({
const ownerAccountID = iouReport?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isReportAPolicyExpenseChat = isPolicyExpenseChat(chatReport);
const {amount: requestAmount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails;
const reportActions = useMemo(() => (iouReport ? getReportActions(iouReport) ?? {} : {}), [iouReport]);

const transactionPreviewCommonArguments = useMemo(
() => ({
Expand Down Expand Up @@ -96,8 +97,9 @@ function TransactionPreviewContent({
...transactionPreviewCommonArguments,
shouldShowRBR,
violationMessage,
reportActions,
}),
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage],
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions],
);
const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : item.text ?? '');

Expand Down
14 changes: 7 additions & 7 deletions src/libs/TransactionPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {convertToDisplayString} from './CurrencyUtils';
import DateUtils from './DateUtils';
import type {PlatformStackRouteProp} from './Navigation/PlatformStackNavigation/types';
import type {TransactionDuplicateNavigatorParamList} from './Navigation/types';
import {getOriginalMessage, getReportAction, getReportActions, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils';
import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils';
import {hasActionsWithErrors, hasReportViolations, isPaidGroupPolicy, isPaidGroupPolicyExpenseReport, isReportApproved, isReportOwner, isSettled} from './ReportUtils';
import type {TransactionDetails} from './ReportUtils';
import StringUtils from './StringUtils';
Expand Down Expand Up @@ -140,10 +140,8 @@ function getViolationTranslatePath(violations: OnyxTypes.TransactionViolations,
* it returns an empty array. It identifies the latest error in each action and filters out duplicates to
* ensure only unique error messages are returned.
*/
function getUniqueActionErrors(report: OnyxEntry<OnyxTypes.Report>) {
const reportActions = Object.values(report ? getReportActions(report) ?? {} : {});

const reportErrors = reportActions.map((reportAction) => {
function getUniqueActionErrors(reportActions: OnyxTypes.ReportActions) {
const reportErrors = Object.values(reportActions).map((reportAction) => {
const errors = reportAction.errors ?? {};
const key = Object.keys(errors).sort().reverse().at(0) ?? '';
const error = errors[key];
Expand All @@ -162,6 +160,7 @@ function getTransactionPreviewTextAndTranslationPaths({
isBillSplit,
shouldShowRBR,
violationMessage,
reportActions,
}: {
iouReport: OnyxEntry<OnyxTypes.Report>;
transaction: OnyxEntry<OnyxTypes.Transaction>;
Expand All @@ -171,6 +170,7 @@ function getTransactionPreviewTextAndTranslationPaths({
isBillSplit: boolean;
shouldShowRBR: boolean;
violationMessage?: string;
reportActions?: OnyxTypes.ReportActions;
}) {
const isFetchingWaypoints = isFetchingWaypointsFromServer(transaction);
const isTransactionOnHold = isOnHold(transaction);
Expand Down Expand Up @@ -216,8 +216,8 @@ function getTransactionPreviewTextAndTranslationPaths({
}
}

if (RBRMessage === undefined && hasActionWithErrors) {
const actionsWithErrors = getUniqueActionErrors(iouReport);
if (RBRMessage === undefined && hasActionWithErrors && !!reportActions) {
const actionsWithErrors = getUniqueActionErrors(reportActions);
RBRMessage = actionsWithErrors.length > 1 ? {translationPath: 'violations.reviewRequired'} : {text: actionsWithErrors.at(0)};
}

Expand Down
9 changes: 5 additions & 4 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7116,21 +7116,22 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT
// STEP 2: Decide if we need to:
// 1. Delete the transactionThread - delete if there are no visible comments in the thread
// 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted
const shouldDeleteTransactionThread = transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false;
const shouldShowDeletedRequestMessage = !!transactionThreadID && !shouldDeleteTransactionThread;
// The current state is that we want to get rid of the [Deleted expense] breadcrumb,
// so we never want to display it if transactionThreadID is present.
const shouldDeleteTransactionThread = !!transactionThreadID;

// STEP 3: Update the IOU reportAction and decide if the iouReport should be deleted. We delete the iouReport if there are no visible comments left in the report.
const updatedReportAction = {
[reportAction.reportActionID]: {
pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
previousMessage: reportAction.message,
message: [
{
type: 'COMMENT',
html: '',
text: '',
isEdited: true,
isDeletedParentAction: shouldShowDeletedRequestMessage,
isDeletedParentAction: shouldDeleteTransactionThread,
},
],
originalMessage: {
Expand Down
9 changes: 8 additions & 1 deletion src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID, canBeMissing: false});
const [currentUserEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.email, canBeMissing: false});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
const {
reportActions: reportActionsWithDeletedExpenses,
linkedAction,
sortedAllReportActions,
hasNewerActions,
hasOlderActions,
} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
const reportActions = reportActionsWithDeletedExpenses.filter((value) => !isDeletedParentAction(value));

const [isBannerVisible, setIsBannerVisible] = useState(true);
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
Expand Down
23 changes: 6 additions & 17 deletions tests/unit/TransactionPreviewUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import {buildOptimisticIOUReport, buildOptimisticIOUReportAction} from '@libs/Re
import {createTransactionPreviewConditionals, getTransactionPreviewTextAndTranslationPaths, getUniqueActionErrors, getViolationTranslatePath} from '@libs/TransactionPreviewUtils';
import {buildOptimisticTransaction} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import * as ReportActionUtils from '@src/libs/ReportActionsUtils';
import * as ReportUtils from '@src/libs/ReportUtils';
import type {Report, ReportActions} from '@src/types/onyx';
import {iouReportR14932 as mockedReport} from '../../__mocks__/reportData/reports';
import type {ReportActions} from '@src/types/onyx';

const basicProps = {
iouReport: buildOptimisticIOUReport(123, 234, 1000, '1', 'USD'),
Expand Down Expand Up @@ -258,14 +256,8 @@ describe('TransactionPreviewUtils', () => {
});

describe('getUniqueActionErrors', () => {
test('returns an empty array if there is no report or it is empty', () => {
expect(getUniqueActionErrors(undefined)).toEqual([]);
expect(getUniqueActionErrors({} as Report)).toEqual([]);
});

test('returns an empty array if there are no actions in the report', () => {
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue({});
expect(getUniqueActionErrors(mockedReport)).toEqual([]);
test('returns an empty array if there are no actions', () => {
expect(getUniqueActionErrors({})).toEqual([]);
});

test('returns unique error messages from report actions', () => {
Expand All @@ -276,10 +268,9 @@ describe('TransactionPreviewUtils', () => {
3: {errors: {a: 'Error A', d: 'Error D'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

const expectedErrors = ['Error B', 'Error C', 'Error D'];
expect(getUniqueActionErrors(mockedReport).sort()).toEqual(expectedErrors.sort());
expect(getUniqueActionErrors(actions).sort()).toEqual(expectedErrors.sort());
});

test('returns the latest error message if multiple errors exist under a single action', () => {
Expand All @@ -288,9 +279,8 @@ describe('TransactionPreviewUtils', () => {
1: {errors: {z: 'Error Z2', a: 'Error A', f: 'Error Z'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

expect(getUniqueActionErrors(mockedReport)).toEqual(['Error Z2']);
expect(getUniqueActionErrors(actions)).toEqual(['Error Z2']);
});

test('filters out non-string error messages', () => {
Expand All @@ -300,9 +290,8 @@ describe('TransactionPreviewUtils', () => {
2: {errors: {c: null, d: 'Error D'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

expect(getUniqueActionErrors(mockedReport)).toEqual(['Error B', 'Error D']);
expect(getUniqueActionErrors(actions)).toEqual(['Error B', 'Error D']);
});
});
});