From 6822a4fb99ec6379eb990cc74949f454ed767f9d Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 11:59:34 +0200 Subject: [PATCH 01/18] Add Export CSV and handle offline for all actions --- src/CONST.ts | 1 + src/components/DecisionModal.tsx | 2 +- .../Search/SearchListWithHeader.tsx | 20 +++++- src/components/Search/SearchPageHeader.tsx | 64 +++++++++++++++++-- src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../ExportSearchItemsToCSVParams.ts | 10 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Search.ts | 30 ++++++++- src/libs/fileDownload/index.ts | 9 ++- src/libs/fileDownload/types.ts | 4 +- 12 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 src/libs/API/parameters/ExportSearchItemsToCSVParams.ts diff --git a/src/CONST.ts b/src/CONST.ts index 7fb0d320c43a..fcac06aeb630 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5220,6 +5220,7 @@ const CONST = { SUBMIT: 'submit', APPROVE: 'approve', PAY: 'pay', + EXPORT: 'export', }, SYNTAX_OPERATORS: { AND: 'and', diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx index 065099867e14..a9bd0b204d79 100644 --- a/src/components/DecisionModal.tsx +++ b/src/components/DecisionModal.tsx @@ -21,7 +21,7 @@ type DecisionModalProps = { secondOptionText: string; /** onSubmit callback fired after clicking on first button */ - onFirstOptionSubmit: () => void; + onFirstOptionSubmit?: () => void; /** onSubmit callback fired after clicking on second button */ onSecondOptionSubmit: () => void; diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 283b68bf17af..8a8970341a70 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react'; import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; @@ -53,6 +54,8 @@ function SearchListWithHeader( const [selectedItems, setSelectedItems] = useState({}); const [selectedItemsToDelete, setSelectedItemsToDelete] = useState([]); const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); + const [offlineModalVisible, setOfflineModalVisible] = useState(false); + const [selectedReports, setSelectedReports] = useState([]); const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { setSelectedItemsToDelete(itemsToDelete); @@ -100,6 +103,7 @@ function SearchListWithHeader( if (item.transactions.every((transaction) => selectedItems[transaction.keyForList]?.isSelected)) { const reducedSelectedItems: SelectedTransactions = {...selectedItems}; + setSelectedReports([...selectedReports.filter((reportID) => reportID !== item.reportID)]); item.transactions.forEach((transaction) => { delete reducedSelectedItems[transaction.keyForList]; @@ -109,12 +113,15 @@ function SearchListWithHeader( return; } + if (item.reportID) { + setSelectedReports([...selectedReports, item.reportID]); + } setSelectedItems({ ...selectedItems, ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)), }); }, - [selectedItems], + [selectedItems, selectedReports], ); const openBottomModal = (item: TransactionListItemType | ReportListItemType | null) => { @@ -178,6 +185,8 @@ function SearchListWithHeader( onSelectDeleteOption={handleOnSelectDeleteOption} isMobileSelectionModeActive={isMobileSelectionModeActive} setIsMobileSelectionModeActive={setIsMobileSelectionModeActive} + selectedReports={selectedReports} + setOfflineModalOpen={() => setOfflineModalVisible(true)} /> // eslint-disable-next-line react/jsx-props-no-spreading @@ -201,6 +210,15 @@ function SearchListWithHeader( cancelText={translate('common.cancel')} danger /> + setOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={offlineModalVisible} + onClose={() => setOfflineModalVisible(false)} + /> void; hash: number; onSelectDeleteOption?: (itemsToDelete: string[]) => void; isMobileSelectionModeActive?: boolean; setIsMobileSelectionModeActive?: (isMobileSelectionModeActive: boolean) => void; + setOfflineModalOpen?: () => void; }; type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, onSelectDeleteOption, isMobileSelectionModeActive, setIsMobileSelectionModeActive}: SearchPageHeaderProps) { +function SearchPageHeader({ + query, + selectedItems = {}, + hash, + clearSelectedItems, + onSelectDeleteOption, + isMobileSelectionModeActive, + setIsMobileSelectionModeActive, + setOfflineModalOpen, + selectedReports, +}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); + const {activeWorkspaceID} = useActiveWorkspace(); const {isSmallScreenWidth} = useResponsiveLayout(); const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = { all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')}, @@ -46,12 +60,27 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, const selectedItemsKeys = Object.keys(selectedItems ?? []); const headerButtonsOptions = useMemo(() => { - const options: Array> = []; - if (selectedItemsKeys.length === 0) { - return options; + return []; } + const options: Array> = [ + { + icon: Expensicons.Download, + text: translate('common.download'), + value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + clearSelectedItems?.(); + SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedItemsKeys, [activeWorkspaceID ?? '']); + }, + }, + ]; + const itemsToDelete = Object.keys(selectedItems ?? {}).filter((id) => selectedItems[id].canDelete); if (itemsToDelete.length > 0) { @@ -60,7 +89,14 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, text: translate('search.bulkActions.delete'), value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, shouldCloseModalOnSelect: true, - onSelected: () => onSelectDeleteOption?.(itemsToDelete), + onSelected: () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + + onSelectDeleteOption?.(itemsToDelete); + }, }); } @@ -71,7 +107,13 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, icon: Expensicons.Stopwatch, text: translate('search.bulkActions.hold'), value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD, + shouldCloseModalOnSelect: true, onSelected: () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + clearSelectedItems?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); @@ -88,7 +130,13 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, icon: Expensicons.Stopwatch, text: translate('search.bulkActions.unhold'), value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD, + shouldCloseModalOnSelect: true, onSelected: () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + clearSelectedItems?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); @@ -129,6 +177,11 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, theme.icon, styles.colorMuted, styles.fontWeightNormal, + query, + isOffline, + setOfflineModalOpen, + activeWorkspaceID, + selectedReports, ]); if (isSmallScreenWidth) { @@ -158,7 +211,6 @@ function SearchPageHeader({query, selectedItems = {}, hash, clearSelectedItems, customText={translate('workspace.common.selected', {selectedNumber: selectedItemsKeys.length})} options={headerButtonsOptions} isSplitButton={false} - isDisabled={isOffline} /> )} diff --git a/src/languages/en.ts b/src/languages/en.ts index d32e228dbb28..f6fe52da76f3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3486,6 +3486,7 @@ export default { unhold: 'Unhold', noOptionsAvailable: 'No options available for the selected group of expenses.', }, + offlinePrompt: 'You can’t take this action right now because you appear to be offline.', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0f6fa177ef82..fbc33eb38986 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3538,6 +3538,7 @@ export default { unhold: 'Desbloquear', noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.', }, + offlinePrompt: 'No puedes realizar esta acción ahora mismo porque pareces estar desconectado.', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts new file mode 100644 index 000000000000..979a7b99886f --- /dev/null +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -0,0 +1,10 @@ +import type {SearchQuery} from '@src/types/onyx/SearchResults'; + +type ExportSearchItemsToCSVParams = { + query: SearchQuery; + reportIDList: string[]; + transactionIDList: string[]; + policyIDs: string[]; +}; + +export default ExportSearchItemsToCSVParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 204fa01ed14c..8cc3fb8f646f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -258,3 +258,4 @@ export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuite export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams'; export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams'; export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams'; +export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsToCSVParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4ecddbdb7406..c28399c97948 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -679,6 +679,7 @@ const READ_COMMANDS = { OPEN_POLICY_INITIAL_PAGE: 'OpenPolicyInitialPage', SEARCH: 'Search', OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage', + EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', } as const; type ReadCommand = ValueOf; @@ -732,6 +733,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams; [READ_COMMANDS.SEARCH]: Parameters.SearchParams; [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; + [READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV]: Parameters.ExportSearchItemsToCSVParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 4ce82a027a12..7e38c2336e34 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -3,6 +3,10 @@ import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {SearchParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ApiUtils from '@libs/ApiUtils'; +import fileDownload from '@libs/fileDownload'; +import enhanceParameters from '@libs/Network/enhanceParameters'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; @@ -83,4 +87,28 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } -export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch}; +type Params = Record; + +function exportSearchItemsToCSV(query: string, reportIDList: string[] | undefined, transactionIDList: string[], policyIDs: string[]) { + const fileName = `Expensify_${query}.csv`; + + const finalParameters = enhanceParameters(READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { + query, + reportIDList, + transactionIDList, + policyIDs, + }) as Params; + + const formData = new FormData(); + + Object.entries(finalParameters).forEach(([key, value]) => { + if (Array.isArray(value)) { + formData.append(key, value.join(',')); + } else { + formData.append(key, String(value)); + } + }); + + fileDownload(ApiUtils.getCommandURL({command: READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); +} +export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV}; diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index b39e65a87f94..8891cf775107 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -9,7 +9,7 @@ import type {FileDownload} from './types'; * The function downloads an attachment on web/desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get') => { const resolvedUrl = tryResolveUrlFromApiRoot(url); if ( // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct @@ -24,7 +24,12 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp return Promise.resolve(); } - return fetch(url) + const fetchOptions: RequestInit = { + method: requestType, + body: formData, + }; + + return fetch(url, fetchOptions) .then((response) => response.blob()) .then((blob) => { // Create blob link to download diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index fcc210c1c42f..d8897331d21c 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,7 +1,7 @@ import type {Asset} from 'react-native-image-picker'; +import type {RequestType} from '@src/types/onyx/Request'; -type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean) => Promise; - +type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From 45141855cb9e86a41766de7fa1823598eadd2367 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 14:59:47 +0200 Subject: [PATCH 02/18] Change request type --- src/components/Search/SearchPageHeader.tsx | 2 +- src/libs/actions/Search.ts | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 4833f8d37b0a..af5badb7cf8a 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -76,7 +76,7 @@ function SearchPageHeader({ return; } clearSelectedItems?.(); - SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedItemsKeys, [activeWorkspaceID ?? '']); + SearchActions.exportSearchItemsToCSV(query, selectedReports ?? [], selectedItemsKeys, [activeWorkspaceID ?? '']); }, }, ]; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 7e38c2336e34..24fd2cd571e6 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -6,7 +6,6 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; import fileDownload from '@libs/fileDownload'; import enhanceParameters from '@libs/Network/enhanceParameters'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; @@ -99,16 +98,11 @@ function exportSearchItemsToCSV(query: string, reportIDList: string[] | undefine policyIDs, }) as Params; - const formData = new FormData(); + // Convert finalParameters to a query string + const queryString = Object.entries(finalParameters) + .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : String(value)}`) + .join('&'); - Object.entries(finalParameters).forEach(([key, value]) => { - if (Array.isArray(value)) { - formData.append(key, value.join(',')); - } else { - formData.append(key, String(value)); - } - }); - - fileDownload(ApiUtils.getCommandURL({command: READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); + fileDownload(`${ApiUtils.getCommandURL({command: READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV})}${queryString}`, fileName); } export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV}; From 2c69e75f37507e559d52fae2e24755b7716d663b Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 15:15:45 +0200 Subject: [PATCH 03/18] Revert fileDownload function changes --- src/libs/fileDownload/index.ts | 9 ++------- src/libs/fileDownload/types.ts | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index 8891cf775107..b39e65a87f94 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -9,7 +9,7 @@ import type {FileDownload} from './types'; * The function downloads an attachment on web/desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get') => { +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); if ( // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct @@ -24,12 +24,7 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp return Promise.resolve(); } - const fetchOptions: RequestInit = { - method: requestType, - body: formData, - }; - - return fetch(url, fetchOptions) + return fetch(url) .then((response) => response.blob()) .then((blob) => { // Create blob link to download diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index d8897331d21c..3af584bb8d3a 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,7 +1,6 @@ import type {Asset} from 'react-native-image-picker'; -import type {RequestType} from '@src/types/onyx/Request'; -type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType) => Promise; +type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From 6c77fa8a69f76e5666c46db74faba099ce86dedf Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 15:16:17 +0200 Subject: [PATCH 04/18] Add back empty line --- src/libs/fileDownload/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index 3af584bb8d3a..fcc210c1c42f 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,6 +1,7 @@ import type {Asset} from 'react-native-image-picker'; type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean) => Promise; + type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From a91b592f66854e8aef23945f8bc51e1ece2c0e75 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 15:40:32 +0200 Subject: [PATCH 05/18] CR fixes --- .../Search/SearchListWithHeader.tsx | 74 +++++++++---------- src/components/Search/SearchPageHeader.tsx | 40 +++++----- src/libs/actions/Search.ts | 2 +- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 8a8970341a70..1c7d3d3f3b07 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -12,7 +12,7 @@ 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'; +import type {SearchDataTypes, SearchQuery, SearchReport} from '@src/types/onyx/SearchResults'; import SearchPageHeader from './SearchPageHeader'; import type {SelectedTransactionInfo, SelectedTransactions} from './types'; @@ -29,17 +29,17 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri return [item.keyForList, {isSelected: true, canDelete: item.canDelete, action: item.action}]; } -function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedItems: SelectedTransactions) { - return {...item, isSelected: !!selectedItems[item.keyForList]?.isSelected}; +function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) { + return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected}; } -function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedItems: SelectedTransactions) { +function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) { return SearchUtils.isTransactionListItemType(item) - ? mapToTransactionItemWithSelectionInfo(item, selectedItems) + ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions) : { ...item, - transactions: item.transactions?.map((tranaction) => mapToTransactionItemWithSelectionInfo(tranaction, selectedItems)), - isSelected: item.transactions.every((transaction) => !!selectedItems[transaction.keyForList]?.isSelected), + transactions: item.transactions?.map((tranaction) => mapToTransactionItemWithSelectionInfo(tranaction, selectedTransactions)), + isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected), }; } @@ -51,36 +51,36 @@ function SearchListWithHeader( const {translate} = useLocalize(); const [isModalVisible, setIsModalVisible] = useState(false); const [longPressedItem, setLongPressedItem] = useState(null); - const [selectedItems, setSelectedItems] = useState({}); - const [selectedItemsToDelete, setSelectedItemsToDelete] = useState([]); + const [selectedTransactions, setSelectedTransactions] = useState({}); + const [selectedReports, setSelectedReports] = useState>([]); + const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); const [offlineModalVisible, setOfflineModalVisible] = useState(false); - const [selectedReports, setSelectedReports] = useState([]); const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { - setSelectedItemsToDelete(itemsToDelete); + setSelectedTransactionsToDelete(itemsToDelete); setDeleteExpensesConfirmModalVisible(true); }; const handleOnCancelConfirmModal = () => { - setSelectedItemsToDelete([]); + setSelectedTransactionsToDelete([]); setDeleteExpensesConfirmModalVisible(false); }; - const clearSelectedItems = () => setSelectedItems({}); + const clearSelectedTransactions = () => setSelectedTransactions({}); const handleDeleteExpenses = () => { - if (selectedItemsToDelete.length === 0) { + if (selectedTransactionsToDelete.length === 0) { return; } - clearSelectedItems(); + clearSelectedTransactions(); setDeleteExpensesConfirmModalVisible(false); - SearchActions.deleteMoneyRequestOnSearch(hash, selectedItemsToDelete); + SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete); }; useEffect(() => { - clearSelectedItems(); + clearSelectedTransactions(); }, [hash]); const toggleTransaction = useCallback( @@ -90,7 +90,7 @@ function SearchListWithHeader( return; } - setSelectedItems((prev) => { + setSelectedTransactions((prev) => { if (prev[item.keyForList]?.isSelected) { const {[item.keyForList]: omittedTransaction, ...transactions} = prev; return transactions; @@ -101,27 +101,27 @@ function SearchListWithHeader( return; } - if (item.transactions.every((transaction) => selectedItems[transaction.keyForList]?.isSelected)) { - const reducedSelectedItems: SelectedTransactions = {...selectedItems}; - setSelectedReports([...selectedReports.filter((reportID) => reportID !== item.reportID)]); + if (item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)) { + const reducedSelectedTransactions: SelectedTransactions = {...selectedTransactions}; + setSelectedReports((prevReports) => prevReports.filter((reportID) => reportID !== item.reportID)); item.transactions.forEach((transaction) => { - delete reducedSelectedItems[transaction.keyForList]; + delete reducedSelectedTransactions[transaction.keyForList]; }); - setSelectedItems(reducedSelectedItems); + setSelectedTransactions(reducedSelectedTransactions); return; } if (item.reportID) { setSelectedReports([...selectedReports, item.reportID]); } - setSelectedItems({ - ...selectedItems, + setSelectedTransactions({ + ...selectedTransactions, ...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)), }); }, - [selectedItems, selectedReports], + [selectedTransactions, selectedReports], ); const openBottomModal = (item: TransactionListItemType | ReportListItemType | null) => { @@ -151,35 +151,35 @@ function SearchListWithHeader( return; } - setSelectedItems({}); - }, [setSelectedItems, isMobileSelectionModeActive]); + setSelectedTransactions({}); + }, [setSelectedTransactions, isMobileSelectionModeActive]); const toggleAllTransactions = () => { const areItemsOfReportType = searchType === CONST.SEARCH.DATA_TYPES.REPORT; const flattenedItems = areItemsOfReportType ? (data as ReportListItemType[]).flatMap((item) => item.transactions) : data; - const isAllSelected = flattenedItems.length === Object.keys(selectedItems).length; + const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; if (isAllSelected) { - clearSelectedItems(); + clearSelectedTransactions(); return; } if (areItemsOfReportType) { - setSelectedItems(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry)))); + setSelectedTransactions(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry)))); return; } - setSelectedItems(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry))); + setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry))); }; - const sortedSelectedData = useMemo(() => data.map((item) => mapToItemWithSelectionInfo(item, selectedItems)), [data, selectedItems]); + const sortedSelectedData = useMemo(() => data.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions)), [data, selectedTransactions]); return ( <> void; + selectedTransactions?: SelectedTransactions; + selectedReports?: Array; + clearSelectedTransactions?: () => void; hash: number; onSelectDeleteOption?: (itemsToDelete: string[]) => void; isMobileSelectionModeActive?: boolean; @@ -35,9 +35,9 @@ type SearchHeaderOptionValue = DeepValueOf { - if (selectedItemsKeys.length === 0) { + if (selectedTransactionsKeys.length === 0) { return []; } @@ -75,13 +75,13 @@ function SearchPageHeader({ setOfflineModalOpen?.(); return; } - clearSelectedItems?.(); - SearchActions.exportSearchItemsToCSV(query, selectedReports ?? [], selectedItemsKeys, [activeWorkspaceID ?? '']); + clearSelectedTransactions?.(); + SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? '']); }, }, ]; - const itemsToDelete = Object.keys(selectedItems ?? {}).filter((id) => selectedItems[id].canDelete); + const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete); if (itemsToDelete.length > 0) { options.push({ @@ -100,7 +100,7 @@ function SearchPageHeader({ }); } - const itemsToHold = selectedItemsKeys.filter((id) => selectedItems[id].action === CONST.SEARCH.BULK_ACTION_TYPES.HOLD); + const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.HOLD); if (itemsToHold.length > 0) { options.push({ @@ -114,7 +114,7 @@ function SearchPageHeader({ return; } - clearSelectedItems?.(); + clearSelectedTransactions?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } @@ -123,7 +123,7 @@ function SearchPageHeader({ }); } - const itemsToUnhold = selectedItemsKeys.filter((id) => selectedItems[id].action === CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD); + const itemsToUnhold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD); if (itemsToUnhold.length > 0) { options.push({ @@ -137,7 +137,7 @@ function SearchPageHeader({ return; } - clearSelectedItems?.(); + clearSelectedTransactions?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } @@ -166,11 +166,11 @@ function SearchPageHeader({ return options; }, [ - selectedItemsKeys, - selectedItems, + selectedTransactionsKeys, + selectedTransactions, translate, onSelectDeleteOption, - clearSelectedItems, + clearSelectedTransactions, isMobileSelectionModeActive, hash, setIsMobileSelectionModeActive, @@ -189,7 +189,7 @@ function SearchPageHeader({ return ( ); } @@ -208,7 +208,7 @@ function SearchPageHeader({ shouldAlwaysShowDropdownMenu pressOnEnter buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {selectedNumber: selectedItemsKeys.length})} + customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})} options={headerButtonsOptions} isSplitButton={false} /> diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 24fd2cd571e6..f6abb552cde0 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -88,7 +88,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV(query: string, reportIDList: string[] | undefined, transactionIDList: string[], policyIDs: string[]) { +function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[]) { const fileName = `Expensify_${query}.csv`; const finalParameters = enhanceParameters(READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { From ed50d80520d48073a00a62ea34e7c1b11fdc2a0d Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 16 Jul 2024 15:49:29 +0200 Subject: [PATCH 06/18] Add confirmed translations --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index fbc33eb38986..3f06ac7756dd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3523,7 +3523,7 @@ export default { screenShareRequest: 'Expensify te está invitando a compartir la pantalla', }, search: { - selectMultiple: 'Seleccionar múltiples', + selectMultiple: 'Seleccionar varios', resultsAreLimited: 'Los resultados de búsqueda están limitados.', searchResults: { emptyResults: { @@ -3538,7 +3538,7 @@ export default { unhold: 'Desbloquear', noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.', }, - offlinePrompt: 'No puedes realizar esta acción ahora mismo porque pareces estar desconectado.', + offlinePrompt: 'No puedes realizar esta acción ahora mismo porque parece que estás desconectado.', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', From 4653dc6acba8c26daab0db9a9387447407759034 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jul 2024 06:47:22 +0200 Subject: [PATCH 07/18] Review fixes --- src/components/Search/SearchListWithHeader.tsx | 15 +++++++++------ src/components/Search/SearchPageHeader.tsx | 12 ++++++------ src/libs/API/types.ts | 4 ++-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 1c7d3d3f3b07..e639b1fd50c6 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -38,7 +38,7 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions) : { ...item, - transactions: item.transactions?.map((tranaction) => mapToTransactionItemWithSelectionInfo(tranaction, selectedTransactions)), + transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)), isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected), }; } @@ -67,20 +67,23 @@ function SearchListWithHeader( setDeleteExpensesConfirmModalVisible(false); }; - const clearSelectedTransactions = () => setSelectedTransactions({}); + const clearSelectedItems = () => { + setSelectedTransactions({}); + setSelectedReports([]); + }; const handleDeleteExpenses = () => { if (selectedTransactionsToDelete.length === 0) { return; } - clearSelectedTransactions(); + clearSelectedItems(); setDeleteExpensesConfirmModalVisible(false); SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsToDelete); }; useEffect(() => { - clearSelectedTransactions(); + clearSelectedItems(); }, [hash]); const toggleTransaction = useCallback( @@ -160,7 +163,7 @@ function SearchListWithHeader( const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length; if (isAllSelected) { - clearSelectedTransactions(); + clearSelectedItems(); return; } @@ -179,7 +182,7 @@ function SearchListWithHeader( <> ; - clearSelectedTransactions?: () => void; + clearSelectedItems?: () => void; hash: number; onSelectDeleteOption?: (itemsToDelete: string[]) => void; isMobileSelectionModeActive?: boolean; @@ -37,7 +37,7 @@ function SearchPageHeader({ query, selectedTransactions = {}, hash, - clearSelectedTransactions, + clearSelectedItems, onSelectDeleteOption, isMobileSelectionModeActive, setIsMobileSelectionModeActive, @@ -75,7 +75,7 @@ function SearchPageHeader({ setOfflineModalOpen?.(); return; } - clearSelectedTransactions?.(); + SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? '']); }, }, @@ -114,7 +114,7 @@ function SearchPageHeader({ return; } - clearSelectedTransactions?.(); + clearSelectedItems?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } @@ -137,7 +137,7 @@ function SearchPageHeader({ return; } - clearSelectedTransactions?.(); + clearSelectedItems?.(); if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } @@ -170,7 +170,7 @@ function SearchPageHeader({ selectedTransactions, translate, onSelectDeleteOption, - clearSelectedTransactions, + clearSelectedItems, isMobileSelectionModeActive, hash, setIsMobileSelectionModeActive, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c28399c97948..70ff94dfe8b9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -311,6 +311,7 @@ const WRITE_COMMANDS = { UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_CREDIT_CARD_CHARGE_EXPORT_DEFAULT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesCreditCardChargeExportDefaultVendor', UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT: 'UpdateSageIntacctNonreimbursableExpensesExportAccount', UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesExportVendor', + EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', } as const; type WriteCommand = ValueOf; @@ -628,6 +629,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_PROJECTS_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>; [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>; [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION]: Parameters.UpdateSageIntacctGenericTypeParams<'dimensions', string>; + [WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV]: Parameters.ExportSearchItemsToCSVParams; }; const READ_COMMANDS = { @@ -679,7 +681,6 @@ const READ_COMMANDS = { OPEN_POLICY_INITIAL_PAGE: 'OpenPolicyInitialPage', SEARCH: 'Search', OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage', - EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', } as const; type ReadCommand = ValueOf; @@ -733,7 +734,6 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams; [READ_COMMANDS.SEARCH]: Parameters.SearchParams; [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; - [READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV]: Parameters.ExportSearchItemsToCSVParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { From ae6e2c405097974dfa145a594064605988924074 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jul 2024 06:55:03 +0200 Subject: [PATCH 08/18] Use POST method --- src/libs/actions/Search.ts | 19 ++++++++++++------- src/libs/fileDownload/index.ts | 9 +++++++-- src/libs/fileDownload/types.ts | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f6abb552cde0..05156f5390d9 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -6,6 +6,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; import fileDownload from '@libs/fileDownload'; import enhanceParameters from '@libs/Network/enhanceParameters'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; @@ -88,21 +89,25 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[]) { +function exportSearchItemsToCSV(query: string, reportIDList: string[] | undefined, transactionIDList: string[], policyIDs: string[]) { const fileName = `Expensify_${query}.csv`; - const finalParameters = enhanceParameters(READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { + const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { query, reportIDList, transactionIDList, policyIDs, }) as Params; - // Convert finalParameters to a query string - const queryString = Object.entries(finalParameters) - .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : String(value)}`) - .join('&'); + const formData = new FormData(); + Object.entries(finalParameters).forEach(([key, value]) => { + if (Array.isArray(value)) { + formData.append(key, value.join(',')); + } else { + formData.append(key, String(value)); + } + }); - fileDownload(`${ApiUtils.getCommandURL({command: READ_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV})}${queryString}`, fileName); + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); } export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV}; diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index b39e65a87f94..8891cf775107 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -9,7 +9,7 @@ import type {FileDownload} from './types'; * The function downloads an attachment on web/desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false) => { +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get') => { const resolvedUrl = tryResolveUrlFromApiRoot(url); if ( // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct @@ -24,7 +24,12 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp return Promise.resolve(); } - return fetch(url) + const fetchOptions: RequestInit = { + method: requestType, + body: formData, + }; + + return fetch(url, fetchOptions) .then((response) => response.blob()) .then((blob) => { // Create blob link to download diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index fcc210c1c42f..d8897331d21c 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,7 +1,7 @@ import type {Asset} from 'react-native-image-picker'; +import type {RequestType} from '@src/types/onyx/Request'; -type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean) => Promise; - +type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From c4dc4aa78201a5fdcfb837234cc29842ee25db99 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jul 2024 07:00:08 +0200 Subject: [PATCH 09/18] Fix TS --- src/libs/actions/Search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 05156f5390d9..7c7d96bcdde0 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -89,7 +89,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV(query: string, reportIDList: string[] | undefined, transactionIDList: string[], policyIDs: string[]) { +function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[]) { const fileName = `Expensify_${query}.csv`; const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { From fa2ccf6578df78e17234facdc8dfb194fe12bcea Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 12:37:07 -0300 Subject: [PATCH 10/18] adding modal for download error and removing download option outside web --- .../SearchActionOptionsUtils.native.tsx | 8 +++++ .../Search/SearchActionOptionsUtils.tsx | 17 ++++++++++ .../Search/SearchListWithHeader.tsx | 11 ++++++ src/components/Search/SearchPageHeader.tsx | 34 +++++++++++-------- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/libs/actions/Search.ts | 4 +-- src/libs/fileDownload/index.ts | 10 ++++-- src/libs/fileDownload/types.ts | 2 +- 9 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 src/components/Search/SearchActionOptionsUtils.native.tsx create mode 100644 src/components/Search/SearchActionOptionsUtils.tsx diff --git a/src/components/Search/SearchActionOptionsUtils.native.tsx b/src/components/Search/SearchActionOptionsUtils.native.tsx new file mode 100644 index 000000000000..8b6b33c1936e --- /dev/null +++ b/src/components/Search/SearchActionOptionsUtils.native.tsx @@ -0,0 +1,8 @@ +import type {DropdownOption} from "@components/ButtonWithDropdownMenu/types"; +import {SearchHeaderOptionValue} from "@components/Search/SearchPageHeader"; + +function getDownloadOption(): DropdownOption | undefined { + return undefined; +} + +export default getDownloadOption; diff --git a/src/components/Search/SearchActionOptionsUtils.tsx b/src/components/Search/SearchActionOptionsUtils.tsx new file mode 100644 index 000000000000..60a7b3431ba7 --- /dev/null +++ b/src/components/Search/SearchActionOptionsUtils.tsx @@ -0,0 +1,17 @@ +import * as Expensicons from "@components/Icon/Expensicons"; +import CONST from "@src/CONST"; +import type {DropdownOption} from "@components/ButtonWithDropdownMenu/types"; +import {SearchHeaderOptionValue} from "@components/Search/SearchPageHeader"; + + +function getDownloadOption(text: string, onSelected?: () => void): DropdownOption { + return { + icon: Expensicons.Download, + text, + value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, + shouldCloseModalOnSelect: true, + onSelected + } +} + +export default getDownloadOption; diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index e639b1fd50c6..25522d84fbf6 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -56,6 +56,7 @@ function SearchListWithHeader( const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]); const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false); const [offlineModalVisible, setOfflineModalVisible] = useState(false); + const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const handleOnSelectDeleteOption = (itemsToDelete: string[]) => { setSelectedTransactionsToDelete(itemsToDelete); @@ -190,6 +191,7 @@ function SearchListWithHeader( setIsMobileSelectionModeActive={setIsMobileSelectionModeActive} selectedReports={selectedReports} setOfflineModalOpen={() => setOfflineModalVisible(true)} + setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)} /> // eslint-disable-next-line react/jsx-props-no-spreading @@ -222,6 +224,15 @@ function SearchListWithHeader( isVisible={offlineModalVisible} onClose={() => setOfflineModalVisible(false)} /> + setDownloadErrorModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={downloadErrorModalVisible} + onClose={() => setDownloadErrorModalVisible(false)} + /> void; setOfflineModalOpen?: () => void; + setDownloadErrorModalOpen?: () => void; }; type SearchHeaderOptionValue = DeepValueOf | undefined; @@ -42,6 +44,7 @@ function SearchPageHeader({ isMobileSelectionModeActive, setIsMobileSelectionModeActive, setOfflineModalOpen, + setDownloadErrorModalOpen, selectedReports, }: SearchPageHeaderProps) { const {translate} = useLocalize(); @@ -64,22 +67,23 @@ function SearchPageHeader({ return []; } - const options: Array> = [ - { - icon: Expensicons.Download, - text: translate('common.download'), - value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setOfflineModalOpen?.(); - return; - } + const options: Array> = []; - SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? '']); - }, - }, - ]; + // Because of some problems with the lib we use for download on native we are only enabling download for web, we should remove the SearchActionOptionsUtils files when https://github.com/Expensify/App/issues/45511 is done + const downloadOption = getDownloadOption(translate('common.download'), () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + + SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? ''], () => { + setDownloadErrorModalOpen?.() + }); + }) + + if (downloadOption) { + options.push(downloadOption); + } const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete); diff --git a/src/languages/en.ts b/src/languages/en.ts index 400999d51824..9606b5cf5732 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -366,6 +366,8 @@ export default { initialValue: 'Initial value', currentDate: 'Current date', value: 'Value', + downloadFailedTitle: 'Download failed', + downloadFailedDescription: 'Your download couldn\'t be completed. Please try again later.', }, location: { useCurrent: 'Use current location', diff --git a/src/languages/es.ts b/src/languages/es.ts index 14fdc3c7cf1b..1524ea258348 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -356,6 +356,8 @@ export default { initialValue: 'Valor inicial', currentDate: 'Fecha actual', value: 'Valor', + downloadFailedTitle: 'Error en la descarga', + downloadFailedDescription: 'No se pudo completar la descarga. Por favor, inténtalo más tarde.', }, connectionComplete: { title: 'Conexión completa', diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 7c7d96bcdde0..24c75aef51eb 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -89,7 +89,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[]) { +function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[], onDownloadFailed: () => void) { const fileName = `Expensify_${query}.csv`; const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { @@ -108,6 +108,6 @@ function exportSearchItemsToCSV(query: string, reportIDList: Array { +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); if ( // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct @@ -55,8 +55,12 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp link.parentNode?.removeChild(link); }) .catch(() => { - // file could not be downloaded, open sourceURL in new tab - Link.openExternalLink(url); + if (onDownloadFailed) { + onDownloadFailed() + } else { + // file could not be downloaded, open sourceURL in new tab + Link.openExternalLink(url); + } }); }; diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index d8897331d21c..41a0c5f59fd2 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,7 +1,7 @@ import type {Asset} from 'react-native-image-picker'; import type {RequestType} from '@src/types/onyx/Request'; -type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType) => Promise; +type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType, onDownloadFailed?: () => void) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From 6f1521469b0d1e73337f26846fcf10f2f06128c4 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 13:13:20 -0300 Subject: [PATCH 11/18] lint + prettier --- .../Search/SearchActionOptionsUtils.native.tsx | 4 ++-- src/components/Search/SearchActionOptionsUtils.tsx | 13 ++++++------- src/components/Search/SearchPageHeader.tsx | 5 +++-- src/languages/en.ts | 2 +- src/libs/fileDownload/index.ts | 2 +- src/libs/fileDownload/types.ts | 10 +++++++++- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/components/Search/SearchActionOptionsUtils.native.tsx b/src/components/Search/SearchActionOptionsUtils.native.tsx index 8b6b33c1936e..1e59543721e0 100644 --- a/src/components/Search/SearchActionOptionsUtils.native.tsx +++ b/src/components/Search/SearchActionOptionsUtils.native.tsx @@ -1,5 +1,5 @@ -import type {DropdownOption} from "@components/ButtonWithDropdownMenu/types"; -import {SearchHeaderOptionValue} from "@components/Search/SearchPageHeader"; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import type {SearchHeaderOptionValue} from './SearchPageHeader'; function getDownloadOption(): DropdownOption | undefined { return undefined; diff --git a/src/components/Search/SearchActionOptionsUtils.tsx b/src/components/Search/SearchActionOptionsUtils.tsx index 60a7b3431ba7..20601abd3696 100644 --- a/src/components/Search/SearchActionOptionsUtils.tsx +++ b/src/components/Search/SearchActionOptionsUtils.tsx @@ -1,8 +1,7 @@ -import * as Expensicons from "@components/Icon/Expensicons"; -import CONST from "@src/CONST"; -import type {DropdownOption} from "@components/ButtonWithDropdownMenu/types"; -import {SearchHeaderOptionValue} from "@components/Search/SearchPageHeader"; - +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import * as Expensicons from '@components/Icon/Expensicons'; +import CONST from '@src/CONST'; +import type {SearchHeaderOptionValue} from './SearchPageHeader'; function getDownloadOption(text: string, onSelected?: () => void): DropdownOption { return { @@ -10,8 +9,8 @@ function getDownloadOption(text: string, onSelected?: () => void): DropdownOptio text, value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, shouldCloseModalOnSelect: true, - onSelected - } + onSelected, + }; } export default getDownloadOption; diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 9d54e62cb739..eeb4b7a8d29f 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -77,9 +77,9 @@ function SearchPageHeader({ } SearchActions.exportSearchItemsToCSV(query, selectedReports, selectedTransactionsKeys, [activeWorkspaceID ?? ''], () => { - setDownloadErrorModalOpen?.() + setDownloadErrorModalOpen?.(); }); - }) + }); if (downloadOption) { options.push(downloadOption); @@ -184,6 +184,7 @@ function SearchPageHeader({ query, isOffline, setOfflineModalOpen, + setDownloadErrorModalOpen, activeWorkspaceID, selectedReports, styles.textWrap, diff --git a/src/languages/en.ts b/src/languages/en.ts index 9606b5cf5732..59ae2eb59fb5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -367,7 +367,7 @@ export default { currentDate: 'Current date', value: 'Value', downloadFailedTitle: 'Download failed', - downloadFailedDescription: 'Your download couldn\'t be completed. Please try again later.', + downloadFailedDescription: "Your download couldn't be completed. Please try again later.", }, location: { useCurrent: 'Use current location', diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index b95b6183ced3..133a18e146a5 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -56,7 +56,7 @@ const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOp }) .catch(() => { if (onDownloadFailed) { - onDownloadFailed() + onDownloadFailed(); } else { // file could not be downloaded, open sourceURL in new tab Link.openExternalLink(url); diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index 41a0c5f59fd2..6b8c1fe6be72 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -1,7 +1,15 @@ import type {Asset} from 'react-native-image-picker'; import type {RequestType} from '@src/types/onyx/Request'; -type FileDownload = (url: string, fileName?: string, successMessage?: string, shouldOpenExternalLink?: boolean, formData?: FormData, requestType?: RequestType, onDownloadFailed?: () => void) => Promise; +type FileDownload = ( + url: string, + fileName?: string, + successMessage?: string, + shouldOpenExternalLink?: boolean, + formData?: FormData, + requestType?: RequestType, + onDownloadFailed?: () => void, +) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; From 0e7b879efd30ff908c2c744bfc8df09c7632e027 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa <5201282+rlinoz@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:30:53 -0300 Subject: [PATCH 12/18] Update src/languages/en.ts Co-authored-by: Rushat Gabhane --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 59ae2eb59fb5..bfc20ff81cbb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3507,7 +3507,7 @@ export default { unhold: 'Unhold', noOptionsAvailable: 'No options available for the selected group of expenses.', }, - offlinePrompt: 'You can’t take this action right now because you appear to be offline.', + offlinePrompt: 'You can’t take this action right now.', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', From 990d6d901446590e3f43ac3b9d48feba95b32eaa Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa <5201282+rlinoz@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:31:00 -0300 Subject: [PATCH 13/18] Update src/languages/es.ts Co-authored-by: Rushat Gabhane --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 1524ea258348..fda8e817eb12 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3562,7 +3562,7 @@ export default { unhold: 'Desbloquear', noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.', }, - offlinePrompt: 'No puedes realizar esta acción ahora mismo porque parece que estás desconectado.', + offlinePrompt: 'No puedes realizar esta acción ahora mismo.', }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', From a48397dc100a7dde3cb41b78e859431064cc4553 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 15:05:38 -0300 Subject: [PATCH 14/18] removing download option from desktop --- src/components/Search/SearchActionOptionsUtils.desktop.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/components/Search/SearchActionOptionsUtils.desktop.ts diff --git a/src/components/Search/SearchActionOptionsUtils.desktop.ts b/src/components/Search/SearchActionOptionsUtils.desktop.ts new file mode 100644 index 000000000000..1e59543721e0 --- /dev/null +++ b/src/components/Search/SearchActionOptionsUtils.desktop.ts @@ -0,0 +1,8 @@ +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import type {SearchHeaderOptionValue} from './SearchPageHeader'; + +function getDownloadOption(): DropdownOption | undefined { + return undefined; +} + +export default getDownloadOption; From 389409b95d61d5c07955ac3dc803f5819745aa8c Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 15:35:14 -0300 Subject: [PATCH 15/18] fix extension --- ...tionsUtils.desktop.ts => SearchActionOptionsUtils.desktop.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/Search/{SearchActionOptionsUtils.desktop.ts => SearchActionOptionsUtils.desktop.tsx} (100%) diff --git a/src/components/Search/SearchActionOptionsUtils.desktop.ts b/src/components/Search/SearchActionOptionsUtils.desktop.tsx similarity index 100% rename from src/components/Search/SearchActionOptionsUtils.desktop.ts rename to src/components/Search/SearchActionOptionsUtils.desktop.tsx From 0815754c9fc704ad199c0758b94628fcfcddfb98 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa <5201282+rlinoz@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:12:03 -0300 Subject: [PATCH 16/18] Update src/languages/en.ts Co-authored-by: Carlos Martins --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 119fe7b339b4..2c210530f06c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3530,7 +3530,7 @@ export default { unhold: 'Unhold', noOptionsAvailable: 'No options available for the selected group of expenses.', }, - offlinePrompt: 'You can’t take this action right now.', + offlinePrompt: 'You can't take this action right now.', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', From 340e92260c755d325395bbe623778a38569079be Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 16:56:42 -0300 Subject: [PATCH 17/18] fix language --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2c210530f06c..0cc3cb3ea011 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3530,7 +3530,7 @@ export default { unhold: 'Unhold', noOptionsAvailable: 'No options available for the selected group of expenses.', }, - offlinePrompt: 'You can't take this action right now.', + offlinePrompt: 'You can\'t take this action right now.', }, genericErrorPage: { title: 'Uh-oh, something went wrong!', From 8885dc5105864ff957ca2e6c30ae386968ccd66c Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 17 Jul 2024 19:06:18 -0300 Subject: [PATCH 18/18] pr comments --- src/languages/en.ts | 2 +- src/libs/actions/Search.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 0cc3cb3ea011..a4dcddcac9c3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3530,7 +3530,7 @@ export default { unhold: 'Unhold', noOptionsAvailable: 'No options available for the selected group of expenses.', }, - offlinePrompt: 'You can\'t take this action right now.', + offlinePrompt: "You can't take this action right now.", }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 24c75aef51eb..969812b02b02 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -90,8 +90,6 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; function exportSearchItemsToCSV(query: string, reportIDList: Array | undefined, transactionIDList: string[], policyIDs: string[], onDownloadFailed: () => void) { - const fileName = `Expensify_${query}.csv`; - const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { query, reportIDList, @@ -108,6 +106,6 @@ function exportSearchItemsToCSV(query: string, reportIDList: Array