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
54 changes: 30 additions & 24 deletions src/components/Search/SearchBulkActionsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {getEmptyObject} from '@src/types/utils/EmptyObject';

import {isUserValidatedSelector} from '@selectors/Account';
import React, {useContext, useMemo, useRef} from 'react';
import {View} from 'react-native';

import type {BulkPaySelectionData, SearchQueryJSON} from './types';
import type {BulkPaySelectionData, SearchQueryJSON, SelectedTransactions} from './types';

import BulkDuplicateHandler from './BulkDuplicateHandler';
import BulkDuplicateReportHandler from './BulkDuplicateReportHandler';
Expand All @@ -48,7 +49,7 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
// We need isSmallScreenWidth (not just shouldUseNarrowLayout) because DecisionModal requires it for correct modal type
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const {selectedTransactions, selectedReports, areAllMatchingItemsSelected} = useSearchSelectionContext();
const {selectedTransactions, excludedTransactions = getEmptyObject<SelectedTransactions>(), selectedReports, areAllMatchingItemsSelected} = useSearchSelectionContext();
const {currentSearchResults} = useSearchResultsContext();
const kycWallRef = useContext(KYCWallContext);
const {isAccountLocked} = useLockedAccountState();
Expand Down Expand Up @@ -99,38 +100,43 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
const pendingPaymentAdditionalDataRef = useRef<BulkPaySelectionData | undefined>(undefined);

const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {});
const isExpenseType = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE;
const isExpenseReportType = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;

const popoverUseScrollView = shouldPopoverUseScrollView(headerButtonsOptions);
const selectedItemsCount = useMemo(() => {
if (!selectedTransactions) {
return 0;
}
const {selectedItemsCount, excludedItemsCount} = useMemo(() => {
const getItemsCount = (transactionsToCount: typeof selectedTransactions) => {
if (isExpenseReportType) {
const reportIDs = new Set(
Object.values(transactionsToCount)
.map((transaction) => transaction?.reportID)
.filter((reportID): reportID is string => !!reportID),
);
return reportIDs.size;
}

if (isExpenseReportType) {
const reportIDs = new Set(
Object.values(selectedTransactions)
.map((transaction) => transaction?.reportID)
.filter((reportID): reportID is string => !!reportID),
);
return reportIDs.size;
}
return Object.keys(transactionsToCount).reduce((count, key) => {
if (key.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
const group = searchData?.[key as keyof typeof searchData] as {count?: number} | undefined;
return count + (group?.count ?? 0);
}
return count + 1;
}, 0);
};

return selectedTransactionsKeys.reduce((count, key) => {
if (key.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
const group = searchData?.[key as keyof typeof searchData] as {count?: number} | undefined;
return count + (group?.count ?? 0);
}
return count + 1;
}, 0);
}, [selectedTransactions, selectedTransactionsKeys, isExpenseReportType, searchData]);
return {
selectedItemsCount: getItemsCount(selectedTransactions),
excludedItemsCount: getItemsCount(excludedTransactions),
};
}, [excludedTransactions, selectedTransactions, isExpenseReportType, searchData]);

const allMatchingItemsCount = currentSearchResults?.search?.count;
const selectedAllMatchingItemsCount = typeof allMatchingItemsCount === 'number' ? Math.max(allMatchingItemsCount - excludedItemsCount, 0) : undefined;
const isAllMatchingItemsCountLoading = areAllMatchingItemsSelected && typeof allMatchingItemsCount !== 'number' && !isOffline && !!currentSearchResults?.search?.isLoading;
let selectionButtonText: string;
if (areAllMatchingItemsSelected) {
selectionButtonText =
typeof allMatchingItemsCount !== 'number' ? translate('search.exportAll.allMatchingItemsSelected') : translate('workspace.common.selected', {count: allMatchingItemsCount});
const count = isExpenseType ? selectedAllMatchingItemsCount : allMatchingItemsCount;
selectionButtonText = typeof count !== 'number' ? translate('search.exportAll.allMatchingItemsSelected') : translate('workspace.common.selected', {count});
} else {
selectionButtonText = translate('workspace.common.selected', {count: selectedItemsCount});
}
Expand Down
1 change: 1 addition & 0 deletions src/components/Search/SearchContextDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const defaultSearchResultsActions: SearchResultsActionsValue = {
const defaultSearchSelectionContext: SearchSelectionContextValue = {
currentSelectedTransactionReportID: undefined,
selectedTransactions: {},
excludedTransactions: {},
selectedTransactionIDs: [],
selectedReports: [],
shouldTurnOffSelectionMode: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function TransactionGroupListItemImpl({

const StyleUtils = useStyleUtils();
const {isSelected: liveRowSelected} = useRowSelection(item?.keyForList);
const isItemSelected = isSelectAllChecked || liveRowSelected;
const isItemSelected = isSelectAllChecked || (liveRowSelected && (isExpenseReportType || transactionsWithoutPendingDelete.length === 0));

const animatedHighlightStyle = useAnimatedHighlightStyle({
shouldHighlight: item?.shouldAnimateInHighlight ?? false,
Expand Down Expand Up @@ -306,7 +306,8 @@ function TransactionGroupListItemImpl({
};

const onPress = (event?: ModifiedMouseEvent) => {
if (isExpenseReportType || transactions.length === 0) {
const isEmptyGroupWithoutTransactionsQuery = transactions.length === 0 && !groupItem.transactionsQueryJSON;
if (isExpenseReportType || isEmptyGroupWithoutTransactionsQuery) {
onSelectRow(item, transactionPreviewData, event);
}
if (!isExpenseReportType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton';
import {useSearchSelectionContext} from '@components/Search/SearchContext';
import {useSelectionCounts} from '@components/Search/SearchSelectionProvider';
import type {SearchQueryJSON} from '@components/Search/types';

import useThemeStyles from '@hooks/useThemeStyles';

import CONST from '@src/CONST';
import type {SearchResults} from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';
Expand All @@ -26,12 +28,13 @@ type SearchActionsBarWideProps = {

function SearchActionsBarWide({queryJSON, searchResults, onSort}: SearchActionsBarWideProps) {
const styles = useThemeStyles();
const {hasSelectedTransactions} = useSearchSelectionContext();
const {selected} = useSelectionCounts();
const hasSelectedItems = selected > 0;
const shouldShowBulkActions = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE ? hasSelectedTransactions : selected > 0;

return (
<View style={[styles.searchActionsBarContainer]}>
{hasSelectedItems ? (
{shouldShowBulkActions ? (
<View style={styles.searchBulkActionsButton}>
<SearchBulkActionsButton queryJSON={queryJSON} />
</View>
Expand Down
44 changes: 29 additions & 15 deletions src/components/Search/SearchSelectionFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {isGroupEntry} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';
import type {SearchResults} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';

import React from 'react';

import type {SelectedTransactions} from './types';

import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionContext} from './SearchContext';
import SearchPageFooter from './SearchPageFooter';

Expand All @@ -20,13 +23,15 @@ type SearchSelectionFooterProps = {
// Self-subscribing footer leaf. Owns the `selectedTransactions` read so a checkbox press re-renders only this
// footer — not SearchPage and the <Search> list it contains.
function SearchSelectionFooter({searchResults}: SearchSelectionFooterProps) {
const {selectedTransactions, areAllMatchingItemsSelected} = useSearchSelectionContext();
const {selectedTransactions, excludedTransactions = getEmptyObject<SelectedTransactions>(), areAllMatchingItemsSelected} = useSearchSelectionContext();
const {currentSearchResults} = useSearchResultsContext();
const {currentSearchKey, currentSearchQueryJSON} = useSearchQueryContext();
const shouldAllowFooterTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true, areAllMatchingItemsSelected);

const metadata = searchResults?.search;
const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {});
const excludedTransactionsKeys = Object.keys(excludedTransactions);
const isExpenseType = currentSearchQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE;
const shouldShowFooter = (!areAllMatchingItemsSelected && selectedTransactionsKeys.length > 0) || (shouldAllowFooterTotals && !!metadata?.count);

if (!shouldShowFooter) {
Expand All @@ -36,20 +41,29 @@ function SearchSelectionFooter({searchResults}: SearchSelectionFooterProps) {
const shouldUseClientTotal = !metadata?.count || (selectedTransactionsKeys.length > 0 && !areAllMatchingItemsSelected);
const selectedTransactionItems = Object.values(selectedTransactions);
const currency = metadata?.currency ?? selectedTransactionItems.at(0)?.groupCurrency ?? selectedTransactionItems.at(0)?.currency;
const count = shouldUseClientTotal
? selectedTransactionsKeys.reduce((acc, key) => {
if (isGroupEntry(key)) {
const group = currentSearchResults?.data?.[key];
return acc + (group?.count ?? 0);
}
const item = selectedTransactions[key];
if (item.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === item.reportID) {
return acc;
}
return acc + 1;
}, 0)
: metadata?.count;
const total = shouldUseClientTotal ? selectedTransactionItems.reduce((acc, transaction) => acc - (transaction.groupAmount ?? -Math.abs(transaction.amount)), 0) : metadata?.total;
const getTransactionCount = (transactionKeys: string[], transactions: typeof selectedTransactions) => {
return transactionKeys.reduce((acc, key) => {
if (isGroupEntry(key)) {
const group = currentSearchResults?.data?.[key];
return acc + (group?.count ?? 0);
}
const item = transactions[key];
if (item.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === item.reportID) {
return acc;
}
return acc + 1;
}, 0);
};
const getTransactionTotal = (transactions: typeof selectedTransactionItems) =>
transactions.reduce((acc, transaction) => acc - (transaction.groupAmount ?? -Math.abs(transaction.amount)), 0);
const excludedCount = isExpenseType ? getTransactionCount(excludedTransactionsKeys, excludedTransactions) : 0;
const count = shouldUseClientTotal ? getTransactionCount(selectedTransactionsKeys, selectedTransactions) : Math.max((metadata?.count ?? 0) - excludedCount, 0);
let total: number | undefined;
if (shouldUseClientTotal) {
total = getTransactionTotal(selectedTransactionItems);
} else if (metadata?.total !== undefined) {
total = isExpenseType ? metadata.total - getTransactionTotal(Object.values(excludedTransactions)) : metadata.total;
}

return (
<SearchPageFooter
Expand Down
Loading
Loading