From 11819fafebc87f51a7bd190e8340efd277dab4fd Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 10 Jul 2024 11:38:16 +0200 Subject: [PATCH 1/8] Display ConfirmModal before deleting expenses --- .../Search/SearchListWithHeader.tsx | 28 +++++++++++++++++++ src/components/Search/SearchPageHeader.tsx | 14 ++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 48d9a2b4ae3a..546d7a6e59b1 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -1,7 +1,10 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useMemo, useState} from 'react'; +import ConfirmModal from '@components/ConfirmModal'; import SelectionList from '@components/SelectionList'; import type {BaseSelectionListProps, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +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'; @@ -34,10 +37,24 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt } function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchType, ...props}: SearchListWithHeaderProps, ref: ForwardedRef) { + const {translate} = useLocalize(); const [selectedItems, setSelectedItems] = useState({}); + const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); const clearSelectedItems = () => setSelectedItems({}); + const handleDeleteExpenses = () => { + const itemsToDelete = Object.keys(selectedItems).filter((id) => selectedItems[id].canDelete); + + if (itemsToDelete.length === 0) { + return; + } + + clearSelectedItems(); + setDeleteExpensesConfirmModalVisible(false); + SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete); + }; + useEffect(() => { clearSelectedItems(); }, [hash]); @@ -104,6 +121,7 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT clearSelectedItems={clearSelectedItems} query={query} hash={hash} + setDeleteModalVisible={setDeleteExpensesConfirmModalVisible} /> // eslint-disable-next-line react/jsx-props-no-spreading @@ -115,6 +133,16 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT onCheckboxPress={toggleTransaction} onSelectAll={toggleAllTransactions} /> + setDeleteExpensesConfirmModalVisible(false)} + title={translate('iou.deleteExpense')} + prompt={translate('iou.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> ); } diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 8d42f9e6da36..b2a513cc7ffd 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -22,11 +22,12 @@ type SearchHeaderProps = { selectedItems?: SelectedTransactions; clearSelectedItems?: () => void; hash: number; + setDeleteModalVisible: (isVisible: boolean) => void; }; type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}: SearchHeaderProps) { +function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, setDeleteModalVisible}: SearchHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -47,17 +48,12 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}: return null; } - const itemsToDelete = selectedItemsKeys.filter((id) => selectedItems[id].canDelete); - - if (itemsToDelete.length > 0) { + if (selectedItemsKeys.some((id) => selectedItems[id].canDelete)) { options.push({ icon: Expensicons.Trashcan, text: translate('search.bulkActions.delete'), value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, - onSelected: () => { - clearSelectedItems?.(); - SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete); - }, + onSelected: () => setDeleteModalVisible(true), }); } @@ -119,7 +115,7 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems}: isDisabled={isOffline} /> ); - }, [clearSelectedItems, hash, isOffline, selectedItems, styles.colorMuted, styles.fontWeightNormal, theme.icon, translate]); + }, [clearSelectedItems, hash, isOffline, selectedItems, setDeleteModalVisible, styles.colorMuted, styles.fontWeightNormal, theme.icon, translate]); if (isSmallScreenWidth) { return null; From a0e10cb382ffd68f84863ef4229c93864e7809e3 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 11 Jul 2024 11:55:34 +0200 Subject: [PATCH 2/8] Add selectedItemsToDelete to SearchListWithHeader --- .../Search/SearchListWithHeader.tsx | 21 +++++++++++++------ src/components/Search/SearchPageHeader.tsx | 12 ++++++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 546d7a6e59b1..0e5096c78634 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -39,20 +39,29 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchType, ...props}: SearchListWithHeaderProps, ref: ForwardedRef) { const {translate} = useLocalize(); const [selectedItems, setSelectedItems] = useState({}); + const [selectedItemsToDelete, setSelectedItemsToDelete] = useState([]); const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); + const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { + setSelectedItemsToDelete(itemsToDelete); + setDeleteExpensesConfirmModalVisible(true); + }; + + const handleOnCancelConfirmModal = () => { + setSelectedItemsToDelete([]); + setDeleteExpensesConfirmModalVisible(false); + }; + const clearSelectedItems = () => setSelectedItems({}); const handleDeleteExpenses = () => { - const itemsToDelete = Object.keys(selectedItems).filter((id) => selectedItems[id].canDelete); - - if (itemsToDelete.length === 0) { + if (selectedItemsToDelete.length === 0) { return; } clearSelectedItems(); setDeleteExpensesConfirmModalVisible(false); - SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedItemsToDelete); }; useEffect(() => { @@ -121,7 +130,7 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT clearSelectedItems={clearSelectedItems} query={query} hash={hash} - setDeleteModalVisible={setDeleteExpensesConfirmModalVisible} + onSelectDeleteOption={handleOnSelectDeleteOption} /> // eslint-disable-next-line react/jsx-props-no-spreading @@ -136,7 +145,7 @@ function SearchListWithHeader({ListItem, onSelectRow, query, hash, data, searchT setDeleteExpensesConfirmModalVisible(false)} + onCancel={handleOnCancelConfirmModal} title={translate('iou.deleteExpense')} prompt={translate('iou.deleteConfirmation')} confirmText={translate('common.delete')} diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index b2a513cc7ffd..655211be0037 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -22,12 +22,12 @@ type SearchHeaderProps = { selectedItems?: SelectedTransactions; clearSelectedItems?: () => void; hash: number; - setDeleteModalVisible: (isVisible: boolean) => void; + onSelectDeleteOption: (itemsToDelete: string[]) => void; }; type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, setDeleteModalVisible}: SearchHeaderProps) { +function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, onSelectDeleteOption}: SearchHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -48,12 +48,14 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, return null; } - if (selectedItemsKeys.some((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: () => setDeleteModalVisible(true), + onSelected: () => onSelectDeleteOption(itemsToDelete), }); } @@ -115,7 +117,7 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, isDisabled={isOffline} /> ); - }, [clearSelectedItems, hash, isOffline, selectedItems, setDeleteModalVisible, styles.colorMuted, styles.fontWeightNormal, theme.icon, translate]); + }, [clearSelectedItems, hash, isOffline, onSelectDeleteOption, selectedItems, styles.colorMuted, styles.fontWeightNormal, theme.icon, translate]); if (isSmallScreenWidth) { return null; From e684ada92e1a79736f3fb2168b12962c51ce4830 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 11 Jul 2024 15:09:36 +0200 Subject: [PATCH 3/8] Add shouldCloseModalOnSelect to DropdownOption --- .../ButtonWithDropdownMenu/types.ts | 1 + .../Search/SearchListWithHeader.tsx | 15 ++++++++++-- src/components/Search/SearchPageHeader.tsx | 23 ++++++++++++------- src/pages/Search/SearchSelectedNarrow.tsx | 7 +++++- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index d1eedd560694..59621a8dd58c 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -27,6 +27,7 @@ type DropdownOption = { interactive?: boolean; numberOfLinesTitle?: number; titleStyle?: ViewStyle; + shouldCloseModalOnSelect?: boolean; }; type ButtonWithDropdownMenuProps = { diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index f7802b2578e9..d038d2a625a6 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -1,5 +1,6 @@ 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'; @@ -7,6 +8,7 @@ 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'; @@ -71,7 +73,7 @@ function SearchListWithHeader( clearSelectedItems(); setDeleteExpensesConfirmModalVisible(false); - // SearchActions.deleteMoneyRequestOnSearch(hash, selectedItemsToDelete); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedItemsToDelete); }; useEffect(() => { @@ -189,7 +191,16 @@ function SearchListWithHeader( onSelectAll={toggleAllTransactions} isMobileSelectionModeActive={isMobileSelectionModeActive} /> - + { - clearSelectedItems?.(); - if (isMobileSelectionModeActive) { - setIsMobileSelectionModeActive?.(false); - } - SearchActions.deleteMoneyRequestOnSearch(hash, itemsToDelete); - }, + shouldCloseModalOnSelect: true, + onSelected: () => onSelectDeleteOption(itemsToDelete), }); } @@ -122,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) { diff --git a/src/pages/Search/SearchSelectedNarrow.tsx b/src/pages/Search/SearchSelectedNarrow.tsx index b90142ff5873..11b0e376c77b 100644 --- a/src/pages/Search/SearchSelectedNarrow.tsx +++ b/src/pages/Search/SearchSelectedNarrow.tsx @@ -43,7 +43,12 @@ function SearchSelectedNarrow({options, itemsLength}: SearchSelectedNarrowProps) { + if (option?.shouldCloseModalOnSelect) { + closeMenu(); + } + option?.onSelected?.(); + }} key={option.value} /> ))} From 488e7a14841bce977b75e2387e5b16cc6f65b91a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 11 Jul 2024 15:55:42 +0200 Subject: [PATCH 4/8] Add translations for deleting multiple expenses --- src/components/Search/SearchListWithHeader.tsx | 4 ++-- src/languages/en.ts | 5 +++-- src/languages/es.ts | 6 ++++-- src/languages/types.ts | 5 +++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index d038d2a625a6..283b68bf17af 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -195,8 +195,8 @@ function SearchListWithHeader( isVisible={deleteExpensesConfirmModalVisible} onConfirm={handleDeleteExpenses} onCancel={handleOnCancelConfirmModal} - title={translate('iou.deleteExpense')} - prompt={translate('iou.deleteConfirmation')} + title={translate('iou.deleteExpense', {count: selectedItemsToDelete.length})} + prompt={translate('iou.deleteConfirmation', {count: selectedItemsToDelete.length})} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} danger diff --git a/src/languages/en.ts b/src/languages/en.ts index a0ec026f79e4..e3e7ac8349dc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -24,6 +24,7 @@ import type { DelegateSubmitParams, DeleteActionParams, DeleteConfirmationParams, + DeleteExpenseParams, DidSplitAmountMessageParams, DistanceRateOperationsParams, EditActionParams, @@ -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}: DeleteExpenseParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`, + deleteConfirmation: ({count}: DeleteExpenseParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`, settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', diff --git a/src/languages/es.ts b/src/languages/es.ts index a1c1b75d4c86..7b346a89b55a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -22,6 +22,7 @@ import type { DelegateSubmitParams, DeleteActionParams, DeleteConfirmationParams, + DeleteExpenseParams, DidSplitAmountMessageParams, DistanceRateOperationsParams, EditActionParams, @@ -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}: DeleteExpenseParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`, + deleteConfirmation: ({count}: DeleteExpenseParams = {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', diff --git a/src/languages/types.ts b/src/languages/types.ts index 78a711fe8282..872ac5e41694 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -341,6 +341,10 @@ type RemoveMembersWarningPrompt = { ownerName: string; }; +type DeleteExpenseParams = { + count: number; +}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -460,4 +464,5 @@ export type { StripePaidParams, UnapprovedParams, RemoveMembersWarningPrompt, + DeleteExpenseParams, }; From acf7bd4079e5a46141d7435bb2ecda83f333df1e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 11 Jul 2024 16:19:32 +0200 Subject: [PATCH 5/8] Fix TS in SearchPageHeader --- src/components/Search/SearchPageHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 72d608213291..3779b217436c 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -23,7 +23,7 @@ type SearchHeaderProps = { selectedItems?: SelectedTransactions; clearSelectedItems?: () => void; hash: number; - onSelectDeleteOption: (itemsToDelete: string[]) => void; + onSelectDeleteOption?: (itemsToDelete: string[]) => void; isMobileSelectionModeActive?: boolean; setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void; }; @@ -60,7 +60,7 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, text: translate('search.bulkActions.delete'), value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, shouldCloseModalOnSelect: true, - onSelected: () => onSelectDeleteOption(itemsToDelete), + onSelected: () => onSelectDeleteOption?.(itemsToDelete), }); } From 5df8ba63b51cf8be7a1cd0cf7a75673ed623cc3d Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 11 Jul 2024 16:22:17 +0200 Subject: [PATCH 6/8] Rename SearchHeaderProps to SearchPageHeaderProps --- src/components/Search/SearchPageHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 3779b217436c..9bfa78849800 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -18,7 +18,7 @@ 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; @@ -30,7 +30,7 @@ type SearchHeaderProps = { type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, onSelectDeleteOption, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchHeaderProps) { +function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, onSelectDeleteOption, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); From 4c7775ca19a9bc1c1516172a2faa13893f429c7c Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 12 Jul 2024 10:09:43 +0200 Subject: [PATCH 7/8] Fix displaying delete modal on iOS --- src/pages/Search/SearchSelectedNarrow.tsx | 34 +++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/pages/Search/SearchSelectedNarrow.tsx b/src/pages/Search/SearchSelectedNarrow.tsx index 11b0e376c77b..4de6ee1eef57 100644 --- a/src/pages/Search/SearchSelectedNarrow.tsx +++ b/src/pages/Search/SearchSelectedNarrow.tsx @@ -15,6 +15,7 @@ type SearchSelectedNarrowProps = {options: Array(null); @@ -22,6 +23,27 @@ function SearchSelectedNarrow({options, itemsLength}: SearchSelectedNarrowProps) const openMenu = () => setIsModalVisible(true); const closeMenu = () => setIsModalVisible(false); + const handleOnModalHide = () => { + if (selectedOptionIndexRef.current === -1) { + return; + } + options[selectedOptionIndexRef.current]?.onSelected?.(); + }; + + const handleOnMenuItemPress = (option: DropdownOption, index: number) => { + if (option?.shouldCloseModalOnSelect) { + selectedOptionIndexRef.current = index; + closeMenu(); + return; + } + option?.onSelected?.(); + }; + + const handleOnCloseMenu = () => { + selectedOptionIndexRef.current = -1; + closeMenu(); + }; + return (