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

[Search] Display ConfirmModal before deleting expenses #45143

Merged
1 change: 1 addition & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type DropdownOption<TValueType> = {
interactive?: boolean;
numberOfLinesTitle?: number;
titleStyle?: ViewStyle;
shouldCloseModalOnSelect?: boolean;
};

type ButtonWithDropdownMenuProps<TValueType> = {
Expand Down
36 changes: 35 additions & 1 deletion src/components/Search/SearchListWithHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import SelectionList from '@components/SelectionList';
import type {BaseSelectionListProps, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types';
import useLocalize from '@hooks/useLocalize';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as SearchActions from '@libs/actions/Search';
import * as SearchUtils from '@libs/SearchUtils';
import CONST from '@src/CONST';
import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults';
Expand Down Expand Up @@ -49,9 +51,31 @@ function SearchListWithHeader(
const [isModalVisible, setIsModalVisible] = useState(false);
const [longPressedItem, setLongPressedItem] = useState<TransactionListItemType | ReportListItemType | null>(null);
const [selectedItems, setSelectedItems] = useState<SelectedTransactions>({});
const [selectedItemsToDelete, setSelectedItemsToDelete] = useState<string[]>([]);
const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);

const handleOnSelectDeleteOption = (itemsToDelete: string[]) => {
setSelectedItemsToDelete(itemsToDelete);
setDeleteExpensesConfirmModalVisible(true);
};

const handleOnCancelConfirmModal = () => {
setSelectedItemsToDelete([]);
setDeleteExpensesConfirmModalVisible(false);
};

const clearSelectedItems = () => setSelectedItems({});

const handleDeleteExpenses = () => {
if (selectedItemsToDelete.length === 0) {
return;
}

clearSelectedItems();
setDeleteExpensesConfirmModalVisible(false);
SearchActions.deleteMoneyRequestOnSearch(hash, selectedItemsToDelete);
};

useEffect(() => {
clearSelectedItems();
}, [hash]);
Expand Down Expand Up @@ -151,6 +175,7 @@ function SearchListWithHeader(
clearSelectedItems={clearSelectedItems}
query={query}
hash={hash}
onSelectDeleteOption={handleOnSelectDeleteOption}
isMobileSelectionModeActive={isMobileSelectionModeActive}
setIsMobileSelectionModeActive={setIsMobileSelectionModeActive}
/>
Expand All @@ -166,7 +191,16 @@ function SearchListWithHeader(
onSelectAll={toggleAllTransactions}
isMobileSelectionModeActive={isMobileSelectionModeActive}
/>

<ConfirmModal
isVisible={deleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
onCancel={handleOnCancelConfirmModal}
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: OnModalHide is not handled here causing this issue - #45447

title={translate('iou.deleteExpense', {count: selectedItemsToDelete.length})}
prompt={translate('iou.deleteConfirmation', {count: selectedItemsToDelete.length})}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<Modal
isVisible={isModalVisible}
type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED}
Expand Down
30 changes: 19 additions & 11 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';
import type {SelectedTransactions} from './types';

type SearchHeaderProps = {
type SearchPageHeaderProps = {
query: SearchQuery;
selectedItems?: SelectedTransactions;
clearSelectedItems?: () => void;
hash: number;
onSelectDeleteOption?: (itemsToDelete: string[]) => void;
isMobileSelectionModeActive?: boolean;
setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void;
};

type SearchHeaderOptionValue = DeepValueOf<typeof CONST.SEARCH.BULK_ACTION_TYPES> | undefined;

function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchHeaderProps) {
function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, onSelectDeleteOption, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchPageHeaderProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -51,20 +52,15 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems,
return options;
}

const itemsToDelete = selectedItemsKeys.filter((id) => selectedItems[id].canDelete);
const itemsToDelete = Object.keys(selectedItems ?? {}).filter((id) => selectedItems[id].canDelete);

if (itemsToDelete.length > 0) {
options.push({
icon: Expensicons.Trashcan,
text: translate('search.bulkActions.delete'),
value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE,
onSelected: () => {
clearSelectedItems?.();
if (isMobileSelectionModeActive) {
setIsMobileSelectionModeActive?.(false);
}
SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete);
},
shouldCloseModalOnSelect: true,
onSelected: () => onSelectDeleteOption?.(itemsToDelete),
});
}

Expand Down Expand Up @@ -121,7 +117,19 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems,
}

return options;
}, [clearSelectedItems, hash, selectedItems, selectedItemsKeys, styles, theme, translate, isMobileSelectionModeActive, setIsMobileSelectionModeActive]);
}, [
selectedItemsKeys,
selectedItems,
translate,
onSelectDeleteOption,
clearSelectedItems,
isMobileSelectionModeActive,
hash,
setIsMobileSelectionModeActive,
theme.icon,
styles.colorMuted,
styles.fontWeightNormal,
]);

if (isSmallScreenWidth) {
if (isMobileSelectionModeActive) {
Expand Down
5 changes: 3 additions & 2 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
DelegateSubmitParams,
DeleteActionParams,
DeleteConfirmationParams,
DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
DistanceRateOperationsParams,
EditActionParams,
Expand Down Expand Up @@ -702,8 +703,8 @@ export default {
`${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pending` : ''
}`,
deleteExpense: 'Delete expense',
deleteConfirmation: 'Are you sure that you want to delete this expense?',
deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`,
deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`,
settledExpensify: 'Paid',
settledElsewhere: 'Paid elsewhere',
individual: 'Individual',
Expand Down
6 changes: 4 additions & 2 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
DelegateSubmitParams,
DeleteActionParams,
DeleteConfirmationParams,
DeleteExpenseTranslationParams,
DidSplitAmountMessageParams,
DistanceRateOperationsParams,
EditActionParams,
Expand Down Expand Up @@ -695,8 +696,9 @@ export default {
`${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${
pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : ''
}`,
deleteExpense: 'Eliminar gasto',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?',

deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`,
deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta solicitud', 'estas solicitudes', count)}?`,
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
individual: 'Individual',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ type RemoveMembersWarningPrompt = {
ownerName: string;
};

type DeleteExpenseTranslationParams = {
count: number;
};

export type {
AddressLineParams,
AdminCanceledRequestParams,
Expand Down Expand Up @@ -460,4 +464,5 @@ export type {
StripePaidParams,
UnapprovedParams,
RemoveMembersWarningPrompt,
DeleteExpenseTranslationParams,
};
29 changes: 26 additions & 3 deletions src/pages/Search/SearchSelectedNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,35 @@ type SearchSelectedNarrowProps = {options: Array<DropdownOption<SearchHeaderOpti
function SearchSelectedNarrow({options, itemsLength}: SearchSelectedNarrowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const selectedOptionIndexRef = useRef(-1);

const [isModalVisible, setIsModalVisible] = useState(false);
const buttonRef = useRef<View>(null);

const openMenu = () => setIsModalVisible(true);
const closeMenu = () => setIsModalVisible(false);

const handleOnModalHide = () => {
if (selectedOptionIndexRef.current === -1) {
return;
}
options[selectedOptionIndexRef.current]?.onSelected?.();
};

const handleOnMenuItemPress = (option: DropdownOption<SearchHeaderOptionValue>, index: number) => {
if (option?.shouldCloseModalOnSelect) {
selectedOptionIndexRef.current = index;
closeMenu();
return;
}
option?.onSelected?.();
};

const handleOnCloseMenu = () => {
selectedOptionIndexRef.current = -1;
closeMenu();
};

return (
<View style={[styles.pb4]}>
<Button
Expand All @@ -37,13 +59,14 @@ function SearchSelectedNarrow({options, itemsLength}: SearchSelectedNarrowProps)
<Modal
isVisible={isModalVisible}
type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED}
onClose={closeMenu}
onClose={handleOnCloseMenu}
onModalHide={handleOnModalHide}
>
{options.map((option) => (
{options.map((option, index) => (
<MenuItem
title={option.text}
icon={option.icon}
onPress={option.onSelected}
onPress={() => handleOnMenuItemPress(option, index)}
key={option.value}
/>
))}
Expand Down
Loading