Skip to content

Commit 7e83182

Browse files
committed
Merge branch 'main' into korytko/generalise-search-context
2 parents e1a2296 + 687c41b commit 7e83182

7 files changed

Lines changed: 113 additions & 17 deletions

File tree

__mocks__/reportData/actions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const REPORT_R14932 = {
1919
childReportID: 'CHILD_REPORT_ID_R14932',
2020
};
2121

22-
const originalMessageR14932: OriginalMessageIOU = {
22+
const originalMessageR14932 = {
2323
currency,
2424
amount,
2525
IOUReportID: REPORT_R14932.IOUReportID,
@@ -28,7 +28,7 @@ const originalMessageR14932: OriginalMessageIOU = {
2828
type: CONST.IOU.TYPE.CREATE,
2929
lastModified: '2025-02-14 08:12:05.165',
3030
comment: '',
31-
};
31+
} satisfies OriginalMessageIOU;
3232

3333
const message = [
3434
{
@@ -50,7 +50,7 @@ const person = [
5050
},
5151
];
5252

53-
const actionR14932: ReportAction = {
53+
const actionR14932: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> = {
5454
person,
5555
message,
5656
reportActionID: REPORT_R14932.reportActionID,
@@ -86,4 +86,4 @@ const actionR98765: ReportAction = {
8686
created: '2025-02-14 08:12:05.165',
8787
};
8888

89-
export {actionR14932, actionR98765};
89+
export {actionR14932, actionR98765, originalMessageR14932};

src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
isConsecutiveChronosAutomaticTimerAction,
4141
isCurrentActionUnread,
4242
isDeletedParentAction,
43+
isIOUActionMatchingTransactionList,
4344
shouldReportActionBeVisible,
4445
wasMessageReceivedWhileOffline,
4546
} from '@libs/ReportActionsUtils';
@@ -123,6 +124,7 @@ function MoneyRequestReportActionsList({
123124
const [isVisible, setIsVisible] = useState(Visibility.isVisible);
124125
const isFocused = useIsFocused();
125126
const route = useRoute<PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
127+
const reportTransactionIDs = transactions.map((transaction) => transaction.transactionID);
126128

127129
const reportID = report?.reportID;
128130
const linkedReportActionID = route?.params?.reportActionID;
@@ -134,7 +136,7 @@ function MoneyRequestReportActionsList({
134136
});
135137

136138
const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]);
137-
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false);
139+
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false, reportTransactionIDs);
138140
const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]);
139141
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
140142
const [currentUserAccountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (session) => session?.accountID});
@@ -164,12 +166,13 @@ function MoneyRequestReportActionsList({
164166
return (
165167
isActionVisibleOnMoneyReport &&
166168
(isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) &&
167-
shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction)
169+
shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) &&
170+
isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)
168171
);
169172
});
170173

171174
return filteredActions.toReversed();
172-
}, [reportActions, isOffline, canPerformWriteAction]);
175+
}, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs]);
173176

174177
const reportActionSize = useRef(visibleReportActions.length);
175178
const lastAction = visibleReportActions.at(-1);

src/libs/ReportActionsUtils.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import CONST from '@src/CONST';
1010
import type {TranslationPaths} from '@src/languages/types';
1111
import ONYXKEYS from '@src/ONYXKEYS';
1212
import ROUTES from '@src/ROUTES';
13-
import type {Card, Locale, OnyxInputOrEntry, PrivatePersonalDetails} from '@src/types/onyx';
13+
import type {Card, Locale, OnyxInputOrEntry, OriginalMessageIOU, PrivatePersonalDetails} from '@src/types/onyx';
1414
import type {JoinWorkspaceResolution, OriginalMessageChangeLog, OriginalMessageExportIntegration} from '@src/types/onyx/OriginalMessage';
1515
import type {PolicyReportFieldType} from '@src/types/onyx/Policy';
1616
import type Report from '@src/types/onyx/Report';
@@ -1185,6 +1185,37 @@ function isTagModificationAction(actionName: string): boolean {
11851185
);
11861186
}
11871187

1188+
/** Whether action has no linked report by design */
1189+
const isIOUActionTypeExcludedFromFiltering = (type: OriginalMessageIOU['type'] | undefined) =>
1190+
[CONST.IOU.REPORT_ACTION_TYPE.SPLIT, CONST.IOU.REPORT_ACTION_TYPE.TRACK, CONST.IOU.REPORT_ACTION_TYPE.PAY].some((actionType) => actionType === type);
1191+
1192+
/**
1193+
* Determines whether the given action is an IOU and, if a list of report transaction IDs is provided,
1194+
* whether it corresponds to one of those transactions. This covers a rare case where IOU report actions was
1195+
* not deleted or moved after the expense was removed from the report.
1196+
*
1197+
* For compatibility and to avoid using isMoneyRequest next to this function as it is checked here already:
1198+
* - If the action is not a money request and `defaultToFalseForNonIOU` is false (default), the result is true.
1199+
* - If no `reportTransactionIDs` are provided, the function returns true if the action is an IOU.
1200+
* - If `reportTransactionIDs` are provided, the function checks if the IOU transaction ID from the action matches any of them.
1201+
*/
1202+
const isIOUActionMatchingTransactionList = (
1203+
action: ReportAction,
1204+
reportTransactionIDs?: string[],
1205+
defaultToFalseForNonIOU = false,
1206+
): action is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> => {
1207+
if (!isMoneyRequestAction(action)) {
1208+
return !defaultToFalseForNonIOU;
1209+
}
1210+
1211+
if (isIOUActionTypeExcludedFromFiltering(getOriginalMessage(action)?.type) || reportTransactionIDs === undefined) {
1212+
return true;
1213+
}
1214+
1215+
const {IOUTransactionID} = getOriginalMessage(action) ?? {};
1216+
return !!IOUTransactionID && reportTransactionIDs.includes(IOUTransactionID);
1217+
};
1218+
11881219
/**
11891220
* Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
11901221
* Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
@@ -1193,6 +1224,7 @@ function getOneTransactionThreadReportID(
11931224
reportID: string | undefined,
11941225
reportActions: OnyxEntry<ReportActions> | ReportAction[],
11951226
isOffline: boolean | undefined = undefined,
1227+
reportTransactionIDs?: string[],
11961228
): string | undefined {
11971229
// If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report.
11981230
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
@@ -1207,7 +1239,7 @@ function getOneTransactionThreadReportID(
12071239

12081240
const iouRequestActions = [];
12091241
for (const action of reportActionsArray) {
1210-
if (!isMoneyRequestAction(action)) {
1242+
if (!isIOUActionMatchingTransactionList(action, reportTransactionIDs, true)) {
12111243
// eslint-disable-next-line no-continue
12121244
continue;
12131245
}
@@ -2529,6 +2561,7 @@ export {
25292561
isForwardedAction,
25302562
isWhisperActionTargetedToOthers,
25312563
isTagModificationAction,
2564+
isIOUActionMatchingTransactionList,
25322565
isResolvedActionableWhisper,
25332566
shouldHideNewMarker,
25342567
shouldReportActionBeVisible,

src/pages/home/ReportScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
303303
selector: (allTransactions): OnyxTypes.Transaction[] => selectAllTransactionsForReport(allTransactions, reportIDFromRoute, reportActions),
304304
canBeMissing: false,
305305
});
306-
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], isOffline);
306+
const reportTransactionIDs = reportTransactions?.map((transaction) => transaction.transactionID);
307+
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], isOffline, reportTransactionIDs);
307308
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
308309
const [transactionThreadReportActions = {}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, {canBeMissing: true});
309310
const combinedReportActions = getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions));

src/pages/home/report/ReportActionsView.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {useIsFocused, useRoute} from '@react-navigation/native';
22
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
33
import {InteractionManager} from 'react-native';
4-
import type {OnyxEntry} from 'react-native-onyx';
4+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
55
import {useOnyx} from 'react-native-onyx';
66
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
77
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
@@ -13,6 +13,7 @@ import {updateLoadingInitialReportAction} from '@libs/actions/Report';
1313
import Timing from '@libs/actions/Timing';
1414
import DateUtils from '@libs/DateUtils';
1515
import getIsReportFullyVisible from '@libs/getIsReportFullyVisible';
16+
import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
1617
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
1718
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
1819
import {generateNewRandomInt, rand64} from '@libs/NumberUtils';
@@ -25,6 +26,7 @@ import {
2526
getSortedReportActionsForDisplay,
2627
isCreatedAction,
2728
isDeletedParentAction,
29+
isIOUActionMatchingTransactionList,
2830
isMoneyRequestAction,
2931
shouldReportActionBeVisible,
3032
} from '@libs/ReportActionsUtils';
@@ -93,6 +95,11 @@ function ReportActionsView({
9395
const prevShouldUseNarrowLayoutRef = useRef(shouldUseNarrowLayout);
9496
const reportID = report.reportID;
9597
const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]);
98+
const [reportTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
99+
selector: (allTransactions: OnyxCollection<OnyxTypes.Transaction>) =>
100+
selectAllTransactionsForReport(allTransactions, reportID, allReportActions ?? []).map((transaction) => transaction.transactionID),
101+
canBeMissing: true,
102+
});
96103

97104
useEffect(() => {
98105
// When we linked to message - we do not need to wait for initial actions - they already exists
@@ -192,9 +199,10 @@ function ReportActionsView({
192199
reportActions.filter(
193200
(reportAction) =>
194201
(isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) &&
195-
shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction),
202+
shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) &&
203+
isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs),
196204
),
197-
[reportActions, isOffline, canPerformWriteAction],
205+
[reportActions, isOffline, canPerformWriteAction, reportTransactionIDs],
198206
);
199207

200208
const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]);

tests/unit/ReportActionsUtilsTest.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type {KeyValueMapping} from 'react-native-onyx';
22
import Onyx from 'react-native-onyx';
33
import {isExpenseReport} from '@libs/ReportUtils';
4+
import {actionR14932 as mockIOUAction, originalMessageR14932 as mockOriginalMessage} from '../../__mocks__/reportData/actions';
45
import CONST from '../../src/CONST';
56
import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils';
7+
import {isIOUActionMatchingTransactionList} from '../../src/libs/ReportActionsUtils';
68
import ONYXKEYS from '../../src/ONYXKEYS';
79
import type {Report, ReportAction} from '../../src/types/onyx';
810
import createRandomReport from '../utils/collections/reports';
@@ -301,6 +303,56 @@ describe('ReportActionsUtils', () => {
301303
});
302304
});
303305

306+
describe('isIOUActionMatchingTransactionList', () => {
307+
const nonIOUAction = {
308+
created: '2022-11-13 22:27:01.825',
309+
reportActionID: '8401445780099176',
310+
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
311+
originalMessage: {
312+
html: 'Hello world',
313+
whisperedTo: [],
314+
},
315+
message: [
316+
{
317+
html: 'Hello world',
318+
type: 'Action type',
319+
text: 'Action text',
320+
},
321+
],
322+
};
323+
324+
it('returns false for non-money request actions when defaultToFalseForNonIOU is true', () => {
325+
expect(isIOUActionMatchingTransactionList(nonIOUAction, undefined, true)).toBeFalsy();
326+
});
327+
328+
it('returns true for non-money request actions when defaultToFalseForNonIOU is false', () => {
329+
expect(isIOUActionMatchingTransactionList(nonIOUAction, undefined, false)).toBeTruthy();
330+
});
331+
332+
it('returns true if no reportTransactionIDs are provided', () => {
333+
expect(isIOUActionMatchingTransactionList(mockIOUAction)).toBeTruthy();
334+
});
335+
336+
it('returns true if action is of excluded type', () => {
337+
const action = {
338+
...mockIOUAction,
339+
originalMessage: {
340+
...mockOriginalMessage,
341+
type: CONST.IOU.REPORT_ACTION_TYPE.TRACK,
342+
},
343+
};
344+
expect(isIOUActionMatchingTransactionList(action, ['124', '125', '126'])).toBeTruthy();
345+
});
346+
347+
it('returns true if IOUTransactionID matches any provided reportTransactionIDs', () => {
348+
expect(isIOUActionMatchingTransactionList(mockIOUAction, ['123', '124', mockOriginalMessage.IOUTransactionID])).toBeTruthy();
349+
});
350+
351+
it('returns false if IOUTransactionID does not match any provided reportTransactionIDs', () => {
352+
expect(isIOUActionMatchingTransactionList(mockIOUAction, ['123', '124'])).toBeFalsy();
353+
});
354+
});
355+
304356
describe('getSortedReportActionsForDisplay', () => {
305357
it('should filter out non-whitelisted actions', () => {
306358
const input: ReportAction[] = [

tests/unit/ReportSecondaryActionUtilsTest.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import Onyx from 'react-native-onyx';
22
import {getSecondaryReportActions, getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionUtils';
33
import CONST from '@src/CONST';
44
import * as ReportActionsUtils from '@src/libs/ReportActionsUtils';
5-
import {getOriginalMessage} from '@src/libs/ReportActionsUtils';
65
import * as ReportUtils from '@src/libs/ReportUtils';
76
import ONYXKEYS from '@src/ONYXKEYS';
8-
import type {OriginalMessageIOU, Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
9-
import {actionR14932} from '../../__mocks__/reportData/actions';
7+
import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
8+
import {actionR14932, originalMessageR14932} from '../../__mocks__/reportData/actions';
109

1110
const EMPLOYEE_ACCOUNT_ID = 1;
1211
const EMPLOYEE_EMAIL = 'employee@mail.com';
@@ -400,7 +399,7 @@ describe('getSecondaryAction', () => {
400399
const policy = {} as unknown as Policy;
401400

402401
jest.spyOn(ReportUtils, 'canHoldUnholdReportAction').mockReturnValueOnce({canHoldRequest: true, canUnholdRequest: true});
403-
jest.spyOn(ReportActionsUtils, 'getOneTransactionThreadReportID').mockReturnValueOnce((getOriginalMessage(actionR14932) as OriginalMessageIOU).IOUTransactionID);
402+
jest.spyOn(ReportActionsUtils, 'getOneTransactionThreadReportID').mockReturnValueOnce(originalMessageR14932.IOUTransactionID);
404403
const result = getSecondaryReportActions(report, [transaction], {}, policy, undefined, [actionR14932]);
405404
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.HOLD)).toBe(true);
406405
});

0 commit comments

Comments
 (0)