Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix - Search - Expense from the list does not disappear after deletion #54407

36 changes: 24 additions & 12 deletions src/hooks/useSearchHighlightAndScroll.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isEqual from 'lodash/isEqual';
import {useCallback, useEffect, useRef, useState} from 'react';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {SearchQueryJSON} from '@components/Search/types';
Expand Down Expand Up @@ -26,6 +27,7 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
// Ref to track if the search was triggered by this hook
const triggeredByHookRef = useRef(false);
const searchTriggeredRef = useRef(false);
const hasNewItemsRef = useRef(false);
const previousSearchResults = usePrevious(searchResults?.data);
const [newSearchResultKey, setNewSearchResultKey] = useState<string | null>(null);
const highlightedIDs = useRef<Set<string>>(new Set());
Expand All @@ -34,20 +36,29 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans

// Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types.
useEffect(() => {
const previousTransactionsLength = previousTransactions && Object.keys(previousTransactions).length;
const transactionsLength = transactions && Object.keys(transactions).length;
const previousTransactionsIDs = Object.keys(previousTransactions ?? {});
const transactionsIDs = Object.keys(transactions ?? {});

const reportActionsLength = reportActions && Object.values(reportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0);
const prevReportActionsLength = previousReportActions && Object.values(previousReportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0);
// Return early if search was already triggered or there's no change in current and previous data length
if (searchTriggeredRef.current || (!isChat && previousTransactionsLength === transactionsLength) || (isChat && reportActionsLength === prevReportActionsLength)) {
const reportActionsIDs = Object.values(reportActions ?? {})
.map((actions) => Object.keys(actions ?? {}))
.flat();
const previousReportActionsIDs = Object.values(previousReportActions ?? {})
.map((actions) => Object.keys(actions ?? {}))
.flat();

if (searchTriggeredRef.current) {
return;
}
const newTransactionAdded = transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength;
const newReportActionAdded = reportActionsLength && typeof prevReportActionsLength === 'number' && reportActionsLength > prevReportActionsLength;
const hasTransactionsIDsChange = !isEqual(transactionsIDs, previousTransactionsIDs);
const hasReportActionsIDsChange = !isEqual(reportActionsIDs, previousReportActionsIDs);

// Check if there is a change in the transactions or report actions list
if ((!isChat && hasTransactionsIDsChange) || (isChat && hasReportActionsIDsChange)) {
// We only want to highlight new items if the addition of transactions or report actions triggered the search.
// This is because, on deletion of items, the backend sometimes returns old items in place of the deleted ones.
// We don't want to highlight these old items, even if they appear new in the current search results.
hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length;

// Check if a new transaction or report action was added
if ((!isChat && !!newTransactionAdded) || (isChat && !!newReportActionAdded)) {
// Set the flag indicating the search is triggered by the hook
triggeredByHookRef.current = true;

Expand Down Expand Up @@ -87,7 +98,7 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
// Find new report action IDs that are not in the previousReportActionIDs and not already highlighted
const newReportActionIDs = currentReportActionIDs.filter((id) => !previousReportActionIDs.includes(id) && !highlightedIDs.current.has(id));

if (!triggeredByHookRef.current || newReportActionIDs.length === 0) {
if (!triggeredByHookRef.current || newReportActionIDs.length === 0 || !hasNewItemsRef.current) {
return;
}

Expand All @@ -103,7 +114,7 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
// Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted
const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id));

if (!triggeredByHookRef.current || newTransactionIDs.length === 0) {
if (!triggeredByHookRef.current || newTransactionIDs.length === 0 || !hasNewItemsRef.current) {
return;
}

Expand Down Expand Up @@ -216,3 +227,4 @@ function extractReportActionIDsFromSearchResults(searchResultsData: Partial<Sear
}

export default useSearchHighlightAndScroll;
export type {UseSearchHighlightAndScroll};
191 changes: 191 additions & 0 deletions tests/unit/useSearchHighlightAndScrollTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {renderHook} from '@testing-library/react-native';
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
import type {UseSearchHighlightAndScroll} from '@hooks/useSearchHighlightAndScroll';
import * as Search from '@libs/actions/Search';

jest.mock('@libs/actions/Search');

describe('useSearchHighlightAndScroll', () => {
it('should trigger Search when transactionIDs list change', () => {
const initialProps: UseSearchHighlightAndScroll = {
searchResults: {
data: {personalDetailsList: {}},
search: {
columnsToShow: {
shouldShowCategoryColumn: true,
shouldShowTagColumn: true,
shouldShowTaxColumn: true,
},
hasMoreResults: false,
hasResults: true,
offset: 0,
status: 'all',
type: 'expense',
isLoading: false,
},
},
transactions: {
transactions_1: {
amount: -100,
bank: '',
billable: false,
cardID: 0,
cardName: 'Cash Expense',
cardNumber: '',
category: '',
comment: {
comment: '',
},
created: '2025-01-08',
currency: 'ETB',
filename: 'w_c989c343d834d48a4e004c38d03c90bff9434768.png',
inserted: '2025-01-08 15:35:32',
managedCard: false,
merchant: 'g',
modifiedAmount: 0,
modifiedCreated: '',
modifiedCurrency: '',
modifiedMerchant: '',
originalAmount: 0,
originalCurrency: '',
parentTransactionID: '',
posted: '',
receipt: {
receiptID: 7409094723954473,
state: 'SCANCOMPLETE',
source: 'https://www.expensify.com/receipts/w_c989c343d834d48a4e004c38d03c90bff9434768.png',
},
reimbursable: true,
reportID: '2309609540437471',
status: 'Posted',
tag: '',
transactionID: '1',
hasEReceipt: false,
},
},
previousTransactions: {
transactions_1: {
amount: -100,
bank: '',
billable: false,
cardID: 0,
cardName: 'Cash Expense',
cardNumber: '',
category: '',
comment: {
comment: '',
},
created: '2025-01-08',
currency: 'ETB',
filename: 'w_c989c343d834d48a4e004c38d03c90bff9434768.png',
inserted: '2025-01-08 15:35:32',
managedCard: false,
merchant: 'g',
modifiedAmount: 0,
modifiedCreated: '',
modifiedCurrency: '',
modifiedMerchant: '',
originalAmount: 0,
originalCurrency: '',
parentTransactionID: '',
posted: '',
receipt: {
receiptID: 7409094723954473,
state: 'SCANCOMPLETE',
source: 'https://www.expensify.com/receipts/w_c989c343d834d48a4e004c38d03c90bff9434768.png',
},
reimbursable: true,
reportID: '2309609540437471',
status: 'Posted',
tag: '',
transactionID: '1',
hasEReceipt: false,
},
},
reportActions: {
reportActions_209647397999267: {
1: {
actionName: 'POLICYCHANGELOG_CORPORATE_UPGRADE',
reportActionID: '1',
created: '',
},
},
},
previousReportActions: {
reportActions_209647397999267: {
1: {
actionName: 'POLICYCHANGELOG_CORPORATE_UPGRADE',
reportActionID: '1',
created: '',
},
},
},
queryJSON: {
type: 'expense',
status: 'all',
sortBy: 'date',
sortOrder: 'desc',
filters: {operator: 'and', left: 'tag', right: ''},
inputQuery: 'type:expense status:all sortBy:date sortOrder:desc',
flatFilters: [],
hash: 243428839,
recentSearchHash: 422547233,
},
offset: 0,
};
const changedProp: UseSearchHighlightAndScroll = {
...initialProps,
transactions: {
transactions_2: {
amount: -100,
bank: '',
billable: false,
cardID: 0,
cardName: 'Cash Expense',
cardNumber: '',
category: '',
comment: {
comment: '',
},
created: '2025-01-08',
currency: 'ETB',
filename: 'w_c989c343d834d48a4e004c38d03c90bff9434768.png',
inserted: '2025-01-08 15:35:32',
managedCard: false,
merchant: 'g',
modifiedAmount: 0,
modifiedCreated: '',
modifiedCurrency: '',
modifiedMerchant: '',
originalAmount: 0,
originalCurrency: '',
parentTransactionID: '',
posted: '',
receipt: {
receiptID: 7409094723954473,
state: 'SCANCOMPLETE',
source: 'https://www.expensify.com/receipts/w_c989c343d834d48a4e004c38d03c90bff9434768.png',
},
reimbursable: true,
reportID: '2309609540437471',
status: 'Posted',
tag: '',
transactionID: '2',
hasEReceipt: false,
},
},
};

const {rerender} = renderHook((prop: UseSearchHighlightAndScroll) => useSearchHighlightAndScroll(prop), {
initialProps,
});
expect(Search.search).not.toHaveBeenCalled();

// When the transaction ids list change though it has the same length as previous value
rerender(changedProp);

// Then Search will be triggerred.
expect(Search.search).toHaveBeenCalled();
});
});
Loading