-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[Better Expense Reports] Add MoneyRequestReportView #58360
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
0040f0d
b544883
b387580
dd279c5
9803b3f
31a13f0
94df311
8a5121b
dd8660a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; | ||
| import React, {useCallback, useMemo} from 'react'; | ||
| import {InteractionManager, View} from 'react-native'; | ||
| import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; | ||
| import {useOnyx} from 'react-native-onyx'; | ||
| import FlatList from '@components/FlatList'; | ||
| import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; | ||
| import useLoadReportActions from '@hooks/useLoadReportActions'; | ||
| import useNetwork from '@hooks/useNetwork'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; | ||
| import { | ||
| getFirstVisibleReportActionID, | ||
| getMostRecentIOURequestActionID, | ||
| getOneTransactionThreadReportID, | ||
| hasNextActionMadeBySameActor, | ||
| isConsecutiveChronosAutomaticTimerAction, | ||
| isDeletedParentAction, | ||
| isReversedTransaction, | ||
| isTransactionThread, | ||
| shouldReportActionBeVisible, | ||
| } from '@libs/ReportActionsUtils'; | ||
| import {canUserPerformWriteAction, chatIncludesChronosWithID, isCanceledTaskReport, isExpenseReport, isInvoiceReport, isIOUReport, isTaskReport} from '@libs/ReportUtils'; | ||
| import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute'; | ||
| import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type * as OnyxTypes from '@src/types/onyx'; | ||
| import type Transaction from '@src/types/onyx/Transaction'; | ||
| import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList'; | ||
|
|
||
| /** | ||
| * In this view we are not handling the special single transaction case, we're just handling the report | ||
| */ | ||
| const EmptyParentReportActionForTransactionThread = undefined; | ||
|
|
||
| type MoneyRequestReportListProps = { | ||
| /** The report */ | ||
| report: OnyxTypes.Report; | ||
|
|
||
| /** Array of report actions for this report */ | ||
| reportActions?: OnyxTypes.ReportAction[]; | ||
|
|
||
| /** If the report has newer actions to load */ | ||
| hasNewerActions: boolean; | ||
|
|
||
| /** If the report has older actions to load */ | ||
| hasOlderActions: boolean; | ||
| }; | ||
|
|
||
| function getParentReportAction(parentReportActions: OnyxEntry<OnyxTypes.ReportActions>, parentReportActionID: string | undefined): OnyxEntry<OnyxTypes.ReportAction> { | ||
| if (!parentReportActions || !parentReportActionID) { | ||
| return; | ||
| } | ||
| return parentReportActions[parentReportActionID]; | ||
| } | ||
|
|
||
| function isChatOnlyReportAction(action: OnyxTypes.ReportAction) { | ||
| return action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU && action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; | ||
| } | ||
|
|
||
| function getTransactionsForReportID(transactions: OnyxCollection<OnyxTypes.Transaction>, reportID: string) { | ||
| return Object.values(transactions ?? {}).filter((transaction): transaction is Transaction => { | ||
| return transaction?.reportID === reportID; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * TODO make this component have the same functionalities as `ReportActionsList` | ||
| * - onLayout | ||
| * - onScroll | ||
| * - onScrollToIndexFailed | ||
| * - shouldEnableAutoScrollToTopThreshold | ||
| * - shouldDisplayNewMarker | ||
| * - shouldHideThreadDividerLine | ||
| */ | ||
| function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActions, hasOlderActions}: MoneyRequestReportListProps) { | ||
|
Contributor
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. I wonder if could we reuse ReportActionList here to avoid duplicated code
Contributor
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. We can consider this suggestion in another follow-up PR |
||
| const styles = useThemeStyles(); | ||
| const {isOffline} = useNetwork(); | ||
|
|
||
| const reportID = report?.reportID; | ||
|
|
||
| const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}`, { | ||
| canEvict: false, | ||
| selector: (parentReportActions) => getParentReportAction(parentReportActions, report?.parentReportActionID), | ||
| }); | ||
|
|
||
| const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); | ||
| const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false); | ||
| const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]); | ||
| const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { | ||
| selector: (allTransactions): OnyxTypes.Transaction[] => getTransactionsForReportID(allTransactions, reportID), | ||
| }); | ||
| const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); | ||
|
|
||
| const canPerformWriteAction = canUserPerformWriteAction(report); | ||
|
|
||
| // We are reversing actions because in this View we are starting at the top and don't use Inverted list | ||
| const visibleReportActions = useMemo(() => { | ||
| const filteredActions = reportActions.filter((reportAction) => { | ||
| const isChatAction = isChatOnlyReportAction(reportAction); | ||
|
|
||
| return ( | ||
| isChatAction && | ||
| (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && | ||
| shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) | ||
| ); | ||
| }); | ||
|
|
||
| return filteredActions.toReversed(); | ||
| }, [reportActions, isOffline, canPerformWriteAction]); | ||
|
|
||
| const reportActionIDs = useMemo(() => { | ||
| return reportActions?.map((action) => action.reportActionID) ?? []; | ||
| }, [reportActions]); | ||
|
|
||
| const {loadOlderChats, loadNewerChats} = useLoadReportActions({ | ||
| reportID, | ||
| reportActions, | ||
| allReportActionIDs: reportActionIDs, | ||
| transactionThreadReport, | ||
| hasOlderActions, | ||
| hasNewerActions, | ||
| }); | ||
|
|
||
| const onStartReached = useCallback(() => { | ||
| if (!isSearchTopmostFullScreenRoute()) { | ||
| loadNewerChats(false); | ||
| return; | ||
| } | ||
|
|
||
| InteractionManager.runAfterInteractions(() => requestAnimationFrame(() => loadNewerChats(false))); | ||
| }, [loadNewerChats]); | ||
|
|
||
| const onEndReached = useCallback(() => { | ||
| loadOlderChats(false); | ||
| }, [loadOlderChats]); | ||
|
|
||
| const shouldUseThreadDividerLine = useMemo(() => { | ||
| const topReport = reportActions.length > 0 ? reportActions.at(reportActions.length - 1) : null; | ||
|
|
||
| if (topReport && topReport.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { | ||
| return false; | ||
| } | ||
|
|
||
| if (isTransactionThread(parentReportAction)) { | ||
| return !isDeletedParentAction(parentReportAction) && !isReversedTransaction(parentReportAction); | ||
| } | ||
|
|
||
| if (isTaskReport(report)) { | ||
| return !isCanceledTaskReport(report, parentReportAction); | ||
| } | ||
|
|
||
| return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report); | ||
| }, [parentReportAction, report, reportActions]); | ||
|
|
||
| const renderItem = useCallback( | ||
| ({item: reportAction, index}: ListRenderItemInfo<OnyxTypes.ReportAction>) => { | ||
| const displayAsGroup = | ||
| !isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && | ||
| hasNextActionMadeBySameActor(visibleReportActions, index); | ||
|
|
||
| return ( | ||
| <ReportActionsListItemRenderer | ||
| reportAction={reportAction} | ||
| reportActions={reportActions} | ||
| parentReportAction={parentReportAction} | ||
| parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread} | ||
| index={index} | ||
| report={report} | ||
| transactionThreadReport={transactionThreadReport} | ||
| displayAsGroup={displayAsGroup} | ||
| mostRecentIOUReportActionID={mostRecentIOUReportActionID} | ||
| shouldDisplayNewMarker={false} | ||
| shouldHideThreadDividerLine | ||
| shouldUseThreadDividerLine={shouldUseThreadDividerLine} | ||
| shouldDisplayReplyDivider={visibleReportActions.length > 1} | ||
| isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID} | ||
| /> | ||
|
Contributor
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. Missed passing |
||
| ); | ||
| }, | ||
| [visibleReportActions, reportActions, parentReportAction, report, transactionThreadReport, mostRecentIOUReportActionID, shouldUseThreadDividerLine, firstVisibleReportActionID], | ||
| ); | ||
|
|
||
| return ( | ||
| <View style={styles.flex1}> | ||
| {report ? ( | ||
| <FlatList | ||
|
Contributor
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. Coming from #66988, we should disable removeClippedSubviews like what we did in reportActionList |
||
| accessibilityLabel="Test" | ||
| testID="report-actions-list" | ||
| style={styles.overscrollBehaviorContain} | ||
| data={visibleReportActions} | ||
| renderItem={renderItem} | ||
| keyExtractor={(item) => item.reportActionID} | ||
| initialNumToRender={10} | ||
| onEndReached={onEndReached} | ||
| onEndReachedThreshold={0.75} | ||
| onStartReached={onStartReached} | ||
| onStartReachedThreshold={0.75} | ||
| ListHeaderComponent={<MoneyRequestReportTransactionList transactions={transactions} />} | ||
| keyboardShouldPersistTaps="handled" | ||
| /> | ||
| ) : ( | ||
| <ReportActionsSkeletonView /> | ||
| )} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| MoneyRequestReportActionsList.displayName = 'MoneyRequestReportActionsList'; | ||
|
|
||
| export default MoneyRequestReportActionsList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import type {SearchColumnType, SortOrder} from '@components/Search/types'; | ||
| import SortableTableHeader from '@components/SelectionList/SortableTableHeader'; | ||
| import type {SortableColumnName} from '@components/SelectionList/types'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import CONST from '@src/CONST'; | ||
| import type {TranslationPaths} from '@src/languages/types'; | ||
|
|
||
| type ColumnConfig = { | ||
| columnName: SortableColumnName; | ||
| translationKey: TranslationPaths | undefined; | ||
| isColumnSortable?: boolean; | ||
| }; | ||
|
|
||
| const columnConfig: ColumnConfig[] = [ | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.RECEIPT, | ||
| translationKey: 'common.receipt', | ||
| isColumnSortable: false, | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE, | ||
| translationKey: 'common.type', | ||
| isColumnSortable: false, | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, | ||
| translationKey: 'common.date', | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT, | ||
| translationKey: 'common.merchant', | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY, | ||
| translationKey: 'common.category', | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.TAG, | ||
| translationKey: 'common.tag', | ||
| }, | ||
| { | ||
| columnName: CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, | ||
| translationKey: undefined, // comments have no title displayed | ||
| isColumnSortable: false, | ||
| }, | ||
| { | ||
| columnName: CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, | ||
| translationKey: 'common.total', | ||
| }, | ||
| ]; | ||
|
|
||
| type SearchTableHeaderProps = { | ||
| sortBy?: SearchColumnType; | ||
| sortOrder?: SortOrder; | ||
| onSortPress: (column: SortableColumnName, order: SortOrder) => void; | ||
| shouldShowSorting: boolean; | ||
| }; | ||
|
|
||
| // At this moment with new Report View we have no extra logic for displaying columns | ||
| const shouldShowColumn = () => true; | ||
|
|
||
| function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, shouldShowSorting}: SearchTableHeaderProps) { | ||
| const styles = useThemeStyles(); | ||
|
|
||
| return ( | ||
| <View style={[styles.ph8, styles.pv3]}> | ||
| <SortableTableHeader | ||
| columns={columnConfig} | ||
| shouldShowColumn={shouldShowColumn} | ||
| dateColumnSize="normal" | ||
| shouldShowSorting={shouldShowSorting} | ||
| sortBy={sortBy} | ||
| sortOrder={sortOrder} | ||
| onSortPress={onSortPress} | ||
| /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| MoneyRequestReportTableHeader.displayName = 'MoneyRequestReportTableHeader'; | ||
|
|
||
| export default MoneyRequestReportTableHeader; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import TransactionItemRow from '@components/TransactionItemRow'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import type * as OnyxTypes from '@src/types/onyx'; | ||
| import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader'; | ||
|
|
||
| type MoneyRequestReportTransactionListProps = { | ||
| /** List of transactions belonging to one report */ | ||
| transactions: OnyxTypes.Transaction[]; | ||
| }; | ||
|
|
||
| function MoneyRequestReportTransactionList({transactions}: MoneyRequestReportTransactionListProps) { | ||
| const styles = useThemeStyles(); | ||
| const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); | ||
|
|
||
| const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout; | ||
|
|
||
| return ( | ||
| <> | ||
| {!displayNarrowVersion && ( | ||
| <MoneyRequestReportTableHeader | ||
|
Contributor
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. From #76352 (comment): we missed wrapping the component with |
||
| shouldShowSorting | ||
| sortBy="date" | ||
| sortOrder="desc" | ||
| onSortPress={() => {}} | ||
| /> | ||
| )} | ||
| <View style={[styles.pv2, styles.ph5]}> | ||
| {transactions.map((transaction) => { | ||
| return ( | ||
| <View style={[styles.mb2]}> | ||
| <TransactionItemRow | ||
| transactionItem={transaction} | ||
| isSelected={false} | ||
| shouldShowTooltip | ||
| shouldUseNarrowLayout={displayNarrowVersion} | ||
| /> | ||
| </View> | ||
| ); | ||
| })} | ||
| </View> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| MoneyRequestReportTransactionList.displayName = 'MoneyRequestReportTransactionList'; | ||
|
|
||
| export default MoneyRequestReportTransactionList; | ||
Uh oh!
There was an error while loading. Please reload this page.