-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add selection to table on MoneyRequestReport page #59433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
df1e9f1
1e4e8a8
aa7d938
a4f1e84
cd3eae9
024d3bd
2b910eb
4996cf8
35056a6
bb135a7
97c2185
11a424a
887a779
bd7eca8
22648d5
c5d8c27
59ba04c
fbb6d37
56ae9b4
7a4718c
3a639da
4374368
108ef07
34b4afd
1f1f049
1366364
28954e5
8e629ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import React, {useCallback, useContext, useMemo, useState} from 'react'; | ||
import type ChildrenProps from '@src/types/utils/ChildrenProps'; | ||
|
||
type TMoneyRequestReportContext = { | ||
selectedTransactionsID: Record<string, string[]>; | ||
setSelectedTransactionsID: (reportID: string) => (transactionsID: string[]) => void; | ||
toggleTransaction: (reportID: string) => (transactionID: string) => void; | ||
removeTransaction: (reportID: string) => (transactionID?: string) => void; | ||
isTransactionSelected: (reportID: string) => (transactionID: string) => boolean; | ||
}; | ||
|
||
const defaultMoneyRequestReportContext = { | ||
selectedTransactionsID: {}, | ||
setSelectedTransactionsID: () => () => {}, | ||
toggleTransaction: () => () => {}, | ||
removeTransaction: () => () => {}, | ||
isTransactionSelected: () => () => false, | ||
}; | ||
|
||
const Context = React.createContext<TMoneyRequestReportContext>(defaultMoneyRequestReportContext); | ||
|
||
// TODO merge it with SearchContext in follow-up - https://github.com/Expensify/App/issues/59431 | ||
jnowakow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
function MoneyRequestReportContextProvider({children}: ChildrenProps) { | ||
const [selectedTransactionsForReport, setSelectedTransactionsForReport] = useState<Record<string, string[]>>({}); | ||
|
||
const setSelectedTransactionsID = useCallback( | ||
(reportID: string) => (transactionsID: string[]) => { | ||
setSelectedTransactionsForReport((prev) => ({...prev, [reportID]: transactionsID})); | ||
}, | ||
[], | ||
); | ||
|
||
const toggleTransaction = useCallback( | ||
(reportID: string) => (transactionID: string) => { | ||
setSelectedTransactionsForReport((prev) => { | ||
const prevTransactions = prev[reportID] ?? []; | ||
if (prevTransactions.includes(transactionID)) { | ||
return {...prev, [reportID]: prevTransactions.filter((t) => t !== transactionID)}; | ||
} | ||
return {...prev, [reportID]: [...prevTransactions, transactionID]}; | ||
}); | ||
}, | ||
[], | ||
); | ||
|
||
const removeTransaction = useCallback( | ||
(reportID: string) => (transactionID?: string) => { | ||
setSelectedTransactionsForReport((prev) => { | ||
const prevTransactions = prev[reportID] ?? []; | ||
return {...prev, [reportID]: prevTransactions.filter((t) => t !== transactionID)}; | ||
}); | ||
}, | ||
[], | ||
); | ||
|
||
const isTransactionSelected = useCallback( | ||
(reportID: string) => (transactionID: string) => (selectedTransactionsForReport[reportID] ?? []).includes(transactionID), | ||
[selectedTransactionsForReport], | ||
); | ||
|
||
const context = useMemo( | ||
() => ({ | ||
selectedTransactionsID: selectedTransactionsForReport, | ||
setSelectedTransactionsID, | ||
toggleTransaction, | ||
isTransactionSelected, | ||
removeTransaction, | ||
}), | ||
[isTransactionSelected, removeTransaction, selectedTransactionsForReport, setSelectedTransactionsID, toggleTransaction], | ||
); | ||
|
||
return <Context.Provider value={context}>{children}</Context.Provider>; | ||
} | ||
|
||
function useMoneyRequestReportContext(reportID?: string) { | ||
const context = useContext(Context); | ||
|
||
return useMemo( | ||
() => ({ | ||
selectedTransactionsID: reportID ? context.selectedTransactionsID[reportID] ?? [] : [], | ||
setSelectedTransactionsID: reportID ? context.setSelectedTransactionsID(reportID) : () => {}, | ||
toggleTransaction: reportID ? context.toggleTransaction(reportID) : () => {}, | ||
isTransactionSelected: reportID ? context.isTransactionSelected(reportID) : () => false, | ||
removeTransaction: reportID ? context.removeTransaction(reportID) : () => {}, | ||
}), | ||
[context, reportID], | ||
); | ||
} | ||
|
||
MoneyRequestReportContextProvider.displayName = 'MoneyRequestReportContextProvider'; | ||
|
||
export {MoneyRequestReportContextProvider, useMoneyRequestReportContext}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import isEmpty from 'lodash/isEmpty'; | ||
import React, {useCallback, useEffect, useMemo, useState} from 'react'; | ||
import React, {memo, useCallback, useMemo, useState} from 'react'; | ||
import {View} from 'react-native'; | ||
import type {TupleToUnion} from 'type-fest'; | ||
import {getButtonRole} from '@components/Button/utils'; | ||
import Checkbox from '@components/Checkbox'; | ||
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; | ||
import type {SortOrder} from '@components/Search/types'; | ||
import Text from '@components/Text'; | ||
|
@@ -11,6 +12,7 @@ import useHover from '@hooks/useHover'; | |
import useLocalize from '@hooks/useLocalize'; | ||
import {useMouseContext} from '@hooks/useMouseContext'; | ||
import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
import useStyleUtils from '@hooks/useStyleUtils'; | ||
import useThemeStyles from '@hooks/useThemeStyles'; | ||
import {convertToDisplayString} from '@libs/CurrencyUtils'; | ||
import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils'; | ||
|
@@ -19,9 +21,11 @@ import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils'; | |
import {compareValues} from '@libs/SearchUIUtils'; | ||
import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; | ||
import Navigation from '@navigation/Navigation'; | ||
import variables from '@styles/variables'; | ||
import CONST from '@src/CONST'; | ||
import ROUTES from '@src/ROUTES'; | ||
import type * as OnyxTypes from '@src/types/onyx'; | ||
import {useMoneyRequestReportContext} from './MoneyRequestReportContext'; | ||
import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader'; | ||
import SearchMoneyRequestReportEmptyState from './SearchMoneyRequestReportEmptyState'; | ||
import {setActiveTransactionReportIDs} from './TransactionReportIDRepository'; | ||
|
@@ -50,7 +54,6 @@ const sortableColumnNames = [ | |
type SortableColumnName = TupleToUnion<typeof sortableColumnNames>; | ||
|
||
type SortedTransactions = { | ||
transactions: OnyxTypes.Transaction[]; | ||
sortBy: SortableColumnName; | ||
sortOrder: SortOrder; | ||
}; | ||
|
@@ -62,18 +65,9 @@ const getTransactionKey = (transaction: OnyxTypes.Transaction, key: SortableColu | |
return key === CONST.SEARCH.TABLE_COLUMNS.DATE ? dateKey : key; | ||
}; | ||
|
||
const areTransactionValuesEqual = (transactions: OnyxTypes.Transaction[], key: SortableColumnName) => { | ||
const firstValidTransaction = transactions.find((transaction) => transaction !== undefined); | ||
if (!firstValidTransaction) { | ||
return true; | ||
} | ||
|
||
const keyOfFirstValidTransaction = getTransactionKey(firstValidTransaction, key); | ||
return transactions.every((transaction) => transaction[getTransactionKey(transaction, key)] === firstValidTransaction[keyOfFirstValidTransaction]); | ||
}; | ||
|
||
function MoneyRequestReportTransactionList({report, transactions, reportActions, hasComments}: MoneyRequestReportTransactionListProps) { | ||
const styles = useThemeStyles(); | ||
const StyleUtils = useStyleUtils(); | ||
const {translate} = useLocalize(); | ||
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); | ||
const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout; | ||
|
@@ -86,29 +80,23 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, | |
const {bind} = useHover(); | ||
const {isMouseDownOnInput, setMouseUp} = useMouseContext(); | ||
|
||
const {selectedTransactionsID, setSelectedTransactionsID, toggleTransaction, isTransactionSelected} = useMoneyRequestReportContext(report.reportID); | ||
|
||
const handleMouseLeave = (e: React.MouseEvent<Element, MouseEvent>) => { | ||
bind.onMouseLeave(); | ||
e.stopPropagation(); | ||
setMouseUp(); | ||
}; | ||
|
||
const [sortedData, setSortedData] = useState<SortedTransactions>({ | ||
transactions, | ||
const [sortConfig, setSortConfig] = useState<SortedTransactions>({ | ||
sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, | ||
sortOrder: CONST.SEARCH.SORT_ORDER.ASC, | ||
}); | ||
|
||
const {sortBy, sortOrder} = sortedData; | ||
const {sortBy, sortOrder} = sortConfig; | ||
|
||
useEffect(() => { | ||
if (areTransactionValuesEqual(transactions, sortBy)) { | ||
return; | ||
} | ||
|
||
setSortedData((prevState) => ({ | ||
...prevState, | ||
transactions: [...transactions].sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy)), | ||
})); | ||
const sortedTransactions = useMemo(() => { | ||
return [...transactions].sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy)); | ||
}, [sortBy, sortOrder, transactions]); | ||
|
||
const navigateToTransaction = useCallback( | ||
|
@@ -123,12 +111,12 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, | |
|
||
// Single transaction report will open in RHP, and we need to find every other report ID for the rest of transactions | ||
// to display prev/next arrows in RHP for navigating between transactions | ||
const sortedSiblingTransactionReportIDs = getThreadReportIDsForTransactions(reportActions, sortedData.transactions); | ||
const sortedSiblingTransactionReportIDs = getThreadReportIDsForTransactions(reportActions, sortedTransactions); | ||
setActiveTransactionReportIDs(sortedSiblingTransactionReportIDs); | ||
|
||
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: reportIDToNavigate, backTo})); | ||
}, | ||
[reportActions, sortedData.transactions], | ||
[reportActions, sortedTransactions], | ||
); | ||
|
||
const dateColumnSize = useMemo(() => { | ||
|
@@ -143,22 +131,37 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, | |
return !isEmpty(transactions) ? ( | ||
<> | ||
{!displayNarrowVersion && ( | ||
<MoneyRequestReportTableHeader | ||
shouldShowSorting | ||
sortBy={sortBy} | ||
sortOrder={sortOrder} | ||
dateColumnSize={dateColumnSize} | ||
onSortPress={(selectedSortBy, selectedSortOrder) => { | ||
if (!isSortableColumnName(selectedSortBy)) { | ||
return; | ||
} | ||
|
||
setSortedData((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); | ||
}} | ||
/> | ||
<View style={[styles.dFlex, styles.flexRow, styles.ph5, styles.pv3, StyleUtils.getPaddingBottom(6), styles.justifyContentCenter, styles.alignItemsCenter]}> | ||
<View style={[styles.pv2, styles.pr4, StyleUtils.getPaddingLeft(variables.w12)]}> | ||
<Checkbox | ||
onPress={() => { | ||
if (selectedTransactionsID.length === transactions.length) { | ||
jnowakow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
setSelectedTransactionsID([]); | ||
} else { | ||
setSelectedTransactionsID(transactions.map((t) => t.transactionID)); | ||
} | ||
}} | ||
accessibilityLabel={CONST.ROLE.CHECKBOX} | ||
isChecked={selectedTransactionsID.length === transactions.length} | ||
/> | ||
</View> | ||
<MoneyRequestReportTableHeader | ||
shouldShowSorting | ||
sortBy={sortBy} | ||
sortOrder={sortOrder} | ||
dateColumnSize={dateColumnSize} | ||
onSortPress={(selectedSortBy, selectedSortOrder) => { | ||
if (!isSortableColumnName(selectedSortBy)) { | ||
return; | ||
} | ||
|
||
setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); | ||
}} | ||
/> | ||
</View> | ||
)} | ||
<View style={[listHorizontalPadding, styles.gap2, styles.pb4, displayNarrowVersion && styles.pt4]}> | ||
{sortedData.transactions.map((transaction) => { | ||
{sortedTransactions.map((transaction) => { | ||
return ( | ||
<PressableWithFeedback | ||
onPress={(e) => { | ||
|
@@ -180,11 +183,12 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, | |
> | ||
<TransactionItemRow | ||
transactionItem={transaction} | ||
isSelected={false} | ||
isSelected={isTransactionSelected(transaction.transactionID)} | ||
shouldShowTooltip | ||
dateColumnSize={dateColumnSize} | ||
shouldUseNarrowLayout={displayNarrowVersion} | ||
shouldShowChatBubbleComponent | ||
onCheckboxPress={toggleTransaction} | ||
/> | ||
</PressableWithFeedback> | ||
); | ||
|
@@ -230,4 +234,4 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, | |
|
||
MoneyRequestReportTransactionList.displayName = 'MoneyRequestReportTransactionList'; | ||
|
||
export default MoneyRequestReportTransactionList; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic behind sorting is quite complicated and as it's done in |
||
export default memo(MoneyRequestReportTransactionList); |
Uh oh!
There was an error while loading. Please reload this page.