Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1465,8 +1465,8 @@ function MoneyReportHeader({
}

const result = await showConfirmModal({
title: translate('iou.deleteReport'),
prompt: translate('iou.deleteReportConfirmation'),
title: translate('iou.deleteReport', {count: 1}),
prompt: translate('iou.deleteReportConfirmation', {count: 1}),
confirmText: translate('common.delete'),
cancelText: translate('common.cancel'),
danger: true,
Expand All @@ -1480,7 +1480,7 @@ function MoneyReportHeader({
Navigation.goBack(backToRoute);
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
deleteAppReport(moneyRequestReport?.reportID, email ?? '', accountID, reportTransactions, allTransactionViolations, bankAccountList);
deleteAppReport(moneyRequestReport?.reportID, email ?? '', accountID, reportTransactions, allTransactionViolations, bankAccountList, currentSearchHash);
});
});
},
Expand Down
10 changes: 9 additions & 1 deletion src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,15 @@ function SearchContextProvider({children}: ChildrenProps) {

if (data.length && data.every(isTransactionReportGroupListItemType)) {
selectedReports = data
.filter((item) => isMoneyRequestReport(item) && item.transactions.length > 0 && item.transactions.every(({keyForList}) => selectedTransactions[keyForList]?.isSelected))
.filter((item) => {
if (!isMoneyRequestReport(item)) {
return false;
}
if (item.transactions.length === 0) {
return !!item.keyForList && selectedTransactions[item.keyForList]?.isSelected;
}
return item.transactions.every(({keyForList}) => selectedTransactions[keyForList]?.isSelected);
})
.map(({reportID, action = CONST.SEARCH.ACTION_TYPES.VIEW, total = CONST.DEFAULT_NUMBER_ID, policyID, allActions = [action], currency, chatReportID}) => ({
reportID,
action,
Expand Down
74 changes: 56 additions & 18 deletions src/components/Search/SearchList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import DateUtils from '@libs/DateUtils';
import navigationRef from '@libs/Navigation/navigationRef';
import {getTableMinWidth} from '@libs/SearchUIUtils';
import {getTableMinWidth, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import type {TransactionPreviewData} from '@userActions/Search';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -239,16 +239,51 @@ function SearchList({
}
return data;
}, [data, groupBy, type]);
const flattenedItemsWithoutPendingDelete = useMemo(() => flattenedItems.filter((t) => t?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [flattenedItems]);
const emptyReports = useMemo(() => {
if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
return data.filter((item) => item.transactions.length === 0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ PERF-13 (docs)

The isTransactionGroupListItemArray(data) type guard is called inside both the emptyReports and selectedItemsLength useMemo blocks. This type check does not depend on the iterator and produces the same result on every iteration.

Suggested fix: Hoist the type check outside the reduce/filter operations:

const emptyReports = useMemo(() => {
    if (type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
        return [];
    }
    if (!isTransactionGroupListItemArray(data)) {
        return [];
    }
    return data.filter((item) => item.transactions.length === 0);
}, [data, type]);

const selectedItemsLength = useMemo(() => {
    const selectedTransactionsCount = flattenedItems.reduce((acc, item) => {
        const isTransactionSelected = !!(item?.keyForList && selectedTransactions[item.keyForList]?.isSelected);
        return acc + (isTransactionSelected ? 1 : 0);
    }, 0);

    const isExpenseReportType = type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
    const isGroupedData = isTransactionGroupListItemArray(data);
    
    if (isExpenseReportType && isGroupedData) {
        const selectedEmptyReports = emptyReports.reduce((acc, item) => {
            const isEmptyReportSelected = !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected);
            return acc + (isEmptyReportSelected ? 1 : 0);
        }, 0);
        return selectedEmptyReports + selectedTransactionsCount;
    }

    return selectedTransactionsCount;
}, [flattenedItems, type, data, emptyReports, selectedTransactions]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return [];
}, [data, type]);

const selectedItemsLength = useMemo(() => {
return flattenedItemsWithoutPendingDelete.reduce((acc, item) => {
if (item.keyForList && selectedTransactions[item.keyForList]?.isSelected) {
return acc + 1;
}
return acc;
const selectedTransactionsCount = flattenedItems.reduce((acc, item) => {
const isTransactionSelected = !!(item?.keyForList && selectedTransactions[item.keyForList]?.isSelected);
return acc + (isTransactionSelected ? 1 : 0);
}, 0);
}, [flattenedItemsWithoutPendingDelete, selectedTransactions]);

if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
const selectedEmptyReports = emptyReports.reduce((acc, item) => {
const isEmptyReportSelected = !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected);
return acc + (isEmptyReportSelected ? 1 : 0);
}, 0);

return selectedEmptyReports + selectedTransactionsCount;
}

return selectedTransactionsCount;
}, [flattenedItems, type, data, emptyReports, selectedTransactions]);

const totalItems = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ PERF-13 (docs)

The isTransactionGroupListItemArray(data) type guard is called inside the totalItems useMemo and does not depend on iteration. Additionally, the type check and filtering logic are duplicated for both branches.

Suggested fix: Hoist the type check and consolidate the filtering logic:

const totalItems = useMemo(() => {
    const isExpenseReportType = type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
    const isGroupedData = isTransactionGroupListItemArray(data);
    
    const selectableTransactions = flattenedItems.filter((item) => {
        if ('pendingAction' in item) {
            return item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
        }
        return true;
    });
    
    if (isExpenseReportType && isGroupedData) {
        const selectableEmptyReports = emptyReports.filter((item) => 
            item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
        );
        return selectableEmptyReports.length + selectableTransactions.length;
    }
    
    return selectableTransactions.length;
}, [data, type, flattenedItems, emptyReports]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
const selectableEmptyReports = emptyReports.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const selectableTransactions = flattenedItems.filter((item) => {
if ('pendingAction' in item) {
return item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
return true;
});
return selectableEmptyReports.length + selectableTransactions.length;
}

const selectableTransactions = flattenedItems.filter((item) => {
if ('pendingAction' in item) {
return item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
return true;
});
return selectableTransactions.length;
}, [data, type, flattenedItems, emptyReports]);

const itemsWithSelection = useMemo(() => {
return data.map((item) => {
Expand All @@ -259,10 +294,16 @@ function SearchList({
if (!canSelectMultiple) {
itemWithSelection = {...item, isSelected: false};
} else {
const hasAnySelected = item.transactions.some((t) => t.keyForList && selectedTransactions[t.keyForList]?.isSelected);
const isEmptyReportSelected =
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ PERF-13 (docs)

The isTransactionReportGroupListItemType(item) type guard is called inside the .map() callback for every item. This type check could be hoisted outside the conditional check.

Suggested fix: Calculate the type check once per item to improve clarity:

const itemsWithSelection = useMemo(() => {
    return data.map((item) => {
        let isSelected = false;
        let itemWithSelection: SearchListItem = item;

        if ('transactions' in item && item.transactions) {
            if (\!canSelectMultiple) {
                itemWithSelection = {...item, isSelected: false};
            } else {
                const isEmpty = item.transactions.length === 0;
                const isReportType = isTransactionReportGroupListItemType(item);
                const isEmptyReportSelected = isEmpty && isReportType && \!\!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected);

                const hasAnySelected = item.transactions.some((t) => t.keyForList && selectedTransactions[t.keyForList]?.isSelected) || isEmptyReportSelected;

                if (\!hasAnySelected) {
                    itemWithSelection = {...item, isSelected: false};
                } else if (isEmptyReportSelected) {
                    isSelected = true;
                    itemWithSelection = {...item, isSelected};
                } else {
                    // ... rest of the logic
                }
            }
        } else {
            isSelected = \!\!(canSelectMultiple && item.keyForList && selectedTransactions[item.keyForList]?.isSelected);
            itemWithSelection = {...item, isSelected};
        }

        return {originalItem: item, itemWithSelection, isSelected};
    });
}, [data, canSelectMultiple, selectedTransactions]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

item.transactions.length === 0 && isTransactionReportGroupListItemType(item) && !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected);

const hasAnySelected = item.transactions.some((t) => t.keyForList && selectedTransactions[t.keyForList]?.isSelected) || isEmptyReportSelected;

if (!hasAnySelected) {
itemWithSelection = {...item, isSelected: false};
} else if (isEmptyReportSelected) {
isSelected = true;
itemWithSelection = {...item, isSelected};
} else {
let allNonDeletedSelected = true;
let hasNonDeletedTransactions = false;
Expand Down Expand Up @@ -351,13 +392,10 @@ function SearchList({
}

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (shouldPreventLongPressRow || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox) {
return;
}
// disable long press for empty expense reports
if ('transactions' in item && item.transactions.length === 0 && !groupBy) {
if (shouldPreventLongPressRow || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return;
}

if (isMobileSelectionModeEnabled) {
onCheckboxPress(item, itemTransactions);
return;
Expand All @@ -366,7 +404,7 @@ function SearchList({
setLongPressedItemTransactions(itemTransactions);
setIsModalVisible(true);
},
[groupBy, route.key, shouldPreventLongPressRow, isSmallScreenWidth, isMobileSelectionModeEnabled, onCheckboxPress],
[route.key, shouldPreventLongPressRow, isSmallScreenWidth, isMobileSelectionModeEnabled, onCheckboxPress],
);

const turnOnSelectionMode = useCallback(() => {
Expand Down Expand Up @@ -493,7 +531,7 @@ function SearchList({

const tableHeaderVisible = canSelectMultiple || !!SearchTableHeader;
const selectAllButtonVisible = canSelectMultiple && !SearchTableHeader;
const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === flattenedItemsWithoutPendingDelete.length && hasLoadedAllTransactions;
const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === totalItems && hasLoadedAllTransactions;

const content = (
<View style={[styles.flex1, !isKeyboardShown && safeAreaPaddingBottomStyle, containerStyle]}>
Expand All @@ -503,11 +541,11 @@ function SearchList({
<Checkbox
accessibilityLabel={translate('workspace.people.selectAll')}
isChecked={isSelectAllChecked}
isIndeterminate={selectedItemsLength > 0 && (selectedItemsLength !== flattenedItemsWithoutPendingDelete.length || !hasLoadedAllTransactions)}
isIndeterminate={selectedItemsLength > 0 && (selectedItemsLength !== totalItems || !hasLoadedAllTransactions)}
onPress={() => {
onAllCheckboxPress();
}}
disabled={flattenedItems.length === 0}
disabled={totalItems === 0}
/>
)}

Expand Down
Loading
Loading