Skip to content
Merged
21 changes: 12 additions & 9 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import Log from '@libs/Log';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import Performance from '@libs/Performance';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, selectFilteredReportActions} from '@libs/ReportUtils';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
import {
createAndOpenSearchTransactionThread,
Expand Down Expand Up @@ -121,7 +121,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outsta
amount: item.modifiedAmount ?? item.amount,
convertedAmount: item.convertedAmount,
currency: item.currency,
isFromOneTransactionReport: item.isFromOneTransactionReport,
isFromOneTransactionReport: isOneTransactionReport(item.report),
ownerAccountID: item.reportAction?.actorAccountID,
},
];
Expand Down Expand Up @@ -203,7 +203,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
convertedAmount: item.convertedAmount,
convertedCurrency: item.convertedCurrency,
currency: item.currency,
isFromOneTransactionReport: item.isFromOneTransactionReport,
isFromOneTransactionReport: isOneTransactionReport(item.report),
ownerAccountID: item.reportAction?.actorAccountID,
},
};
Expand Down Expand Up @@ -675,7 +675,7 @@ function Search({
// If we're trying to open a transaction without a transaction thread, let's create the thread and navigate the user
if (isTransactionItem && item.transactionThreadReportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
// If the report is unreported (self DM), we want to open the track expense thread instead of a report with an ID of 0
const shouldOpenTransactionThread = !item.isFromOneTransactionReport || item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const shouldOpenTransactionThread = !isOneTransactionReport(item.report) || item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
createAndOpenSearchTransactionThread(item, hash, backTo, undefined, shouldOpenTransactionThread);
if (shouldOpenTransactionThread) {
return;
Expand Down Expand Up @@ -721,12 +721,15 @@ function Search({
return;
}

const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
let reportID = item.reportID;
if (isTransactionItem && item.transactionThreadReportID !== CONST.REPORT.UNREPORTED_REPORT_ID) {
const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const isFromOneTransactionReport = isOneTransactionReport(item.report);

const reportID =
isTransactionItem && (!item.isFromOneTransactionReport || isFromSelfDM) && item.transactionThreadReportID !== CONST.REPORT.UNREPORTED_REPORT_ID
? item.transactionThreadReportID
: item.reportID;
if (isFromSelfDM || !isFromOneTransactionReport) {
reportID = item.transactionThreadReportID;
}
}

if (!reportID) {
return;
Expand Down
4 changes: 3 additions & 1 deletion src/libs/MoneyRequestReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
hasUpdatedTotal,
isInvoiceReport,
isMoneyRequestReport,
isOneTransactionReport,
isReportTransactionThread,
} from './ReportUtils';
import {isTransactionPendingDelete} from './TransactionUtils';
Expand Down Expand Up @@ -62,8 +63,9 @@ function getThreadReportIDsForTransactions(reportActions: ReportAction[], transa
*/
function getReportIDForTransaction(transactionItem: TransactionListItemType) {
const isFromSelfDM = transactionItem.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const isFromOneTransactionReport = isOneTransactionReport(transactionItem.report);

return (!transactionItem.isFromOneTransactionReport || isFromSelfDM) && transactionItem.transactionThreadReportID !== CONST.REPORT.UNREPORTED_REPORT_ID
return (!isFromOneTransactionReport || isFromSelfDM) && transactionItem.transactionThreadReportID !== CONST.REPORT.UNREPORTED_REPORT_ID
? transactionItem.transactionThreadReportID
: transactionItem.reportID;
}
Expand Down
15 changes: 13 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,7 @@
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 963 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportID = value;
Expand All @@ -968,7 +968,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 971 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -986,7 +986,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 989 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -998,14 +998,14 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 1001 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 1008 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand All @@ -1020,7 +1020,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 1023 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1058,14 +1058,14 @@
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 1061 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 1068 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1091,7 +1091,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1094 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -1104,7 +1104,7 @@

let allReportMetadata: OnyxCollection<ReportMetadata>;
const allReportMetadataKeyValue: Record<string, ReportMetadata> = {};
Onyx.connect({

Check warning on line 1107 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -2551,6 +2551,14 @@
* Checks if a report has only one transaction associated with it
*/
function isOneTransactionReport(report: OnyxEntry<Report>): boolean {
return report?.transactionCount === 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware of this existing function before. Uhmm, if we touch this function, we probably need to optimistically update transactionCount on adding/deleting/moving transactions to support offline mode.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I have added back the original function as a deprecated function and kept its use in that file. New usage should use the new function and handle the optimistic data accordingly.

}

/**
* Checks if a report has only one transaction associated with it
* @deprecated - Use isOneTransactionReport instead
*/
function isOneTransactionReportDeprecated(report: OnyxEntry<Report>): boolean {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? ([] as ReportAction[]);
const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`];
return !!getOneTransactionThreadReportID(report, chatReport, reportActions);
Expand All @@ -2560,7 +2568,8 @@
* Whether the report contains only one expense and the expense should be paid later
*/
function isPayAtEndExpenseReport(report: OnyxEntry<Report>, transactions: Transaction[] | undefined): boolean {
if ((!!transactions && transactions.length !== 1) || !isOneTransactionReport(report)) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
if ((!!transactions && transactions.length !== 1) || !isOneTransactionReportDeprecated(report)) {
return false;
}

Expand Down Expand Up @@ -3601,7 +3610,8 @@
const isManager = currentUserAccountID === report?.managerID;

// For one transaction IOUs, display a simplified report icon
if (isOneTransactionReport(report)) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (isOneTransactionReportDeprecated(report)) {
return [ownerIcon];
}

Expand Down Expand Up @@ -13056,6 +13066,7 @@
getUnresolvedCardFraudAlertAction,
shouldBlockSubmitDueToStrictPolicyRules,
isWorkspaceChat,
isOneTransactionReport,
};
export type {
Ancestor,
Expand Down
3 changes: 2 additions & 1 deletion src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
isClosedReport,
isInvoiceReport,
isMoneyRequestReport,
isOneTransactionReport,
isOpenExpenseReport,
isOpenReport,
isSettled,
Expand Down Expand Up @@ -1291,7 +1292,7 @@ function getActions(
}
// Submit/Approve/Pay can only be taken on transactions if the transaction is the only one on the report, otherwise `View` is the only option.
// If this condition is not met, return early for performance reasons
if (isTransaction && !transaction?.isFromOneTransactionReport) {
if (isTransaction && !isOneTransactionReport(report)) {
return allActions.length > 0 ? allActions : [CONST.SEARCH.ACTION_TYPES.VIEW];
}

Expand Down
6 changes: 3 additions & 3 deletions src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ type SearchReport = {
/** The status of the current report */
statusNum?: ValueOf<typeof CONST.REPORT.STATUS_NUM>;

/** Number of transactions in the report */
transactionCount?: number;

/** For expense reports, this is the total amount requested */
unheldTotal?: number;

Expand Down Expand Up @@ -248,9 +251,6 @@ type SearchTransaction = {
/** The ID of the money request reportAction associated with the transaction */
moneyRequestReportActionID?: string;

/** Whether the transaction report has only a single transaction */
isFromOneTransactionReport?: boolean;

/** Whether the transaction has violations or errors */
errors?: OnyxCommon.Errors;

Expand Down
4 changes: 2 additions & 2 deletions tests/actions/EnforceActionExportRestrictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ describe('ReportUtils', () => {
expect(ReportUtils.getReport).toBeUndefined();
});

it('does not export isOneTransactionReport', () => {
it('does not export isOneTransactionReportDeprecated', () => {
// @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
expect(ReportUtils.isOneTransactionReport).toBeUndefined();
expect(ReportUtils.isOneTransactionReportDeprecated).toBeUndefined();
});

it('does not export getPolicy', () => {
Expand Down
125 changes: 113 additions & 12 deletions tests/unit/MoneyRequestReportUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,141 @@
import type {TransactionListItemType} from '@components/SelectionListWithSections/types';
import {getReportIDForTransaction} from '@libs/MoneyRequestReportUtils';
import CONST from '@src/CONST';
import type {Policy, Report, ReportAction} from '@src/types/onyx';

const transactionItemBaseMock = {
reportID: 'report123',
transactionThreadReportID: 'thread123',
} as TransactionListItemType;
const policyBaseMock: Policy = {
id: '123456789A',
name: 'Policy',
role: 'admin',
outputCurrency: 'USD',
type: 'team',
owner: 'admin@test.com',
isPolicyExpenseChatEnabled: true,
};

const reportBaseMock: Report = {
chatReportID: '1706144653204915',
created: '2024-12-21 13:05:20',
currency: 'USD',
isWaitingOnBankAccount: false,
managerID: 100,
nonReimbursableTotal: 0,
ownerAccountID: 100,
policyID: policyBaseMock.id,
reportID: '123',
reportName: 'Expense Report #123',
stateNum: 1,
statusNum: 1,
total: -5000,
type: 'expense',
unheldTotal: -5000,
transactionCount: 5,
};

const reportActionBaseMock: ReportAction = {
accountID: 100,
actorAccountID: 100,
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
created: '2024-12-21 13:05:21',
message: [{type: 'COMMENT', html: 'IOU', text: 'IOU'}],
reportActionID: '11111111',
originalMessage: {
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
IOUTransactionID: '555',
IOUReportID: reportBaseMock.reportID,
},
reportID: reportBaseMock.reportID,
};

const transactionItemBaseMock: TransactionListItemType = {
action: 'submit',
allActions: ['submit'],
amount: -5000,
report: reportBaseMock,
policy: policyBaseMock,
reportAction: reportActionBaseMock,
holdReportAction: undefined,
canDelete: true,
cardID: undefined,
cardName: undefined,
category: '',
comment: {comment: ''},
created: '2024-12-21',
currency: 'USD',
date: '2024-12-21',
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: '',
formattedTotal: 5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
displayName: 'Admin',
login: 'admin@test.com',
},
hasEReceipt: false,
keyForList: '1',
merchant: 'Expense',
modifiedAmount: 0,
modifiedCreated: '',
modifiedCurrency: '',
modifiedMerchant: 'Expense',
parentTransactionID: '',
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
reportID: reportBaseMock.reportID,
shouldShowMerchant: true,
shouldShowYear: true,
isAmountColumnWide: false,
isTaxAmountColumnWide: false,
tag: '',
to: {
accountID: 0,
avatar: '',
displayName: undefined,
login: undefined,
},
transactionID: '1',
transactionThreadReportID: '456',
receipt: undefined,
taxAmount: undefined,
mccGroup: undefined,
modifiedMCCGroup: undefined,
moneyRequestReportActionID: '789',
errors: undefined,
filename: undefined,
violations: [],
convertedAmount: -5000,
convertedCurrency: 'USD',
};

describe('MoneyRequestReportUtils', () => {
describe('getReportIDForTransaction', () => {
it('returns transaction thread ID if its not from one transaction report', () => {
const transactionItem = {...transactionItemBaseMock};
const transactionItem: TransactionListItemType = {...transactionItemBaseMock};
const resultID = getReportIDForTransaction(transactionItem);

expect(resultID).toBe('thread123');
expect(resultID).toBe('456');
});

it('returns transaction thread ID if its from self DM', () => {
const transactionItem = {...transactionItemBaseMock, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
const transactionItem: TransactionListItemType = {...transactionItemBaseMock, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
const resultID = getReportIDForTransaction(transactionItem);

expect(resultID).toBe('thread123');
expect(resultID).toBe('456');
});

it('returns expense reportID if its from one transaction report', () => {
const transactionItem = {...transactionItemBaseMock, isFromOneTransactionReport: true};
const transactionItem: TransactionListItemType = {...transactionItemBaseMock, report: {...reportBaseMock, transactionCount: 1}};
const resultID = getReportIDForTransaction(transactionItem);

expect(resultID).toBe('report123');
expect(resultID).toBe('123');
});

it('returns reportID if transaction thread ID is 0 - unreported', () => {
const transactionItem = {...transactionItemBaseMock, transactionThreadReportID: CONST.REPORT.UNREPORTED_REPORT_ID};
const transactionItem: TransactionListItemType = {...transactionItemBaseMock, transactionThreadReportID: CONST.REPORT.UNREPORTED_REPORT_ID};
const resultID = getReportIDForTransaction(transactionItem);

expect(resultID).toBe('report123');
expect(resultID).toBe('123');
});
});
});
1 change: 1 addition & 0 deletions tests/unit/ReportUtilsGetIconsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ describe('getIcons', () => {
type: CONST.REPORT.TYPE.IOU,
ownerAccountID: 1,
managerID: 2,
transactionCount: 1,
};

// Verify report type conditions
Expand Down
Loading
Loading