Skip to content
49 changes: 29 additions & 20 deletions src/components/SelectionList/Search/TransactionGroupListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,13 @@ function TransactionGroupListItem<TItem extends ListItem>({

useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus);

const pendingAction =
(item.pendingAction ?? groupItem.transactions.every((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should have checked if we have transactions in the first place. If we don't, this will return true even though we don't have any pending action.

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.

Yeah, thanks for pointing @s77rt. @bernhardoj already convered it in this PR #69406

? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
: undefined;

return (
<OfflineWithFeedback pendingAction={item.pendingAction}>
<OfflineWithFeedback pendingAction={pendingAction}>
<PressableWithFeedback
ref={pressableRef}
onLongPress={onLongPress}
Expand Down Expand Up @@ -204,26 +209,30 @@ function TransactionGroupListItem<TItem extends ListItem>({
</View>
) : (
groupItem.transactions.map((transaction) => (
<TransactionItemRow
<OfflineWithFeedback
key={transaction.transactionID}
report={transaction.report}
transactionItem={transaction}
isSelected={!!transaction.isSelected}
dateColumnSize={dateColumnSize}
amountColumnSize={amountColumnSize}
taxAmountColumnSize={taxAmountColumnSize}
shouldShowTooltip={showTooltip}
shouldUseNarrowLayout={!isLargeScreenWidth}
shouldShowCheckbox={!!canSelectMultiple}
onCheckboxPress={() => onCheckboxPress?.(transaction as unknown as TItem)}
columns={columns}
onButtonPress={() => {
openReportInRHP(transaction);
}}
style={[styles.noBorderRadius, shouldUseNarrowLayout ? [styles.p3, styles.pt2] : [styles.ph3, styles.pv1Half]]}
isReportItemChild
isInSingleTransactionReport={groupItem.transactions.length === 1}
/>
pendingAction={transaction.pendingAction}
>
<TransactionItemRow
report={transaction.report}
transactionItem={transaction}
isSelected={!!transaction.isSelected}
dateColumnSize={dateColumnSize}
amountColumnSize={amountColumnSize}
taxAmountColumnSize={taxAmountColumnSize}
shouldShowTooltip={showTooltip}
shouldUseNarrowLayout={!isLargeScreenWidth}
shouldShowCheckbox={!!canSelectMultiple}
onCheckboxPress={() => onCheckboxPress?.(transaction as unknown as TItem)}
columns={columns}
onButtonPress={() => {
openReportInRHP(transaction);
}}
style={[styles.noBorderRadius, shouldUseNarrowLayout ? [styles.p3, styles.pt2] : [styles.ph3, styles.pv1Half]]}
isReportItemChild
isInSingleTransactionReport={groupItem.transactions.length === 1}
/>
</OfflineWithFeedback>
))
)}
</View>
Expand Down
6 changes: 5 additions & 1 deletion src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1691,7 +1691,11 @@ function getSortedReportActionData(data: ReportActionListItemType[], localeCompa
* Checks if the search results contain any data, useful for determining if the search results are empty.
*/
function isSearchResultsEmpty(searchResults: SearchResults) {
return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION));
return !Object.keys(searchResults?.data).some(
(key) =>
key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION) &&
(searchResults?.data[key as keyof typeof searchResults.data] as SearchTransaction)?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
);
}

/**
Expand Down
27 changes: 25 additions & 2 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,31 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) {
}

function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) {
const {optimisticData, finallyData} = getOnyxLoadingData(hash);
API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData});
const {optimisticData: loadingOptimisticData, finallyData} = getOnyxLoadingData(hash);
const optimisticData: OnyxUpdate[] = [
...loadingOptimisticData,
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
value: {
data: Object.fromEntries(
transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]),
) as Partial<SearchTransaction>,
},
},
];
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
value: {
data: Object.fromEntries(
transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {pendingAction: null}]),
) as Partial<SearchTransaction>,
},
},
];
API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, failureData, finallyData});
}

type Params = Record<string, ExportSearchItemsToCSVParams>;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,11 @@ function SearchPage({route}: SearchPageProps) {
}

setIsDeleteExpensesConfirmModalVisible(false);
deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys);

// Translations copy for delete modal depends on amount of selected items,
// We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural
InteractionManager.runAfterInteractions(() => {
deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys);
clearSelectedTransactions();
});
};
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/Search/SearchUIUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,67 @@ describe('SearchUIUtils', () => {
});
});

describe('Test isSearchResultsEmpty', () => {
it('should return true when all transactions have delete pending action', () => {
const results: OnyxTypes.SearchResults = {
data: {
personalDetailsList: {},
// eslint-disable-next-line @typescript-eslint/naming-convention
transactions_1805965960759424086: {
accountID: 2074551,
amount: 0,
canDelete: false,
canHold: true,
canUnhold: false,
category: 'Employee Meals Remote (Fringe Benefit)',
action: 'approve',
allActions: ['approve'],
comment: {
comment: '',
},
created: '2025-05-26',
currency: 'USD',
hasEReceipt: false,
isFromOneTransactionReport: true,
managerID: adminAccountID,
merchant: '(none)',
modifiedAmount: -1000,
modifiedCreated: '2025-05-22',
modifiedCurrency: 'USD',
modifiedMerchant: 'Costco Wholesale',
parentTransactionID: '',
policyID: '137DA25D273F2423',
receipt: {
source: 'https://www.expensify.com/receipts/fake.jpg',
state: CONST.IOU.RECEIPT_STATE.SCAN_COMPLETE,
},
reportID: '6523565988285061',
reportType: 'expense',
tag: '',
transactionID: '1805965960759424086',
transactionThreadReportID: '4139222832581831',
transactionType: 'cash',
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
},
},
search: {
type: 'expense',
status: CONST.SEARCH.STATUS.EXPENSE.ALL,
offset: 0,
hasMoreResults: false,
hasResults: true,
isLoading: false,
columnsToShow: {
shouldShowCategoryColumn: true,
shouldShowTagColumn: true,
shouldShowTaxColumn: true,
},
},
};
expect(SearchUIUtils.isSearchResultsEmpty(results)).toBe(true);
});
});

test('Should show `View` to overlimit approver', () => {
Onyx.merge(ONYXKEYS.SESSION, {accountID: overlimitApproverAccountID});
searchResults.data[`policy_${policyID}`].role = CONST.POLICY.ROLE.USER;
Expand Down
Loading