diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts
index fc94d7e07bea2..581f9231129be 100644
--- a/.storybook/webpack.config.ts
+++ b/.storybook/webpack.config.ts
@@ -8,6 +8,7 @@ import dotenv from 'dotenv';
import path from 'path';
import {DefinePlugin} from 'webpack';
import type {Configuration, RuleSetRule} from 'webpack';
+import webpackMockPaths from './webpackMockPaths';
type CustomWebpackConfig = {
resolve: {
@@ -54,11 +55,7 @@ const webpackConfig = ({config}: {config: Configuration}) => {
}
config.resolve.alias = {
- 'react-native-config': 'react-web-config',
- 'react-native$': 'react-native-web',
- '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'),
- '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'),
- '@libs/TransactionPreviewUtils': path.resolve(__dirname, '../src/libs/__mocks__/TransactionPreviewUtils.ts'),
+ ...webpackMockPaths,
...custom.resolve.alias,
};
diff --git a/.storybook/webpackMockPaths.ts b/.storybook/webpackMockPaths.ts
new file mode 100644
index 0000000000000..78a6b8dbda908
--- /dev/null
+++ b/.storybook/webpackMockPaths.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+
+/* eslint-disable @typescript-eslint/naming-convention */
+export default {
+ 'react-native-config': 'react-web-config',
+ 'react-native$': 'react-native-web',
+ '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'),
+ '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'),
+ '@libs/TransactionPreviewUtils': path.resolve(__dirname, '../src/libs/__mocks__/TransactionPreviewUtils.ts'),
+};
+/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/__mocks__/reportData/actions.ts b/__mocks__/reportData/actions.ts
new file mode 100644
index 0000000000000..795dbc1617936
--- /dev/null
+++ b/__mocks__/reportData/actions.ts
@@ -0,0 +1,89 @@
+import CONST from '@src/CONST';
+import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx';
+
+const usersIDs = [15593135, 51760358, 26502375];
+const amount = 10402;
+const currency = CONST.CURRENCY.USD;
+
+const REPORT_R98765 = {
+ IOUReportID: 'IOU_REPORT_ID_R98765',
+ IOUTransactionID: 'TRANSACTION_ID_R98765',
+ reportActionID: 'REPORT_ACTION_ID_R98765',
+ childReportID: 'CHILD_REPORT_ID_R98765',
+};
+
+const REPORT_R14932 = {
+ IOUReportID: 'IOU_REPORT_ID_R14932',
+ IOUTransactionID: 'TRANSACTION_ID_R14932',
+ reportActionID: 'REPORT_ACTION_ID_R14932',
+ childReportID: 'CHILD_REPORT_ID_R14932',
+};
+
+const originalMessageR14932: OriginalMessageIOU = {
+ currency,
+ amount,
+ IOUReportID: REPORT_R14932.IOUReportID,
+ IOUTransactionID: REPORT_R14932.IOUTransactionID,
+ participantAccountIDs: usersIDs,
+ type: CONST.IOU.TYPE.CREATE,
+ lastModified: '2025-02-14 08:12:05.165',
+ comment: '',
+};
+
+const message = [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ html: '$0.01 expense',
+ text: '$0.01 expense',
+ isEdited: false,
+ whisperedTo: [],
+ isDeletedParentAction: false,
+ deleted: '',
+ },
+];
+
+const person = [
+ {
+ type: 'TEXT',
+ style: 'strong',
+ text: 'John Smith',
+ },
+];
+
+const actionR14932: ReportAction = {
+ person,
+ message,
+ reportActionID: REPORT_R14932.reportActionID,
+ childReportID: REPORT_R14932.childReportID,
+ originalMessage: originalMessageR14932,
+ actorAccountID: usersIDs.at(0),
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ childType: CONST.REPORT.TYPE.CHAT,
+ childReportName: 'Expense #R14932',
+ created: '2025-02-14 08:12:05.165',
+};
+
+const originalMessageR98765: OriginalMessageIOU = {
+ amount,
+ currency,
+ IOUReportID: REPORT_R98765.IOUReportID,
+ IOUTransactionID: REPORT_R98765.IOUTransactionID,
+ participantAccountIDs: usersIDs,
+ type: CONST.IOU.TYPE.CREATE,
+ comment: '',
+ lastModified: '2025-02-20 08:10:05.165',
+};
+
+const actionR98765: ReportAction = {
+ message,
+ person,
+ reportActionID: REPORT_R98765.reportActionID,
+ childReportID: REPORT_R98765.childReportID,
+ originalMessage: originalMessageR98765,
+ actorAccountID: usersIDs.at(0),
+ childType: CONST.REPORT.TYPE.CHAT,
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ created: '2025-02-14 08:12:05.165',
+};
+
+export {actionR14932, actionR98765};
diff --git a/__mocks__/reportData/personalDetails.ts b/__mocks__/reportData/personalDetails.ts
new file mode 100644
index 0000000000000..b3c6179497f6c
--- /dev/null
+++ b/__mocks__/reportData/personalDetails.ts
@@ -0,0 +1,68 @@
+import type {PersonalDetailsList} from '@src/types/onyx';
+
+const usersIDs = [15593135, 51760358, 26502375] as const;
+
+const personalDetails: PersonalDetailsList = {
+ [usersIDs[0]]: {
+ accountID: usersIDs[0],
+ avatar: '@assets/images/avatars/user/default-avatar_1.svg',
+ firstName: 'John',
+ lastName: 'Smith',
+ status: {
+ clearAfter: '',
+ emojiCode: '🚲',
+ text: '0% cycling in Canary islands',
+ },
+ displayName: 'John Smith',
+ login: 'johnsmith@mail.com',
+ pronouns: '__predefined_heHimHis',
+ timezone: {
+ automatic: true,
+ selected: 'Europe/Luxembourg',
+ },
+ phoneNumber: '11111111',
+ validated: true,
+ },
+ [usersIDs[1]]: {
+ accountID: usersIDs[1],
+ avatar: '@assets/images/avatars/user/default-avatar_2.svg',
+ firstName: 'Ted',
+ lastName: 'Kowalski',
+ status: {
+ clearAfter: '',
+ emojiCode: '🚲',
+ text: '0% cycling in Canary islands',
+ },
+ displayName: 'Ted Kowalski',
+ login: 'tedkowalski@mail.com',
+ pronouns: '__predefined_heHimHis',
+ timezone: {
+ automatic: true,
+ selected: 'Europe/Warsaw',
+ },
+ phoneNumber: '22222222',
+ validated: true,
+ },
+ [usersIDs[2]]: {
+ accountID: usersIDs[2],
+ avatar: '@assets/images/avatars/user/default-avatar_3.svg',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ status: {
+ clearAfter: '',
+ emojiCode: '🚲',
+ text: '0% cycling in Canary islands',
+ },
+ displayName: 'Jane Doe',
+ login: 'janedoe@mail.com',
+ pronouns: '__predefined_sheHerHers',
+ timezone: {
+ automatic: true,
+ selected: 'Europe/London',
+ },
+ phoneNumber: '33333333',
+ validated: true,
+ },
+};
+
+export default personalDetails;
diff --git a/__mocks__/reportData/reports.ts b/__mocks__/reportData/reports.ts
new file mode 100644
index 0000000000000..59f5e0759e5a3
--- /dev/null
+++ b/__mocks__/reportData/reports.ts
@@ -0,0 +1,104 @@
+import CONST from '@src/CONST';
+import type {Report} from '@src/types/onyx';
+
+const usersIDs = [15593135, 51760358, 26502375];
+const amount = 10402;
+const currency = CONST.CURRENCY.USD;
+
+const REPORT_ID_R14932 = 'REPORT_ID_R14932';
+const CHAT_REPORT_ID_R14932 = 'CHAT_REPORT_ID_R14932';
+const IOU_REPORT_ID_R14932 = 'IOU_REPORT_ID_R14932';
+const PARENT_REPORT_ACTION_ID_R14932 = 'PARENT_ACTION_ID_R14932';
+const PARENT_REPORT_ID_R14932 = 'PARENT_REPORT_ID_R14932';
+const LAST_MESSAGE_R14932 = 'LAST_MESSAGE_R14932';
+
+const participants = usersIDs.reduce((prev, userID) => {
+ return {
+ [userID]: {
+ notificationPreference: 'always',
+ },
+ };
+}, {});
+
+const iouReportR14932: Report = {
+ currency,
+ participants,
+ total: amount,
+ unheldTotal: amount,
+ chatReportID: CHAT_REPORT_ID_R14932,
+ lastMessageHtml: LAST_MESSAGE_R14932,
+ lastMessageText: LAST_MESSAGE_R14932,
+ parentReportActionID: PARENT_REPORT_ACTION_ID_R14932,
+ parentReportID: PARENT_REPORT_ID_R14932,
+ reportID: REPORT_ID_R14932,
+ lastActorAccountID: usersIDs.at(0),
+ ownerAccountID: usersIDs.at(0),
+ managerID: usersIDs.at(1),
+ permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE],
+ policyID: CONST.POLICY.ID_FAKE,
+ reportName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
+ lastActionType: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ hasOutstandingChildRequest: false,
+ hasOutstandingChildTask: false,
+ hasParentAccess: true,
+ isCancelledIOU: false,
+ isOwnPolicyExpenseChat: false,
+ isPinned: false,
+ isWaitingOnBankAccount: false,
+ lastReadTime: '2025-03-07 07:23:39.335',
+ lastVisibleActionCreated: '2025-03-07 07:23:39.335',
+ lastVisibleActionLastModified: '2025-03-07 07:23:39.335',
+ lastReadSequenceNumber: 0,
+ unheldNonReimbursableTotal: 0,
+ nonReimbursableTotal: 0,
+ errorFields: {},
+ welcomeMessage: '',
+ description: '',
+ oldPolicyName: '',
+};
+
+const chatReportR14932: Report = {
+ currency,
+ participants,
+ lastMessageText: LAST_MESSAGE_R14932,
+ reportID: REPORT_ID_R14932,
+ iouReportID: IOU_REPORT_ID_R14932,
+ lastActorAccountID: usersIDs.at(0),
+ ownerAccountID: usersIDs.at(0),
+ managerID: usersIDs.at(1),
+ total: amount,
+ unheldTotal: amount,
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ policyID: CONST.POLICY.ID_FAKE,
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
+ lastActionType: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
+ writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
+ permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE],
+ type: CONST.REPORT.TYPE.CHAT,
+ lastMessageHtml: ` `,
+ lastReadTime: '2025-03-11 08:51:38.736',
+ lastVisibleActionCreated: '2025-03-11 08:47:56.654',
+ lastVisibleActionLastModified: '2025-03-11 08:47:56.654',
+ hasOutstandingChildRequest: false,
+ hasOutstandingChildTask: false,
+ isCancelledIOU: false,
+ isOwnPolicyExpenseChat: false,
+ isPinned: false,
+ isWaitingOnBankAccount: false,
+ lastReadSequenceNumber: 0,
+ unheldNonReimbursableTotal: 0,
+ stateNum: 0,
+ statusNum: 0,
+ nonReimbursableTotal: 0,
+ errorFields: {},
+ description: '',
+ oldPolicyName: '',
+ welcomeMessage: '',
+};
+
+export {chatReportR14932, iouReportR14932};
diff --git a/__mocks__/reportData/transactions.ts b/__mocks__/reportData/transactions.ts
new file mode 100644
index 0000000000000..4dc0cfdd19021
--- /dev/null
+++ b/__mocks__/reportData/transactions.ts
@@ -0,0 +1,79 @@
+import CONST from '@src/CONST';
+import type {Transaction} from '@src/types/onyx';
+
+const amount = 10402;
+const currency = CONST.CURRENCY.USD;
+const REPORT_ID_R14932 = 'REPORT_ID_R14932';
+const TRANSACTION_ID_R14932 = 'TRANSACTION_ID_R14932';
+const REPORT_ID_R98765 = 'REPORT_ID_R98765';
+const TRANSACTION_ID_R98765 = 'TRANSACTION_ID_R98765';
+
+const receiptR14932 = {
+ state: CONST.IOU.RECEIPT_STATE.OPEN,
+ source: 'mockData/eReceiptBGs/eReceiptBG_pink.png',
+};
+
+const transactionR14932: Transaction = {
+ amount,
+ currency,
+ cardName: CONST.EXPENSE.TYPE.CASH_CARD_NAME,
+ transactionID: TRANSACTION_ID_R14932,
+ reportID: REPORT_ID_R14932,
+ status: CONST.TRANSACTION.STATUS.POSTED,
+ receipt: receiptR14932,
+ merchant: 'Acme',
+ filename: 'test.html',
+ created: '2025-02-14',
+ inserted: '2025-02-14 08:12:19',
+ billable: false,
+ managedCard: false,
+ reimbursable: true,
+ hasEReceipt: true,
+ cardID: 0,
+ modifiedAmount: 0,
+ originalAmount: 0,
+ comment: {},
+ bank: '',
+ cardNumber: '',
+ category: '',
+ modifiedCreated: '',
+ modifiedCurrency: '',
+ modifiedMerchant: '',
+ originalCurrency: '',
+ parentTransactionID: '',
+ posted: '',
+ tag: '',
+};
+
+const transactionR98765: Transaction = {
+ currency,
+ amount,
+ transactionID: TRANSACTION_ID_R98765,
+ reportID: REPORT_ID_R98765,
+ status: CONST.TRANSACTION.STATUS.POSTED,
+ cardName: CONST.EXPENSE.TYPE.CASH_CARD_NAME,
+ created: '2025-02-14',
+ inserted: '2025-02-14 08:12:19',
+ merchant: 'Acme',
+ reimbursable: true,
+ hasEReceipt: true,
+ managedCard: false,
+ billable: false,
+ modifiedAmount: 0,
+ cardID: 0,
+ originalAmount: 0,
+ comment: {},
+ bank: '',
+ cardNumber: '',
+ category: '',
+ filename: '',
+ modifiedCreated: '',
+ modifiedCurrency: '',
+ modifiedMerchant: '',
+ originalCurrency: '',
+ parentTransactionID: '',
+ posted: '',
+ tag: '',
+};
+
+export {transactionR14932, transactionR98765};
diff --git a/__mocks__/reportData/violations.ts b/__mocks__/reportData/violations.ts
new file mode 100644
index 0000000000000..f1fce6ed8021d
--- /dev/null
+++ b/__mocks__/reportData/violations.ts
@@ -0,0 +1,38 @@
+import CONST from '@src/CONST';
+import type {TransactionViolations} from '@src/types/onyx';
+import type {ReceiptErrors} from '@src/types/onyx/Transaction';
+
+const RECEIPT_ERRORS_ID_R14932 = 1201421;
+const RECEIPT_ERRORS_TRANSACTION_ID_R14932 = 'IOU_TRANSACTION_ID_R14932';
+
+const violationsR14932: TransactionViolations = [
+ {
+ name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION,
+ type: CONST.VIOLATION_TYPES.VIOLATION,
+ showInReview: true,
+ },
+ {
+ name: CONST.VIOLATIONS.MISSING_CATEGORY,
+ type: CONST.VIOLATION_TYPES.VIOLATION,
+ showInReview: true,
+ },
+ {
+ name: CONST.VIOLATIONS.FIELD_REQUIRED,
+ type: CONST.VIOLATION_TYPES.VIOLATION,
+ showInReview: true,
+ },
+];
+
+const receiptErrorsR14932: ReceiptErrors = {
+ [RECEIPT_ERRORS_ID_R14932]: {
+ source: CONST.POLICY.ID_FAKE,
+ filename: CONST.POLICY.ID_FAKE,
+ action: CONST.POLICY.ID_FAKE,
+ retryParams: {
+ transactionID: RECEIPT_ERRORS_TRANSACTION_ID_R14932,
+ source: CONST.POLICY.ID_FAKE,
+ },
+ },
+};
+
+export {receiptErrorsR14932, violationsR14932};
diff --git a/src/components/VideoPlayerContexts/PlaybackContext/index.tsx b/src/components/VideoPlayerContexts/PlaybackContext/index.tsx
index 74641f5579d13..f39a9621475b9 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext/index.tsx
+++ b/src/components/VideoPlayerContexts/PlaybackContext/index.tsx
@@ -5,7 +5,7 @@ import {getReportOrDraftReport, isChatThread} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {ProtectedCurrentRouteReportID} from './playbackContextReportIDUtils';
-import {findUrlInReportOrAncestorAttachments, getCurrentRouteReportID, NO_REPORT_ID, NO_REPORT_ID_IN_PARAMS, normalizeReportID} from './playbackContextReportIDUtils';
+import {findURLInReportOrAncestorAttachments, getCurrentRouteReportID, NO_REPORT_ID, NO_REPORT_ID_IN_PARAMS, normalizeReportID} from './playbackContextReportIDUtils';
import type {OriginalParent, PlaybackContext, PlaybackContextValues} from './types';
import usePlaybackContextVideoRefs from './usePlaybackContextVideoRefs';
@@ -39,7 +39,7 @@ function PlaybackContextProvider({children}: ChildrenProps) {
const isReportAChatThread = isChatThread(report);
let reportIDtoSet;
if (isReportAChatThread) {
- reportIDtoSet = findUrlInReportOrAncestorAttachments(report, url) ?? NO_REPORT_ID;
+ reportIDtoSet = findURLInReportOrAncestorAttachments(report, url) ?? NO_REPORT_ID;
} else {
reportIDtoSet = reportID;
}
diff --git a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
index 5d85d039263fc..25cfd4b8f6a0f 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
+++ b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
@@ -32,9 +32,9 @@ type RouteWithReportIDInParams = T & {params: ReportDetailsNavigatorParamList
const getCurrentRouteReportID: (url: string) => string | ProtectedCurrentRouteReportID = (url): string | typeof NO_REPORT_ID_IN_PARAMS | typeof NO_REPORT_ID => {
const route = Navigation.getActiveRouteWithoutParams() as ActiveRoute;
const focusedRoute = findFocusedRoute(getStateFromPath(route));
- const reportIDFromUrlParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID');
+ const reportIDFromURLParams = new URLSearchParams(Navigation.getActiveRoute()).get('reportID');
- const focusedRouteReportID = hasReportIdInRouteParams(focusedRoute) ? focusedRoute.params.reportID : reportIDFromUrlParams;
+ const focusedRouteReportID = hasReportIdInRouteParams(focusedRoute) ? focusedRoute.params.reportID : reportIDFromURLParams;
if (!focusedRouteReportID) {
return NO_REPORT_ID_IN_PARAMS;
@@ -42,7 +42,7 @@ const getCurrentRouteReportID: (url: string) => string | ProtectedCurrentRouteRe
const report = getReportOrDraftReport(focusedRouteReportID);
const isFocusedRouteAChatThread = isChatThread(report);
- const firstReportThatHasURLInAttachments = findUrlInReportOrAncestorAttachments(report, url);
+ const firstReportThatHasURLInAttachments = findURLInReportOrAncestorAttachments(report, url);
return isFocusedRouteAChatThread ? firstReportThatHasURLInAttachments : focusedRouteReportID;
};
@@ -53,7 +53,15 @@ function hasReportIdInRouteParams(route: SearchRoute): route is RouteWithReportI
return !!route && !!route.params && !!screensWithReportID.find((screen) => screen === route.name) && 'reportID' in route.params;
}
-function findUrlInReportOrAncestorAttachments(currentReport: OnyxEntry, url: string | null): string | typeof NO_REPORT_ID {
+/**
+ * Searches recursively through a report and its ancestor reports to find a specified URL in their attachments.
+ * The search continues up the ancestry chain until the URL is found or there are no more ancestors.
+ *
+ * @param currentReport - The current report entry, potentially containing the URL.
+ * @param url - The URL to be located in the report or its ancestors' attachments.
+ * @returns The report ID where the URL is found, or undefined if not found.
+ */
+function findURLInReportOrAncestorAttachments(currentReport: OnyxEntry, url: string | null): string | typeof NO_REPORT_ID {
const {parentReportID, reportID} = currentReport ?? {};
const reportActions = getAllReportActions(reportID);
@@ -68,11 +76,11 @@ function findUrlInReportOrAncestorAttachments(currentReport: OnyxEntry,
if (parentReportID) {
const parentReport = getReportOrDraftReport(parentReportID);
- return findUrlInReportOrAncestorAttachments(parentReport, url);
+ return findURLInReportOrAncestorAttachments(parentReport, url);
}
return NO_REPORT_ID;
}
-export {NO_REPORT_ID, NO_REPORT_ID_IN_PARAMS, getCurrentRouteReportID, normalizeReportID, findUrlInReportOrAncestorAttachments};
+export {NO_REPORT_ID, NO_REPORT_ID_IN_PARAMS, getCurrentRouteReportID, normalizeReportID, findURLInReportOrAncestorAttachments};
export type {ProtectedCurrentRouteReportID};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 4026c77d066ad..10007b7b04b4c 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
+import usePrevious from '@hooks/usePrevious';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -1130,6 +1131,24 @@ function isMessageDeleted(reportAction: OnyxInputOrEntry): boolean
return getReportActionMessage(reportAction)?.isDeletedParentAction ?? false;
}
+/**
+ * Simple hook to check whether the PureReportActionItem should return item based on whether the ReportPreview was recently deleted and the PureReportActionItem has not yet unloaded
+ */
+function useNewTableReportViewActionRenderConditionals({childMoneyRequestCount, childVisibleActionCount, pendingAction, actionName}: ReportAction) {
+ const previousChildMoneyRequestCount = usePrevious(childMoneyRequestCount);
+
+ const isActionAReportPreview = actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
+ const isActionInUpdateState = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE;
+ const reportsCount = childMoneyRequestCount;
+ const previousReportsCount = previousChildMoneyRequestCount ?? 0;
+ const commentsCount = childVisibleActionCount ?? 0;
+
+ const isEmptyPreviewWithComments = reportsCount === 0 && commentsCount > 0 && previousReportsCount > 0;
+
+ // We only want to remove the item if the ReportPreview has comments but no reports, so we avoid having a PureReportActionItem with no ReportPreview but only comments
+ return !(isActionAReportPreview && isActionInUpdateState && isEmptyPreviewWithComments);
+}
+
/**
* Returns the number of expenses associated with a report preview
*/
@@ -2412,6 +2431,7 @@ export {
isMemberChangeAction,
isExportIntegrationAction,
isMessageDeleted,
+ useNewTableReportViewActionRenderConditionals,
isModifiedExpenseAction,
isMoneyRequestAction,
isNotifiableReportAction,
diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index 9dfe47a497734..c852009c37926 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -108,6 +108,7 @@ import {
isTripPreview,
isUnapprovedAction,
isWhisperActionTargetedToOthers,
+ useNewTableReportViewActionRenderConditionals,
} from '@libs/ReportActionsUtils';
import {
canWriteInReport,
@@ -342,18 +343,6 @@ type PureReportActionItemProps = {
const emptyHTML = ;
const isEmptyHTML = ({props: {html}}: T): boolean => typeof html === 'string' && html.length === 0;
-const useNewTableReportViewActionRenderConditionals = ({childMoneyRequestCount, childVisibleActionCount, pendingAction, actionName}: OnyxTypes.ReportAction) => {
- const previousChildMoneyRequestCount = usePrevious(childMoneyRequestCount);
-
- return !(
- actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW &&
- pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE &&
- childMoneyRequestCount === 0 &&
- (childVisibleActionCount ?? 0) > 0 &&
- (previousChildMoneyRequestCount ?? 0) > 0
- );
-};
-
/**
* This is a pure version of ReportActionItem, used in ReportActionList and Search result chat list items.
* Since the search result has a separate Onyx key under the 'snapshot_' prefix, we should not connect this component with Onyx.
@@ -810,7 +799,7 @@ function PureReportActionItem({
// Table Report View does not display these components as separate messages, except for self-DM
if (canUseTableReportView && report?.type === CONST.REPORT.TYPE.CHAT) {
- if (report.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM) {
+ if (report.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM && !isDeletedAction(action)) {
children = (
{
- return {...transaction, transactionID: `${index}`};
+ return {...transactionR14932, transactionID: `${transactionR14932.transactionID}${index}`};
});
const mockTransactionsBig = Array.from({length: 12}).map((item, index) => {
- return {...transaction, transactionID: `${index}`};
+ return {...transactionR14932, transactionID: `${transactionR14932.transactionID}${index}`};
});
const style = getMoneyRequestReportPreviewStyle(false);
const mockRenderItem: ListRenderItem = ({item}) => (
undefined}
offlineWithFeedbackOnClose={() => undefined}
onPreviewPressed={() => {}}
@@ -114,12 +118,12 @@ export default {
},
},
args: {
- action,
- chatReport,
+ action: actionR14932,
+ chatReport: chatReportR14932,
policy: undefined,
- iouReport,
+ iouReport: iouReportR14932,
transactions: mockTransactionsMedium,
- violations,
+ violations: violationsR14932,
invoiceReceiverPersonalDetail: undefined,
invoiceReceiverPolicy: undefined,
renderItem: mockRenderItem,
@@ -162,7 +166,7 @@ DarkTheme.parameters = {
};
OneTransaction.args = {
- transactions: [transaction],
+ transactions: [transactionR14932],
};
ManyTransactions.parameters = {
@@ -172,7 +176,7 @@ ManyTransactions.parameters = {
HasErrors.args = {
transactions: mockTransactionsMedium.map((t) => ({
...t,
- errors: receiptErrors,
+ errors: receiptErrorsR14932,
})),
};
diff --git a/src/stories/TransactionPreviewContent.stories.tsx b/src/stories/TransactionPreviewContent.stories.tsx
index df08d7e8ec662..2044013675408 100644
--- a/src/stories/TransactionPreviewContent.stories.tsx
+++ b/src/stories/TransactionPreviewContent.stories.tsx
@@ -9,12 +9,16 @@ import ThemeStylesProvider from '@components/ThemeStylesProvider';
import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
-import {action, chatReport, iouReport, personalDetails, transaction, violations} from './mockData/transactions';
+import {actionR14932} from '../../__mocks__/reportData/actions';
+import personalDetails from '../../__mocks__/reportData/personalDetails';
+import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
+import {transactionR14932} from '../../__mocks__/reportData/transactions';
+import {violationsR14932} from '../../__mocks__/reportData/violations';
const veryLongString = 'W'.repeat(1000);
const veryBigNumber = Number('9'.repeat(12));
const modifiedTransaction = ({category, tag, merchant = '', amount = 1000, hold = false}: {category?: string; tag?: string; merchant?: string; amount?: number; hold?: boolean}) => ({
- ...transaction,
+ ...transactionR14932,
category,
tag,
merchant,
@@ -23,8 +27,8 @@ const modifiedTransaction = ({category, tag, merchant = '', amount = 1000, hold
hold: hold ? 'true' : undefined,
},
});
-const iouReportWithModifiedType = (type: string) => ({...iouReport, type});
-const actionWithModifiedPendingAction = (pendingAction: PendingAction) => ({...action, pendingAction});
+const iouReportWithModifiedType = (type: string) => ({...iouReportR14932, type});
+const actionWithModifiedPendingAction = (pendingAction: PendingAction) => ({...actionR14932, pendingAction});
const disabledProperties = [
'onPreviewPressed',
@@ -66,19 +70,19 @@ const transactionsMap = {
const violationsMap = {
None: [],
- Duplicate: [violations.at(0)],
- 'Missing Category': [violations.at(1)],
- 'Field Required': [violations.at(2)],
+ Duplicate: [violationsR14932.at(0)],
+ 'Missing Category': [violationsR14932.at(1)],
+ 'Field Required': [violationsR14932.at(2)],
};
const actionMap = {
'Pending delete': actionWithModifiedPendingAction(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
- 'No pending action': action,
+ 'No pending action': actionR14932,
};
const iouReportMap = {
IOU: iouReportWithModifiedType(CONST.REPORT.TYPE.IOU),
- 'Normal report': iouReport,
+ 'Normal report': iouReportR14932,
};
/* eslint-enable @typescript-eslint/naming-convention */
@@ -93,13 +97,13 @@ const story: Meta = {
title: 'Components/TransactionPreview',
component: TransactionPreviewContent,
args: {
- action,
+ action: actionR14932,
isWhisper: false,
isHovered: false,
- chatReport,
+ chatReport: chatReportR14932,
personalDetails,
- iouReport,
- transaction,
+ iouReport: iouReportR14932,
+ transaction: transactionR14932,
violations: [],
showContextMenu: () => undefined,
offlineWithFeedbackOnClose(): void {},
@@ -112,7 +116,7 @@ const story: Meta = {
walletTermsErrors: undefined,
routeName: SCREENS.TRANSACTION_DUPLICATE.REVIEW,
shouldHideOnDelete: false,
- wrapperStyle: {width: 256},
+ wrapperStyle: {width: 303},
},
argTypes: {
...disabledProperties,
@@ -166,7 +170,7 @@ KeepButtonCategoriesAndTag.args = {
KeepButtonRBRCategoriesAndTag.args = {
...KeepButtonCategoriesAndTag.args,
- violations,
+ violations: violationsR14932,
transaction: modifiedTransaction({...storiesTransactionData, hold: true}),
};
diff --git a/src/stories/mockData/transactions.ts b/src/stories/mockData/transactions.ts
deleted file mode 100644
index b228c404edba8..0000000000000
--- a/src/stories/mockData/transactions.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-/* eslint-disable @typescript-eslint/naming-convention */
-import CONST from '@src/CONST';
-import type {OriginalMessageIOU, PersonalDetailsList, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
-import type {ReceiptErrors} from '@src/types/onyx/Transaction';
-
-const amount = 1000;
-const currency = CONST.CURRENCY.USD;
-
-const REPORT_ID_456 = 'R98765';
-const REPORT_ID_111 = '1111111111111111';
-
-const personalDetails: PersonalDetailsList = {
- 11111111: {
- accountID: 11111111,
- avatar: '@assets/images/avatars/user/default-avatar_1.svg',
- firstName: 'John',
- lastName: 'Smith',
- status: {
- clearAfter: '',
- emojiCode: '🚲',
- text: '0% cycling in Canary islands',
- },
- displayName: 'John Smith',
- login: 'johnsmith@mail.com',
- pronouns: '__predefined_heHimHis',
- timezone: {
- automatic: true,
- selected: 'Europe/Luxembourg',
- },
- phoneNumber: '11111111',
- validated: true,
- },
- 22222222: {
- accountID: 22222222,
- avatar: '@assets/images/avatars/user/default-avatar_2.svg',
- firstName: 'Ted',
- lastName: 'Kowalski',
- status: {
- clearAfter: '',
- emojiCode: '🚲',
- text: '0% cycling in Canary islands',
- },
- displayName: 'Ted Kowalski',
- login: 'tedkowalski@mail.com',
- pronouns: '__predefined_heHimHis',
- timezone: {
- automatic: true,
- selected: 'Europe/Warsaw',
- },
- phoneNumber: '22222222',
- validated: true,
- },
- 33333333: {
- accountID: 33333333,
- avatar: '@assets/images/avatars/user/default-avatar_3.svg',
- firstName: 'Jane',
- lastName: 'Doe',
- status: {
- clearAfter: '',
- emojiCode: '🚲',
- text: '0% cycling in Canary islands',
- },
- displayName: 'Jane Doe',
- login: 'janedoe@mail.com',
- pronouns: '__predefined_sheHerHers',
- timezone: {
- automatic: true,
- selected: 'Europe/London',
- },
- phoneNumber: '33333333',
- validated: true,
- },
-};
-
-const iouReport: Report = {
- chatReportID: REPORT_ID_111,
- chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
- currency,
- description: '',
- errorFields: {},
- hasOutstandingChildRequest: false,
- hasOutstandingChildTask: false,
- hasParentAccess: true,
- isCancelledIOU: false,
- isOwnPolicyExpenseChat: false,
- isPinned: false,
- isWaitingOnBankAccount: false,
- lastActionType: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
- lastActorAccountID: 11111111,
- lastMessageHtml: 'abc',
- lastMessageText: 'abc',
- lastReadSequenceNumber: 0,
- lastReadTime: '2025-03-07 07:23:39.335',
- lastVisibleActionCreated: '2025-03-07 07:23:39.335',
- lastVisibleActionLastModified: '2025-03-07 07:23:39.335',
- managerID: 22222222,
- nonReimbursableTotal: 0,
- oldPolicyName: '',
- ownerAccountID: 11111111,
- parentReportActionID: '1111111111111111111',
- parentReportID: '1111111111111111',
- participants: {
- '11111111': {
- notificationPreference: 'always',
- },
- '22222222': {
- notificationPreference: 'always',
- },
- '33333333': {
- notificationPreference: 'always',
- },
- },
- permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE],
- policyID: CONST.POLICY.ID_FAKE,
- reportID: '1111111111111111',
- reportName: 'IOU',
- stateNum: CONST.REPORT.STATE_NUM.APPROVED,
- statusNum: CONST.REPORT.STATUS_NUM.OPEN,
- total: 112298,
- // type: CONST.REPORT.TYPE.IOU,
- type: CONST.REPORT.TYPE.EXPENSE,
- unheldNonReimbursableTotal: 0,
- unheldTotal: 112298,
- welcomeMessage: '',
- writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
-};
-
-const chatReport: Report = {
- chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
- currency,
- description: '',
- errorFields: {},
- hasOutstandingChildRequest: false,
- hasOutstandingChildTask: false,
- isCancelledIOU: false,
- isOwnPolicyExpenseChat: false,
- isPinned: false,
- isWaitingOnBankAccount: false,
- lastActionType: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
- lastActorAccountID: 11111111,
- lastMessageHtml: ' ',
- lastMessageText: 'Test abc',
- lastReadSequenceNumber: 0,
- lastReadTime: '2025-03-11 08:51:38.736',
- lastVisibleActionCreated: '2025-03-11 08:47:56.654',
- lastVisibleActionLastModified: '2025-03-11 08:47:56.654',
- nonReimbursableTotal: 0,
- oldPolicyName: '',
- ownerAccountID: 11111111,
- participants: {
- '11111111': {
- notificationPreference: 'always',
- },
- '22222222': {
- notificationPreference: 'always',
- },
- '33333333': {
- notificationPreference: 'always',
- },
- },
- permissions: ['read', 'write'],
- policyID: CONST.POLICY.ID_FAKE,
- reportID: REPORT_ID_111,
- reportName: 'Chat Report',
- stateNum: 0,
- statusNum: 0,
- total: 100,
- type: 'chat',
- unheldNonReimbursableTotal: 0,
- unheldTotal: 0,
- welcomeMessage: '',
- writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
- iouReportID: REPORT_ID_111,
- managerID: 0,
-};
-
-const transaction: Transaction = {
- amount,
- bank: '',
- billable: false,
- cardID: 0,
- cardName: 'Cash Expense',
- cardNumber: '',
- category: '',
- comment: {},
- created: '2025-02-14',
- currency,
- filename: 'test.html',
- inserted: '2025-02-14 08:12:19',
- managedCard: false,
- merchant: 'Acme',
- modifiedAmount: 0,
- modifiedCreated: '',
- modifiedCurrency: '',
- modifiedMerchant: '',
- originalAmount: 0,
- originalCurrency: '',
- parentTransactionID: '',
- posted: '',
- receipt: {
- state: CONST.IOU.RECEIPT_STATE.OPEN,
- source: 'mockData/eReceiptBGs/eReceiptBG_pink.png',
- },
- reimbursable: true,
- reportID: '1111111111111111',
- status: CONST.TRANSACTION.STATUS.POSTED,
- tag: '',
- transactionID: '1111111111111111111',
- hasEReceipt: true,
-};
-
-const fakeTransaction456: Transaction = {
- amount,
- transactionID: 'trsx456',
- bank: '',
- billable: false,
- cardID: 0,
- cardName: 'Cash Expense',
- cardNumber: '',
- category: '',
- comment: {},
- created: '2025-02-14',
- currency,
- filename: '',
- inserted: '2025-02-14 08:12:19',
- managedCard: false,
- merchant: 'Acme',
- modifiedAmount: 0,
- modifiedCreated: '',
- modifiedCurrency: '',
- modifiedMerchant: '',
- originalAmount: 0,
- originalCurrency: '',
- parentTransactionID: '',
- posted: '',
- reimbursable: true,
- reportID: '111111111111111',
- status: CONST.TRANSACTION.STATUS.POSTED,
- tag: '',
- hasEReceipt: true,
-};
-
-const violations: TransactionViolations = [
- {
- name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION,
- type: CONST.VIOLATION_TYPES.VIOLATION,
- showInReview: true,
- },
- {
- name: CONST.VIOLATIONS.MISSING_CATEGORY,
- type: CONST.VIOLATION_TYPES.VIOLATION,
- showInReview: true,
- },
- {
- name: CONST.VIOLATIONS.FIELD_REQUIRED,
- type: CONST.VIOLATION_TYPES.VIOLATION,
- showInReview: true,
- },
-];
-
-const originalMessage: OriginalMessageIOU = {
- IOUReportID: '1111111111111111',
- IOUTransactionID: '590639150582440369',
- amount,
- comment: '',
- currency,
- lastModified: '2025-02-14 08:12:05.165',
- participantAccountIDs: [11111111, 22222222],
- type: 'create',
-};
-
-const fakeOriginalMessage456: OriginalMessageIOU = {
- IOUReportID: 'rep456',
- IOUTransactionID: 'trsx456',
- amount,
- comment: '',
- currency,
- lastModified: '2025-02-20 08:10:05.165',
- participantAccountIDs: [11111111, 22222222],
- type: 'create',
-};
-
-const action: ReportAction = {
- reportActionID: '1111111111111111111',
- message: [
- {
- type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
- html: '$0.01 expense',
- text: '$0.01 expense',
- isEdited: false,
- whisperedTo: [],
- isDeletedParentAction: false,
- deleted: '',
- },
- ],
- actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
- originalMessage,
- childReportID: REPORT_ID_111,
- childReportName: 'Expense #123456789',
- created: '2025-02-14 08:12:05.165',
- actorAccountID: 11111111,
- childType: 'chat',
- person: [
- {
- type: 'TEXT',
- style: 'strong',
- text: 'John Smith',
- },
- ],
-};
-
-const receiptErrors: ReceiptErrors = {
- '1201421': {
- source: 'fake',
- filename: 'fake',
- action: 'fake',
- retryParams: {
- transactionID: '1111111111111111111',
- source: 'fake',
- },
- },
-};
-
-/* eslint-enable @typescript-eslint/naming-convention */
-
-const fakeAction456: ReportAction = {
- reportActionID: 'ra456',
- message: [
- {
- type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
- html: '$0.01 expense',
- text: '$0.01 expense',
- isEdited: false,
- whisperedTo: [],
- isDeletedParentAction: false,
- deleted: '',
- },
- ],
- actionName: 'IOU',
- originalMessage: fakeOriginalMessage456,
- childReportID: REPORT_ID_456,
- created: '2025-02-14 08:12:05.165',
- actorAccountID: 11111111,
- childType: 'chat',
- person: [
- {
- type: 'TEXT',
- style: 'strong',
- text: 'John Smith',
- },
- ],
-};
-
-export {personalDetails, iouReport, chatReport, transaction, violations, action, fakeAction456, fakeTransaction456, receiptErrors};
diff --git a/tests/ui/MoneyRequestReportPreview.test.tsx b/tests/ui/MoneyRequestReportPreview.test.tsx
index 925dc3d64f4bb..f8be907b2d43c 100644
--- a/tests/ui/MoneyRequestReportPreview.test.tsx
+++ b/tests/ui/MoneyRequestReportPreview.test.tsx
@@ -19,19 +19,16 @@ import CONST from '@src/CONST';
import * as ReportActionUtils from '@src/libs/ReportActionsUtils';
import * as ReportUtils from '@src/libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
-import {
- action as mockAction,
- chatReport as mockChatReport,
- iouReport as mockIOUReport,
- transaction as mockTransaction,
- violations as mockViolations,
-} from '@src/stories/mockData/transactions';
import type {Report, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
+import {actionR14932 as mockAction} from '../../__mocks__/reportData/actions';
+import {chatReportR14932 as mockChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports';
+import {transactionR14932 as mockTransaction} from '../../__mocks__/reportData/transactions';
+import {violationsR14932 as mockViolations} from '../../__mocks__/reportData/violations';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
-const mockSecondTransactionID = `${mockTransaction.transactionID.substring(0, mockTransaction.transactionID.length - 1)}2`;
+const mockSecondTransactionID = `${mockTransaction.transactionID}2`;
jest.mock('@react-navigation/native');
diff --git a/tests/unit/libs/MoneyRequestReportUtils.ts b/tests/unit/libs/MoneyRequestReportUtils.ts
index b2ad6b80c476c..c8eafb6c8f935 100644
--- a/tests/unit/libs/MoneyRequestReportUtils.ts
+++ b/tests/unit/libs/MoneyRequestReportUtils.ts
@@ -1,30 +1,31 @@
import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils';
-import {fakeAction456, action as fakeReportAction, transaction as fakeTransaction, fakeTransaction456} from '@src/stories/mockData/transactions';
import type {ReportAction, Transaction} from '@src/types/onyx';
+import {actionR14932, actionR98765} from '../../../__mocks__/reportData/actions';
+import {transactionR14932, transactionR98765} from '../../../__mocks__/reportData/transactions';
describe('getThreadReportIDsForTransactions', () => {
test('returns empty list for no transactions', () => {
- const result = getThreadReportIDsForTransactions([fakeReportAction], []);
+ const result = getThreadReportIDsForTransactions([actionR14932], []);
expect(result).toEqual([]);
});
test('returns empty list for transactions but no reportActions', () => {
- const result = getThreadReportIDsForTransactions([], [fakeTransaction]);
+ const result = getThreadReportIDsForTransactions([], [transactionR14932]);
expect(result).toEqual([]);
});
test('returns list of reportIDs for transactions which have matching reportActions', () => {
- const reportActions = [fakeReportAction, fakeAction456] satisfies ReportAction[];
- const transactions = [{...fakeTransaction, transactionID: '590639150582440369'}, {...fakeTransaction456}] satisfies Transaction[];
+ const reportActions = [actionR14932, actionR98765] satisfies ReportAction[];
+ const transactions = [{...transactionR14932}, {...transactionR98765}] satisfies Transaction[];
const result = getThreadReportIDsForTransactions(reportActions, transactions);
- expect(result).toEqual(['1111111111111111', 'R98765']);
+ expect(result).toEqual(['CHILD_REPORT_ID_R14932', 'CHILD_REPORT_ID_R98765']);
});
test('returns empty list for transactions which have no matching reportActions', () => {
// fakeAction456 has originalMessage with undefined id, so cannot be mapped
- const reportActions = [{...fakeAction456, originalMessage: {}}] satisfies ReportAction[];
- const transactions = [{...fakeTransaction, transactionID: '590639150582440369'}, {...fakeTransaction456}] satisfies Transaction[];
+ const reportActions = [{...actionR98765, originalMessage: {}}] satisfies ReportAction[];
+ const transactions = [{...transactionR14932}, {...transactionR98765}] satisfies Transaction[];
const result = getThreadReportIDsForTransactions(reportActions, transactions);
expect(result).toEqual([]);