Skip to content

Commit

Permalink
Merge pull request #45614 from Expensify/rodrigo-fix-download-csv
Browse files Browse the repository at this point in the history
Add Export CSV and handle offline for all actions
  • Loading branch information
luacmartins authored Jul 17, 2024
2 parents 4daf9e9 + 8885dc5 commit 7bb89e1
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5229,6 +5229,7 @@ const CONST = {
SUBMIT: 'submit',
APPROVE: 'approve',
PAY: 'pay',
EXPORT: 'export',
},
SYNTAX_OPERATORS: {
AND: 'and',
Expand Down
2 changes: 1 addition & 1 deletion src/components/DecisionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/components/Search/SearchActionOptionsUtils.desktop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import type {SearchHeaderOptionValue} from './SearchPageHeader';

function getDownloadOption(): DropdownOption<SearchHeaderOptionValue> | undefined {
return undefined;
}

export default getDownloadOption;
8 changes: 8 additions & 0 deletions src/components/Search/SearchActionOptionsUtils.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import type {SearchHeaderOptionValue} from './SearchPageHeader';

function getDownloadOption(): DropdownOption<SearchHeaderOptionValue> | undefined {
return undefined;
}

export default getDownloadOption;
16 changes: 16 additions & 0 deletions src/components/Search/SearchActionOptionsUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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<SearchHeaderOptionValue> {
return {
icon: Expensicons.Download,
text,
value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT,
shouldCloseModalOnSelect: true,
onSelected,
};
}

export default getDownloadOption;
94 changes: 63 additions & 31 deletions src/components/Search/SearchListWithHeader.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,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';

Expand All @@ -28,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((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)),
isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected),
};
}

Expand All @@ -50,30 +51,36 @@ function SearchListWithHeader(
const {translate} = useLocalize();
const [isModalVisible, setIsModalVisible] = useState(false);
const [longPressedItem, setLongPressedItem] = useState<TransactionListItemType | ReportListItemType | null>(null);
const [selectedItems, setSelectedItems] = useState<SelectedTransactions>({});
const [selectedItemsToDelete, setSelectedItemsToDelete] = useState<string[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<SelectedTransactions>({});
const [selectedReports, setSelectedReports] = useState<Array<SearchReport['reportID']>>([]);
const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState<string[]>([]);
const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);

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

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

const clearSelectedItems = () => setSelectedItems({});
const clearSelectedItems = () => {
setSelectedTransactions({});
setSelectedReports([]);
};

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

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

useEffect(() => {
Expand All @@ -87,7 +94,7 @@ function SearchListWithHeader(
return;
}

setSelectedItems((prev) => {
setSelectedTransactions((prev) => {
if (prev[item.keyForList]?.isSelected) {
const {[item.keyForList]: omittedTransaction, ...transactions} = prev;
return transactions;
Expand All @@ -98,23 +105,27 @@ function SearchListWithHeader(
return;
}

if (item.transactions.every((transaction) => selectedItems[transaction.keyForList]?.isSelected)) {
const reducedSelectedItems: SelectedTransactions = {...selectedItems};
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;
}

setSelectedItems({
...selectedItems,
if (item.reportID) {
setSelectedReports([...selectedReports, item.reportID]);
}
setSelectedTransactions({
...selectedTransactions,
...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)),
});
},
[selectedItems],
[selectedTransactions, selectedReports],
);

const openBottomModal = (item: TransactionListItemType | ReportListItemType | null) => {
Expand Down Expand Up @@ -144,40 +155,43 @@ 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();
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 (
<>
<SearchPageHeader
selectedItems={selectedItems}
selectedTransactions={selectedTransactions}
clearSelectedItems={clearSelectedItems}
query={query}
hash={hash}
onSelectDeleteOption={handleOnSelectDeleteOption}
isMobileSelectionModeActive={isMobileSelectionModeActive}
setIsMobileSelectionModeActive={setIsMobileSelectionModeActive}
selectedReports={selectedReports}
setOfflineModalOpen={() => setOfflineModalVisible(true)}
setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)}
/>
<SelectionList<ReportListItemType | TransactionListItemType>
// eslint-disable-next-line react/jsx-props-no-spreading
Expand All @@ -195,12 +209,30 @@ function SearchListWithHeader(
isVisible={deleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
onCancel={handleOnCancelConfirmModal}
title={translate('iou.deleteExpense', {count: selectedItemsToDelete.length})}
prompt={translate('iou.deleteConfirmation', {count: selectedItemsToDelete.length})}
title={translate('iou.deleteExpense', {count: selectedTransactionsToDelete.length})}
prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsToDelete.length})}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<DecisionModal
title={translate('common.youAppearToBeOffline')}
prompt={translate('search.offlinePrompt')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setOfflineModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={offlineModalVisible}
onClose={() => setOfflineModalVisible(false)}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setDownloadErrorModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={downloadErrorModalVisible}
onClose={() => setDownloadErrorModalVisible(false)}
/>
<Modal
isVisible={isModalVisible}
type={CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED}
Expand Down
Loading

0 comments on commit 7bb89e1

Please sign in to comment.