Skip to content

Commit

Permalink
Merge pull request Expensify#45143 from software-mansion-labs/search/…
Browse files Browse the repository at this point in the history
…bulk-delete-modal

[Search] Display ConfirmModal before deleting expenses
  • Loading branch information
Hayata Suenaga authored Jul 15, 2024
2 parents 1002bfd + 97a916a commit 20b5192
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 19 deletions.
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}
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 @@ -709,8 +710,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 @@ -704,8 +705,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

0 comments on commit 20b5192

Please sign in to comment.